From dc09453a77f33c4dfdff306321ce93cf5fbd2d57 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Feb 2010 11:06:03 -0800 Subject: First steps on converting FeedSub into the pub/sub basis for OStatus communications: * renamed FeedSub plugin to OStatus * now setting avatar on subscriptions * general fixes for subscription * integrated PuSH hub to handle only user timelines on canonical ID url; sends updates directly * set $config['feedsub']['nohub'] = true to test w/ foreign feeds that don't have hubs (won't actually receive updates though) * a few bits of code documentation * HMAC support for verified distributions (safest if sub setup is on HTTPS) And a couple core changes: * minimizing HTML output for exceptions in API requests to aid in debugging * fix for rel=self link in apitimelineuser when id given This does not not yet include any of the individual subscription management (Salmon notifications for sub/unsub, etc) nor a nice UI for user subscriptions. Needs some further cleanup to treat posts as status updates instead of link references. --- plugins/OStatus/lib/feeddiscovery.php | 221 ++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 plugins/OStatus/lib/feeddiscovery.php (limited to 'plugins/OStatus/lib/feeddiscovery.php') diff --git a/plugins/OStatus/lib/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php new file mode 100644 index 000000000..9bc7892fb --- /dev/null +++ b/plugins/OStatus/lib/feeddiscovery.php @@ -0,0 +1,221 @@ +. + */ + +/** + * @package FeedSubPlugin + * @maintainer Brion Vibber + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +class FeedSubBadURLException extends FeedSubException +{ +} + +class FeedSubBadResponseException extends FeedSubException +{ +} + +class FeedSubEmptyException extends FeedSubException +{ +} + +class FeedSubBadHTMLException extends FeedSubException +{ +} + +class FeedSubUnrecognizedTypeException extends FeedSubException +{ +} + +class FeedSubNoFeedException extends FeedSubException +{ +} + +/** + * Given a web page or feed URL, discover the final location of the feed + * and return its current contents. + * + * @example + * $feed = new FeedDiscovery(); + * if ($feed->discoverFromURL($url)) { + * print $feed->uri; + * print $feed->type; + * processFeed($feed->body); + * } + */ +class FeedDiscovery +{ + public $uri; + public $type; + public $body; + + + public function feedMunger() + { + require_once 'XML/Feed/Parser.php'; + $feed = new XML_Feed_Parser($this->body, false, false, true); // @fixme + return new FeedMunger($feed, $this->uri); + } + + /** + * @param string $url + * @param bool $htmlOk pass false here if you don't want to follow web pages. + * @return string with validated URL + * @throws FeedSubBadURLException + * @throws FeedSubBadHtmlException + * @throws FeedSubNoFeedException + * @throws FeedSubEmptyException + * @throws FeedSubUnrecognizedTypeException + */ + function discoverFromURL($url, $htmlOk=true) + { + try { + $client = new HTTPClient(); + $response = $client->get($url); + } catch (HTTP_Request2_Exception $e) { + throw new FeedSubBadURLException($e); + } + + if ($htmlOk) { + $type = $response->getHeader('Content-Type'); + $isHtml = preg_match('!^(text/html|application/xhtml\+xml)!i', $type); + if ($isHtml) { + $target = $this->discoverFromHTML($response->getUrl(), $response->getBody()); + if (!$target) { + throw new FeedSubNoFeedException($url); + } + return $this->discoverFromURL($target, false); + } + } + + return $this->initFromResponse($response); + } + + function initFromResponse($response) + { + if (!$response->isOk()) { + throw new FeedSubBadResponseException($response->getCode()); + } + + $sourceurl = $response->getUrl(); + $body = $response->getBody(); + if (!$body) { + throw new FeedSubEmptyException($sourceurl); + } + + $type = $response->getHeader('Content-Type'); + if (preg_match('!^(text/xml|application/xml|application/(rss|atom)\+xml)!i', $type)) { + $this->uri = $sourceurl; + $this->type = $type; + $this->body = $body; + return true; + } else { + common_log(LOG_WARNING, "Unrecognized feed type $type for $sourceurl"); + throw new FeedSubUnrecognizedTypeException($type); + } + } + + /** + * @param string $url source URL, used to resolve relative links + * @param string $body HTML body text + * @return mixed string with URL or false if no target found + */ + function discoverFromHTML($url, $body) + { + // DOMDocument::loadHTML may throw warnings on unrecognized elements. + $old = error_reporting(error_reporting() & ~E_WARNING); + $dom = new DOMDocument(); + $ok = $dom->loadHTML($body); + error_reporting($old); + + if (!$ok) { + throw new FeedSubBadHtmlException(); + } + + // Autodiscovery links may be relative to the page's URL or + $base = false; + $nodes = $dom->getElementsByTagName('base'); + for ($i = 0; $i < $nodes->length; $i++) { + $node = $nodes->item($i); + if ($node->hasAttributes()) { + $href = $node->attributes->getNamedItem('href'); + if ($href) { + $base = trim($href->value); + } + } + } + if ($base) { + $base = $this->resolveURI($base, $url); + } else { + $base = $url; + } + + // Ok... now on to the links! + // @fixme merge with the munger link checks + $nodes = $dom->getElementsByTagName('link'); + for ($i = 0; $i < $nodes->length; $i++) { + $node = $nodes->item($i); + if ($node->hasAttributes()) { + $rel = $node->attributes->getNamedItem('rel'); + $type = $node->attributes->getNamedItem('type'); + $href = $node->attributes->getNamedItem('href'); + if ($rel && $type && $href) { + $rel = trim($rel->value); + $type = trim($type->value); + $href = trim($href->value); + + $feedTypes = array( + 'application/rss+xml', + 'application/atom+xml', + ); + if (trim($rel) == 'alternate' && in_array($type, $feedTypes)) { + return $this->resolveURI($href, $base); + } + } + } + } + + return false; + } + + /** + * Resolve a possibly relative URL against some absolute base URL + * @param string $rel relative or absolute URL + * @param string $base absolute URL + * @return string absolute URL, or original URL if could not be resolved. + */ + function resolveURI($rel, $base) + { + require_once "Net/URL2.php"; + try { + $relUrl = new Net_URL2($rel); + if ($relUrl->isAbsolute()) { + return $rel; + } + $baseUrl = new Net_URL2($base); + $absUrl = $baseUrl->resolve($relUrl); + return $absUrl->getURL(); + } catch (Exception $e) { + common_log(LOG_WARNING, 'Unable to resolve relative link "' . + $rel . '" against base "' . $base . '": ' . $e->getMessage()); + return $rel; + } + } +} -- cgit v1.2.3-54-g00ecf From 384387c9b05aefb438f5dbe7e272b1f234ede172 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Feb 2010 14:06:36 -0800 Subject: OStatus cleanup... * Treat linkless feed posts as status updates; drop the "New post:" prefix and quotes on them. * Use stable user IDs for atom/rss2 feed links instead of unstable nicknames * Pull Atom feed preferentially when subscribing -- can now put the remote user's profile page straight into the feed subscription form and get to the right place. * Clean up naming for push endpoints --- actions/showstream.php | 4 +- classes/Notice.php | 4 + lib/util.php | 3 + plugins/OStatus/OStatusPlugin.php | 8 +- plugins/OStatus/actions/feedsubcallback.php | 105 ----------------- plugins/OStatus/actions/hub.php | 176 ---------------------------- plugins/OStatus/actions/pushcallback.php | 105 +++++++++++++++++ plugins/OStatus/actions/pushhub.php | 176 ++++++++++++++++++++++++++++ plugins/OStatus/classes/Feedinfo.php | 2 +- plugins/OStatus/lib/feeddiscovery.php | 22 +++- plugins/OStatus/lib/feedmunger.php | 47 +++++--- 11 files changed, 340 insertions(+), 312 deletions(-) delete mode 100644 plugins/OStatus/actions/feedsubcallback.php delete mode 100644 plugins/OStatus/actions/hub.php create mode 100644 plugins/OStatus/actions/pushcallback.php create mode 100644 plugins/OStatus/actions/pushhub.php (limited to 'plugins/OStatus/lib/feeddiscovery.php') diff --git a/actions/showstream.php b/actions/showstream.php index 07cc68b76..f9407e35a 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -131,14 +131,14 @@ class ShowstreamAction extends ProfileAction new Feed(Feed::RSS2, common_local_url('ApiTimelineUser', array( - 'id' => $this->user->nickname, + 'id' => $this->user->id, 'format' => 'rss')), sprintf(_('Notice feed for %s (RSS 2.0)'), $this->user->nickname)), new Feed(Feed::ATOM, common_local_url('ApiTimelineUser', array( - 'id' => $this->user->nickname, + 'id' => $this->user->id, 'format' => 'atom')), sprintf(_('Notice feed for %s (Atom)'), $this->user->nickname)), diff --git a/classes/Notice.php b/classes/Notice.php index f9f386357..fca1c599c 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1176,6 +1176,10 @@ class Notice extends Memcached_DataObject // Figure out who that is. $sender = Profile::staticGet('id', $profile_id); + if (empty($sender)) { + return null; + } + $recipient = common_relative_profile($sender, $nickname, common_sql_now()); if (empty($recipient)) { diff --git a/lib/util.php b/lib/util.php index f0f262dc5..00c21aeb2 100644 --- a/lib/util.php +++ b/lib/util.php @@ -665,6 +665,9 @@ function common_valid_profile_tag($str) function common_at_link($sender_id, $nickname) { $sender = Profile::staticGet($sender_id); + if (!$sender) { + return $nickname; + } $recipient = common_relative_profile($sender, common_canonical_nickname($nickname)); if ($recipient) { $user = User::staticGet('id', $recipient->id); diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 941912112..4e8b892c6 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -53,10 +53,10 @@ class OStatusPlugin extends Plugin */ function onRouterInitialized($m) { - $m->connect('push/hub', array('action' => 'hub')); + $m->connect('main/push/hub', array('action' => 'pushhub')); - $m->connect('feedsub/callback/:feed', - array('action' => 'feedsubcallback'), + $m->connect('main/push/callback/:feed', + array('action' => 'pushcallback'), array('feed' => '[0-9]+')); $m->connect('settings/feedsub', array('action' => 'feedsubsettings')); @@ -97,7 +97,7 @@ class OStatusPlugin extends Plugin // Canonical form of id in URL? // Updates will be handled for our internal PuSH hub. $action->element('link', array('rel' => 'hub', - 'href' => common_local_url('hub'))); + 'href' => common_local_url('pushhub'))); } } return true; diff --git a/plugins/OStatus/actions/feedsubcallback.php b/plugins/OStatus/actions/feedsubcallback.php deleted file mode 100644 index c57ea5b10..000000000 --- a/plugins/OStatus/actions/feedsubcallback.php +++ /dev/null @@ -1,105 +0,0 @@ -. - */ - -/** - * @package FeedSubPlugin - * @maintainer Brion Vibber - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - - -class FeedSubCallbackAction extends Action -{ - function handle() - { - parent::handle(); - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $this->handlePost(); - } else { - $this->handleGet(); - } - } - - /** - * Handler for POST content updates from the hub - */ - function handlePost() - { - $feedid = $this->arg('feed'); - common_log(LOG_INFO, "POST for feed id $feedid"); - if (!$feedid) { - throw new ServerException('Empty or invalid feed id', 400); - } - - $feedinfo = Feedinfo::staticGet('id', $feedid); - if (!$feedinfo) { - throw new ServerException('Unknown feed id ' . $feedid, 400); - } - - $hmac = ''; - if (isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) { - $hmac = $_SERVER['HTTP_X_HUB_SIGNATURE']; - } - - $post = file_get_contents('php://input'); - $feedinfo->postUpdates($post, $hmac); - } - - /** - * Handler for GET verification requests from the hub - */ - function handleGet() - { - $mode = $this->arg('hub_mode'); - $topic = $this->arg('hub_topic'); - $challenge = $this->arg('hub_challenge'); - $lease_seconds = $this->arg('hub_lease_seconds'); - $verify_token = $this->arg('hub_verify_token'); - - if ($mode != 'subscribe' && $mode != 'unsubscribe') { - common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\""); - throw new ServerException("Bogus hub callback: bad mode", 404); - } - - $feedinfo = Feedinfo::staticGet('feeduri', $topic); - if (!$feedinfo) { - common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); - throw new ServerException("Bogus hub callback: unknown feed", 404); - } - - # Can't currently set the token in our sub api - #if ($feedinfo->verify_token !== $verify_token) { - # common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); - # throw new ServerError("Bogus hub callback: bad token", 404); - #} - - // OK! - common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); - $feedinfo->sub_start = common_sql_date(time()); - if ($lease_seconds > 0) { - $feedinfo->sub_end = common_sql_date(time() + $lease_seconds); - } else { - $feedinfo->sub_end = null; - } - $feedinfo->update(); - - print $challenge; - } -} diff --git a/plugins/OStatus/actions/hub.php b/plugins/OStatus/actions/hub.php deleted file mode 100644 index 5caf4b48e..000000000 --- a/plugins/OStatus/actions/hub.php +++ /dev/null @@ -1,176 +0,0 @@ -. - */ - -/** - * Integrated PuSH hub; lets us only ping them what need it. - * @package Hub - * @maintainer Brion Vibber - */ - -/** - - -Things to consider... -* should we purge incomplete subscriptions that never get a verification pingback? -* when can we send subscription renewal checks? - - at next send time probably ok -* when can we handle trimming of subscriptions? - - at next send time probably ok -* should we keep a fail count? - -*/ - - -class HubAction extends Action -{ - function arg($arg, $def=null) - { - // PHP converts '.'s in incoming var names to '_'s. - // It also merges multiple values, which'll break hub.verify and hub.topic for publishing - // @fixme handle multiple args - $arg = str_replace('.', '_', $arg); - return parent::arg($arg, $def); - } - - function prepare($args) - { - StatusNet::setApi(true); // reduce exception reports to aid in debugging - return parent::prepare($args); - } - - function handle() - { - $mode = $this->trimmed('hub.mode'); - switch ($mode) { - case "subscribe": - $this->subscribe(); - break; - case "unsubscribe": - $this->unsubscribe(); - break; - case "publish": - throw new ServerException("Publishing outside feeds not supported.", 400); - default: - throw new ServerException("Unrecognized mode '$mode'.", 400); - } - } - - /** - * Process a PuSH feed subscription request. - * - * HTTP return codes: - * 202 Accepted - request saved and awaiting verification - * 204 No Content - already subscribed - * 403 Forbidden - rejecting this (not specifically spec'd) - */ - function subscribe() - { - $feed = $this->argUrl('hub.topic'); - $callback = $this->argUrl('hub.callback'); - - common_log(LOG_DEBUG, __METHOD__ . ": checking sub'd to $feed $callback"); - if ($this->getSub($feed, $callback)) { - // Already subscribed; return 204 per spec. - header('HTTP/1.1 204 No Content'); - common_log(LOG_DEBUG, __METHOD__ . ': already subscribed'); - return; - } - - common_log(LOG_DEBUG, __METHOD__ . ': setting up'); - $sub = new HubSub(); - $sub->topic = $feed; - $sub->callback = $callback; - $sub->secret = $this->arg('hub.secret', null); - $sub->setLease(intval($this->arg('hub.lease_seconds'))); - - // @fixme check for feeds we don't manage - // @fixme check the verification mode, might want a return immediately? - - common_log(LOG_DEBUG, __METHOD__ . ': inserting'); - $ok = $sub->insert(); - - if (!$ok) { - throw new ServerException("Failed to save subscription record", 500); - } - - // @fixme check errors ;) - - $data = array('sub' => $sub, 'mode' => 'subscribe'); - $qm = QueueManager::get(); - $qm->enqueue($data, 'hubverify'); - - header('HTTP/1.1 202 Accepted'); - common_log(LOG_DEBUG, __METHOD__ . ': done'); - } - - /** - * Process a PuSH feed unsubscription request. - * - * HTTP return codes: - * 202 Accepted - request saved and awaiting verification - * 204 No Content - already subscribed - * 400 Bad Request - invalid params or rejected feed - */ - function unsubscribe() - { - $feed = $this->argUrl('hub.topic'); - $callback = $this->argUrl('hub.callback'); - $sub = $this->getSub($feed, $callback); - - if ($sub) { - if ($sub->verify('unsubscribe')) { - $sub->delete(); - common_log(LOG_INFO, "PuSH unsubscribed $feed for $callback"); - } else { - throw new ServerException("Failed PuSH unsubscription: verification failed! $feed for $callback"); - } - } else { - throw new ServerException("Failed PuSH unsubscription: not subscribed! $feed for $callback"); - } - } - - /** - * Grab and validate a URL from POST parameters. - * @throws ServerException for malformed or non-http/https URLs - */ - protected function argUrl($arg) - { - $url = $this->arg($arg); - $params = array('domain_check' => false, // otherwise breaks my local tests :P - 'allowed_schemes' => array('http', 'https')); - if (Validate::uri($url, $params)) { - return $url; - } else { - throw new ServerException("Invalid URL passed for $arg: '$url'", 400); - } - } - - /** - * Get HubSub subscription record for a given feed & subscriber. - * - * @param string $feed - * @param string $callback - * @return mixed HubSub or false - */ - protected function getSub($feed, $callback) - { - return HubSub::staticGet($feed, $callback); - } -} - diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php new file mode 100644 index 000000000..a5e02e08f --- /dev/null +++ b/plugins/OStatus/actions/pushcallback.php @@ -0,0 +1,105 @@ +. + */ + +/** + * @package FeedSubPlugin + * @maintainer Brion Vibber + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + + +class PushCallbackAction extends Action +{ + function handle() + { + parent::handle(); + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->handlePost(); + } else { + $this->handleGet(); + } + } + + /** + * Handler for POST content updates from the hub + */ + function handlePost() + { + $feedid = $this->arg('feed'); + common_log(LOG_INFO, "POST for feed id $feedid"); + if (!$feedid) { + throw new ServerException('Empty or invalid feed id', 400); + } + + $feedinfo = Feedinfo::staticGet('id', $feedid); + if (!$feedinfo) { + throw new ServerException('Unknown feed id ' . $feedid, 400); + } + + $hmac = ''; + if (isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) { + $hmac = $_SERVER['HTTP_X_HUB_SIGNATURE']; + } + + $post = file_get_contents('php://input'); + $feedinfo->postUpdates($post, $hmac); + } + + /** + * Handler for GET verification requests from the hub + */ + function handleGet() + { + $mode = $this->arg('hub_mode'); + $topic = $this->arg('hub_topic'); + $challenge = $this->arg('hub_challenge'); + $lease_seconds = $this->arg('hub_lease_seconds'); + $verify_token = $this->arg('hub_verify_token'); + + if ($mode != 'subscribe' && $mode != 'unsubscribe') { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\""); + throw new ServerException("Bogus hub callback: bad mode", 404); + } + + $feedinfo = Feedinfo::staticGet('feeduri', $topic); + if (!$feedinfo) { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); + throw new ServerException("Bogus hub callback: unknown feed", 404); + } + + # Can't currently set the token in our sub api + #if ($feedinfo->verify_token !== $verify_token) { + # common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); + # throw new ServerError("Bogus hub callback: bad token", 404); + #} + + // OK! + common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); + $feedinfo->sub_start = common_sql_date(time()); + if ($lease_seconds > 0) { + $feedinfo->sub_end = common_sql_date(time() + $lease_seconds); + } else { + $feedinfo->sub_end = null; + } + $feedinfo->update(); + + print $challenge; + } +} diff --git a/plugins/OStatus/actions/pushhub.php b/plugins/OStatus/actions/pushhub.php new file mode 100644 index 000000000..901c18f70 --- /dev/null +++ b/plugins/OStatus/actions/pushhub.php @@ -0,0 +1,176 @@ +. + */ + +/** + * Integrated PuSH hub; lets us only ping them what need it. + * @package Hub + * @maintainer Brion Vibber + */ + +/** + + +Things to consider... +* should we purge incomplete subscriptions that never get a verification pingback? +* when can we send subscription renewal checks? + - at next send time probably ok +* when can we handle trimming of subscriptions? + - at next send time probably ok +* should we keep a fail count? + +*/ + + +class PushHubAction extends Action +{ + function arg($arg, $def=null) + { + // PHP converts '.'s in incoming var names to '_'s. + // It also merges multiple values, which'll break hub.verify and hub.topic for publishing + // @fixme handle multiple args + $arg = str_replace('.', '_', $arg); + return parent::arg($arg, $def); + } + + function prepare($args) + { + StatusNet::setApi(true); // reduce exception reports to aid in debugging + return parent::prepare($args); + } + + function handle() + { + $mode = $this->trimmed('hub.mode'); + switch ($mode) { + case "subscribe": + $this->subscribe(); + break; + case "unsubscribe": + $this->unsubscribe(); + break; + case "publish": + throw new ServerException("Publishing outside feeds not supported.", 400); + default: + throw new ServerException("Unrecognized mode '$mode'.", 400); + } + } + + /** + * Process a PuSH feed subscription request. + * + * HTTP return codes: + * 202 Accepted - request saved and awaiting verification + * 204 No Content - already subscribed + * 403 Forbidden - rejecting this (not specifically spec'd) + */ + function subscribe() + { + $feed = $this->argUrl('hub.topic'); + $callback = $this->argUrl('hub.callback'); + + common_log(LOG_DEBUG, __METHOD__ . ": checking sub'd to $feed $callback"); + if ($this->getSub($feed, $callback)) { + // Already subscribed; return 204 per spec. + header('HTTP/1.1 204 No Content'); + common_log(LOG_DEBUG, __METHOD__ . ': already subscribed'); + return; + } + + common_log(LOG_DEBUG, __METHOD__ . ': setting up'); + $sub = new HubSub(); + $sub->topic = $feed; + $sub->callback = $callback; + $sub->secret = $this->arg('hub.secret', null); + $sub->setLease(intval($this->arg('hub.lease_seconds'))); + + // @fixme check for feeds we don't manage + // @fixme check the verification mode, might want a return immediately? + + common_log(LOG_DEBUG, __METHOD__ . ': inserting'); + $ok = $sub->insert(); + + if (!$ok) { + throw new ServerException("Failed to save subscription record", 500); + } + + // @fixme check errors ;) + + $data = array('sub' => $sub, 'mode' => 'subscribe'); + $qm = QueueManager::get(); + $qm->enqueue($data, 'hubverify'); + + header('HTTP/1.1 202 Accepted'); + common_log(LOG_DEBUG, __METHOD__ . ': done'); + } + + /** + * Process a PuSH feed unsubscription request. + * + * HTTP return codes: + * 202 Accepted - request saved and awaiting verification + * 204 No Content - already subscribed + * 400 Bad Request - invalid params or rejected feed + */ + function unsubscribe() + { + $feed = $this->argUrl('hub.topic'); + $callback = $this->argUrl('hub.callback'); + $sub = $this->getSub($feed, $callback); + + if ($sub) { + if ($sub->verify('unsubscribe')) { + $sub->delete(); + common_log(LOG_INFO, "PuSH unsubscribed $feed for $callback"); + } else { + throw new ServerException("Failed PuSH unsubscription: verification failed! $feed for $callback"); + } + } else { + throw new ServerException("Failed PuSH unsubscription: not subscribed! $feed for $callback"); + } + } + + /** + * Grab and validate a URL from POST parameters. + * @throws ServerException for malformed or non-http/https URLs + */ + protected function argUrl($arg) + { + $url = $this->arg($arg); + $params = array('domain_check' => false, // otherwise breaks my local tests :P + 'allowed_schemes' => array('http', 'https')); + if (Validate::uri($url, $params)) { + return $url; + } else { + throw new ServerException("Invalid URL passed for $arg: '$url'", 400); + } + } + + /** + * Get HubSub subscription record for a given feed & subscriber. + * + * @param string $feed + * @param string $callback + * @return mixed HubSub or false + */ + protected function getSub($feed, $callback) + { + return HubSub::staticGet($feed, $callback); + } +} + diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php index f29d08cb0..107faf012 100644 --- a/plugins/OStatus/classes/Feedinfo.php +++ b/plugins/OStatus/classes/Feedinfo.php @@ -248,7 +248,7 @@ class Feedinfo extends Memcached_DataObject #$this->verify_token = $token; #$this->update(); // @fixme try { - $callback = common_local_url('feedsubcallback', array('feed' => $this->id)); + $callback = common_local_url('pushcallback', array('feed' => $this->id)); $headers = array('Content-Type: application/x-www-form-urlencoded'); $post = array('hub.mode' => 'subscribe', 'hub.callback' => $callback, diff --git a/plugins/OStatus/lib/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php index 9bc7892fb..39985fc90 100644 --- a/plugins/OStatus/lib/feeddiscovery.php +++ b/plugins/OStatus/lib/feeddiscovery.php @@ -168,7 +168,13 @@ class FeedDiscovery } // Ok... now on to the links! + // Types listed in order of priority -- we'll prefer Atom if available. // @fixme merge with the munger link checks + $feeds = array( + 'application/atom+xml' => false, + 'application/rss+xml' => false, + ); + $nodes = $dom->getElementsByTagName('link'); for ($i = 0; $i < $nodes->length; $i++) { $node = $nodes->item($i); @@ -181,17 +187,21 @@ class FeedDiscovery $type = trim($type->value); $href = trim($href->value); - $feedTypes = array( - 'application/rss+xml', - 'application/atom+xml', - ); - if (trim($rel) == 'alternate' && in_array($type, $feedTypes)) { - return $this->resolveURI($href, $base); + if (trim($rel) == 'alternate' && array_key_exists($type, $feeds) && empty($feeds[$type])) { + // Save the first feed found of each type... + $feeds[$type] = $this->resolveURI($href, $base); } } } } + // Return the highest-priority feed found + foreach ($feeds as $type => $url) { + if ($url) { + return $url; + } + } + return false; } diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index eeb8d2df3..948017702 100644 --- a/plugins/OStatus/lib/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -235,34 +235,45 @@ class FeedMunger */ function noticeFromEntry($entry) { + $max = Notice::maxContent(); + $ellipsis = "\xe2\x80\xa6"; // U+2026 HORIZONTAL ELLIPSIS $title = $entry->title; $link = $entry->link; - + // @todo We can get entries like this: // $cats = $entry->getCategory('category', array(0, true)); // but it feels like an awful hack. If it's accessible cleanly, // try adding #hashtags from the categories/tags on a post. - - // @todo Should we force a language here? - $format = _m('New post: "%1$s" %2$s'); + $title = $entry->title; $link = $this->getAltLink($entry); - $out = sprintf($format, $title, $link); - - // Trim link if needed... - $max = Notice::maxContent(); - if (mb_strlen($out) > $max) { - $link = common_shorten_url($link); + if ($link) { + // Blog post or such... + // @todo Should we force a language here? + $format = _m('New post: "%1$s" %2$s'); $out = sprintf($format, $title, $link); - } - // Trim title if needed... - if (mb_strlen($out) > $max) { - $ellipsis = "\xe2\x80\xa6"; // U+2026 HORIZONTAL ELLIPSIS - $used = mb_strlen($out) - mb_strlen($title); - $available = $max - $used - mb_strlen($ellipsis); - $title = mb_substr($title, 0, $available) . $ellipsis; - $out = sprintf($format, $title, $link); + // Trim link if needed... + if (mb_strlen($out) > $max) { + $link = common_shorten_url($link); + $out = sprintf($format, $title, $link); + } + + // Trim title if needed... + if (mb_strlen($out) > $max) { + $used = mb_strlen($out) - mb_strlen($title); + $available = $max - $used - mb_strlen($ellipsis); + $title = mb_substr($title, 0, $available) . $ellipsis; + $out = sprintf($format, $title, $link); + } + } else { + // No link? Consider a bare status update. + if (mb_strlen($title) > $max) { + $available = $max - mb_strlen($ellipsis); + $out = mb_substr($title, 0, $available) . $ellipsis; + } else { + $out = $title; + } } return $out; -- cgit v1.2.3-54-g00ecf