diff --git a/modules/Share/ShareModule.php b/modules/Share/ShareModule.php index 19eeeb95b3..7c41bbbc8b 100644 --- a/modules/Share/ShareModule.php +++ b/modules/Share/ShareModule.php @@ -230,22 +230,31 @@ class ShareModule extends ActivityVerbHandlerModule */ public function onEndShowNoticeOptionItems($nli) { + $notice = $nli->notice; + + // We shouldn't be restricting Shares for received unlisted notices, + // but without subscription_policy working we treat both this type + // and followers-only notices the same, so we also restrict both. + if (!$notice->isPublic()) { + return; + } + // FIXME: Use bitmasks (but be aware that PUBLIC_SCOPE is 0!) // Also: AHHH, $scope and $scoped are scarily similar looking. - $scope = $nli->notice->getScope(); + $scope = $notice->getScope(); if ($scope === Notice::PUBLIC_SCOPE || $scope === Notice::SITE_SCOPE) { $scoped = Profile::current(); if ($scoped instanceof Profile && - $scoped->getID() !== $nli->notice->getProfile()->getID()) { + $scoped->getID() !== $notice->getProfile()->getID()) { - if ($scoped->hasRepeated($nli->notice)) { + if ($scoped->hasRepeated($notice)) { $nli->out->element('span', array('class' => 'repeated', // TRANS: Title for repeat form status in notice list when a notice has been repeated. 'title' => _('Notice repeated.')), // TRANS: Repeat form status in notice list when a notice has been repeated. _('Repeated')); } else { - $repeat = new RepeatForm($nli->out, $nli->notice); + $repeat = new RepeatForm($nli->out, $notice); $repeat->show(); } } diff --git a/plugins/ActivityPub/ActivityPubPlugin.php b/plugins/ActivityPub/ActivityPubPlugin.php index d5afd63116..35d5af8faf 100644 --- a/plugins/ActivityPub/ActivityPubPlugin.php +++ b/plugins/ActivityPub/ActivityPubPlugin.php @@ -256,6 +256,36 @@ class ActivityPubPlugin extends Plugin return true; } + /** + * Update notice before saving. + * We'll use this as a hack to maintain replies to unlisted/followers-only + * notices away from the public timelines. + * + * @param Notice &$notice notice to be saved + * @return bool event hook return + */ + public function onStartNoticeSave(Notice &$notice): bool { + if ($notice->reply_to) { + try { + $parent = $notice->getParent(); + $is_local = (int)$parent->is_local; + + // if we're replying unlisted/followers-only notices received by AP + // or replying to replies of such notices, then we make sure to set + // the correct type flag. + if ( ($parent->source === 'ActivityPub' && $is_local === Notice::GATEWAY) || + ($parent->source === 'web' && $is_local === Notice::LOCAL_NONPUBLIC) ) { + $this->log(LOG_INFO, "Enforcing type flag LOCAL_NONPUBLIC for new notice"); + $notice->is_local = Notice::LOCAL_NONPUBLIC; + } + } catch (NoParentNoticeException $e) { + // This is not a reply to something (has no parent) + } + } + + return true; + } + /** * Plugin Nodeinfo information * diff --git a/plugins/ActivityPub/lib/inbox_handler.php b/plugins/ActivityPub/lib/inbox_handler.php index 41956dd50e..f1e3a231dc 100644 --- a/plugins/ActivityPub/lib/inbox_handler.php +++ b/plugins/ActivityPub/lib/inbox_handler.php @@ -122,25 +122,25 @@ class Activitypub_inbox_handler { switch ($this->activity['type']) { case 'Accept': - $this->handle_accept($this->actor, $this->object); + $this->handle_accept(); break; case 'Create': - $this->handle_create($this->actor, $this->object); + $this->handle_create(); break; case 'Delete': - $this->handle_delete($this->actor, $this->object); + $this->handle_delete(); break; case 'Follow': - $this->handle_follow($this->actor, $this->activity); + $this->handle_follow(); break; case 'Like': - $this->handle_like($this->actor, $this->object); + $this->handle_like(); break; case 'Undo': - $this->handle_undo($this->actor, $this->object); + $this->handle_undo(); break; case 'Announce': - $this->handle_announce($this->actor, $this->object); + $this->handle_announce(); break; } } @@ -148,18 +148,16 @@ class Activitypub_inbox_handler /** * Handles an Accept Activity received by our inbox. * - * @param Profile $actor Actor - * @param array $object Activity * @throws HTTP_Request2_Exception * @throws NoProfileException * @throws ServerException * @author Diogo Cordeiro */ - private function handle_accept($actor, $object) + private function handle_accept() { - switch ($object['type']) { + switch ($this->object['type']) { case 'Follow': - $this->handle_accept_follow($actor, $object); + $this->handle_accept_follow(); break; } } @@ -167,54 +165,63 @@ class Activitypub_inbox_handler /** * Handles an Accept Follow Activity received by our inbox. * - * @param Profile $actor Actor - * @param array $object Activity * @throws HTTP_Request2_Exception * @throws NoProfileException * @throws ServerException * @author Diogo Cordeiro */ - private function handle_accept_follow($actor, $object) + private function handle_accept_follow() { // Get valid Object profile // Note that, since this an accept_follow, the $object // profile is actually the actor that followed someone $object_profile = new Activitypub_explorer; - $object_profile = $object_profile->lookup($object['object'])[0]; + $object_profile = $object_profile->lookup($this->object['object'])[0]; - Activitypub_profile::subscribeCacheUpdate($object_profile, $actor); + Activitypub_profile::subscribeCacheUpdate($object_profile, $this->actor); - $pending_list = new Activitypub_pending_follow_requests($object_profile->getID(), $actor->getID()); + $pending_list = new Activitypub_pending_follow_requests($object_profile->getID(), $this->actor->getID()); $pending_list->remove(); } /** * Handles a Create Activity received by our inbox. * - * @param Profile $actor Actor - * @param array $object Activity * @throws Exception * @author Diogo Cordeiro */ - private function handle_create($actor, $object) + private function handle_create() { - switch ($object['type']) { + switch ($this->object['type']) { case 'Note': - Activitypub_notice::create_notice($object, $actor); + $this->handle_create_note(); break; } } + /** + * Handle a Create Note Activity received by our inbox. + * + * @author Bruno Casteleiro + */ + private function handle_create_note() + { + if (Activitypub_notice::isPrivateNote($this->activity)) { + // Plugin DirectMessage must handle this + } else { + Activitypub_notice::create_notice($this->object, $this->actor); + } + } + /** * Handles a Delete Activity received by our inbox. * - * @param Profile $actor Actor - * @param array|string $object Activity's object * @throws AuthorizationException * @author Diogo Cordeiro */ - private function handle_delete(Profile $actor, $object) + private function handle_delete() { + $object = $this->object; if (is_array($object)) { $object = $object['id']; } @@ -230,15 +237,13 @@ class Activitypub_inbox_handler if (!$deleted) { $notice = ActivityPubPlugin::grab_notice_from_url($object); - $notice->deleteAs($actor); + $notice->deleteAs($this->actor); } } /** * Handles a Follow Activity received by our inbox. * - * @param Profile $actor Actor - * @param array $activity Activity * @throws AlreadyFulfilledException * @throws HTTP_Request2_Exception * @throws NoProfileException @@ -247,100 +252,90 @@ class Activitypub_inbox_handler * @throws \HttpSignatures\Exception * @author Diogo Cordeiro */ - private function handle_follow($actor, $activity) + private function handle_follow() { - Activitypub_follow::follow($actor, $activity['object'], $activity['id']); + Activitypub_follow::follow($this->actor, $this->object, $this->activity['id']); } /** * Handles a Like Activity received by our inbox. * - * @param Profile $actor Actor - * @param array $object Activity * @throws Exception * @author Diogo Cordeiro */ - private function handle_like($actor, $object) + private function handle_like() { - $notice = ActivityPubPlugin::grab_notice_from_url($object); - Fave::addNew($actor, $notice); + $notice = ActivityPubPlugin::grab_notice_from_url($this->object); + Fave::addNew($this->actor, $notice); } /** * Handles a Undo Activity received by our inbox. * - * @param Profile $actor Actor - * @param array $object Activity * @throws AlreadyFulfilledException * @throws HTTP_Request2_Exception * @throws NoProfileException * @throws ServerException * @author Diogo Cordeiro */ - private function handle_undo($actor, $object) + private function handle_undo() { - switch ($object['type']) { + switch ($this->object['type']) { case 'Follow': - $this->handle_undo_follow($actor, $object['object']); + $this->handle_undo_follow(); break; case 'Like': - $this->handle_undo_like($actor, $object['object']); + $this->handle_undo_like(); break; } } - /** - * Handles a Undo Like Activity received by our inbox. - * - * @param Profile $actor Actor - * @param array $object Activity - * @throws AlreadyFulfilledException - * @throws ServerException - * @author Diogo Cordeiro - */ - private function handle_undo_like($actor, $object) - { - $notice = ActivityPubPlugin::grab_notice_from_url($object); - Fave::removeEntry($actor, $notice); - } - /** * Handles a Undo Follow Activity received by our inbox. * - * @author Diogo Cordeiro - * @param Profile $actor Actor - * @param array $object Activity * @throws AlreadyFulfilledException * @throws HTTP_Request2_Exception * @throws NoProfileException * @throws ServerException + * @author Diogo Cordeiro */ - private function handle_undo_follow($actor, $object) + private function handle_undo_follow() { // Get Object profile $object_profile = new Activitypub_explorer; - $object_profile = $object_profile->lookup($object)[0]; + $object_profile = $object_profile->lookup($this->object['object'])[0]; - if (Subscription::exists($actor, $object_profile)) { - Subscription::cancel($actor, $object_profile); + if (Subscription::exists($this->actor, $object_profile)) { + Subscription::cancel($this->actor, $object_profile); // You are no longer following this person. - Activitypub_profile::unsubscribeCacheUpdate($actor, $object_profile); + Activitypub_profile::unsubscribeCacheUpdate($this->actor, $object_profile); } else { // 409: You are not following this person already. } } + /** + * Handles a Undo Like Activity received by our inbox. + * + * @throws AlreadyFulfilledException + * @throws ServerException + * @author Diogo Cordeiro + */ + private function handle_undo_like() + { + $notice = ActivityPubPlugin::grab_notice_from_url($this->object['object']); + Fave::removeEntry($this->actor, $notice); + } + /** * Handles a Announce Activity received by our inbox. * - * @author Diogo Cordeiro - * @param Profile $actor Actor - * @param array $object Activity * @throws Exception + * @author Diogo Cordeiro */ - private function handle_announce($actor, $object) + private function handle_announce() { - $object_notice = ActivityPubPlugin::grab_notice_from_url($object); - $object_notice->repeat($actor, 'ActivityPub'); + $object_notice = ActivityPubPlugin::grab_notice_from_url($this->object); + $object_notice->repeat($this->actor, 'ActivityPub'); } } diff --git a/plugins/ActivityPub/lib/models/Activitypub_create.php b/plugins/ActivityPub/lib/models/Activitypub_create.php index 6ad1c8e9a1..6af6253995 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_create.php +++ b/plugins/ActivityPub/lib/models/Activitypub_create.php @@ -44,16 +44,17 @@ class Activitypub_create * @param array $object * @return array pretty array to be used in a response */ - public static function create_to_array($actor, $object) + public static function create_to_array(string $actor, array $object): array { $res = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $object['id'].'/create', - 'type' => 'Create', - 'to' => $object['to'], - 'cc' => $object['cc'], - 'actor' => $actor, - 'object' => $object + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $object['id'].'/create', + 'type' => 'Create', + 'directMessage' => false, + 'to' => $object['to'], + 'cc' => $object['cc'], + 'actor' => $actor, + 'object' => $object ]; return $res; } @@ -68,11 +69,17 @@ class Activitypub_create public static function validate_object($object) { if (!is_array($object)) { + common_debug('ActivityPub Create Validator: Rejected because of invalid Object format.'); throw new Exception('Invalid Object Format for Create Activity.'); } if (!isset($object['type'])) { + common_debug('ActivityPub Create Validator: Rejected because of Type.'); throw new Exception('Object type was not specified for Create Activity.'); } + if (isset($object['directMessage']) && !is_bool($object['directMessage'])) { + common_debug('ActivityPub Create Validator: Rejected because Object directMessage is invalid.'); + throw new Exception('Invalid Object directMessage.'); + } switch ($object['type']) { case 'Note': // Validate data diff --git a/plugins/ActivityPub/lib/models/Activitypub_notice.php b/plugins/ActivityPub/lib/models/Activitypub_notice.php index 4d7f45f86a..46893be4e5 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_notice.php +++ b/plugins/ActivityPub/lib/models/Activitypub_notice.php @@ -61,13 +61,22 @@ class Activitypub_notice } } - $to = ['https://www.w3.org/ns/activitystreams#Public']; - foreach ($notice->getAttentionProfiles() as $to_profile) { - $to[] = $href = $to_profile->getUri(); - $tags[] = Activitypub_mention_tag::mention_tag_to_array_from_values($href, $to_profile->getNickname().'@'.parse_url($href, PHP_URL_HOST)); + if ($notice->isPublic()) { + $to = ['https://www.w3.org/ns/activitystreams#Public']; + $cc = [common_local_url('apActorFollowers', ['id' => $profile->getID()])]; + } else { + // Since we currently don't support sending unlisted/followers-only + // notices, arriving here means we're instead answering to that type + // of posts. Not having subscription policy working, its safer to + // always send answers of type unlisted. + $to = []; + $cc = ['https://www.w3.org/ns/activitystreams#Public']; } - $cc = [common_local_url('apActorFollowers', ['id' => $profile->getID()])]; + foreach ($notice->getAttentionProfiles() as $to_profile) { + $to[] = $href = $to_profile->getUri(); + $tags[] = Activitypub_mention_tag::mention_tag_to_array_from_values($href, $to_profile->getNickname().'@'.parse_url($href, PHP_URL_HOST)); + } $item = [ '@context' => 'https://www.w3.org/ns/activitystreams', @@ -108,11 +117,11 @@ class Activitypub_notice * * @author Diogo Cordeiro * @param array $object - * @param Profile|null $actor_profile + * @param Profile $actor_profile * @return Notice * @throws Exception */ - public static function create_notice($object, $actor_profile = null) + public static function create_notice(array $object, Profile $actor_profile = null) { $id = $object['id']; // int $url = isset($object['url']) ? $object['url'] : $id; // string @@ -140,7 +149,10 @@ class Activitypub_notice $act->time = time(); $act->actor = $actor_profile->asActivityObject(); $act->context = new ActivityContext(); - $options = ['source' => 'ActivityPub', 'uri' => $id, 'url' => $url]; + $options = ['source' => 'ActivityPub', + 'uri' => $id, + 'url' => $url, + 'is_local' => self::getNotePolicyType($object, $actor_profile)]; // Is this a reply? if (isset($settings['inReplyTo'])) { @@ -238,9 +250,9 @@ class Activitypub_notice common_debug('ActivityPub Notice Validator: Rejected because Object URL is invalid.'); throw new Exception('Invalid Object URL.'); } - if (!(isset($object['to']) || isset($object['cc']))) { - common_debug('ActivityPub Notice Validator: Rejected because neither Object CC and TO were specified.'); - throw new Exception('Neither Object CC and TO were specified.'); + if (!(isset($object['to']) && isset($object['cc']))) { + common_debug('ActivityPub Notice Validator: Rejected because either Object CC or TO wasn\'t specified.'); + throw new Exception('Either Object CC or TO wasn\'t specified.'); } return true; } @@ -253,10 +265,46 @@ class Activitypub_notice * @author Bruno Casteleiro */ public static function getUrl(Notice $notice): string { - if ($notice->isLocal()) { - return common_local_url('apNotice', ['id' => $notice->getID()]); - } else { - return $notice->getUrl(); - } + if ($notice->isLocal()) { + return common_local_url('apNotice', ['id' => $notice->getID()]); + } else { + return $notice->getUrl(); + } + } + + /** + * Extract note policy type from note targets. + * + * @param array $note received Note + * @param Profile $actor_profile Note author + * @return int Notice policy type + * @author Bruno Casteleiro + */ + public static function getNotePolicyType(array $note, Profile $actor_profile): int { + if (in_array('https://www.w3.org/ns/activitystreams#Public', $note['to'])) { + return $actor_profile->isLocal() ? Notice::LOCAL_PUBLIC : Notice::REMOTE; + } else { + // either an unlisted or followers-only note, we'll handle + // both as a GATEWAY notice since this type is not visible + // from the public timelines, hence partially enough while + // we don't have subscription_policy working. + return Notice::GATEWAY; + } + } + + /** + * Verify if received note is private (direct). + * Note that we're conformant with the (yet) non-standard directMessage attribute: + * https://github.com/w3c/activitypub/issues/196#issuecomment-304958984 + * + * @param array $activity received Create-Note activity + * @return bool true if note is private, false otherwise + */ + public static function isPrivateNote(array $activity): bool { + if (isset($activity['directMessage'])) { + return $activity['directMessage']; + } + + return empty($activity['cc']) && !in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to']); } }