diff --git a/components/Blog/Blog.php b/components/Blog/Blog.php new file mode 100644 index 0000000000..6d475ed47f --- /dev/null +++ b/components/Blog/Blog.php @@ -0,0 +1,37 @@ +. + +// }}} + +namespace Component\Blog; + +use App\Core\Event; +use App\Core\Modules\Plugin; +use App\Core\Router\RouteLoader; +use Component\Blog\Controller as C; + +class Blog extends Plugin +{ + public function onAddRoute(RouteLoader $r): bool + { + $r->connect(id: 'blog_post', uri_path: '/blog/post', target: [C\Post::class, 'makePost']); + return Event::next; + } +} diff --git a/components/Blog/Controller/Post.php b/components/Blog/Controller/Post.php new file mode 100644 index 0000000000..ca5c655049 --- /dev/null +++ b/components/Blog/Controller/Post.php @@ -0,0 +1,177 @@ +. + +// }}} + +namespace Component\Blog\Controller; + +use App\Core\ActorLocalRoles; +use App\Core\Controller; +use App\Core\Event; +use App\Core\Form; +use function App\Core\I18n\_m; +use App\Core\Router\Router; +use App\Core\VisibilityScope; +use App\Entity\Actor; +use App\Util\Common; +use App\Util\Exception\ClientException; +use App\Util\Exception\RedirectException; +use App\Util\Form\FormFields; +use Component\Posting\Posting; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Validator\Constraints\Length; + +class Post extends Controller +{ + /** + * Creates and handles Blog post creation form + * + * @throws \App\Util\Exception\DuplicateFoundException + * @throws \App\Util\Exception\NoLoggedInUser + * @throws \App\Util\Exception\ServerException + * @throws ClientException + * @throws RedirectException + */ + public function makePost(Request $request) + { + $actor = Common::ensureLoggedIn()->getActor(); + + $placeholder_strings = ['How are you feeling?', 'Have something to share?', 'How was your day?']; + Event::handle('PostingPlaceHolderString', [&$placeholder_strings]); + $placeholder = $placeholder_strings[array_rand($placeholder_strings)]; + + $initial_content = ''; + Event::handle('PostingInitialContent', [&$initial_content]); + + $available_content_types = [ + _m('Plain Text') => 'text/plain', + ]; + Event::handle('PostingAvailableContentTypes', [&$available_content_types]); + + $context_actor = Actor::getById($this->int('in')); + if (!$context_actor->isGroup()) { + throw new \InvalidArgumentException('Only group blog posts are supported for now.'); + } + $in_targets = ["!{$context_actor->getNickname()}" => $context_actor->getId()]; + $form_params[] = ['in', ChoiceType::class, ['label' => _m('In:'), 'multiple' => false, 'expanded' => false, 'choices' => $in_targets]]; + + + $visibility_options = [ + _m('Public') => VisibilityScope::EVERYWHERE->value, + _m('Local') => VisibilityScope::LOCAL->value, + _m('Addressee') => VisibilityScope::ADDRESSEE->value, + ]; + if (!\is_null($context_actor) && $context_actor->isGroup()) { + if ($actor->canModerate($context_actor)) { + if ($context_actor->getRoles() & ActorLocalRoles::PRIVATE_GROUP) { + $visibility_options = array_merge([_m('Group') => VisibilityScope::GROUP->value], $visibility_options); + } else { + $visibility_options[_m('Group')] = VisibilityScope::GROUP->value; + } + } + } + $form_params[] = ['visibility', ChoiceType::class, ['label' => _m('Visibility:'), 'multiple' => false, 'expanded' => false, 'choices' => $visibility_options]]; + + $form_params[] = ['content', TextareaType::class, ['label' => _m('Content:'), 'data' => $initial_content, 'attr' => ['placeholder' => _m($placeholder)], 'constraints' => [new Length(['max' => Common::config('site', 'text_limit')])]]]; + $form_params[] = ['attachments', FileType::class, ['label' => _m('Attachments:'), 'multiple' => true, 'required' => false, 'invalid_message' => _m('Attachment not valid.')]]; + $form_params[] = FormFields::language($actor, $context_actor, label: _m('Note language'), help: _m('The selected language will be federated and added as a lang attribute, preferred language can be set up in settings')); + + if (\count($available_content_types) > 1) { + $form_params[] = ['content_type', ChoiceType::class, + [ + 'label' => _m('Text format:'), 'multiple' => false, 'expanded' => false, + 'data' => $available_content_types[array_key_first($available_content_types)], + 'choices' => $available_content_types, + ], + ]; + } + + Event::handle('PostingAddFormEntries', [$request, $actor, &$form_params]); + + $form_params[] = ['post_note', SubmitType::class, ['label' => _m('Post')]]; + $form = Form::create($form_params); + + $form->handleRequest($request); + if ($form->isSubmitted()) { + try { + if ($form->isValid()) { + $data = $form->getData(); + Event::handle('PostingModifyData', [$request, $actor, &$data, $form_params, $form]); + + if (empty($data['content']) && empty($data['attachments'])) { + // TODO Display error: At least one of `content` and `attachments` must be provided + throw new ClientException(_m('You must enter content or provide at least one attachment to post a note.')); + } + + if (\is_null(VisibilityScope::tryFrom($data['visibility']))) { + throw new ClientException(_m('You have selected an impossible visibility.')); + } + + $content_type = $data['content_type'] ?? $available_content_types[array_key_first($available_content_types)]; + $extra_args = []; + Event::handle('AddExtraArgsToNoteContent', [$request, $actor, $data, &$extra_args, $form_params, $form]); + + if (\array_key_exists('in', $data) && $data['in'] !== 'public') { + $target = $data['in']; + } + + Posting::storeLocalNote( + actor: $actor, + content: $data['content'], + content_type: $content_type, + locale: $data['language'], + scope: VisibilityScope::from($data['visibility']), + target: $target ?? null, + reply_to_id: $data['reply_to_id'], + attachments: $data['attachments'], + process_note_content_extra_args: $extra_args, + ); + + try { + if ($request->query->has('from')) { + $from = $request->query->get('from'); + if (str_contains($from, '#')) { + [$from, $fragment] = explode('#', $from); + } + Router::match($from); + throw new RedirectException(url: $from . (isset($fragment) ? '#' . $fragment : '')); + } + } catch (ResourceNotFoundException $e) { + // continue + } + throw new RedirectException(); + } + } catch (FormSizeFileException $e) { + throw new ClientException(_m('Invalid file size given'), previous: $e); + } + } + return [ + '_template' => 'blog/make_post.html.twig', + 'blog_entry_form' => $form->createView(), + ]; + } +} diff --git a/components/Blog/templates/blog/make_post.html.twig b/components/Blog/templates/blog/make_post.html.twig new file mode 100644 index 0000000000..16f85f84d9 --- /dev/null +++ b/components/Blog/templates/blog/make_post.html.twig @@ -0,0 +1,10 @@ +{% extends 'stdgrid.html.twig' %} +{% block title %}{{ 'Create a blog post' | trans }}{% endblock %} + +{% block body %} + {{ parent() }} +
+

{{ 'Create a blog post' | trans }}

+ {{ form(blog_entry_form) }} +
+{% endblock body %}