X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;ds=inline;f=plugins%2FOStatus%2Fclasses%2FOstatus_profile.php;h=e3b3daa2c56e7d4fa8037119936a4991794a8ff5;hb=727ea5a5163249eb40fa0c4b2c63054fc997473b;hp=73f5d23229c705df09405a8e34e25e3ad8e3d0c3;hpb=4761c07ad8d76f7c34d4db53d32d15e806ba1e88;p=quix0rs-gnu-social.git
diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php
index 73f5d23229..e3b3daa2c5 100644
--- a/plugins/OStatus/classes/Ostatus_profile.php
+++ b/plugins/OStatus/classes/Ostatus_profile.php
@@ -194,52 +194,6 @@ class Ostatus_profile extends Memcached_DataObject
}
}
- /**
- * Subscribe a local user to this remote user.
- * PuSH subscription will be started if necessary, and we'll
- * send a Salmon notification to the remote server if available
- * notifying them of the sub.
- *
- * @param User $user
- * @return boolean success
- * @throws FeedException
- */
- public function subscribeLocalToRemote(User $user)
- {
- if ($this->isGroup()) {
- throw new ServerException("Can't subscribe to a remote group");
- }
-
- if ($this->subscribe()) {
- if ($user->subscribeTo($this->localProfile())) {
- $this->notify($user->getProfile(), ActivityVerb::FOLLOW, $this);
- return true;
- }
- }
- return false;
- }
-
- /**
- * Mark this remote profile as subscribing to the given local user,
- * and send appropriate notifications to the user.
- *
- * This will generally be in response to a subscription notification
- * from a foreign site to our local Salmon response channel.
- *
- * @param User $user
- * @return boolean success
- */
- public function subscribeRemoteToLocal(User $user)
- {
- if ($this->isGroup()) {
- throw new ServerException("Remote groups can't subscribe to local users");
- }
-
- Subscription::start($this->localProfile(), $user->getProfile());
-
- return true;
- }
-
/**
* Send a subscription request to the hub for this feed.
* The hub will later send us a confirmation POST to /main/push/callback.
@@ -250,12 +204,13 @@ class Ostatus_profile extends Memcached_DataObject
public function subscribe()
{
$feedsub = FeedSub::ensureFeed($this->feeduri);
- if ($feedsub->sub_state == 'active' || $feedsub->sub_state == 'subscribe') {
+ if ($feedsub->sub_state == 'active') {
+ // Active subscription, we don't need to do anything.
return true;
- } else if ($feedsub->sub_state == '' || $feedsub->sub_state == 'inactive') {
+ } else {
+ // Inactive or we got left in an inconsistent state.
+ // Run a subscription request to make sure we're current!
return $feedsub->subscribe();
- } else if ('unsubscribe') {
- throw new FeedSubException("Unsub is pending, can't subscribe...");
}
}
@@ -268,15 +223,13 @@ class Ostatus_profile extends Memcached_DataObject
*/
public function unsubscribe() {
$feedsub = FeedSub::staticGet('uri', $this->feeduri);
- if (!$feedsub) {
+ if (!$feedsub || $feedsub->sub_state == '' || $feedsub->sub_state == 'inactive') {
+ // No active PuSH subscription, we can just leave it be.
return true;
- }
- if ($feedsub->sub_state == 'active') {
+ } else {
+ // PuSH subscription is either active or in an indeterminate state.
+ // Send an unsubscribe.
return $feedsub->unsubscribe();
- } else if ($feedsub->sub_state == '' || $feedsub->sub_state == 'inactive' || $feedsub->sub_state == 'unsubscribe') {
- return true;
- } else if ($feedsub->sub_state == 'subscribe') {
- throw new FeedSubException("Feed is awaiting subscription, can't unsub...");
}
}
@@ -435,11 +388,17 @@ class Ostatus_profile extends Memcached_DataObject
{
$feed = $doc->documentElement;
- if ($feed->localName != 'feed' || $feed->namespaceURI != Activity::ATOM) {
- common_log(LOG_ERR, __METHOD__ . ": not an Atom feed, ignoring");
- return;
+ if ($feed->localName == 'feed' && $feed->namespaceURI == Activity::ATOM) {
+ $this->processAtomFeed($feed, $source);
+ } else if ($feed->localName == 'rss') { // @fixme check namespace
+ $this->processRssFeed($feed, $source);
+ } else {
+ throw new Exception("Unknown feed format.");
}
+ }
+ public function processAtomFeed(DOMElement $feed, $source)
+ {
$entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
if ($entries->length == 0) {
common_log(LOG_ERR, __METHOD__ . ": no entries in feed update, ignoring");
@@ -452,6 +411,26 @@ class Ostatus_profile extends Memcached_DataObject
}
}
+ public function processRssFeed(DOMElement $rss, $source)
+ {
+ $channels = $rss->getElementsByTagName('channel');
+
+ if ($channels->length == 0) {
+ throw new Exception("RSS feed without a channel.");
+ } else if ($channels->length > 1) {
+ common_log(LOG_WARNING, __METHOD__ . ": more than one channel in an RSS feed");
+ }
+
+ $channel = $channels->item(0);
+
+ $items = $channel->getElementsByTagName('item');
+
+ for ($i = 0; $i < $items->length; $i++) {
+ $item = $items->item($i);
+ $this->processEntry($item, $channel, $source);
+ }
+ }
+
/**
* Process a posted entry from this feed source.
*
@@ -463,6 +442,18 @@ class Ostatus_profile extends Memcached_DataObject
{
$activity = new Activity($entry, $feed);
+ // @todo process all activity objects
+ switch ($activity->objects[0]->type) {
+ case ActivityObject::ARTICLE:
+ case ActivityObject::BLOGENTRY:
+ case ActivityObject::NOTE:
+ case ActivityObject::STATUS:
+ case ActivityObject::COMMENT:
+ break;
+ default:
+ throw new ClientException("Can't handle that kind of post.");
+ }
+
if ($activity->verb == ActivityVerb::POST) {
$this->processPost($activity, $source);
} else {
@@ -489,24 +480,27 @@ class Ostatus_profile extends Memcached_DataObject
return false;
}
} else {
- // Individual user feeds may contain only posts from themselves.
- // Authorship is validated against the profile URI on upper layers,
- // through PuSH setup or Salmon signature checks.
- $actorUri = self::getActorProfileURI($activity);
- if ($actorUri == $this->uri) {
- // Check if profile info has changed and update it
- $this->updateFromActivityObject($activity->actor);
+ $actor = $activity->actor;
+
+ if (empty($actor)) {
+ // OK here! assume the default
+ } else if ($actor->id == $this->uri || $actor->link == $this->uri) {
+ $this->updateFromActivityObject($actor);
} else {
- common_log(LOG_WARNING, "OStatus: skipping post with bad author: got $actorUri expected $this->uri");
- return false;
+ throw new Exception("Got an actor '{$actor->title}' ({$actor->id}) on single-user feed for {$this->uri}");
}
+
$oprofile = $this;
}
+ // It's not always an ActivityObject::NOTE, but... let's just say it is.
+
+ $note = $activity->objects[0];
+
// The id URI will be used as a unique identifier for for the notice,
// protecting against duplicate saves. It isn't required to be a URL;
// tag: URIs for instance are found in Google Buzz feeds.
- $sourceUri = $activity->object->id;
+ $sourceUri = $note->id;
$dupe = Notice::staticGet('uri', $sourceUri);
if ($dupe) {
common_log(LOG_INFO, "OStatus: ignoring duplicate post: $sourceUri");
@@ -515,16 +509,30 @@ class Ostatus_profile extends Memcached_DataObject
// We'll also want to save a web link to the original notice, if provided.
$sourceUrl = null;
- if ($activity->object->link) {
- $sourceUrl = $activity->object->link;
+ if ($note->link) {
+ $sourceUrl = $note->link;
} else if ($activity->link) {
$sourceUrl = $activity->link;
- } else if (preg_match('!^https?://!', $activity->object->id)) {
- $sourceUrl = $activity->object->id;
+ } else if (preg_match('!^https?://!', $note->id)) {
+ $sourceUrl = $note->id;
+ }
+
+ // Use summary as fallback for content
+
+ if (!empty($note->content)) {
+ $sourceContent = $note->content;
+ } else if (!empty($note->summary)) {
+ $sourceContent = $note->summary;
+ } else if (!empty($note->title)) {
+ $sourceContent = $note->title;
+ } else {
+ // @fixme fetch from $sourceUrl?
+ throw new ClientException("No content for notice {$sourceUri}");
}
// Get (safe!) HTML and text versions of the content
- $rendered = $this->purify($activity->object->content);
+
+ $rendered = $this->purify($sourceContent);
$content = html_entity_decode(strip_tags($rendered));
$shortened = common_shorten_links($content);
@@ -535,21 +543,29 @@ class Ostatus_profile extends Memcached_DataObject
$attachment = null;
if (Notice::contentTooLong($shortened)) {
- $attachment = $this->saveHTMLFile($activity->object->title, $rendered);
- $summary = $activity->object->summary;
+ $attachment = $this->saveHTMLFile($note->title, $rendered);
+ $summary = html_entity_decode(strip_tags($note->summary));
if (empty($summary)) {
$summary = $content;
}
$shortSummary = common_shorten_links($summary);
if (Notice::contentTooLong($shortSummary)) {
- $url = common_shorten_url(common_local_url('attachment',
- array('attachment' => $attachment->id)));
+ $url = common_shorten_url($sourceUrl);
$shortSummary = substr($shortSummary,
0,
Notice::maxContent() - (mb_strlen($url) + 2));
- $shortSummary .= '⦠' . $url;
- $content = $shortSummary;
- $rendered = common_render_text($content);
+ $content = $shortSummary . ' ' . $url;
+
+ // We mark up the attachment link specially for the HTML output
+ // so we can fold-out the full version inline.
+ $attachUrl = common_local_url('attachment',
+ array('attachment' => $attachment->id));
+ $rendered = common_render_text($shortSummary) .
+ '' .
+ '…' .
+ '';
}
}
@@ -704,9 +720,14 @@ class Ostatus_profile extends Memcached_DataObject
}
/**
+ * Look up and if necessary create an Ostatus_profile for the remote entity
+ * with the given profile page URL. This should never return null -- you
+ * will either get an object or an exception will be thrown.
+ *
* @param string $profile_url
* @return Ostatus_profile
- * @throws FeedSubException
+ * @throws Exception on various error conditions
+ * @throws OStatusShadowException if this reference would obscure a local user/group
*/
public static function ensureProfileURL($profile_url, $hints=array())
@@ -727,7 +748,7 @@ class Ostatus_profile extends Memcached_DataObject
$response = $client->get($profile_url);
if (!$response->isOk()) {
- return null;
+ throw new Exception("Could not reach profile page: " . $profile_url);
}
// Check if we have a non-canonical URL
@@ -781,11 +802,20 @@ class Ostatus_profile extends Memcached_DataObject
if (!empty($feedurl)) {
$hints['feedurl'] = $feedurl;
-
return self::ensureFeedURL($feedurl, $hints);
}
+
+ throw new Exception("Could not find a feed URL for profile page " . $finalUrl);
}
+ /**
+ * Look up the Ostatus_profile, if present, for a remote entity with the
+ * given profile page URL. Will return null for both unknown and invalid
+ * remote profiles.
+ *
+ * @return mixed Ostatus_profile or null
+ * @throws OStatusShadowException for local profiles
+ */
static function getFromProfileURL($profile_url)
{
$profile = Profile::staticGet('profileurl', $profile_url);
@@ -807,7 +837,7 @@ class Ostatus_profile extends Memcached_DataObject
$user = User::staticGet('id', $profile->id);
if (!empty($user)) {
- throw new Exception("'$profile_url' is the profile for local user '{$user->nickname}'.");
+ throw new OStatusShadowException($profile, "'$profile_url' is the profile for local user '{$user->nickname}'.");
}
// Continue discovery; it's a remote profile
@@ -817,6 +847,14 @@ class Ostatus_profile extends Memcached_DataObject
return null;
}
+ /**
+ * Look up and if necessary create an Ostatus_profile for remote entity
+ * with the given update feed. This should never return null -- you will
+ * either get an object or an exception will be thrown.
+ *
+ * @return Ostatus_profile
+ * @throws Exception
+ */
public static function ensureFeedURL($feed_url, $hints=array())
{
$discover = new FeedDiscovery();
@@ -834,9 +872,32 @@ class Ostatus_profile extends Memcached_DataObject
throw new FeedSubNoHubException();
}
- // Try to get a profile from the feed activity:subject
+ $feedEl = $discover->root;
- $feedEl = $discover->feed->documentElement;
+ if ($feedEl->tagName == 'feed') {
+ return self::ensureAtomFeed($feedEl, $hints);
+ } else if ($feedEl->tagName == 'channel') {
+ return self::ensureRssChannel($feedEl, $hints);
+ } else {
+ throw new FeedSubBadXmlException($feeduri);
+ }
+ }
+
+ /**
+ * Look up and, if necessary, create an Ostatus_profile for the remote
+ * profile with the given Atom feed - actually loaded from the feed.
+ * This should never return null -- you will either get an object or
+ * an exception will be thrown.
+ *
+ * @param DOMElement $feedEl root element of a loaded Atom feed
+ * @param array $hints additional discovery information passed from higher levels
+ * @fixme should this be marked public?
+ * @return Ostatus_profile
+ * @throws Exception
+ */
+ public static function ensureAtomFeed($feedEl, $hints)
+ {
+ // Try to get a profile from the feed activity:subject
$subject = ActivityUtils::child($feedEl, Activity::SUBJECT, Activity::SPEC);
@@ -857,7 +918,7 @@ class Ostatus_profile extends Memcached_DataObject
// Sheesh. Not a very nice feed! Let's try fingerpoken in the
// entries.
- $entries = $discover->feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
+ $entries = $feedEl->getElementsByTagNameNS(Activity::ATOM, 'entry');
if (!empty($entries) && $entries->length > 0) {
@@ -885,8 +946,51 @@ class Ostatus_profile extends Memcached_DataObject
}
/**
+ * Look up and, if necessary, create an Ostatus_profile for the remote
+ * profile with the given RSS feed - actually loaded from the feed.
+ * This should never return null -- you will either get an object or
+ * an exception will be thrown.
*
+ * @param DOMElement $feedEl root element of a loaded RSS feed
+ * @param array $hints additional discovery information passed from higher levels
+ * @fixme should this be marked public?
+ * @return Ostatus_profile
+ * @throws Exception
+ */
+ public static function ensureRssChannel($feedEl, $hints)
+ {
+ // Special-case for Posterous. They have some nice metadata in their
+ // posterous:author elements. We should use them instead of the channel.
+
+ $items = $feedEl->getElementsByTagName('item');
+
+ if ($items->length > 0) {
+ $item = $items->item(0);
+ $authorEl = ActivityUtils::child($item, ActivityObject::AUTHOR, ActivityObject::POSTEROUS);
+ if (!empty($authorEl)) {
+ $obj = ActivityObject::fromPosterousAuthor($authorEl);
+ // Posterous has multiple authors per feed, and multiple feeds
+ // per author. We check if this is the "main" feed for this author.
+ if (array_key_exists('profileurl', $hints) &&
+ !empty($obj->poco) &&
+ common_url_to_nickname($hints['profileurl']) == $obj->poco->preferredUsername) {
+ return self::ensureActivityObjectProfile($obj, $hints);
+ }
+ }
+ }
+
+ // @fixme we should check whether this feed has elements
+ // with different or elements, and... I dunno.
+ // Do something about that.
+
+ $obj = ActivityObject::fromRssChannel($feedEl);
+
+ return self::ensureActivityObjectProfile($obj, $hints);
+ }
+
+ /**
* Download and update given avatar image
+ *
* @param string $url
* @throws Exception in various failure cases
*/
@@ -896,6 +1000,9 @@ class Ostatus_profile extends Memcached_DataObject
// We've already got this one.
return;
}
+ if (!common_valid_http_url($url)) {
+ throw new ServerException(_m("Invalid avatar URL %s"), $url);
+ }
if ($this->isGroup()) {
$self = $this->localGroup();
@@ -1013,11 +1120,14 @@ class Ostatus_profile extends Memcached_DataObject
/**
* Fetch, or build if necessary, an Ostatus_profile for the actor
* in a given Activity Streams activity.
+ * This should never return null -- you will either get an object or
+ * an exception will be thrown.
*
* @param Activity $activity
* @param string $feeduri if we already know the canonical feed URI!
* @param string $salmonuri if we already know the salmon return channel URI
* @return Ostatus_profile
+ * @throws Exception
*/
public static function ensureActorProfile($activity, $hints=array())
@@ -1025,6 +1135,18 @@ class Ostatus_profile extends Memcached_DataObject
return self::ensureActivityObjectProfile($activity->actor, $hints);
}
+ /**
+ * Fetch, or build if necessary, an Ostatus_profile for the profile
+ * in a given Activity Streams object (can be subject, actor, or object).
+ * This should never return null -- you will either get an object or
+ * an exception will be thrown.
+ *
+ * @param ActivityObject $object
+ * @param array $hints additional discovery information passed from higher levels
+ * @return Ostatus_profile
+ * @throws Exception
+ */
+
public static function ensureActivityObjectProfile($object, $hints=array())
{
$profile = self::getActivityObjectProfile($object);
@@ -1039,35 +1161,45 @@ class Ostatus_profile extends Memcached_DataObject
/**
* @param Activity $activity
* @return mixed matching Ostatus_profile or false if none known
+ * @throws ServerException if feed info invalid
*/
public static function getActorProfile($activity)
{
return self::getActivityObjectProfile($activity->actor);
}
+ /**
+ * @param ActivityObject $activity
+ * @return mixed matching Ostatus_profile or false if none known
+ * @throws ServerException if feed info invalid
+ */
protected static function getActivityObjectProfile($object)
{
$uri = self::getActivityObjectProfileURI($object);
return Ostatus_profile::staticGet('uri', $uri);
}
- protected static function getActorProfileURI($activity)
- {
- return self::getActivityObjectProfileURI($activity->actor);
- }
-
/**
- * @param Activity $activity
+ * Get the identifier URI for the remote entity described
+ * by this ActivityObject. This URI is *not* guaranteed to be
+ * a resolvable HTTP/HTTPS URL.
+ *
+ * @param ActivityObject $object
* @return string
- * @throws ServerException
+ * @throws ServerException if feed info invalid
*/
protected static function getActivityObjectProfileURI($object)
{
- $opts = array('allowed_schemes' => array('http', 'https'));
- if ($object->id && Validate::uri($object->id, $opts)) {
- return $object->id;
+ if ($object->id) {
+ if (ActivityUtils::validateUri($object->id)) {
+ return $object->id;
+ }
}
- if ($object->link && Validate::uri($object->link, $opts)) {
+
+ // If the id is missing or invalid (we've seen feeds mistakenly listing
+ // things like local usernames in that field) then we'll use the profile
+ // page link, if valid.
+ if ($object->link && common_valid_http_url($object->link)) {
return $object->link;
}
throw new ServerException("No author ID URI found");
@@ -1080,6 +1212,8 @@ class Ostatus_profile extends Memcached_DataObject
/**
* Create local ostatus_profile and profile/user_group entries for
* the provided remote user or group.
+ * This should never return null -- you will either get an object or
+ * an exception will be thrown.
*
* @param ActivityObject $object
* @param array $hints
@@ -1096,7 +1230,8 @@ class Ostatus_profile extends Memcached_DataObject
throw new Exception("No profile URI");
}
- if (OStatusPlugin::localProfileFromUrl($homeuri)) {
+ $user = User::staticGet('uri', $homeuri);
+ if ($user) {
throw new Exception("Local user can't be referenced as remote.");
}
@@ -1343,9 +1478,19 @@ class Ostatus_profile extends Memcached_DataObject
return $hints['nickname'];
}
- // Try the definitive ID
+ // Try the profile url (like foo.example.com or example.com/user/foo)
+
+ $profileUrl = ($object->link) ? $object->link : $hints['profileurl'];
- $nickname = self::nicknameFromURI($object->id);
+ if (!empty($profileUrl)) {
+ $nickname = self::nicknameFromURI($profileUrl);
+ }
+
+ // Try the URI (may be a tag:, http:, acct:, ...
+
+ if (empty($nickname)) {
+ $nickname = self::nicknameFromURI($object->id);
+ }
// Try a Webfinger if one was passed (way) down
@@ -1386,9 +1531,15 @@ class Ostatus_profile extends Memcached_DataObject
}
/**
+ * Look up, and if necessary create, an Ostatus_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.
+ *
* @param string $addr webfinger address
* @return Ostatus_profile
* @throws Exception on error conditions
+ * @throws OStatusShadowException if this reference would obscure a local user/group
*/
public static function ensureWebfinger($addr)
{
@@ -1449,7 +1600,7 @@ class Ostatus_profile extends Memcached_DataObject
if (array_key_exists('feedurl', $hints)) {
try {
- common_log(LOG_INFO, "Discovery on acct:$addr with feed URL $feedUrl");
+ common_log(LOG_INFO, "Discovery on acct:$addr with feed URL " . $hints['feedurl']);
$oprofile = self::ensureFeedURL($hints['feedurl'], $hints);
self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri);
return $oprofile;
@@ -1464,12 +1615,21 @@ class Ostatus_profile extends Memcached_DataObject
if (array_key_exists('profileurl', $hints)) {
try {
common_log(LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl");
- $oprofile = self::ensureProfile($hints['profileurl'], $hints);
+ $oprofile = self::ensureProfileURL($hints['profileurl'], $hints);
self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri);
return $oprofile;
+ } catch (OStatusShadowException $e) {
+ // We've ended up with a remote reference to a local user or group.
+ // @fixme ideally we should be able to say who it was so we can
+ // go back and refer to it the regular way
+ throw $e;
} catch (Exception $e) {
common_log(LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage());
// keep looking
+ //
+ // @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.
}
}
@@ -1526,10 +1686,22 @@ class Ostatus_profile extends Memcached_DataObject
throw new Exception("Couldn't find a valid profile for '$addr'");
}
+ /**
+ * Store the full-length scrubbed HTML of a remote notice to an attachment
+ * file on our server. We'll link to this at the end of the cropped version.
+ *
+ * @param string $title plaintext for HTML page's title
+ * @param string $rendered HTML fragment for HTML page's body
+ * @return File
+ */
function saveHTMLFile($title, $rendered)
{
- $final = sprintf("\n%s".
- '%s
',
+ $final = sprintf("\n" .
+ '' .
+ '' .
+ '%s' .
+ '' .
+ '%s',
htmlspecialchars($title),
$rendered);
@@ -1559,3 +1731,24 @@ class Ostatus_profile extends Memcached_DataObject
return $file;
}
}
+
+/**
+ * Exception indicating we've got a remote reference to a local user,
+ * not a remote user!
+ *
+ * If we can ue a local profile after all, it's available as $e->profile.
+ */
+class OStatusShadowException extends Exception
+{
+ public $profile;
+
+ /**
+ * @param Profile $profile
+ * @param string $message
+ */
+ function __construct($profile, $message) {
+ $this->profile = $profile;
+ parent::__construct($message);
+ }
+}
+