From 30c073538c90ebc42ab58d11f34d6c3fde72b963 Mon Sep 17 00:00:00 2001 From: Diogo Cordeiro Date: Fri, 20 Jul 2018 16:04:17 +0100 Subject: [PATCH] Add webfinger integration, bug fixes and minor changes ( #21 ) Added author credits in every function so that we know who to bother when something goes wrong Refer to issues #34 and #35 --- ActivityPubPlugin.php | 325 +++++++++++++++++++++++++++-- actions/apactorfollowers.php | 1 + actions/apactorfollowing.php | 1 + actions/apactorinbox.php | 1 + actions/apactorlikedcollection.php | 3 + actions/apactorprofile.php | 1 + actions/apsharedinbox.php | 2 +- actions/inbox/Create.php | 2 + classes/Activitypub_attachment.php | 1 + classes/Activitypub_error.php | 1 + classes/Activitypub_notice.php | 2 + classes/Activitypub_profile.php | 131 +++++++++++- classes/Activitypub_tag.php | 1 + utils/discoveryhints.php | 154 ++++++++++++++ utils/explorer.php | 14 +- utils/postman.php | 11 + 16 files changed, 625 insertions(+), 26 deletions(-) create mode 100644 utils/discoveryhints.php diff --git a/ActivityPubPlugin.php b/ActivityPubPlugin.php index 4319080..72436c2 100644 --- a/ActivityPubPlugin.php +++ b/ActivityPubPlugin.php @@ -1,5 +1,4 @@ + * @param string $arg A remote user identifier + * @return Activitypub_profile|null Valid profile in success | null otherwise + */ + protected 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::get_from_uri ($url); + } catch (Exception $e) { + common_log(LOG_ERR, 'Profile lookup failed for ' . + $arg . ': ' . $e->getMessage ()); + } + } + return null; + } + /** * Route/Reroute urls * @@ -77,7 +125,7 @@ class ActivityPubPlugin extends Plugin * Plugin version information * * @param array $versions - * @return boolean true + * @return boolean hook true */ public function onPluginVersion (array &$versions) { @@ -94,6 +142,8 @@ class ActivityPubPlugin extends Plugin /** * Make sure necessary tables are filled out. + * + * @return boolean hook true */ function onCheckSchema () { @@ -102,6 +152,251 @@ class ActivityPubPlugin extends Plugin return true; } + /******************************************************** + * Discovery Events * + ********************************************************/ + + /** + * Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz + * + * @author GNU Social + * @param string $text The text from which to extract webfinger IDs + * @param string $preMention Character(s) that signals a mention ('@', '!'...) + * @return array The matching IDs (without $preMention) and each respective position in the given string. + */ + static function extractWebfingerIds ($text, $preMention='@') + { + $wmatches = array (); + $result = preg_match_all ('/(? + * @param Profile $sender + * @param string $text input markup text + * @param array &$mention in/out param: set of found mentions + * @return boolean hook return value + */ + function onEndFindMentions(Profile $sender, $text, &$mentions) + { + $matches = array(); + + foreach (self::extractWebfingerIds($text, '@') as $wmatch) { + list($target, $pos) = $wmatch; + $this->log(LOG_INFO, "Checking webfinger person '$target'"); + $profile = null; + try { + $aprofile = Activitypub_profile::ensure_web_finger($target); + $profile = $aprofile->local_profile(); + } catch (Exception $e) { + $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage()); + continue; + } + assert ($profile instanceof Profile); + + $displayName = !empty ($profile->nickname) && mb_strlen ($profile->nickname) < mb_strlen ($target) + ? $profile->getNickname () // TODO: we could do getBestName() or getFullname() here + : $target; + $url = $profile->getUri (); + if (!common_valid_http_url ($url)) { + $url = $profile->getUrl (); + } + $matches[$pos] = array('mentioned' => array ($profile), + 'type' => 'mention', + 'text' => $displayName, + 'position' => $pos, + 'length' => mb_strlen ($target), + 'url' => $url); + } + + foreach (self::extractUrlMentions ($text) as $wmatch) { + list ($target, $pos) = $wmatch; + $schemes = array('https', 'http'); + foreach ($schemes as $scheme) { + $url = "$scheme://$target"; + $this->log(LOG_INFO, "Checking profile address '$url'"); + try { + $aprofile = Activitypub_profile::get_from_uri ($url); + $profile = $aprofile->local_profile(); + $displayName = !empty ($profile->nickname) && mb_strlen ($profile->nickname) < mb_strlen ($target) ? + $profile->nickname : $target; + $matches[$pos] = array('mentioned' => array ($profile), + 'type' => 'mention', + 'text' => $displayName, + 'position' => $pos, + 'length' => mb_strlen ($target), + 'url' => $profile->getUrl()); + break; + } catch (Exception $e) { + $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage()); + } + } + } + + foreach ($mentions as $i => $other) { + // If we share a common prefix with a local user, override it! + $pos = $other['position']; + if (isset ($matches[$pos])) { + $mentions[$i] = $matches[$pos]; + unset ($matches[$pos]); + } + } + foreach ($matches as $mention) { + $mentions[] = $mention; + } + + return true; + } + + /** + * Allow remote profile references to be used in commands: + * sub update@status.net + * whois evan@identi.ca + * reply http://identi.ca/evan hey what's up + * + * @author GNU Social + * @author Diogo Cordeiro + * @param Command $command + * @param string $arg + * @param Profile &$profile + * @return hook return code + */ + function onStartCommandGetProfile ($command, $arg, &$profile) + { + try { + $aprofile = $this->pull_remote_profile ($arg); + $profile = $aprofile->local_profile(); + } catch (Exception $e) { + // No remote ActivityPub profile found + return true; + } + + return false; + } + + /** + * Profile URI for remote profiles. + * + * @author GNU Social + * @author Diogo Cordeiro + * @param Profile $profile + * @param string $uri in/out + * @return mixed hook return code + */ + function onStartGetProfileUri ($profile, &$uri) + { + $aprofile = Activitypub_profile::getKV ('profile_id', $profile->id); + if ($aprofile instanceof Activitypub_profile) { + $uri = $aprofile->get_uri (); + return false; + } + return true; + } + + + /** + * Dummy string on AccountProfileBlock stating that ActivityPub is active + * this is more of a placeholder for eventual useful stuff ._. + * + * @author Diogo Cordeiro + * @return void + */ + function onEndShowAccountProfileBlock (HTMLOutputter $out, Profile $profile) + { + if ($profile->isLocal()) { + return true; + } + try { + $aprofile = Activitypub_profile::getKV ('profile_id', $profile->id); + } catch (NoResultException $e) { + // Not a remote ActivityPub_profile! Maybe some other network + // that has imported a non-local user (e.g.: OStatus)? + return true; + } + + $out->elementStart('dl', 'entity_tags activitypub_profile'); + $out->element('dt', null, _m('ActivityPub')); + $out->element('dd', null, _m('Active')); + $out->elementEnd('dl'); + } + + /** + * Profile from URI. + * + * @author GNU Social + * @author Diogo Cordeiro + * @param string $uri + * @param Profile &$profile in/out param: Profile got from URI + * @return mixed hook return code + */ + 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 (); + return false; + } catch (Exception $e) { + return true; // It's not an ActivityPub profile as far as we know, continue event handling + } + } + /******************************************************** * Delivery Events * ********************************************************/ @@ -110,6 +405,7 @@ class ActivityPubPlugin extends Plugin * Having established a remote subscription, send a notification to the * remote ActivityPub profile's endpoint. * + * @author Diogo Cordeiro * @param Profile $profile subscriber * @param Profile $other subscribee * @return hook return value @@ -137,6 +433,7 @@ class ActivityPubPlugin extends Plugin /** * Notify remote server on unsubscribe. * + * @author Diogo Cordeiro * @param Profile $profile * @param Profile $other * @return hook return value @@ -163,6 +460,7 @@ class ActivityPubPlugin extends Plugin /** * Notify remote users when their notices get favorited. * + * @author Diogo Cordeiro * @param Profile $profile of local user doing the faving * @param Notice $notice Notice being favored * @return hook return value @@ -221,6 +519,7 @@ class ActivityPubPlugin extends Plugin /** * Notify remote users when their notices get de-favorited. * + * @author Diogo Cordeiro * @param Profile $profile of local user doing the de-faving * @param Notice $notice Notice being favored * @return hook return value @@ -279,6 +578,7 @@ class ActivityPubPlugin extends Plugin /** * Notify remote users when their notices get deleted * + * @author Diogo Cordeiro * @return boolean hook flag */ public function onEndDeleteOwnNotice ($user, $notice) @@ -331,6 +631,7 @@ class ActivityPubPlugin extends Plugin /** * Insert notifications for replies, mentions and repeats * + * @author Diogo Cordeiro * @return boolean hook flag */ function onStartNoticeDistribute ($notice) @@ -416,6 +717,7 @@ class ActivityPubPlugin extends Plugin * Override the "from ActivityPub" bit in notice lists to link to the * original post and show the domain it came from. * + * @author Diogo Cordeiro * @param Notice in $notice * @param string out &$name * @param string out &$url @@ -451,23 +753,6 @@ class ActivityPubPlugin extends Plugin return true; } } - - /** - * Profile URI for remote profiles. - * - * @param Profile $profile - * @param string $uri in/out - * @return mixed hook return code - */ - function onStartGetProfileUri ($profile, &$uri) - { - $aprofile = Activitypub_profile::getKV ('profile_id', $profile->id); - if ($aprofile instanceof Activitypub_profile) { - $uri = $aprofile->uri; - return false; - } - return true; - } } /** @@ -504,6 +789,7 @@ class ActivityPubReturn /** * Return a valid answer * + * @author Diogo Cordeiro * @param array $res * @return void */ @@ -517,6 +803,7 @@ class ActivityPubReturn /** * Return an error * + * @author Diogo Cordeiro * @param string $m * @param int32 $code * @return void diff --git a/actions/apactorfollowers.php b/actions/apactorfollowers.php index 9205bd7..639cace 100644 --- a/actions/apactorfollowers.php +++ b/actions/apactorfollowers.php @@ -46,6 +46,7 @@ class apActorFollowersAction extends ManagedAction /** * Handle the Followers Collection request * + * @author Diogo Cordeiro * @return void */ protected function handle () diff --git a/actions/apactorfollowing.php b/actions/apactorfollowing.php index 4bb695e..86c895e 100644 --- a/actions/apactorfollowing.php +++ b/actions/apactorfollowing.php @@ -46,6 +46,7 @@ class apActorFollowingAction extends ManagedAction /** * Handle the Following Collection request * + * @author Diogo Cordeiro * @return void */ protected function handle () diff --git a/actions/apactorinbox.php b/actions/apactorinbox.php index 35bbb74..fbbe395 100644 --- a/actions/apactorinbox.php +++ b/actions/apactorinbox.php @@ -46,6 +46,7 @@ class apActorInboxAction extends ManagedAction /** * Handle the Actor Inbox request * + * @author Diogo Cordeiro * @return void */ protected function handle () diff --git a/actions/apactorlikedcollection.php b/actions/apactorlikedcollection.php index 3cc3cba..1f93a7d 100644 --- a/actions/apactorlikedcollection.php +++ b/actions/apactorlikedcollection.php @@ -46,6 +46,7 @@ class apActorLikedCollectionAction extends ManagedAction /** * Handle the Liked Collection request * + * @author Diogo Cordeiro * @return void */ protected function handle () @@ -99,6 +100,7 @@ class apActorLikedCollectionAction extends ManagedAction * Take a fave object and turns it in a pretty array to be used * as a plugin answer * + * @author Diogo Cordeiro * @param Fave $fave_object * @return array pretty array representating a Fave */ @@ -114,6 +116,7 @@ class apActorLikedCollectionAction extends ManagedAction /** * Fetch faves * + * @author Diogo Cordeiro * @param int32 $user_id * @param int32 $limit * @param int32 $since_id diff --git a/actions/apactorprofile.php b/actions/apactorprofile.php index b32d595..8ac1210 100644 --- a/actions/apactorprofile.php +++ b/actions/apactorprofile.php @@ -47,6 +47,7 @@ class apActorProfileAction extends ManagedAction /** * Handle the Actor Profile request * + * @author Daniel Supernault * @return void */ protected function handle() diff --git a/actions/apsharedinbox.php b/actions/apsharedinbox.php index cec4e42..7c278b8 100644 --- a/actions/apsharedinbox.php +++ b/actions/apsharedinbox.php @@ -1,5 +1,4 @@ * @return void */ protected function handle () diff --git a/actions/inbox/Create.php b/actions/inbox/Create.php index c12309a..d10409a 100644 --- a/actions/inbox/Create.php +++ b/actions/inbox/Create.php @@ -40,6 +40,8 @@ if (!isset ($data->object->content)) { } if (!isset ($data->object->url)) { ActivityPubReturn::error ("Object url was not specified."); +} else if (!filter_var ($data->object->url, FILTER_VALIDATE_URL)) { + ActivityPubReturn::error ("Invalid Object Url."); } $content = $data->object->content; diff --git a/classes/Activitypub_attachment.php b/classes/Activitypub_attachment.php index e20a487..075015a 100644 --- a/classes/Activitypub_attachment.php +++ b/classes/Activitypub_attachment.php @@ -43,6 +43,7 @@ class Activitypub_attachment extends Managed_DataObject /** * Generates a pretty array from an Attachment object * + * @author Diogo Cordeiro * @param Attachment $attachment * @return pretty array to be used in a response */ diff --git a/classes/Activitypub_error.php b/classes/Activitypub_error.php index e0637cd..35638b4 100644 --- a/classes/Activitypub_error.php +++ b/classes/Activitypub_error.php @@ -43,6 +43,7 @@ class Activitypub_error extends Managed_DataObject /** * Generates a pretty error from a string * + * @author Diogo Cordeiro * @param string $m * @return pretty array to be used in a response */ diff --git a/classes/Activitypub_notice.php b/classes/Activitypub_notice.php index 88f8f5c..954c1f6 100644 --- a/classes/Activitypub_notice.php +++ b/classes/Activitypub_notice.php @@ -44,6 +44,8 @@ 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 */ diff --git a/classes/Activitypub_profile.php b/classes/Activitypub_profile.php index a1cb4da..701c762 100644 --- a/classes/Activitypub_profile.php +++ b/classes/Activitypub_profile.php @@ -1,5 +1,4 @@ * @return array array of column definitions */ static function schemaDef () @@ -76,6 +76,7 @@ class Activitypub_profile extends Profile /** * Generates a pretty profile from a Profile object * + * @author Diogo Cordeiro * @param Profile $profile * @return pretty array to be used in a response */ @@ -118,6 +119,7 @@ class Activitypub_profile extends Profile /** * Insert the current objects variables into the database * + * @author Diogo Cordeiro * @access public * @throws ServerException */ @@ -154,6 +156,8 @@ class Activitypub_profile extends Profile /** * Fetch the locally stored profile for this Activitypub_profile + * + * @author Diogo Cordeiro * @return Profile * @throws NoProfileException if it was not found */ @@ -169,6 +173,7 @@ class Activitypub_profile extends Profile /** * Generates an Activitypub_profile from a Profile * + * @author Diogo Cordeiro * @param Profile $profile * @return Activitypub_profile * @throws Exception if no Activitypub_profile exists for given Profile @@ -177,14 +182,14 @@ class Activitypub_profile extends Profile { $profile_id = $profile->getID (); - $aprofile = Activitypub_profile::getKV ('profile_id', $profile_id); + $aprofile = self::getKV ('profile_id', $profile_id); if (!$aprofile instanceof Activitypub_profile) { // No Activitypub_profile for this profile_id, if (!$profile->isLocal ()) { // create one! $aprofile = self::create_from_local_profile ($profile); } else { - throw new Exception ('No Activitypub_profile for Profile ID: '.$profile_id. ', this probably is a local profile.'); + throw new Exception ('No Activitypub_profile for Profile ID: '.$profile_id. ', this is a local user.'); } } @@ -200,6 +205,7 @@ class Activitypub_profile extends Profile * One must be careful not to give a user profile to this function * as only remote users have ActivityPub_profiles on local instance * + * @author Diogo Cordeiro * @param Profile $profile * @return Activitypub_profile */ @@ -227,6 +233,7 @@ class Activitypub_profile extends Profile /** * Returns sharedInbox if possible, inbox otherwise * + * @author Diogo Cordeiro * @return string Inbox URL */ public function get_inbox () @@ -237,4 +244,122 @@ class Activitypub_profile extends Profile return $this->sharedInboxuri; } + + /** + * Getter for uri property + * + * @author Diogo Cordeiro + * @return string URI + */ + public function get_uri () + { + return $this->uri; + } + + /** + * Ensures a valid Activitypub_profile when provided with a valid URI. + * + * @author Diogo Cordeiro + * @param string $url + * @return Activitypub_profile + * @throws Exception if it isn't possible to return an Activitypub_profile + */ + public static function get_from_uri ($url) + { + $explorer = new Activitypub_explorer (); + $profiles_found = $explorer->lookup ($url); + if (!empty ($profiles_found)) { + return self::from_profile ($profiles_found[0]); + } else { + throw new Exception ('No valid ActivityPub profile found for given URI'); + } + // If it doesn't return a valid Activitypub_profile an exception will + // have been thrown before getting to this point. + } + + /** + * Look up, and if necessary create, an Activitypub_profile for the remote + * entity with the given webfinger address. + * This should never return null -- you will either get an object or + * an exception will be thrown. + * + * @author GNU Social + * @author Diogo Cordeiro + * @param string $addr webfinger address + * @return Activitypub_profile + * @throws Exception on error conditions + */ + public static function ensure_web_finger ($addr) + { + // Normalize $addr, i.e. add 'acct:' if missing + $addr = Discovery::normalize ($addr); + + // Try the cache + $uri = self::cacheGet (sprintf ('activitypub_profile:webfinger:%s', $addr)); + + if ($uri !== false) { + if (is_null ($uri)) { + // Negative cache entry + // TRANS: Exception. + throw new Exception (_m ('Not a valid webfinger address (via cache).')); + } + try { + return self::get_from_uri ($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); + } + } + + // Now, try some discovery + + $disco = new Discovery (); + + try { + $xrd = $disco->lookup ($addr); + } catch (Exception $e) { + // Save negative cache entry so we don't waste time looking it up again. + // @todo FIXME: Distinguish temporary failures? + self::cacheSet (sprintf ('activitypub_profile:webfinger:%s', $addr), null); + // TRANS: Exception. + throw new Exception (_m ('Not a valid webfinger address.')); + } + + $hints = array_merge (array ('webfinger' => $addr), + DiscoveryHints::fromXRD ($xrd)); + + // If there's an Hcard, let's grab its info + if (array_key_exists ('hcard', $hints)) { + if (!array_key_exists ('profileurl', $hints) || + $hints['hcard'] != $hints['profileurl']) { + $hcardHints = DiscoveryHints::fromHcardUrl ($hints['hcard']); + $hints = array_merge ($hcardHints, $hints); + } + } + + // If we got a profile page, try that! + $profileUrl = null; + if (array_key_exists ('profileurl', $hints)) { + $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 ()); + return $aprofile; + } catch (Exception $e) { + common_log (LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage ()); + // keep looking + // + // @todo FIXME: This means an error discovering from profile page + // may give us a corrupt entry using the webfinger URI, which + // will obscure the correct page-keyed profile later on. + } + } + + // XXX: try hcard + // XXX: try FOAF + + // TRANS: Exception. %s is a webfinger address. + throw new Exception (sprintf (_m ('Could not find a valid profile for "%s".'), $addr)); + } } diff --git a/classes/Activitypub_tag.php b/classes/Activitypub_tag.php index 1e94862..7c1a838 100644 --- a/classes/Activitypub_tag.php +++ b/classes/Activitypub_tag.php @@ -43,6 +43,7 @@ class Activitypub_tag extends Managed_DataObject /** * Generates a pretty tag from a Tag object * + * @author Diogo Cordeiro * @param Tag $tag * @return pretty array to be used in a response */ diff --git a/utils/discoveryhints.php b/utils/discoveryhints.php new file mode 100644 index 0000000..b14a174 --- /dev/null +++ b/utils/discoveryhints.php @@ -0,0 +1,154 @@ +. + * + * @category Plugin + * @package GNUsocial + * @author GNUsocial + * @copyright 2010 Free Software Foundation http://fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link https://www.gnu.org/software/social/ + */ +if (!defined ('GNUSOCIAL')) { + exit (1); +} + +class DiscoveryHints { + static function fromXRD(XML_XRD $xrd) + { + $hints = array(); + + if (Event::handle('StartDiscoveryHintsFromXRD', array($xrd, &$hints))) { + foreach ($xrd->links as $link) { + switch ($link->rel) { + case WebFingerResource_Profile::PROFILEPAGE: + $hints['profileurl'] = $link->href; + break; + $hints['salmon'] = $link->href; + break; + case Discovery::UPDATESFROM: + if (empty($link->type) || $link->type == 'application/atom+xml') { + $hints['feedurl'] = $link->href; + } + break; + case Discovery::HCARD: + case Discovery::MF2_HCARD: + $hints['hcard'] = $link->href; + break; + default: + break; + } + } + Event::handle('EndDiscoveryHintsFromXRD', array($xrd, &$hints)); + } + + return $hints; + } + + static function fromHcardUrl($url) + { + $client = new HTTPClient(); + $client->setHeader('Accept', 'text/html,application/xhtml+xml'); + try { + $response = $client->get($url); + + if (!$response->isOk()) { + return null; + } + } catch (HTTP_Request2_Exception $e) { + // Any HTTPClient error that might've been thrown + common_log(LOG_ERR, __METHOD__ . ':'.$e->getMessage()); + return null; + } + + return self::hcardHints($response->getBody(), + $response->getEffectiveUrl()); + } + + static function hcardHints($body, $url) + { + $hcard = self::_hcard($body, $url); + + if (empty($hcard)) { + return array(); + } + + $hints = array(); + + // XXX: don't copy stuff into an array and then copy it again + + if (array_key_exists('nickname', $hcard) && !empty($hcard['nickname'][0])) { + $hints['nickname'] = $hcard['nickname'][0]; + } + + if (array_key_exists('name', $hcard) && !empty($hcard['name'][0])) { + $hints['fullname'] = $hcard['name'][0]; + } + + if (array_key_exists('photo', $hcard) && count($hcard['photo'])) { + $hints['avatar'] = $hcard['photo'][0]; + } + + if (array_key_exists('note', $hcard) && !empty($hcard['note'][0])) { + $hints['bio'] = $hcard['note'][0]; + } + + if (array_key_exists('adr', $hcard) && !empty($hcard['adr'][0])) { + $hints['location'] = $hcard['adr'][0]['value']; + } + + if (array_key_exists('url', $hcard) && !empty($hcard['url'][0])) { + $hints['homepage'] = $hcard['url'][0]; + } + + return $hints; + } + + static function _hcard($body, $url) + { + $mf2 = new Mf2\Parser($body, $url); + $mf2 = $mf2->parse(); + + if (empty($mf2['items'])) { + return null; + } + + $hcards = array(); + + foreach ($mf2['items'] as $item) { + if (!in_array('h-card', $item['type'])) { + continue; + } + + // We found a match, return it immediately + if (isset($item['properties']['url']) && in_array($url, $item['properties']['url'])) { + return $item['properties']; + } + + // Let's keep all the hcards for later, to return one of them at least + $hcards[] = $item['properties']; + } + + // No match immediately for the url we expected, but there were h-cards found + if (count($hcards) > 0) { + return $hcards[0]; + } + + return null; + } +} diff --git a/utils/explorer.php b/utils/explorer.php index 8a70c5e..51ebd97 100644 --- a/utils/explorer.php +++ b/utils/explorer.php @@ -49,6 +49,7 @@ class Activitypub_explorer * This function cleans the $this->discovered_actor_profiles array * so that there is no erroneous data * + * @author Diogo Cordeiro * @param string $url User's url * @return array of Profile objects */ @@ -64,6 +65,7 @@ class Activitypub_explorer * This is a recursive function that will accumulate the results on * $discovered_actor_profiles array * + * @author Diogo Cordeiro * @param string $url User's url * @return array of Profile objects */ @@ -83,6 +85,7 @@ class Activitypub_explorer * Get a local user profiles from its URL and joins it on * $this->discovered_actor_profiles * + * @author Diogo Cordeiro * @param string $url User's url * @return boolean success state */ @@ -116,6 +119,7 @@ class Activitypub_explorer * Get a remote user(s) profile(s) from its URL and joins it on * $this->discovered_actor_profiles * + * @author Diogo Cordeiro * @param string $url User's url * @return boolean success state */ @@ -152,6 +156,7 @@ class Activitypub_explorer /** * Save remote user profile in local instance * + * @author Diogo Cordeiro * @param array $res remote response * @return Profile remote Profile object */ @@ -174,6 +179,7 @@ class Activitypub_explorer * Validates a remote response in order to determine whether this * response is a valid profile or not * + * @author Diogo Cordeiro * @param array $res remote response * @return boolean success state */ @@ -192,12 +198,13 @@ class Activitypub_explorer * 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|Profile false if fails | Profile object if successful */ - static function get_profile_by_url ($v) + public static function get_profile_by_url ($v) { - $i = Managed_DataObject::getcached(Profile, "profileurl", $v); + $i = Managed_DataObject::getcached("Profile", "profileurl", $v); if (empty ($i)) { // false = cache miss $i = new Profile; $result = $i->get ("profileurl", $v); @@ -214,10 +221,11 @@ class Activitypub_explorer /** * Given a valid actor profile url returns its inboxes * + * @author Diogo Cordeiro * @param string $url of Actor profile * @return boolean|array false if fails | array with inbox and shared inbox if successful */ - static function get_actor_inboxes_uri ($url) + public static function get_actor_inboxes_uri ($url) { $client = new HTTPClient (); $headers = array(); diff --git a/utils/postman.php b/utils/postman.php index d7ae125..15dffa3 100644 --- a/utils/postman.php +++ b/utils/postman.php @@ -51,6 +51,7 @@ class Activitypub_postman /** * Create a postman to deliver something to someone * + * @author Diogo Cordeiro * @param Profile of sender * @param Activitypub_profile $to array of destinataries */ @@ -66,6 +67,8 @@ class Activitypub_postman /** * Send a follow notification to remote instance + * + * @author Diogo Cordeiro */ public function follow () { @@ -79,6 +82,8 @@ class Activitypub_postman /** * Send a Undo Follow notification to remote instance + * + * @author Diogo Cordeiro */ public function undo_follow () { @@ -97,6 +102,7 @@ class Activitypub_postman /** * Send a Like notification to remote instances holding the notice * + * @author Diogo Cordeiro * @param Notice $notice */ public function like ($notice) @@ -114,6 +120,7 @@ class Activitypub_postman /** * Send a Undo Like notification to remote instances holding the notice * + * @author Diogo Cordeiro * @param Notice $notice */ public function undo_like ($notice) @@ -135,6 +142,7 @@ class Activitypub_postman /** * Send a Announce notification to remote instances * + * @author Diogo Cordeiro * @param Notice $notice */ public function announce ($notice) @@ -156,6 +164,7 @@ class Activitypub_postman /** * Send a Create notification to remote instances * + * @author Diogo Cordeiro * @param Notice $notice */ public function create ($notice) @@ -183,6 +192,7 @@ class Activitypub_postman /** * Send a Delete notification to remote instances holding the notice * + * @author Diogo Cordeiro * @param Notice $notice */ public function delete ($notice) @@ -201,6 +211,7 @@ class Activitypub_postman /** * Clean list of inboxes to deliver messages * + * @author Diogo Cordeiro * @return array To Inbox URLs */ private function to_inbox ()