123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- <?php
- declare(strict_types = 1);
- // {{{ License
- // This file is part of GNU social - https://www.gnu.org/software/social
- //
- // GNU social is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // GNU social is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
- // }}}
- namespace Component\Posting;
- use App\Core\Cache;
- use App\Core\DB\DB;
- use App\Core\Event;
- use App\Core\GSFile;
- use function App\Core\I18n\_m;
- use App\Core\Modules\Component;
- use App\Core\Router\RouteLoader;
- use App\Core\Router\Router;
- use App\Core\VisibilityScope;
- use App\Entity\Activity;
- use App\Entity\Actor;
- use App\Entity\Note;
- use App\Util\Common;
- use App\Util\Exception\BugFoundException;
- use App\Util\Exception\ClientException;
- use App\Util\Exception\DuplicateFoundException;
- use App\Util\Exception\RedirectException;
- use App\Util\Exception\ServerException;
- use App\Util\Formatting;
- use App\Util\HTML;
- use Component\Attachment\Entity\ActorToAttachment;
- use Component\Attachment\Entity\AttachmentToNote;
- use Component\Conversation\Conversation;
- use Component\Language\Entity\Language;
- use Component\Notification\Entity\Attention;
- use Functional as F;
- use Symfony\Component\HttpFoundation\File\UploadedFile;
- use Symfony\Component\HttpFoundation\Request;
- class Posting extends Component
- {
- public const route = 'posting_form_action';
- public function onAddRoute(RouteLoader $r): bool
- {
- $r->connect(self::route, '/form/posting', Controller\Posting::class);
- return Event::next;
- }
- /**
- * HTML render event handler responsible for adding and handling
- * the result of adding the note submission form, only if a user is logged in
- *
- * @throws BugFoundException
- * @throws ClientException
- * @throws DuplicateFoundException
- * @throws RedirectException
- * @throws ServerException
- */
- public function onAddMainRightPanelBlock(Request $request, array &$res): bool
- {
- if (\is_null($user = Common::user()) || preg_match('(feed|conversation|group|view)', $request->get('_route')) === 0) {
- return Event::next;
- }
- $res['post_form'] = Form\Posting::create($request)->createView();
- return Event::next;
- }
- /**
- * @throws ClientException
- * @throws DuplicateFoundException
- * @throws ServerException
- */
- public static function storeLocalPage(
- Actor $actor,
- ?string $content,
- string $content_type,
- ?string $locale = null,
- ?VisibilityScope $scope = null,
- array $attentions = [],
- null|int|Note $reply_to = null,
- array $attachments = [],
- array $processed_attachments = [],
- array $process_note_content_extra_args = [],
- bool $flush_and_notify = true,
- ?string $rendered = null,
- string $source = 'web',
- ?string $title = null,
- ): array {
- [$activity, $note, $notification_targets] = self::storeLocalNote(
- actor: $actor,
- content: $content,
- content_type: $content_type,
- locale: $locale,
- scope: $scope,
- attentions: $attentions,
- reply_to: $reply_to,
- attachments: $attachments,
- processed_attachments: $processed_attachments,
- process_note_content_extra_args: $process_note_content_extra_args,
- flush_and_notify: false,
- rendered: $rendered,
- source: $source,
- );
- $note->setType('page');
- $note->setTitle($title);
- if ($flush_and_notify) {
- // Flush before notification
- DB::flush();
- Event::handle('NewNotification', [
- $actor,
- $activity,
- $notification_targets,
- _m('Actor {actor_id} created page {note_id}.', [
- '{actor_id}' => $actor->getId(),
- '{note_id}' => $activity->getObjectId(),
- ]),
- ]);
- }
- return [$activity, $note, $notification_targets];
- }
- /**
- * Store the given note with $content and $attachments, created by
- * $actor_id, possibly as a reply to note $reply_to and with flag
- * $is_local. Sanitizes $content and $attachments
- *
- * @param Actor $actor The Actor responsible for the creation of this Note
- * @param null|string $content The raw text content
- * @param string $content_type Indicating one of the various supported content format (Plain Text, Markdown, LaTeX...)
- * @param null|string $locale Note's written text language, set by the default Actor language or upon filling
- * @param null|VisibilityScope $scope The visibility of this Note
- * @param array $attentions Actor|int[]: In Group/To Person or Bot, registers an attention between note and target
- * @param null|int|Note $reply_to The soon-to-be Note parent's id, if it's a Reply itself
- * @param array $attachments UploadedFile[] to be stored as GSFiles associated to this note
- * @param array $processed_attachments Array of [Attachment, Attachment's name][] to be associated to this $actor and Note
- * @param array $process_note_content_extra_args Extra arguments for the event ProcessNoteContent
- * @param bool $flush_and_notify True if the newly created Note activity should be passed on as a Notification
- * @param null|string $rendered The Note's content post RenderNoteContent event, which sanitizes and processes the raw content sent
- * @param string $source The source of this Note
- *
- * @throws ClientException
- * @throws DuplicateFoundException
- * @throws ServerException
- *
- * @return array [Activity, Note, Notification Targets]
- */
- public static function storeLocalNote(
- Actor $actor,
- ?string $content,
- string $content_type,
- ?string $locale = null,
- ?VisibilityScope $scope = null,
- array $attentions = [],
- null|int|Note $reply_to = null,
- array $attachments = [],
- array $processed_attachments = [],
- array $process_note_content_extra_args = [],
- bool $flush_and_notify = true,
- ?string $rendered = null,
- string $source = 'web',
- ): array {
- $scope ??= VisibilityScope::EVERYWHERE; // TODO: If site is private, default to LOCAL
- $reply_to_id = \is_null($reply_to) ? null : (\is_int($reply_to) ? $reply_to : $reply_to->getId());
- $mentions = [];
- if (\is_null($rendered) && !empty($content)) {
- Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, $actor, $locale, &$mentions]);
- }
- $note = Note::create([
- 'actor_id' => $actor->getId(),
- 'content' => $content,
- 'content_type' => $content_type,
- 'rendered' => $rendered,
- 'language_id' => !\is_null($locale) ? Language::getByLocale($locale)->getId() : null,
- 'is_local' => true,
- 'scope' => $scope,
- 'reply_to' => $reply_to_id,
- 'source' => $source,
- ]);
- /** @var UploadedFile[] $attachments */
- foreach ($attachments as $f) {
- $filesize = $f->getSize();
- $max_file_size = Common::getUploadLimit();
- if ($max_file_size < $filesize) {
- throw new ClientException(_m('No file may be larger than {quota} bytes and the file you sent was {size} bytes. '
- . 'Try to upload a smaller version.', ['quota' => $max_file_size, 'size' => $filesize], ));
- }
- Event::handle('EnforceUserFileQuota', [$filesize, $actor->getId()]);
- $processed_attachments[] = [GSFile::storeFileAsAttachment($f), $f->getClientOriginalName()];
- }
- DB::persist($note);
- Conversation::assignLocalConversation($note, $reply_to_id);
- // Update replies cache
- if (!\is_null($reply_to_id)) {
- Cache::incr(Note::cacheKeys($reply_to_id)['replies-count']);
- // Not having them cached doesn't mean replies don't exist, but don't push it to the
- // list, as that means they need to be refetched, or some would be missed
- if (Cache::exists(Note::cacheKeys($reply_to_id)['replies'])) {
- Cache::listPushRight(Note::cacheKeys($reply_to_id)['replies'], $note);
- }
- }
- // Need file and note ids for the next step
- $note->setUrl(Router::url('note_view', ['id' => $note->getId()], Router::ABSOLUTE_URL));
- if (!empty($content)) {
- Event::handle('ProcessNoteContent', [$note, $content, $content_type, $process_note_content_extra_args]);
- }
- // These are note attachments now, and not just attachments, ensure these relations are respected
- if ($processed_attachments !== []) {
- foreach ($processed_attachments as [$a, $fname]) {
- // Most attachments should already be associated with its author, but maybe it didn't make sense
- //for this attachment, or it's simply a repost of an attachment by a different actor
- if (DB::count(ActorToAttachment::class, $args = ['attachment_id' => $a->getId(), 'actor_id' => $actor->getId()]) === 0) {
- DB::persist(ActorToAttachment::create($args));
- }
- DB::persist(AttachmentToNote::create(['attachment_id' => $a->getId(), 'note_id' => $note->getId(), 'title' => $fname]));
- }
- }
- $activity = Activity::create([
- 'actor_id' => $actor->getId(),
- 'verb' => 'create',
- 'object_type' => 'note',
- 'object_id' => $note->getId(),
- 'source' => $source,
- ]);
- DB::persist($activity);
- $attention_ids = [];
- foreach ($attentions as $target) {
- $target_id = \is_int($target) ? $target : $target->getId();
- DB::persist(Attention::create(['object_type' => 'note', 'object_id' => $note->getId(), 'target_id' => $target_id]));
- $attention_ids[$target_id] = true;
- }
- $attention_ids = array_keys($attention_ids);
- if ($flush_and_notify) {
- // Flush before notification
- DB::flush();
- Event::handle('NewNotification', [
- $actor,
- $activity,
- $notification_targets = [
- 'note-attention' => $attention_ids,
- 'note-mention' => F\unique(F\flat_map($mentions, fn (array $m) => F\map($m['mentioned'] ?? [], fn (Actor $a) => $a->getId()))),
- ],
- _m('Actor {actor_id} created note {note_id}.', [
- '{actor_id}' => $actor->getId(),
- '{note_id}' => $activity->getObjectId(),
- ]),
- ]);
- }
- return [$activity, $note, $notification_targets];
- }
- public function onRenderNoteContent(string $content, string $content_type, ?string &$rendered, Actor $author, ?string $language = null, array &$mentions = [])
- {
- switch ($content_type) {
- case 'text/plain':
- $rendered = Formatting::renderPlainText($content, $language);
- [$rendered, $mentions] = Formatting::linkifyMentions($rendered, $author, $language);
- return Event::stop;
- case 'text/html':
- // TODO: It has to linkify and stuff as well
- $rendered = HTML::sanitize($content);
- return Event::stop;
- default:
- return Event::next;
- }
- }
- }
|