From edb3633bcd77aa0b74e8872d134bdcacccc3241b Mon Sep 17 00:00:00 2001 From: Diogo Cordeiro Date: Sun, 29 Jul 2018 02:35:04 +0100 Subject: [PATCH] Proper AP Notices (fixed #46) Explorer now works both for local and remote URIs Fixed various serious errors (most of them in Explorer and some in AP Profiles) --- ActivityPubPlugin.php | 136 ++++++++++++++------------ actions/apactorfollowers.php | 6 +- actions/apactorfollowing.php | 6 +- actions/apactorinbox.php | 4 + actions/apactorliked.php | 4 + actions/inbox/Announce.php | 2 +- actions/inbox/Create.php | 53 +++++++--- actions/inbox/Delete.php | 2 +- actions/inbox/Like.php | 9 +- actions/inbox/Undo.php | 4 +- classes/Activitypub_notice.php | 31 +++--- classes/Activitypub_profile.php | 46 +++++++-- tests/Unit/ActivitypubProfileTest.php | 30 ++++-- tests/Unit/HTTPSignatureTest.php | 4 +- utils/explorer.php | 64 ++++++++++-- utils/postman.php | 6 +- 16 files changed, 278 insertions(+), 129 deletions(-) diff --git a/ActivityPubPlugin.php b/ActivityPubPlugin.php index 7ff68e4..a6f2f4d 100755 --- a/ActivityPubPlugin.php +++ b/ActivityPubPlugin.php @@ -34,6 +34,9 @@ require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "di require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php"; require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "postman.php"; +// So that this isn't hardcoded everywhere +define('ACTIVITYPUB_BASE_INSTANCE_URI', common_root_url()."index.php/user/"); + /** * @category Plugin * @package GNUsocial @@ -54,7 +57,7 @@ class ActivityPubPlugin extends Plugin public static function actor_uri($profile) { if ($profile->isLocal()) { - return common_root_url()."index.php/user/".$profile->getID(); + return ACTIVITYPUB_BASE_INSTANCE_URI.$profile->getID(); } else { return $profile->getUri(); } @@ -74,46 +77,30 @@ class ActivityPubPlugin extends Plugin } /** - * Get remote user's ActivityPub_profile via a identifier + * Returns a notice from its URL since GNU Social doesn't provide + * this functionality * - * @author GNU Social * @author Diogo Cordeiro - * @param string $arg A remote user identifier - * @return Activitypub_profile|null Valid profile in success | null otherwise + * @param string $url Notice's URL + * @return Notice The Notice object + * @throws Exception This function or provides a Notice or fails with exception */ - public static function pull_remote_profile($arg) + public static function get_local_notice_from_url($url) { - if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) { - // webfinger lookup + try { + return Notice::getByUri($data->object->inReplyTo); + } catch (Exception $e) { try { - return Activitypub_profile::ensure_web_finger($arg); + $candidate = Notice::getByID(intval(substr($url, strlen(common_local_url('shownotice', ['notice' => '']))))); + if ($candidate->getUrl() == $url) { + return $candidate; + } else { + throw new Exception("Notice not found."); + } } catch (Exception $e) { - common_log(LOG_ERR, 'Webfinger lookup failed for ' . - $arg . ': ' . $e->getMessage()); + throw new Exception("Notice not found."); } } - - // Look for profile URLs, with or without scheme: - $urls = array(); - if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) { - $urls[] = $arg; - } - if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) { - $schemes = array('http', 'https'); - foreach ($schemes as $scheme) { - $urls[] = "$scheme://$arg"; - } - } - - foreach ($urls as $url) { - try { - return Activitypub_profile::get_from_uri($url); - } catch (Exception $e) { - common_log(LOG_ERR, 'Profile lookup failed for ' . - $arg . ': ' . $e->getMessage()); - } - } - return null; } /** @@ -127,18 +114,18 @@ class ActivityPubPlugin extends Plugin ActivityPubURLMapperOverwrite::overwrite_variable( $m, 'user/:id', - ['action' => 'showstream'], - ['id' => '[0-9]+'], - 'apActorProfile' - ); + ['action' => 'showstream'], + ['id' => '[0-9]+'], + 'apActorProfile' + ); // Special route for webfinger purposes ActivityPubURLMapperOverwrite::overwrite_variable( $m, ':nickname', - ['action' => 'showstream'], - ['nickname' => Nickname::DISPLAY_FMT], - 'apActorProfile' + ['action' => 'showstream'], + ['nickname' => Nickname::DISPLAY_FMT], + 'apActorProfile' ); $m->connect( @@ -234,6 +221,49 @@ class ActivityPubPlugin extends Plugin * WebFinger Events * ********************************************************/ + /** + * Get remote user's ActivityPub_profile via a identifier + * + * @author GNU Social + * @author Diogo Cordeiro + * @param string $arg A remote user identifier + * @return Activitypub_profile|null Valid profile in success | null otherwise + */ + public static function pull_remote_profile($arg) + { + if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) { + // webfinger lookup + try { + return Activitypub_profile::ensure_web_finger($arg); + } catch (Exception $e) { + common_log(LOG_ERR, 'Webfinger lookup failed for ' . + $arg . ': ' . $e->getMessage()); + } + } + + // Look for profile URLs, with or without scheme: + $urls = array(); + if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) { + $urls[] = $arg; + } + if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) { + $schemes = array('http', 'https'); + foreach ($schemes as $scheme) { + $urls[] = "$scheme://$arg"; + } + } + + foreach ($urls as $url) { + try { + return Activitypub_profile::fromUri($url); + } catch (Exception $e) { + common_log(LOG_ERR, 'Profile lookup failed for ' . + $arg . ': ' . $e->getMessage()); + } + } + return null; + } + /** * Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz * @@ -357,7 +387,7 @@ class ActivityPubPlugin extends Plugin $url = "$scheme://$target"; $this->log(LOG_INFO, "Checking profile address '$url'"); try { - $aprofile = Activitypub_profile::get_from_uri($url); + $aprofile = Activitypub_profile::fromUri($url); $profile = $aprofile->local_profile(); $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target) ? $profile->nickname : $target; @@ -432,7 +462,7 @@ class ActivityPubPlugin extends Plugin { $aprofile = Activitypub_profile::getKV('profile_id', $profile->id); if ($aprofile instanceof Activitypub_profile) { - $uri = $aprofile->get_uri(); + $uri = $aprofile->getUri(); return false; } return true; @@ -449,27 +479,9 @@ class ActivityPubPlugin extends Plugin */ public function onStartGetProfileFromURI($uri, &$profile) { - // Don't want to do Web-based discovery on our own server, - // so we check locally first. This duplicates the functionality - // in the Profile class, since the plugin always runs before - // that local lookup, but since we return false it won't run double. - - $user = User::getKV('uri', $uri); - if ($user instanceof User) { - $profile = $user->getProfile(); - return false; - } else { - $group = User_group::getKV('uri', $uri); - if ($group instanceof User_group) { - $profile = $group->getProfile(); - return false; - } - } - - // Now, check remotely try { - $aprofile = Activitypub_profile::get_from_uri($uri); - $profile = $aprofile->local_profile(); + $explorer = new Activitypub_explorer(); + $profile = $explorer->lookup($uri)[0]; return false; } catch (Exception $e) { return true; // It's not an ActivityPub profile as far as we know, continue event handling diff --git a/actions/apactorfollowers.php b/actions/apactorfollowers.php index 4594232..b1cac40 100755 --- a/actions/apactorfollowers.php +++ b/actions/apactorfollowers.php @@ -58,6 +58,10 @@ class apActorFollowersAction extends ManagedAction ActivityPubReturn::error('Invalid Actor URI.', 404); } + if (!$profile->isLocal()) { + ActivityPubReturn::error("This is not a local user."); + } + if (!isset($_GET["page"])) { $page = 1; } else { @@ -93,7 +97,7 @@ class apActorFollowersAction extends ManagedAction /* Get followers' URLs */ $subs = array(); while ($sub->fetch()) { - $subs[] = $sub->profileurl; + $subs[] = ActivityPubPlugin::actor_uri($sub); } $res = [ diff --git a/actions/apactorfollowing.php b/actions/apactorfollowing.php index 098e13b..30777fc 100755 --- a/actions/apactorfollowing.php +++ b/actions/apactorfollowing.php @@ -58,6 +58,10 @@ class apActorFollowingAction extends ManagedAction ActivityPubReturn::error('Invalid Actor URI.', 404); } + if (!$profile->isLocal()) { + ActivityPubReturn::error("This is not a local user."); + } + if (!isset($_GET["page"])) { $page = 1; } else { @@ -93,7 +97,7 @@ class apActorFollowingAction extends ManagedAction /* Get followed' URLs */ $subs = array(); while ($sub->fetch()) { - $subs[] = $sub->profileurl; + $subs[] = ActivityPubPlugin::actor_uri($sub); } $res = [ diff --git a/actions/apactorinbox.php b/actions/apactorinbox.php index 0c362c1..fadd177 100755 --- a/actions/apactorinbox.php +++ b/actions/apactorinbox.php @@ -57,6 +57,10 @@ class apActorInboxAction extends ManagedAction ActivityPubReturn::error('Invalid Actor URI.', 404); } + if (!$profile->isLocal()) { + ActivityPubReturn::error("This is not a local user."); + } + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { ActivityPubReturn::error("C2S not implemented just yet."); } diff --git a/actions/apactorliked.php b/actions/apactorliked.php index 9a77c92..436baf9 100755 --- a/actions/apactorliked.php +++ b/actions/apactorliked.php @@ -58,6 +58,10 @@ class apActorLikedAction extends ManagedAction ActivityPubReturn::error('Invalid Actor URI.', 404); } + if (!$profile->isLocal()) { + ActivityPubReturn::error("This is not a local user."); + } + $limit = intval($this->trimmed('limit')); $since_id = intval($this->trimmed('since_id')); $max_id = intval($this->trimmed('max_id')); diff --git a/actions/inbox/Announce.php b/actions/inbox/Announce.php index 7169be5..a3b9b65 100755 --- a/actions/inbox/Announce.php +++ b/actions/inbox/Announce.php @@ -30,7 +30,7 @@ if (!defined('GNUSOCIAL')) { } try { - Notice::getByUri($data->object->id)->repeat($actor_profile, "ActivityPub"); + ActivityPubPlugin::get_local_notice_from_url($data->object->id)->repeat($actor_profile, "ActivityPub"); ActivityPubReturn::answer("Notice repeated successfully."); } catch (Exception $e) { ActivityPubReturn::error($e->getMessage(), 403); diff --git a/actions/inbox/Create.php b/actions/inbox/Create.php index f99ec90..2a89553 100755 --- a/actions/inbox/Create.php +++ b/actions/inbox/Create.php @@ -32,8 +32,10 @@ if (!defined('GNUSOCIAL')) { $valid_object_types = array("Note"); // Validate data -if (!isset($data->id)) { - ActivityPubReturn::error("Id not specified."); +if (!isset($data->object->id)) { + ActivityPubReturn::error("Object ID not specified."); +} elseif (!filter_var($data->object->id, FILTER_VALIDATE_URL)) { + ActivityPubReturn::error("Invalid Object ID."); } if (!(isset($data->object->type) && in_array($data->object->type, $valid_object_types))) { ActivityPubReturn::error("Invalid Object type."); @@ -60,19 +62,19 @@ $act->actor = $actor_profile->asActivityObject(); $act->context = new ActivityContext(); // Is this a reply? -if (isset($data->object->reply_to)) { +if (isset($data->object->inReplyTo)) { try { - $reply_to = Notice::getByUri($data->object->reply_to); + $inReplyTo = ActivityPubPlugin::get_local_notice_from_url($data->object->inReplyTo); } catch (Exception $e) { - ActivityPubReturn::error("Invalid Object reply_to value."); + ActivityPubReturn::error("Invalid Object inReplyTo value."); } - $act->context->replyToID = $reply_to->getUri(); - $act->context->replyToUrl = $reply_to->getUrl(); + $act->context->replyToID = $inReplyTo->getUri(); + $act->context->replyToUrl = $inReplyTo->getUrl(); } else { - $reply_to = null; + $inReplyTo = null; } -$act->context->attention = common_get_attentions($content, $actor_profile, $reply_to); +$act->context->attention = common_get_attentions($content, $actor_profile, $inReplyTo); $discovery = new Activitypub_explorer; if ($to_profiles == "https://www.w3.org/ns/activitystreams#Public") { @@ -86,22 +88,43 @@ if (is_array($data->object->to)) { try { $to_profiles = array_merge($to_profiles, $discovery->lookup($to_url)); } catch (Exception $e) { - // XXX: Invalid actor found, not sure how we handle those + // Invalid actor found, just let it go. } } } elseif (empty($data->object->to) || in_array($data->object->to, $public_to)) { // No need to do anything else at this point, let's just break out the if } else { try { - $to_profiles[]= $discovery->lookup($data->object->to); + $to_profiles = array_merge($to_profiles, $discovery->lookup($data->object->to)); } catch (Exception $e) { - ActivityPubReturn::error("Invalid Actor.", 404); + // Invalid actor found, just let it go. } } +// Generate Cc objects +if (isset($data->object->cc) && is_array($data->object->cc)) { + // Remove duplicates from Cc actors set + array_unique($data->object->to); + foreach ($data->object->cc as $cc_url) { + try { + $to_profiles = array_merge($to_profiles, $discovery->lookup($cc_url)); + } catch (Exception $e) { + // Invalid actor found, just let it go. + } + } +} elseif (empty($data->object->cc) || in_array($data->object->cc, $public_to)) { + // No need to do anything else at this point, let's just break out the if +} else { + try { + $to_profiles = array_merge($to_profiles, $discovery->lookup($data->object->cc)); + } catch (Exception $e) { + // Invalid actor found, just let it go. + } +} + unset($discovery); -foreach ($to_profiles as $to) { - $act->context->attention[ActivityPubPlugin::actor_uri($to)] = "http://activitystrea.ms/schema/1.0/person"; +foreach ($to_profiles as $tp) { + $act->context->attention[ActivityPubPlugin::actor_uri($tp)] = "http://activitystrea.ms/schema/1.0/person"; } // Reject notice if it is too long (without the HTML) @@ -116,7 +139,7 @@ ToSelector::fillActivity($this, $act, $options); $actobj = new ActivityObject(); $actobj->type = ActivityObject::NOTE; -$actobj->content = common_render_content($content, $actor_profile, $reply_to); +$actobj->content = common_render_content($content, $actor_profile, $inReplyTo); // Finally add the activity object to our activity $act->objects[] = $actobj; diff --git a/actions/inbox/Delete.php b/actions/inbox/Delete.php index 71e979c..35fff06 100755 --- a/actions/inbox/Delete.php +++ b/actions/inbox/Delete.php @@ -30,7 +30,7 @@ if (!defined('GNUSOCIAL')) { } try { - $notice = Notice::getByUri($data->object->id); + $notice = ActivityPubPlugin::get_local_notice_from_url($data->object->id); $notice_to_array = Activitypub_notice::notice_to_array($notice); $notice->deleteAs($actor_profile); ActivityPubReturn::answer(Activitypub_delete::delete_to_array($notice_to_array)); diff --git a/actions/inbox/Like.php b/actions/inbox/Like.php index ecd5b27..7fc09d4 100755 --- a/actions/inbox/Like.php +++ b/actions/inbox/Like.php @@ -34,8 +34,13 @@ if (!isset($data->object->id)) { } try { - Fave::addNew($actor_profile, Notice::getByUri($data->object->id)); - ActivityPubReturn::answer(Activitypub_like::like_to_array(Activitypub_notice::notice_to_array($data->actor, json_decode($data->object)))); + try { + $object_notice = ActivityPubPlugin::get_local_notice_from_url($data->object->id); + } catch (Exception $e) { + ActivityPubReturn::error("Invalid Object ID value."); + } + Fave::addNew($actor_profile, $object_notice); + ActivityPubReturn::answer(Activitypub_like::like_to_array($data->actor, Activitypub_notice::notice_to_array($object_notice))); } catch (Exception $e) { ActivityPubReturn::error($e->getMessage(), 403); } diff --git a/actions/inbox/Undo.php b/actions/inbox/Undo.php index 91b4970..68c651a 100755 --- a/actions/inbox/Undo.php +++ b/actions/inbox/Undo.php @@ -41,14 +41,14 @@ case "Like": if (!isset($data->object->object->id)) { ActivityPubReturn::error("Notice ID was not specified."); } - Fave::removeEntry($actor_profile, Notice::getByUri($data->object->object->id)); + Fave::removeEntry($actor_profile, ActivityPubPlugin::get_local_notice_from_url($data->object->object->id)); // Notice disfavorited successfully. ActivityPubReturn::answer( Activitypub_undo::undo_to_array( Activitypub_like::like_to_array( Activitypub_notice::notice_to_array( $actor_profile->getUrl(), - $data->object->object + $data->object->object ) ) ) diff --git a/classes/Activitypub_notice.php b/classes/Activitypub_notice.php index a474360..d90fda7 100755 --- a/classes/Activitypub_notice.php +++ b/classes/Activitypub_notice.php @@ -34,7 +34,6 @@ if (!defined('GNUSOCIAL')) { * * @category Plugin * @package GNUsocial - * @author Daniel Supernault * @author Diogo Cordeiro * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://www.gnu.org/software/social/ @@ -44,13 +43,13 @@ class Activitypub_notice extends Managed_DataObject /** * Generates a pretty notice from a Notice object * - * @author Daniel Supernault * @author Diogo Cordeiro * @param Notice $notice * @return pretty array to be used in a response */ public static function notice_to_array($notice) { + $profile = $notice->getProfile(); $attachments = array(); foreach ($notice->attachments() as $attachment) { $attachments[] = Activitypub_attachment::attachment_to_array($attachment); @@ -63,7 +62,7 @@ class Activitypub_notice extends Managed_DataObject } } - $to = array(); + $to = []; foreach ($notice->getAttentionProfiles() as $to_profile) { $to[] = $to_profile->getUri(); } @@ -72,18 +71,20 @@ class Activitypub_notice extends Managed_DataObject } $item = [ - 'id' => $notice->getUri(), - 'type' => 'Note', - 'actor' => $notice->getProfile()->getUrl(), - 'published' => $notice->getCreated(), - 'to' => $to, - 'content' => $notice->getContent(), - 'url' => $notice->getUrl(), - 'reply_to' => empty($notice->reply_to) ? null : Notice::getById($notice->reply_to)->getUri(), - 'is_local' => $notice->isLocal(), - 'conversation' => $notice->getConversationUrl(), - 'attachment' => $attachments, - 'tag' => $tags + 'id' => $notice->getUrl(), + 'type' => 'Note', + 'inReplyTo' => empty($notice->reply_to) ? null : Notice::getById($notice->reply_to)->getUrl(), + 'published' => $notice->getCreated(), + 'url' => $notice->getUrl(), + 'atributedTo' => ActivityPubPlugin::actor_uri($profile), + 'to' => $to, + 'atomUri' => $notice->getUrl(), + 'inReplyToAtomUri' => empty($notice->reply_to) ? null : Notice::getById($notice->reply_to)->getUrl(), + 'conversation' => $notice->getConversationUrl(), + 'content' => $notice->getContent(), + 'is_local' => $notice->isLocal(), + 'attachment' => $attachments, + 'tag' => $tags ]; return $item; diff --git a/classes/Activitypub_profile.php b/classes/Activitypub_profile.php index 9180862..6eedc6f 100755 --- a/classes/Activitypub_profile.php +++ b/classes/Activitypub_profile.php @@ -41,6 +41,18 @@ if (!defined('GNUSOCIAL')) { class Activitypub_profile extends Managed_DataObject { public $__table = 'Activitypub_profile'; + public $uri; // text() not_null + public $profile_id; // int(4) primary_key not_null + public $inboxuri; // text() not_null + public $sharedInboxuri; // text() + public $nickname; // varchar(64) multiple_key not_null + public $fullname; // text() + public $profileurl; // text() + public $homepage; // text() + public $bio; // text() multiple_key + public $location; // text() + public $created; // datetime() not_null + public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP /** * Return table definition for Schema setup and DB_DataObject usage. @@ -108,7 +120,7 @@ class Activitypub_profile extends Managed_DataObject 'liked' => common_local_url("apActorLiked", array("id" => $id)), 'inbox' => common_local_url("apActorInbox", array("id" => $id)), 'preferredUsername' => $profile->getNickname(), - 'name' => $profile->getFullname(), + 'name' => $profile->getBestName(), 'summary' => ($desc = $profile->getDescription()) == null ? "" : $desc, 'url' => $profile->getUrl(), 'manuallyApprovesFollowers' => false, @@ -280,11 +292,33 @@ class Activitypub_profile extends Managed_DataObject * @author Diogo Cordeiro * @return string URI */ - public function get_uri() + public function getUri() { return $this->uri; } + /** + * Getter for url property + * + * @author Diogo Cordeiro + * @return string URL + */ + public function getUrl() + { + return $this->getUri(); + } + + /** + * Getter for id property + * + * @author Diogo Cordeiro + * @return int32 + */ + public function getID() + { + return $this->profile_id; + } + /** * Ensures a valid Activitypub_profile when provided with a valid URI. * @@ -293,7 +327,7 @@ class Activitypub_profile extends Managed_DataObject * @return Activitypub_profile * @throws Exception if it isn't possible to return an Activitypub_profile */ - public static function get_from_uri($url) + public static function fromUri($url) { $explorer = new Activitypub_explorer(); $profiles_found = $explorer->lookup($url); @@ -331,7 +365,7 @@ class Activitypub_profile extends Managed_DataObject throw new Exception(_m('Not a valid webfinger address (via cache).')); } try { - return self::get_from_uri($uri); + return self::fromUri($uri); } catch (Exception $e) { common_log(LOG_ERR, sprintf(__METHOD__ . ': Webfinger address cache inconsistent with database, did not find Activitypub_profile uri==%s', $uri)); self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), false); @@ -372,8 +406,8 @@ class Activitypub_profile extends Managed_DataObject $profileUrl = $hints['profileurl']; try { common_log(LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl"); - $aprofile = self::get_from_uri($hints['profileurl']); - self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), $aprofile->get_uri()); + $aprofile = self::fromUri($hints['profileurl']); + self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), $aprofile->getUri()); return $aprofile; } catch (Exception $e) { common_log(LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage()); diff --git a/tests/Unit/ActivitypubProfileTest.php b/tests/Unit/ActivitypubProfileTest.php index 341bb1b..5a8f8ae 100755 --- a/tests/Unit/ActivitypubProfileTest.php +++ b/tests/Unit/ActivitypubProfileTest.php @@ -49,11 +49,17 @@ class ProfileObjectTest extends TestCase /* Test get_inbox() */ $this->assertTrue($aprofile->sharedInboxuri == $aprofile->get_inbox()); - /* Test get_uri() */ - $this->assertTrue($aprofile->uri == $aprofile->get_uri()); + /* Test getUri() */ + $this->assertTrue($aprofile->uri == $aprofile->getUri()); - /* Test get_from_uri() */ - $this->assertTrue($this->compare_aprofiles($aprofile, \Activitypub_profile::get_from_uri($aprofile->uri))); + /* Test getUrl() */ + $this->assertTrue($profile->getUrl() == $aprofile->getUrl()); + + /* Test getID() */ + $this->assertTrue($profile->getID() == $aprofile->getID()); + + /* Test fromUri() */ + $this->assertTrue($this->compare_aprofiles($aprofile, \Activitypub_profile::fromUri($aprofile->uri))); /* Remove Remote User Test 1 */ $old_id = $profile->getID(); @@ -124,20 +130,28 @@ class ProfileObjectTest extends TestCase private function compare_aprofiles(\Activitypub_profile $a, \Activitypub_profile $b) { - if (($av = $a->get_uri()) != ($bv = $b->get_uri())) { + if (($av = $a->getUri()) != ($bv = $b->getUri())) { throw new Exception('Compare AProfiles 1 Fail: $a: '.$av.' is different from $b: '.$bv); } - if (($av = $a->profile_id) != ($bv = $b->profile_id)) { + if (($av = $a->getUrl()) != ($bv = $b->getUrl())) { throw new Exception('Compare AProfiles 2 Fail: $a: '.$av.' is different from $b: '.$bv); } - if (($av = $a->inboxuri) != ($bv = $b->inboxuri)) { + if (($av = $a->getID()) != ($bv = $b->getID())) { throw new Exception('Compare AProfiles 3 Fail: $a: '.$av.' is different from $b: '.$bv); } + if (($av = $a->profile_id) != ($bv = $b->profile_id)) { + throw new Exception('Compare AProfiles 4 Fail: $a: '.$av.' is different from $b: '.$bv); + } + + if (($av = $a->inboxuri) != ($bv = $b->inboxuri)) { + throw new Exception('Compare AProfiles 5 Fail: $a: '.$av.' is different from $b: '.$bv); + } + if (($av = $a->sharedInboxuri) != ($bv = $b->sharedInboxuri)) { - throw new Exception('Compare AProfiles 1 Fail: $a: '.$av.' is different from $b: '.$bv); + throw new Exception('Compare AProfiles 6 Fail: $a: '.$av.' is different from $b: '.$bv); } return true; diff --git a/tests/Unit/HTTPSignatureTest.php b/tests/Unit/HTTPSignatureTest.php index 6b29b37..fa53e67 100644 --- a/tests/Unit/HTTPSignatureTest.php +++ b/tests/Unit/HTTPSignatureTest.php @@ -16,8 +16,8 @@ use HttpSignatures\GuzzleHttpSignatures; class HTTPSignatureTest extends TestCase { /** - * @var Context - */ + * @var Context + */ private $context; /** * @var Client diff --git a/utils/explorer.php b/utils/explorer.php index 583507e..d807941 100755 --- a/utils/explorer.php +++ b/utils/explorer.php @@ -32,7 +32,7 @@ if (!defined('GNUSOCIAL')) { /** * ActivityPub's own Explorer * - * Allows to discovery new (or the same) ActivityPub profiles + * Allows to discovery new (or the same) Profiles (both local or remote) * * @category Plugin * @package GNUsocial @@ -55,6 +55,7 @@ class Activitypub_explorer */ public function lookup($url) { + common_debug("Explorer started now looking for ".$url); $this->discovered_actor_profiles = array(); return $this->_lookup($url); @@ -95,9 +96,6 @@ class Activitypub_explorer $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; $response = $client->get($url, $headers); - if (!$response->isOk()) { - throw new Exception("Invalid Actor URL."); - } $res = json_decode($response->getBody(), JSON_UNESCAPED_SLASHES); if (self::validate_remote_response($res)) { $this->temp_res = $res; @@ -108,7 +106,7 @@ class Activitypub_explorer } /** - * Get a local user profiles from its URL and joins it on + * Get a local user profile from its URL and joins it on * $this->discovered_actor_profiles * * @author Diogo Cordeiro @@ -117,6 +115,11 @@ class Activitypub_explorer */ private function grab_local_user($uri, $online = false) { + if ($online) { + common_debug("Explorer is searching locally for ".$uri. " online."); + } else { + common_debug("Explorer is searching locally for ".$uri. " offline."); + } // Ensure proper remote URI // If an exception occurs here it's better to just leave everything // break than to continue processing @@ -126,14 +129,31 @@ class Activitypub_explorer // Try standard ActivityPub route // Is this a known filthy little mudblood? - $aprofile = Activitypub_profile::getKV("uri", $uri); + $aprofile = self::get_aprofile_by_url($uri); if ($aprofile instanceof Activitypub_profile) { $profile = $aprofile->local_profile(); - + common_debug("Explorer found a local Aprofile for ".$uri); // We found something! $this->discovered_actor_profiles[]= $profile; unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system return true; + } else { + common_debug("Explorer didn't find a local Aprofile for ".$uri); + // Well, maybe it is a pure blood? + // Iff, we are in the same instance: + $ACTIVITYPUB_BASE_INSTANCE_URI_length = strlen(ACTIVITYPUB_BASE_INSTANCE_URI); + if (substr($uri, 0, $ACTIVITYPUB_BASE_INSTANCE_URI_length) == ACTIVITYPUB_BASE_INSTANCE_URI) { + try { + $profile = Profile::getByID(intval(substr($uri, $ACTIVITYPUB_BASE_INSTANCE_URI_length))); + + // We found something! + $this->discovered_actor_profiles[]= $profile; + unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system + return true; + } catch (Exception $e) { + // Let the exception go on its merry way. + } + } } // If offline grabbing failed, attempt again with online resources @@ -154,15 +174,13 @@ class Activitypub_explorer */ private function grab_remote_user($url) { + common_debug("Explorer is grabbing a remote profile for ".$url); if (!isset($this->temp_res)) { $client = new HTTPClient(); $headers = array(); $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; $response = $client->get($url, $headers); - if (!$response->isOk()) { - throw new Exception("Invalid Actor URL."); - } $res = json_decode($response->getBody(), JSON_UNESCAPED_SLASHES); } else { $res = $this->temp_res; @@ -234,6 +252,32 @@ class Activitypub_explorer return true; } + /** + * Get a ActivityPub Profile from it's uri + * Unfortunately GNU Social cache is not truly reliable when handling + * potential ActivityPub remote profiles, as so it is important to use + * this hacky workaround (at least for now) + * + * @author Diogo Cordeiro + * @param string $v URL + * @return boolean|Activitypub_profile false if fails | Aprofile object if successful + */ + public static function get_aprofile_by_url($v) + { + $i = Managed_DataObject::getcached("Activitypub_profile", "uri", $v); + if (empty($i)) { // false = cache miss + $i = new Activitypub_profile; + $result = $i->get("uri", $v); + if ($result) { + // Hit! + $i->encache(); + } else { + return false; + } + } + return $i; + } + /** * Given a valid actor profile url returns its inboxes * diff --git a/utils/postman.php b/utils/postman.php index 90e0682..d68f747 100755 --- a/utils/postman.php +++ b/utils/postman.php @@ -168,12 +168,12 @@ class Activitypub_postman public function create($notice) { $data = Activitypub_create::create_to_array( - $notice->getUri(), + $notice->getUrl(), ActivityPubPlugin::actor_uri($this->actor), - Activitypub_notice::notice_to_array($notice) + array_merge(Activitypub_notice::notice_to_array($notice), ['cc' => common_local_url('apActorFollowers', ['id' => $this->actor->getID()]),]) ); if (isset($notice->reply_to)) { - $data["object"]["reply_to"] = $notice->getParent()->getUri(); + $data["object"]["reply_to"] = $notice->getParent()->getUrl(); } $this->client->setBody(json_encode($data)); foreach ($this->to_inbox() as $inbox) {