diff --git a/classes/Notice.php b/classes/Notice.php index 5c70953cd8..fe5ddf5442 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -231,12 +231,12 @@ class Notice extends Managed_DataObject return $notice; } - /* - * @param $root boolean If true, link to just the conversation root. + /** + * @param bool $anchor If false, link to just the conversation root. * - * @return URL to conversation + * @return string URL to conversation */ - public function getConversationUrl($anchor=true) + public function getConversationUrl(bool $anchor = true): string { return Conversation::getUrlFromNotice($this, $anchor); } diff --git a/plugins/ActivityPub/ActivityPubPlugin.php b/plugins/ActivityPub/ActivityPubPlugin.php index cc0f45c826..10943f2b63 100644 --- a/plugins/ActivityPub/ActivityPubPlugin.php +++ b/plugins/ActivityPub/ActivityPubPlugin.php @@ -132,7 +132,7 @@ class ActivityPubPlugin extends Plugin throw new Exception("The acclaimed actor didn't create this note."); } } else { - throw new Exception("Valid ActivityPub Notice object but unsupported by GNU social."); + throw new Exception("Invalid Note Object. Maybe it's a Tombstone?"); } } diff --git a/plugins/ActivityPub/actions/apnotice.php b/plugins/ActivityPub/actions/apnotice.php index c3b643ac32..9b88c18ae6 100644 --- a/plugins/ActivityPub/actions/apnotice.php +++ b/plugins/ActivityPub/actions/apnotice.php @@ -39,6 +39,96 @@ class apNoticeAction extends ManagedAction protected $needLogin = false; protected $canPost = true; + /** + * Notice id + * @var int + */ + public $notice_id; + + /** + * Notice object to show + */ + public $notice = null; + + /** + * Profile of the notice object + */ + public $profile = null; + + /** + * Avatar of the profile of the notice object + */ + public $avatar = null; + + /** + * Load attributes based on database arguments + * + * Loads all the DB stuff + * + * @param array $args $_REQUEST array + * + * @return bool success flag + */ + protected function prepare(array $args = []): bool + { + parent::prepare($args); + + $this->notice_id = (int)$this->trimmed('id'); + + try { + $this->notice = $this->getNotice(); + } catch (ClientException $e) { + //ActivityPubReturn::error('Activity deleted.', 410); + ActivityPubReturn::answer(Activitypub_tombstone::tombstone_to_array(common_local_url('apNotice', ['id' => $this->notice_id])), 410); + } + $this->target = $this->notice; + + if (!$this->notice->inScope($this->scoped)) { + // TRANS: Client exception thrown when trying a view a notice the user has no access to. + throw new ClientException(_m('Access restricted.'), 403); + } + + $this->profile = $this->notice->getProfile(); + + if (!$this->profile instanceof Profile) { + // TRANS: Server error displayed trying to show a notice without a connected profile. + $this->serverError(_m('Notice has no profile.'), 500); + } + + try { + $this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); + } catch (Exception $e) { + $this->avatar = null; + } + + return true; + } + + /** + * Is this action read-only? + * + * @return bool true + */ + public function isReadOnly($args): bool + { + return true; + } + + /** + * Last-modified date for page + * + * When was the content of this page last modified? Based on notice, + * profile, avatar. + * + * @return int last-modified date as unix timestamp + */ + public function lastModified(): int + { + return max(strtotime($this->notice->modified), + strtotime($this->profile->modified), + ($this->avatar) ? strtotime($this->avatar->modified) : 0); + } + /** * Handle the Notice request * @@ -48,21 +138,45 @@ class apNoticeAction extends ManagedAction * @throws ServerException * @author Diogo Cordeiro */ - protected function handle() + protected function handle(): void { - try { - $notice = Notice::getByID($this->trimmed('id')); - } catch (Exception $e) { + if (is_null($this->notice)) { ActivityPubReturn::error('Invalid Activity URI.', 404); } - - if (!$notice->isLocal()) { + if (!$this->notice->isLocal()) { // We have no authority on the requested activity. ActivityPubReturn::error("This is not a local activity.", 403); } - $res = Activitypub_notice::notice_to_array($notice); + $res = Activitypub_notice::notice_to_array($this->notice); ActivityPubReturn::answer($res); } + + /** + * Fetch the notice to show. This may be overridden by child classes to + * customize what we fetch without duplicating all of the prepare() method. + * + * @return null|Notice null if not found + * @throws ClientException If GONE + */ + protected function getNotice(): ?Notice + { + $notice = null; + try { + $notice = Notice::getByID($this->notice_id); + // Alright, got it! + return $notice; + } catch (NoResultException $e) { + // Hm, not found. + $deleted = null; + Event::handle('IsNoticeDeleted', [$this->notice_id, &$deleted]); + if ($deleted === true) { + // TRANS: Client error displayed trying to show a deleted notice. + throw new ClientException(_m('Notice deleted.'), 410); + } + } + // No such notice. + return null; + } } diff --git a/plugins/ActivityPub/lib/activitypubqueuehandler.php b/plugins/ActivityPub/lib/activitypubqueuehandler.php index 8408916537..b34138fb97 100644 --- a/plugins/ActivityPub/lib/activitypubqueuehandler.php +++ b/plugins/ActivityPub/lib/activitypubqueuehandler.php @@ -204,7 +204,7 @@ class ActivityPubQueueHandler extends QueueHandler * * @param $user * @param $notice - * @return boolean hook flag + * @return bool hook flag * @throws HTTP_Request2_Exception * @throws InvalidUrlException * @author Diogo Cordeiro @@ -218,10 +218,6 @@ class ActivityPubQueueHandler extends QueueHandler return true; } - $other = Activitypub_profile::from_profile_collection( - $notice->getAttentionProfiles() - ); - if ($notice->reply_to) { try { $parent_notice = $notice->getParent(); diff --git a/plugins/ActivityPub/lib/inbox_handler.php b/plugins/ActivityPub/lib/inbox_handler.php index 9fb466d6c3..215bc6c187 100644 --- a/plugins/ActivityPub/lib/inbox_handler.php +++ b/plugins/ActivityPub/lib/inbox_handler.php @@ -229,8 +229,6 @@ class Activitypub_inbox_handler /** * Handles a Delete Activity received by our inbox. * - * @throws NoProfileException - * @throws Exception * @author Bruno Casteleiro * @author Diogo Cordeiro */ @@ -240,8 +238,8 @@ class Activitypub_inbox_handler if (is_string($object)) { $client = new HTTPClient(); $response = $client->get($object, ACTIVITYPUB_HTTP_CLIENT_HEADERS); - $gone = !$response->isOk(); - if (!$gone) { // It's not gone, we're updating it. + $not_gone = $response->isOk(); + if ($not_gone) { // It's not gone, we're updating it. $object = json_decode($response->getBody(), true); switch ($object['type']) { case 'Person': @@ -254,57 +252,57 @@ class Activitypub_inbox_handler Activitypub_explorer::get_profile_from_url($object['id']); } break; - case 'Tombstone': + case 'Note': // XXX: We do not support updating a note's contents so, we'll delete and re-fetch for now... try { - $notice = ActivityPubPlugin::grab_notice_from_url($object, false); + $notice = ActivityPubPlugin::grab_notice_from_url($object['id'], false); if ($notice instanceof Notice) { $notice->delete(); } + ActivityPubPlugin::grab_notice_from_url($object['id'], true); return; } catch (Exception $e) { // either already deleted or not an object at all // nothing to do.. } break; - case 'Note': - // XXX: We do not support updating a note's contents so, we'll ignore it for now... default: common_log(LOG_INFO, "Ignoring Delete activity, we do not understand for {$object['type']}."); } } - } else { - // We don't know the type of the deleted object :( - // Nor if it's gone or not. - try { - if (is_array($object)) { - $object = $object['id']; - } - $aprofile = Activitypub_profile::fromUri($object, false); - $res = Activitypub_explorer::get_remote_user_activity($object); - Activitypub_profile::update_profile($aprofile, $res); - return; - } catch (Exception $e) { - // Means this wasn't a profile - } + } - try { - $client = new HTTPClient(); - $response = $client->get($object, ACTIVITYPUB_HTTP_CLIENT_HEADERS); - // If it was deleted - if ($response->getStatus() == 410) { - $notice = ActivityPubPlugin::grab_notice_from_url($object, false); - if ($notice instanceof Notice) { - $notice->delete(); - } - } else { - // We can't update a note's contents so, we'll ignore it for now... - } - return; - } catch (Exception $e) { - // Means we didn't have this note already - } + // IFF we reached this point, it either is gone or it's an array + // If it's gone, we don't know the type of the deleted object, we only have a Tombstone + // If we were given an array, we don't know if it's Gone or not via status code... + // In both cases, we will want to fetch the ID and act on that as it is easier than updating the fields + // Was it a profile? + try { + $object = $object['id']; + $aprofile = Activitypub_profile::fromUri($object, false); + $res = Activitypub_explorer::get_remote_user_activity($object); + Activitypub_profile::update_profile($aprofile, $res); return; + } catch (Exception $e) { + // Means this wasn't a profile + } + + // Was it a note? + try { + $client = new HTTPClient(); + /*$response =*/ $client->get($object, ACTIVITYPUB_HTTP_CLIENT_HEADERS); + // If it was deleted + //if (!$response->isOk()) { // 410 or 404 + $notice = ActivityPubPlugin::grab_notice_from_url($object, false); + if ($notice instanceof Notice) { + $notice->delete(); + } + // } else + ActivityPubPlugin::grab_notice_from_url($object, true); + // XXX: We do not support updating a note's contents so, we'll delete and re-fetch for now... + } catch (Exception $e) { + // Means we didn't have this note already + // Or we had, deleted and it exploded trying to fetch the Tombstone, either way, we're good. } } diff --git a/plugins/ActivityPub/lib/models/Activitypub_delete.php b/plugins/ActivityPub/lib/models/Activitypub_delete.php index 439bd953db..0d19fcc6fa 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_delete.php +++ b/plugins/ActivityPub/lib/models/Activitypub_delete.php @@ -39,22 +39,13 @@ class Activitypub_delete /** * Generates an ActivityPub representation of a Delete * - * @param string $actor actor URI - * @param string $object object URI + * @param Notice $notice * @return array pretty array to be used in a response * @author Diogo Cordeiro */ - public static function delete_to_array(string $actor, string $object): array + public static function delete_to_array(Notice $notice): array { - $res = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $object . '#delete', - 'type' => 'Delete', - 'to' => ['https://www.w3.org/ns/activitystreams#Public'], - 'actor' => $actor, - 'object' => $object - ]; - return $res; + return Activitypub_notice::notice_to_array($notice); } /** diff --git a/plugins/ActivityPub/lib/models/Activitypub_error.php b/plugins/ActivityPub/lib/models/Activitypub_error.php index 1570ebdf3b..aa182029d1 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_error.php +++ b/plugins/ActivityPub/lib/models/Activitypub_error.php @@ -43,11 +43,10 @@ class Activitypub_error * @param string $m * @return array pretty array to be used in a response */ - public static function error_message_to_array($m) + public static function error_message_to_array(string $m): array { - $res = [ + return [ 'error'=> $m ]; - return $res; } } diff --git a/plugins/ActivityPub/lib/models/Activitypub_like.php b/plugins/ActivityPub/lib/models/Activitypub_like.php index a6e9864532..147025b29c 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_like.php +++ b/plugins/ActivityPub/lib/models/Activitypub_like.php @@ -27,7 +27,7 @@ defined('GNUSOCIAL') || die(); /** - * ActivityPub error representation + * ActivityPub Like representation * * @category Plugin * @package GNUsocial diff --git a/plugins/ActivityPub/lib/models/Activitypub_notice.php b/plugins/ActivityPub/lib/models/Activitypub_notice.php index cc1312b292..96431f30ca 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_notice.php +++ b/plugins/ActivityPub/lib/models/Activitypub_notice.php @@ -79,21 +79,42 @@ class Activitypub_notice $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', - 'id' => self::getUri($notice), - 'type' => 'Note', - 'published' => str_replace(' ', 'T', $notice->getCreated()) . 'Z', - 'url' => $notice->getUrl(), - 'attributedTo' => $profile->getUri(), - 'to' => $to, - 'cc' => $cc, - 'conversation' => $notice->getConversationUrl(), - 'content' => $notice->getRendered(), - 'isLocal' => $notice->isLocal(), - 'attachment' => $attachments, - 'tag' => $tags - ]; + if (ActivityUtils::compareVerbs($notice->getVerb(), ActivityVerb::DELETE)) { + $item = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => self::getUri($notice), + 'type' => 'Delete', + // XXX: A bit of ugly code here + 'object' => array_merge(Activitypub_tombstone::tombstone_to_array(common_local_url('apNotice', ['id' => (int)substr(explode(':', $notice->getUri())[2],9)])), ['deleted' => str_replace(' ', 'T', $notice->getCreated()) . 'Z']), + 'url' => $notice->getUrl(), + 'actor' => $profile->getUri(), + 'to' => $to, + 'cc' => $cc, + 'conversationId' => $notice->getConversationUrl(false), + 'conversationUrl' => $notice->getConversationUrl(), + 'content' => $notice->getRendered(), + 'isLocal' => $notice->isLocal(), + 'attachment' => $attachments, + 'tag' => $tags + ]; + } else { // Note + $item = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => self::getUri($notice), + 'type' => 'Note', + 'published' => str_replace(' ', 'T', $notice->getCreated()) . 'Z', + 'url' => $notice->getUrl(), + 'attributedTo' => $profile->getUri(), + 'to' => $to, + 'cc' => $cc, + 'conversationId' => $notice->getConversationUrl(false), + 'conversationUrl' => $notice->getConversationUrl(), + 'content' => $notice->getRendered(), + 'isLocal' => $notice->isLocal(), + 'attachment' => $attachments, + 'tag' => $tags + ]; + } // Is this a reply? if (!empty($notice->reply_to)) { diff --git a/plugins/ActivityPub/lib/models/Activitypub_tombstone.php b/plugins/ActivityPub/lib/models/Activitypub_tombstone.php new file mode 100644 index 0000000000..39f9176081 --- /dev/null +++ b/plugins/ActivityPub/lib/models/Activitypub_tombstone.php @@ -0,0 +1,55 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * @link http://www.gnu.org/software/social/ + */ + +defined('GNUSOCIAL') || die(); + +/** + * ActivityPub Tombstone representation + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_tombstone +{ + /** + * Generates an ActivityPub representation of a Tombstone + * + * @param string $id Activity id + * @return array pretty array to be used in a response + * @author Diogo Cordeiro + */ + public static function tombstone_to_array(string $id): array + { + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $id, + 'type' => 'Tombstone' + ]; + return $res; + } +} \ No newline at end of file diff --git a/plugins/ActivityPub/lib/postman.php b/plugins/ActivityPub/lib/postman.php index db82e3f1f6..1cd8dec12f 100644 --- a/plugins/ActivityPub/lib/postman.php +++ b/plugins/ActivityPub/lib/postman.php @@ -365,10 +365,7 @@ class Activitypub_postman */ public function delete_note($notice) { - $data = Activitypub_delete::delete_to_array( - $notice->getProfile()->getUri(), - Activitypub_notice::getUri($notice) - ); + $data = Activitypub_delete::delete_to_array($notice); $errors = []; $data = json_encode($data, JSON_UNESCAPED_SLASHES); foreach ($this->to_inbox() as $inbox) {