From 66323c5a735bc90801a223cb2d066b3b3dde2af1 Mon Sep 17 00:00:00 2001 From: Diogo Peralta Cordeiro Date: Sun, 13 Feb 2022 22:55:54 +0000 Subject: [PATCH] [PLUGIN][ActivityPub] Fix several issues with target and notifications inserted by AP --- components/Link/Link.php | 9 +- plugins/ActivityPub/Controller/Inbox.php | 6 +- .../Entity/ActivitypubActivity.php | 20 +++++ plugins/ActivityPub/Util/Explorer.php | 2 +- .../ActivityPub/Util/Model/ActivityCreate.php | 40 ++++++--- .../ActivityPub/Util/Model/ActivityFollow.php | 2 +- plugins/ActivityPub/Util/Model/Note.php | 84 +++++++++++-------- src/Core/DB/DB.php | 2 +- src/Util/HTML.php | 1 + 9 files changed, 114 insertions(+), 52 deletions(-) diff --git a/components/Link/Link.php b/components/Link/Link.php index 20c2b2f932..01504274f8 100644 --- a/components/Link/Link.php +++ b/components/Link/Link.php @@ -38,14 +38,17 @@ class Link extends Component /** * Extract URLs from $content and create the appropriate Link and NoteToLink entities */ - public function onProcessNoteContent(Note $note, string $content): bool + public function onProcessNoteContent(Note $note, string $content, string $content_type, array $process_note_content_extra_args = []): bool { + $ignore = $process_note_content_extra_args['ignoreLinks'] ?? []; if (Common::config('attachments', 'process_links')) { $matched_urls = []; - // TODO: This solution to ignore mentions when content is in html is far from ideal - preg_match_all($this->getURLRegex(), preg_replace('##', '', $content), $matched_urls); + preg_match_all($this->getURLRegex(), $content, $matched_urls); $matched_urls = array_unique($matched_urls[1]); foreach ($matched_urls as $match) { + if (in_array($match, $ignore)) { + continue; + } try { $link_id = Entity\Link::getOrCreate($match)->getId(); DB::persist(NoteToLink::create(['link_id' => $link_id, 'note_id' => $note->getId()])); diff --git a/plugins/ActivityPub/Controller/Inbox.php b/plugins/ActivityPub/Controller/Inbox.php index 0b1a2435f5..db78cb909f 100644 --- a/plugins/ActivityPub/Controller/Inbox.php +++ b/plugins/ActivityPub/Controller/Inbox.php @@ -166,7 +166,11 @@ class Inbox extends Controller $ap_actor->getActorId(), Discovery::normalize($actor->getNickname() . '@' . parse_url($ap_actor->getInboxUri(), PHP_URL_HOST)), ); - Event::handle('NewNotification', [$actor, $ap_act->getActivity(), [], _m('{nickname} mentioned you.', ['{nickname}' => $actor->getNickname()])]); + $already_known_ids = []; + if (!empty($ap_act->_object_mention_ids)) { + $already_known_ids = $ap_act->_object_mention_ids; + } + Event::handle('NewNotification', [$actor, $ap_act->getActivity(), $already_known_ids, _m('{nickname} mentioned you.', ['{nickname}' => $actor->getNickname()])]); DB::flush(); dd($ap_act, $act = $ap_act->getActivity(), $act->getActor(), $act->getObject()); diff --git a/plugins/ActivityPub/Entity/ActivitypubActivity.php b/plugins/ActivityPub/Entity/ActivitypubActivity.php index 75b842c129..288e162075 100644 --- a/plugins/ActivityPub/Entity/ActivitypubActivity.php +++ b/plugins/ActivityPub/Entity/ActivitypubActivity.php @@ -104,6 +104,26 @@ class ActivitypubActivity extends Entity return DB::findOneBy('activity', ['id' => $this->getActivityId()]); } + public array $_object_mention_ids = []; + public function setObjectMentionIds(array $mentions): self + { + $this->_object_mention_ids = $mentions; + return $this; + } + + /** + * @see Entity->getNotificationTargetIds + */ + public function getNotificationTargetIds(array $ids_already_known = [], ?int $sender_id = null, bool $include_additional = true): array + { + // Additional actors that should know about this + if (\array_key_exists('additional', $ids_already_known)) { + return $ids_already_known['additional']; + } else { + return $this->_object_mention_ids; + } + } + public static function schemaDef(): array { return [ diff --git a/plugins/ActivityPub/Util/Explorer.php b/plugins/ActivityPub/Util/Explorer.php index 80b0f217b6..19689e1809 100644 --- a/plugins/ActivityPub/Util/Explorer.php +++ b/plugins/ActivityPub/Util/Explorer.php @@ -94,7 +94,7 @@ class Explorer * * @return array of Actor objects */ - public function lookup(string $url, bool $grab_online = true) + public function lookup(string $url, bool $grab_online = true): array { if (\in_array($url, ActivityPub::PUBLIC_TO)) { return []; diff --git a/plugins/ActivityPub/Util/Model/ActivityCreate.php b/plugins/ActivityPub/Util/Model/ActivityCreate.php index 5f9957bdd0..4e5234fc0b 100644 --- a/plugins/ActivityPub/Util/Model/ActivityCreate.php +++ b/plugins/ActivityPub/Util/Model/ActivityCreate.php @@ -1,6 +1,6 @@ get('type') === 'Note') { + $actual_to = array_flip(is_string($type_object->get('to')) ? [$type_object->get('to')] : $type_object->get('to')); + $actual_cc = array_flip(is_string($type_object->get('cc')) ? [$type_object->get('cc')] : $type_object->get('cc')); + foreach (is_string($type_activity->get('to')) ? [$type_activity->get('to')] : $type_activity->get('to') as $to) { + if ($to !== 'https://www.w3.org/ns/activitystreams#Public') { + $actual_to[$to] = true; + } + } + foreach (is_string($type_activity->get('cc')) ? [$type_activity->get('cc')] : $type_activity->get('cc') as $cc) { + if ($cc !== 'https://www.w3.org/ns/activitystreams#Public') { + $actual_cc[$cc] = true; + } + } + $type_object->set('to', array_keys($actual_to)); + $type_object->set('cc', array_keys($actual_cc)); $note = Note::fromJson($type_object, ['test_authority' => true, 'actor_uri' => $type_activity->get('actor'), 'actor' => $actor, 'actor_id' => $actor->getId()]); } else { throw new NotImplementedException('ActivityPub plugin can only handle Create with objects of type Note.'); @@ -59,26 +74,27 @@ class ActivityCreate extends Activity } elseif ($type_object instanceof \App\Entity\Note) { $note = $type_object; } else { - throw new \http\Exception\InvalidArgumentException('Create{:Object} should be either an AbstractObject or a Note.'); + throw new InvalidArgumentException('Create{:Object} should be either an AbstractObject or a Note.'); } // Store Activity $act = GSActivity::create([ - 'actor_id' => $actor->getId(), - 'verb' => 'create', + 'actor_id' => $actor->getId(), + 'verb' => 'create', 'object_type' => 'note', - 'object_id' => $note->getId(), - 'created' => new DateTime($type_activity->get('published') ?? 'now'), - 'source' => 'ActivityPub', + 'object_id' => $note->getId(), + 'created' => new DateTime($type_activity->get('published') ?? 'now'), + 'source' => 'ActivityPub', ]); DB::persist($act); // Store ActivityPub Activity $ap_act = ActivitypubActivity::create([ - 'activity_id' => $act->getId(), + 'activity_id' => $act->getId(), 'activity_uri' => $type_activity->get('id'), - 'created' => new DateTime($type_activity->get('published') ?? 'now'), - 'modified' => new DateTime(), + 'created' => new DateTime($type_activity->get('published') ?? 'now'), + 'modified' => new DateTime(), ]); DB::persist($ap_act); + $ap_act->setObjectMentionIds($note->_object_mentions_ids); return $ap_act; } } diff --git a/plugins/ActivityPub/Util/Model/ActivityFollow.php b/plugins/ActivityPub/Util/Model/ActivityFollow.php index 94368bd056..8c71d869dd 100644 --- a/plugins/ActivityPub/Util/Model/ActivityFollow.php +++ b/plugins/ActivityPub/Util/Model/ActivityFollow.php @@ -42,7 +42,7 @@ use InvalidArgumentException; use Plugin\ActivityPub\Entity\ActivitypubActivity; /** - * This class handles translation between JSON and ActivityPub Activities + * This class handles translation between JSON and ActivityPub Follows * * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later diff --git a/plugins/ActivityPub/Util/Model/Note.php b/plugins/ActivityPub/Util/Model/Note.php index 0ea3dfb367..a294f6c01a 100644 --- a/plugins/ActivityPub/Util/Model/Note.php +++ b/plugins/ActivityPub/Util/Model/Note.php @@ -39,6 +39,8 @@ use App\Core\DB\DB; use App\Core\Event; use App\Core\GSFile; use App\Core\HTTPClient; +use App\Util\HTML; +use Plugin\ActivityPub\Util\Explorer; use function App\Core\I18n\_m; use App\Core\Log; use App\Core\Router\Router; @@ -114,8 +116,10 @@ class Note extends Model $source = $options['source'] ?? 'ActivityPub'; $type_note = \is_string($json) ? self::jsonToType($json) : $json; - $actor = null; $actor_id = null; + $actor = null; + $to = $type_note->has('to') ? (is_string($type_note->get('to')) ? [$type_note->get('to')] : $type_note->get('to')) : []; + $cc = $type_note->has('cc') ? (is_string($type_note->get('cc')) ? [$type_note->get('cc')] : $type_note->get('cc')) : []; if ($json instanceof AbstractObject && \array_key_exists('test_authority', $options) && $options['test_authority'] @@ -140,7 +144,7 @@ class Note extends Model 'is_local' => false, 'created' => new DateTime($type_note->get('published') ?? 'now'), 'content' => $type_note->get('content') ?? null, - 'rendered' => null, + 'rendered' => $type_note->has('content') ? HTML::sanitize($type_note->get('content')) : null, 'content_type' => 'text/html', 'language_id' => $type_note->get('contentLang') ?? null, 'url' => $type_note->get('url') ?? $type_note->get('id'), @@ -149,17 +153,6 @@ class Note extends Model 'modified' => new DateTime(), 'source' => $source, ]; - if ($map['content'] !== null) { - $mentions = []; - Event::handle('RenderNoteContent', [ - $map['content'], - $map['content_type'], - &$map['rendered'], - $actor, - $map['language_id'], - &$mentions, - ]); - } if (!\is_null($map['language_id'])) { $map['language_id'] = Language::getByLocale($map['language_id'])->getId(); @@ -168,10 +161,10 @@ class Note extends Model } // Scope - if (\in_array('https://www.w3.org/ns/activitystreams#Public', $type_note->get('to'))) { + if (\in_array('https://www.w3.org/ns/activitystreams#Public', $to)) { // Public: Visible for all, shown in public feeds $map['scope'] = VisibilityScope::EVERYWHERE; - } elseif (\in_array('https://www.w3.org/ns/activitystreams#Public', $type_note->get('cc'))) { + } elseif (\in_array('https://www.w3.org/ns/activitystreams#Public', $cc)) { // Unlisted: Visible for all but not shown in public feeds // It isn't the note that dictates what feed is shown in but the feed, it only dictates who can access it. $map['scope'] = VisibilityScope::EVERYWHERE; @@ -186,20 +179,30 @@ class Note extends Model } $object_mentions_ids = []; - foreach ([$type_note->get('to'), $type_note->get('cc')] as $target) { - foreach ($target as $to) { - if ($to === 'https://www.w3.org/ns/activitystreams#Public') { - continue; - } - try { - $actor = ActivityPub::getActorByUri($to); - if ($actor->getIsLocal()) { - $object_mentions_ids[] = $actor->getId(); - } - // TODO: If group, set note's scope as Group - } catch (Exception $e) { - Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]); + foreach ($to as $target) { + if ($target === 'https://www.w3.org/ns/activitystreams#Public') { + continue; + } + try { + $actor = ActivityPub::getActorByUri($target); + $object_mentions_ids[$actor->getId()] = $target; + // If $to is a group, set note's scope as Group + if ($actor->isGroup()) { + $map['scope'] = VisibilityScope::GROUP; } + } catch (Exception $e) { + Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]); + } + } + foreach ($cc as $target) { + if ($target === 'https://www.w3.org/ns/activitystreams#Public') { + continue; + } + try { + $actor = ActivityPub::getActorByUri($target); + $object_mentions_ids[$actor->getId()] = $target; + } catch (Exception $e) { + Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]); } } @@ -243,10 +246,20 @@ class Note extends Model foreach ($type_note->get('tag') as $ap_tag) { switch ($ap_tag->get('type')) { case 'Mention': + case 'Group': try { $actor = ActivityPub::getActorByUri($ap_tag->get('href')); - if ($actor->getIsLocal()) { - $object_mentions_ids[] = $actor->getId(); + $object_mentions_ids[$actor->getId()] = $ap_tag->get('href'); + } catch (Exception $e) { + Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]); + } + break; + case 'Collection': + $explorer = new Explorer(); + try { + $actors = $explorer->lookup($ap_tag->get('href')); + foreach($actors as $actor) { + $object_mentions_ids[$actor->getId()] = $ap_tag->get('href'); } } catch (Exception $e) { Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]); @@ -270,9 +283,12 @@ class Note extends Model break; } } - $obj->setObjectMentionsIds(array_unique($object_mentions_ids)); + // The content would be non-sanitized text/html - Event::handle('ProcessNoteContent', [$obj, $obj->getRendered(), $obj->getContentType(), $process_note_content_extra_args = ['TagProcessed' => true]]); + Event::handle('ProcessNoteContent', [$obj, $obj->getRendered(), $obj->getContentType(), $process_note_content_extra_args = ['TagProcessed' => true, 'ignoreLinks' => $object_mentions_ids]]); + + $object_mentions_ids = array_keys($object_mentions_ids); + $obj->setObjectMentionsIds($object_mentions_ids); if ($processed_attachments !== []) { foreach ($processed_attachments as [$a, $fname]) { @@ -338,7 +354,9 @@ class Note extends Model $attr['to'] = []; // Will be filled later $attr['cc'] = []; break; - case VisibilityScope::GROUP: // Will have the group in the To + case VisibilityScope::GROUP: + // Will have the group in the To + // no break case VisibilityScope::COLLECTION: // Since we don't support sending unlisted/followers-only // notices, arriving here means we're instead answering to that type diff --git a/src/Core/DB/DB.php b/src/Core/DB/DB.php index aaaabcd445..d9c9051d3c 100644 --- a/src/Core/DB/DB.php +++ b/src/Core/DB/DB.php @@ -303,7 +303,7 @@ class DB } /** - * Intercept static function calls to allow refering to entities + * Intercept static function calls to allow referring to entities * without writing the namespace (which is deduced from the call * context) */ diff --git a/src/Util/HTML.php b/src/Util/HTML.php index 88dee16add..6807a77dff 100644 --- a/src/Util/HTML.php +++ b/src/Util/HTML.php @@ -36,6 +36,7 @@ use InvalidArgumentException; /** * @mixin SanitizerInterface + * @method static string sanitize(string $html) */ abstract class HTML {