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
This commit is contained in:
Diogo Cordeiro 2018-07-20 16:04:17 +01:00
parent 2d0f3de52a
commit 30c073538c
16 changed files with 625 additions and 26 deletions

View File

@ -1,5 +1,4 @@
<?php <?php
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "postman.php";
/** /**
* GNU social - a federating social network * GNU social - a federating social network
* *
@ -30,6 +29,11 @@ if (!defined ('GNUSOCIAL')) {
exit (1); exit (1);
} }
// Import required files by the plugin
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "discoveryhints.php";
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php";
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "postman.php";
/** /**
* @category Plugin * @category Plugin
* @package GNUsocial * @package GNUsocial
@ -40,6 +44,50 @@ if (!defined ('GNUSOCIAL')) {
*/ */
class ActivityPubPlugin extends Plugin class ActivityPubPlugin extends Plugin
{ {
/**
* Get remote user's ActivityPub_profile via a identifier
* (https://www.w3.org/TR/activitypub/#obj-id)
*
* @author GNU Social
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @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 * Route/Reroute urls
* *
@ -77,7 +125,7 @@ class ActivityPubPlugin extends Plugin
* Plugin version information * Plugin version information
* *
* @param array $versions * @param array $versions
* @return boolean true * @return boolean hook true
*/ */
public function onPluginVersion (array &$versions) public function onPluginVersion (array &$versions)
{ {
@ -94,6 +142,8 @@ class ActivityPubPlugin extends Plugin
/** /**
* Make sure necessary tables are filled out. * Make sure necessary tables are filled out.
*
* @return boolean hook true
*/ */
function onCheckSchema () function onCheckSchema ()
{ {
@ -102,6 +152,251 @@ class ActivityPubPlugin extends Plugin
return true; 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 ('/(?<!\S)'.preg_quote ($preMention, '/').'('.Nickname::WEBFINGER_FMT.')/',
$text,
$wmatches,
PREG_OFFSET_CAPTURE);
if ($result === false) {
common_log (LOG_ERR, __METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error=='.preg_last_error ().').');
} elseif (count ($wmatches)) {
common_debug (sprintf ('Found %d matches for WebFinger IDs: %s', count ($wmatches), _ve ($wmatches)));
}
return $wmatches[1];
}
/**
* Profile URL matches: @example.com/mublog/user
*
* @author GNU Social
* @param string $text The text from which to extract URL mentions
* @param string $preMention Character(s) that signals a mention ('@', '!'...)
* @return array The matching URLs (without @ or acct:) and each respective position in the given string.
*/
static function extractUrlMentions ($text, $preMention='@')
{
$wmatches = array ();
// In the regexp below we need to match / _before_ URL_REGEX_VALID_PATH_CHARS because it otherwise gets merged
// with the TLD before (but / is in URL_REGEX_VALID_PATH_CHARS anyway, it's just its positioning that is important)
$result = preg_match_all ('/(?:^|\s+)'.preg_quote ($preMention, '/').'('.URL_REGEX_DOMAIN_NAME.'(?:\/['.URL_REGEX_VALID_PATH_CHARS.']*)*)/',
$text,
$wmatches,
PREG_OFFSET_CAPTURE);
if ($result === false) {
common_log (LOG_ERR, __METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error=='.preg_last_error ().').');
} elseif (count ($wmatches)) {
common_debug(sprintf('Found %d matches for profile URL mentions: %s', count ($wmatches), _ve ($wmatches)));
}
return $wmatches[1];
}
/**
* Find any explicit remote mentions. Accepted forms:
* Webfinger: @user@example.com
* Profile link: @example.com/mublog/user
*
* @author GNU Social
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @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 <diogo@fc.up.pt>
* @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 <diogo@fc.up.pt>
* @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 <diogo@fc.up.pt>
* @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 <diogo@fc.up.pt>
* @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 * * Delivery Events *
********************************************************/ ********************************************************/
@ -110,6 +405,7 @@ class ActivityPubPlugin extends Plugin
* Having established a remote subscription, send a notification to the * Having established a remote subscription, send a notification to the
* remote ActivityPub profile's endpoint. * remote ActivityPub profile's endpoint.
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Profile $profile subscriber * @param Profile $profile subscriber
* @param Profile $other subscribee * @param Profile $other subscribee
* @return hook return value * @return hook return value
@ -137,6 +433,7 @@ class ActivityPubPlugin extends Plugin
/** /**
* Notify remote server on unsubscribe. * Notify remote server on unsubscribe.
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Profile $profile * @param Profile $profile
* @param Profile $other * @param Profile $other
* @return hook return value * @return hook return value
@ -163,6 +460,7 @@ class ActivityPubPlugin extends Plugin
/** /**
* Notify remote users when their notices get favorited. * Notify remote users when their notices get favorited.
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Profile $profile of local user doing the faving * @param Profile $profile of local user doing the faving
* @param Notice $notice Notice being favored * @param Notice $notice Notice being favored
* @return hook return value * @return hook return value
@ -221,6 +519,7 @@ class ActivityPubPlugin extends Plugin
/** /**
* Notify remote users when their notices get de-favorited. * Notify remote users when their notices get de-favorited.
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Profile $profile of local user doing the de-faving * @param Profile $profile of local user doing the de-faving
* @param Notice $notice Notice being favored * @param Notice $notice Notice being favored
* @return hook return value * @return hook return value
@ -279,6 +578,7 @@ class ActivityPubPlugin extends Plugin
/** /**
* Notify remote users when their notices get deleted * Notify remote users when their notices get deleted
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return boolean hook flag * @return boolean hook flag
*/ */
public function onEndDeleteOwnNotice ($user, $notice) public function onEndDeleteOwnNotice ($user, $notice)
@ -331,6 +631,7 @@ class ActivityPubPlugin extends Plugin
/** /**
* Insert notifications for replies, mentions and repeats * Insert notifications for replies, mentions and repeats
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return boolean hook flag * @return boolean hook flag
*/ */
function onStartNoticeDistribute ($notice) function onStartNoticeDistribute ($notice)
@ -416,6 +717,7 @@ class ActivityPubPlugin extends Plugin
* Override the "from ActivityPub" bit in notice lists to link to the * Override the "from ActivityPub" bit in notice lists to link to the
* original post and show the domain it came from. * original post and show the domain it came from.
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice in $notice * @param Notice in $notice
* @param string out &$name * @param string out &$name
* @param string out &$url * @param string out &$url
@ -451,23 +753,6 @@ class ActivityPubPlugin extends Plugin
return true; 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 * Return a valid answer
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $res * @param array $res
* @return void * @return void
*/ */
@ -517,6 +803,7 @@ class ActivityPubReturn
/** /**
* Return an error * Return an error
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $m * @param string $m
* @param int32 $code * @param int32 $code
* @return void * @return void

View File

@ -46,6 +46,7 @@ class apActorFollowersAction extends ManagedAction
/** /**
* Handle the Followers Collection request * Handle the Followers Collection request
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void * @return void
*/ */
protected function handle () protected function handle ()

View File

@ -46,6 +46,7 @@ class apActorFollowingAction extends ManagedAction
/** /**
* Handle the Following Collection request * Handle the Following Collection request
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void * @return void
*/ */
protected function handle () protected function handle ()

View File

@ -46,6 +46,7 @@ class apActorInboxAction extends ManagedAction
/** /**
* Handle the Actor Inbox request * Handle the Actor Inbox request
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void * @return void
*/ */
protected function handle () protected function handle ()

View File

@ -46,6 +46,7 @@ class apActorLikedCollectionAction extends ManagedAction
/** /**
* Handle the Liked Collection request * Handle the Liked Collection request
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void * @return void
*/ */
protected function handle () 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 * Take a fave object and turns it in a pretty array to be used
* as a plugin answer * as a plugin answer
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Fave $fave_object * @param Fave $fave_object
* @return array pretty array representating a Fave * @return array pretty array representating a Fave
*/ */
@ -114,6 +116,7 @@ class apActorLikedCollectionAction extends ManagedAction
/** /**
* Fetch faves * Fetch faves
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param int32 $user_id * @param int32 $user_id
* @param int32 $limit * @param int32 $limit
* @param int32 $since_id * @param int32 $since_id

View File

@ -47,6 +47,7 @@ class apActorProfileAction extends ManagedAction
/** /**
* Handle the Actor Profile request * Handle the Actor Profile request
* *
* @author Daniel Supernault <danielsupernault@gmail.com>
* @return void * @return void
*/ */
protected function handle() protected function handle()

View File

@ -1,5 +1,4 @@
<?php <?php
require_once dirname (__DIR__) . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php";
/** /**
* GNU social - a federating social network * GNU social - a federating social network
* *
@ -47,6 +46,7 @@ class apSharedInboxAction extends ManagedAction
/** /**
* Handle the Shared Inbox request * Handle the Shared Inbox request
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void * @return void
*/ */
protected function handle () protected function handle ()

View File

@ -40,6 +40,8 @@ if (!isset ($data->object->content)) {
} }
if (!isset ($data->object->url)) { if (!isset ($data->object->url)) {
ActivityPubReturn::error ("Object url was not specified."); 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; $content = $data->object->content;

View File

@ -43,6 +43,7 @@ class Activitypub_attachment extends Managed_DataObject
/** /**
* Generates a pretty array from an Attachment object * Generates a pretty array from an Attachment object
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Attachment $attachment * @param Attachment $attachment
* @return pretty array to be used in a response * @return pretty array to be used in a response
*/ */

View File

@ -43,6 +43,7 @@ class Activitypub_error extends Managed_DataObject
/** /**
* Generates a pretty error from a string * Generates a pretty error from a string
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $m * @param string $m
* @return pretty array to be used in a response * @return pretty array to be used in a response
*/ */

View File

@ -44,6 +44,8 @@ class Activitypub_notice extends Managed_DataObject
/** /**
* Generates a pretty notice from a Notice object * Generates a pretty notice from a Notice object
* *
* @author Daniel Supernault <danielsupernault@gmail.com>
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice * @param Notice $notice
* @return pretty array to be used in a response * @return pretty array to be used in a response
*/ */

View File

@ -1,5 +1,4 @@
<?php <?php
require_once dirname (__DIR__) . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php";
/** /**
* GNU social - a federating social network * GNU social - a federating social network
* *
@ -49,6 +48,7 @@ class Activitypub_profile extends Profile
/** /**
* Return table definition for Schema setup and DB_DataObject usage. * Return table definition for Schema setup and DB_DataObject usage.
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return array array of column definitions * @return array array of column definitions
*/ */
static function schemaDef () static function schemaDef ()
@ -76,6 +76,7 @@ class Activitypub_profile extends Profile
/** /**
* Generates a pretty profile from a Profile object * Generates a pretty profile from a Profile object
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Profile $profile * @param Profile $profile
* @return pretty array to be used in a response * @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 * Insert the current objects variables into the database
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @access public * @access public
* @throws ServerException * @throws ServerException
*/ */
@ -154,6 +156,8 @@ class Activitypub_profile extends Profile
/** /**
* Fetch the locally stored profile for this Activitypub_profile * Fetch the locally stored profile for this Activitypub_profile
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return Profile * @return Profile
* @throws NoProfileException if it was not found * @throws NoProfileException if it was not found
*/ */
@ -169,6 +173,7 @@ class Activitypub_profile extends Profile
/** /**
* Generates an Activitypub_profile from a Profile * Generates an Activitypub_profile from a Profile
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Profile $profile * @param Profile $profile
* @return Activitypub_profile * @return Activitypub_profile
* @throws Exception if no Activitypub_profile exists for given Profile * @throws Exception if no Activitypub_profile exists for given Profile
@ -177,14 +182,14 @@ class Activitypub_profile extends Profile
{ {
$profile_id = $profile->getID (); $profile_id = $profile->getID ();
$aprofile = Activitypub_profile::getKV ('profile_id', $profile_id); $aprofile = self::getKV ('profile_id', $profile_id);
if (!$aprofile instanceof Activitypub_profile) { if (!$aprofile instanceof Activitypub_profile) {
// No Activitypub_profile for this profile_id, // No Activitypub_profile for this profile_id,
if (!$profile->isLocal ()) { if (!$profile->isLocal ()) {
// create one! // create one!
$aprofile = self::create_from_local_profile ($profile); $aprofile = self::create_from_local_profile ($profile);
} else { } 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 * One must be careful not to give a user profile to this function
* as only remote users have ActivityPub_profiles on local instance * as only remote users have ActivityPub_profiles on local instance
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Profile $profile * @param Profile $profile
* @return Activitypub_profile * @return Activitypub_profile
*/ */
@ -227,6 +233,7 @@ class Activitypub_profile extends Profile
/** /**
* Returns sharedInbox if possible, inbox otherwise * Returns sharedInbox if possible, inbox otherwise
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return string Inbox URL * @return string Inbox URL
*/ */
public function get_inbox () public function get_inbox ()
@ -237,4 +244,122 @@ class Activitypub_profile extends Profile
return $this->sharedInboxuri; return $this->sharedInboxuri;
} }
/**
* Getter for uri property
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return string URI
*/
public function get_uri ()
{
return $this->uri;
}
/**
* Ensures a valid Activitypub_profile when provided with a valid URI.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @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 <diogo@fc.up.pt>
* @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));
}
} }

View File

@ -43,6 +43,7 @@ class Activitypub_tag extends Managed_DataObject
/** /**
* Generates a pretty tag from a Tag object * Generates a pretty tag from a Tag object
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Tag $tag * @param Tag $tag
* @return pretty array to be used in a response * @return pretty array to be used in a response
*/ */

154
utils/discoveryhints.php Normal file
View File

@ -0,0 +1,154 @@
<?php
/**
* GNU social - a federating social network
*
* Some utilities for generating hint data
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @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;
}
}

View File

@ -49,6 +49,7 @@ class Activitypub_explorer
* This function cleans the $this->discovered_actor_profiles array * This function cleans the $this->discovered_actor_profiles array
* so that there is no erroneous data * so that there is no erroneous data
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $url User's url * @param string $url User's url
* @return array of Profile objects * @return array of Profile objects
*/ */
@ -64,6 +65,7 @@ class Activitypub_explorer
* This is a recursive function that will accumulate the results on * This is a recursive function that will accumulate the results on
* $discovered_actor_profiles array * $discovered_actor_profiles array
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $url User's url * @param string $url User's url
* @return array of Profile objects * @return array of Profile objects
*/ */
@ -83,6 +85,7 @@ class Activitypub_explorer
* Get a local user profiles from its URL and joins it on * Get a local user profiles from its URL and joins it on
* $this->discovered_actor_profiles * $this->discovered_actor_profiles
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $url User's url * @param string $url User's url
* @return boolean success state * @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 * Get a remote user(s) profile(s) from its URL and joins it on
* $this->discovered_actor_profiles * $this->discovered_actor_profiles
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $url User's url * @param string $url User's url
* @return boolean success state * @return boolean success state
*/ */
@ -152,6 +156,7 @@ class Activitypub_explorer
/** /**
* Save remote user profile in local instance * Save remote user profile in local instance
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $res remote response * @param array $res remote response
* @return Profile remote Profile object * @return Profile remote Profile object
*/ */
@ -174,6 +179,7 @@ class Activitypub_explorer
* Validates a remote response in order to determine whether this * Validates a remote response in order to determine whether this
* response is a valid profile or not * response is a valid profile or not
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $res remote response * @param array $res remote response
* @return boolean success state * @return boolean success state
*/ */
@ -192,12 +198,13 @@ class Activitypub_explorer
* potential ActivityPub remote profiles, as so it is important to use * potential ActivityPub remote profiles, as so it is important to use
* this hacky workaround (at least for now) * this hacky workaround (at least for now)
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $v URL * @param string $v URL
* @return boolean|Profile false if fails | Profile object if successful * @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 if (empty ($i)) { // false = cache miss
$i = new Profile; $i = new Profile;
$result = $i->get ("profileurl", $v); $result = $i->get ("profileurl", $v);
@ -214,10 +221,11 @@ class Activitypub_explorer
/** /**
* Given a valid actor profile url returns its inboxes * Given a valid actor profile url returns its inboxes
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $url of Actor profile * @param string $url of Actor profile
* @return boolean|array false if fails | array with inbox and shared inbox if successful * @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 (); $client = new HTTPClient ();
$headers = array(); $headers = array();

View File

@ -51,6 +51,7 @@ class Activitypub_postman
/** /**
* Create a postman to deliver something to someone * Create a postman to deliver something to someone
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Profile of sender * @param Profile of sender
* @param Activitypub_profile $to array of destinataries * @param Activitypub_profile $to array of destinataries
*/ */
@ -66,6 +67,8 @@ class Activitypub_postman
/** /**
* Send a follow notification to remote instance * Send a follow notification to remote instance
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
*/ */
public function follow () public function follow ()
{ {
@ -79,6 +82,8 @@ class Activitypub_postman
/** /**
* Send a Undo Follow notification to remote instance * Send a Undo Follow notification to remote instance
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
*/ */
public function undo_follow () public function undo_follow ()
{ {
@ -97,6 +102,7 @@ class Activitypub_postman
/** /**
* Send a Like notification to remote instances holding the notice * Send a Like notification to remote instances holding the notice
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice * @param Notice $notice
*/ */
public function like ($notice) public function like ($notice)
@ -114,6 +120,7 @@ class Activitypub_postman
/** /**
* Send a Undo Like notification to remote instances holding the notice * Send a Undo Like notification to remote instances holding the notice
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice * @param Notice $notice
*/ */
public function undo_like ($notice) public function undo_like ($notice)
@ -135,6 +142,7 @@ class Activitypub_postman
/** /**
* Send a Announce notification to remote instances * Send a Announce notification to remote instances
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice * @param Notice $notice
*/ */
public function announce ($notice) public function announce ($notice)
@ -156,6 +164,7 @@ class Activitypub_postman
/** /**
* Send a Create notification to remote instances * Send a Create notification to remote instances
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice * @param Notice $notice
*/ */
public function create ($notice) public function create ($notice)
@ -183,6 +192,7 @@ class Activitypub_postman
/** /**
* Send a Delete notification to remote instances holding the notice * Send a Delete notification to remote instances holding the notice
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice * @param Notice $notice
*/ */
public function delete ($notice) public function delete ($notice)
@ -201,6 +211,7 @@ class Activitypub_postman
/** /**
* Clean list of inboxes to deliver messages * Clean list of inboxes to deliver messages
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return array To Inbox URLs * @return array To Inbox URLs
*/ */
private function to_inbox () private function to_inbox ()