diff options
author | Brion Vibber <brion@pobox.com> | 2010-02-08 15:48:52 -0800 |
---|---|---|
committer | Brion Vibber <brion@pobox.com> | 2010-02-08 15:48:52 -0800 |
commit | b2e8d8407cc7f1abb5e8767cd3403ac356775eaa (patch) | |
tree | 5cfaed5f1f21a697e147b41695c7e8c5282324a3 /plugins | |
parent | 3833dc8c1f3f9ba5e3b12bf2715e4a4fb3adabf1 (diff) | |
parent | 4e6f587f868d71f08c618d0dedf6ddf0331619c2 (diff) |
Merge branch 'testing' of git@gitorious.org:statusnet/mainline into 0.9.x
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/FeedSub/feedinfo.sql | 14 | ||||
-rw-r--r-- | plugins/MemcachePlugin.php | 18 | ||||
-rw-r--r-- | plugins/OStatus/OStatusPlugin.php (renamed from plugins/FeedSub/FeedSubPlugin.php) | 52 | ||||
-rw-r--r-- | plugins/OStatus/README (renamed from plugins/FeedSub/README) | 0 | ||||
-rw-r--r-- | plugins/OStatus/actions/feedsubsettings.php (renamed from plugins/FeedSub/actions/feedsubsettings.php) | 7 | ||||
-rw-r--r-- | plugins/OStatus/actions/pushcallback.php (renamed from plugins/FeedSub/actions/feedsubcallback.php) | 11 | ||||
-rw-r--r-- | plugins/OStatus/actions/pushhub.php | 176 | ||||
-rw-r--r-- | plugins/OStatus/classes/Feedinfo.php (renamed from plugins/FeedSub/feedinfo.php) | 107 | ||||
-rw-r--r-- | plugins/OStatus/classes/HubSub.php | 272 | ||||
-rw-r--r-- | plugins/OStatus/extlib/README (renamed from plugins/FeedSub/extlib/README) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/Parser.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser.php) | 0 | ||||
-rw-r--r-- | plugins/OStatus/extlib/XML/Feed/Parser/Atom.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/Parser/AtomElement.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/Parser/Exception.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/Parser/RSS09Element.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/Parser/RSS11Element.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/Parser/RSS1Element.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php) | 0 | ||||
-rw-r--r-- | plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/Parser/RSS2Element.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php) | 0 | ||||
-rw-r--r-- | plugins/OStatus/extlib/XML/Feed/Parser/Type.php (renamed from plugins/FeedSub/extlib/XML/Feed/Parser/Type.php) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/atom10-entryonly.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/atom10-example1.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/atom10-example2.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/delicious.feed (renamed from plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/flickr.feed (renamed from plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/grwifi-atom.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/hoder.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/illformed_atom10.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/rss091-complete.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/rss091-international.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/rss091-simple.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/rss092-sample.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/rss10-example1.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/rss10-example2.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/rss2sample.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/sixapart-jp.xml (renamed from plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/samples/technorati.feed (renamed from plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc (renamed from plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc (renamed from plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc) | 0 | ||||
-rwxr-xr-x | plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc (renamed from plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc) | 0 | ||||
-rw-r--r-- | plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch (renamed from plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch) | 0 | ||||
-rw-r--r-- | plugins/OStatus/images/24px-Feed-icon.svg.png (renamed from plugins/FeedSub/images/24px-Feed-icon.svg.png) | bin | 1204 -> 1204 bytes | |||
-rw-r--r-- | plugins/OStatus/images/48px-Feed-icon.svg.png (renamed from plugins/FeedSub/images/48px-Feed-icon.svg.png) | bin | 2434 -> 2434 bytes | |||
-rw-r--r-- | plugins/OStatus/images/96px-Feed-icon.svg.png (renamed from plugins/FeedSub/images/96px-Feed-icon.svg.png) | bin | 5440 -> 5440 bytes | |||
-rw-r--r-- | plugins/OStatus/images/README (renamed from plugins/FeedSub/images/README) | 0 | ||||
-rw-r--r-- | plugins/OStatus/lib/feeddiscovery.php (renamed from plugins/FeedSub/feeddiscovery.php) | 36 | ||||
-rw-r--r-- | plugins/OStatus/lib/feedmunger.php (renamed from plugins/FeedSub/feedmunger.php) | 122 | ||||
-rw-r--r-- | plugins/OStatus/lib/hubdistribqueuehandler.php | 87 | ||||
-rw-r--r-- | plugins/OStatus/lib/huboutqueuehandler.php | 52 | ||||
-rw-r--r-- | plugins/OStatus/lib/hubverifyqueuehandler.php | 53 | ||||
-rw-r--r-- | plugins/OStatus/locale/OStatus.po (renamed from plugins/FeedSub/locale/FeedSub.po) | 0 | ||||
-rw-r--r-- | plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po (renamed from plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po) | 0 | ||||
-rw-r--r-- | plugins/OStatus/tests/FeedDiscoveryTest.php (renamed from plugins/FeedSub/tests/FeedDiscoveryTest.php) | 0 | ||||
-rw-r--r-- | plugins/OStatus/tests/FeedMungerTest.php (renamed from plugins/FeedSub/tests/FeedMungerTest.php) | 0 | ||||
-rw-r--r-- | plugins/OStatus/tests/gettext-speedtest.php (renamed from plugins/FeedSub/tests/gettext-speedtest.php) | 0 |
58 files changed, 938 insertions, 69 deletions
diff --git a/plugins/FeedSub/feedinfo.sql b/plugins/FeedSub/feedinfo.sql deleted file mode 100644 index e9b53d26e..000000000 --- a/plugins/FeedSub/feedinfo.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE `feedinfo` ( - `id` int(11) NOT NULL auto_increment, - `profile_id` int(11) NOT NULL, - `feeduri` varchar(255) NOT NULL, - `homeuri` varchar(255) NOT NULL, - `huburi` varchar(255) NOT NULL, - `verify_token` varchar(32) default NULL, - `sub_start` datetime default NULL, - `sub_end` datetime default NULL, - `created` datetime NOT NULL, - `lastupdate` datetime NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `feedinfo_feeduri_idx` (`feeduri`) -) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; diff --git a/plugins/MemcachePlugin.php b/plugins/MemcachePlugin.php index 2bc4b892b..c5e74fb41 100644 --- a/plugins/MemcachePlugin.php +++ b/plugins/MemcachePlugin.php @@ -102,7 +102,7 @@ class MemcachePlugin extends Plugin * * @param string &$key in; Key to use for lookups * @param mixed &$value in; Value to associate - * @param integer &$flag in; Flag (passed through to Memcache) + * @param integer &$flag in; Flag empty or Cache::COMPRESSED * @param integer &$expiry in; Expiry (passed through to Memcache) * @param boolean &$success out; Whether the set was successful * @@ -115,7 +115,7 @@ class MemcachePlugin extends Plugin if ($expiry === null) { $expiry = $this->defaultExpiry; } - $success = $this->_conn->set($key, $value, $flag, $expiry); + $success = $this->_conn->set($key, $value, $this->flag(intval($flag)), $expiry); Event::handle('EndCacheSet', array($key, $value, $flag, $expiry)); return false; @@ -197,6 +197,20 @@ class MemcachePlugin extends Plugin } } + /** + * Translate general flags to Memcached-specific flags + * @param int $flag + * @return int + */ + protected function flag($flag) + { + $out = 0; + if ($flag & Cache::COMPRESSED == Cache::COMPRESSED) { + $out |= MEMCACHE_COMPRESSED; + } + return $out; + } + function onPluginVersion(&$versions) { $versions[] = array('name' => 'Memcache', diff --git a/plugins/FeedSub/FeedSubPlugin.php b/plugins/OStatus/OStatusPlugin.php index e49e2a648..4e8b892c6 100644 --- a/plugins/FeedSub/FeedSubPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -43,7 +43,7 @@ class FeedSubException extends Exception { } -class FeedSubPlugin extends Plugin +class OStatusPlugin extends Plugin { /** * Hook for RouterInitialized event. @@ -53,8 +53,10 @@ class FeedSubPlugin extends Plugin */ function onRouterInitialized($m) { - $m->connect('feedsub/callback/:feed', - array('action' => 'feedsubcallback'), + $m->connect('main/push/hub', array('action' => 'pushhub')); + + $m->connect('main/push/callback/:feed', + array('action' => 'pushcallback'), array('feed' => '[0-9]+')); $m->connect('settings/feedsub', array('action' => 'feedsubsettings')); @@ -62,6 +64,46 @@ class FeedSubPlugin extends Plugin } /** + * Set up queue handlers for outgoing hub pushes + * @param QueueManager $qm + * @return boolean hook return + */ + function onEndInitializeQueueManager(QueueManager $qm) + { + $qm->connect('hubverify', 'HubVerifyQueueHandler'); + $qm->connect('hubdistrib', 'HubDistribQueueHandler'); + $qm->connect('hubout', 'HubOutQueueHandler'); + return true; + } + + /** + * Put saved notices into the queue for pubsub distribution. + */ + function onStartEnqueueNotice($notice, &$transports) + { + $transports[] = 'hubdistrib'; + return true; + } + + /** + * Set up a PuSH hub link to our internal link for canonical timeline + * Atom feeds for users. + */ + function onStartApiAtom(Action $action) + { + if ($action instanceof ApiTimelineUserAction) { + $id = $action->arg('id'); + if (strval(intval($id)) === strval($id)) { + // 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('pushhub'))); + } + } + return true; + } + + /** * Add the feed settings page to the Connect Settings menu * * @param Action &$action The calling page @@ -92,7 +134,8 @@ class FeedSubPlugin extends Plugin { $base = dirname(__FILE__); $lower = strtolower($cls); - $files = array("$base/$lower.php"); + $files = array("$base/classes/$cls.php", + "$base/lib/$lower.php"); if (substr($lower, -6) == 'action') { $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php"; } @@ -110,6 +153,7 @@ class FeedSubPlugin extends Plugin // alter table feedinfo change column id id int(11) not null auto_increment; $schema = Schema::get(); $schema->ensureTable('feedinfo', Feedinfo::schemaDef()); + $schema->ensureTable('hubsub', HubSub::schemaDef()); return true; } } diff --git a/plugins/FeedSub/README b/plugins/OStatus/README index cbf3adbb9..cbf3adbb9 100644 --- a/plugins/FeedSub/README +++ b/plugins/OStatus/README diff --git a/plugins/FeedSub/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php index 0fba20a39..4d5b7b60f 100644 --- a/plugins/FeedSub/actions/feedsubsettings.php +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -184,7 +184,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction $this->munger = $discover->feedMunger(); $this->feedinfo = $this->munger->feedInfo(); - if ($this->feedinfo->huburi == '') { + if ($this->feedinfo->huburi == '' && !common_config('feedsub', 'nohub')) { $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); return false; } @@ -213,7 +213,10 @@ class FeedSubSettingsAction extends ConnectSettingsAction // And subscribe the current user to the local profile $user = common_current_user(); $profile = $this->feedinfo->getProfile(); - + if (!$profile) { + throw new ServerException("Feed profile was not saved properly."); + } + if ($user->isSubscribed($profile)) { $this->showForm(_m('Already subscribed!')); } elseif ($user->subscribeTo($profile)) { diff --git a/plugins/FeedSub/actions/feedsubcallback.php b/plugins/OStatus/actions/pushcallback.php index 0c4280c1f..a5e02e08f 100644 --- a/plugins/FeedSub/actions/feedsubcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -25,7 +25,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -class FeedSubCallbackAction extends Action +class PushCallbackAction extends Action { function handle() { @@ -52,9 +52,14 @@ class FeedSubCallbackAction extends Action 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); + $feedinfo->postUpdates($post, $hmac); } /** 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 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * 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/>. + */ + +/** + * Integrated PuSH hub; lets us only ping them what need it. + * @package Hub + * @maintainer Brion Vibber <brion@status.net> + */ + +/** + + +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/FeedSub/feedinfo.php b/plugins/OStatus/classes/Feedinfo.php index b166bd6e1..107faf012 100644 --- a/plugins/FeedSub/feedinfo.php +++ b/plugins/OStatus/classes/Feedinfo.php @@ -1,8 +1,29 @@ <?php - /* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009-2010, StatusNet, Inc. + * + * 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/>. + */ + +/** + * @package FeedSubPlugin + * @maintainer Brion Vibber <brion@status.net> + */ -Subscription flow: +/* +PuSH subscription flow: $feedinfo->subscribe() generate random verification token @@ -16,7 +37,6 @@ Subscription flow: feedsub/callback hub sends us updates via POST - ? */ @@ -43,6 +63,7 @@ class Feedinfo extends Memcached_DataObject public $huburi; // PuSH subscription data + public $secret; public $verify_token; public $sub_start; public $sub_end; @@ -72,6 +93,7 @@ class Feedinfo extends Memcached_DataObject 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, 'huburi' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'secret' => DB_DATAOBJECT_STR, 'verify_token' => DB_DATAOBJECT_STR, 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, @@ -98,6 +120,8 @@ class Feedinfo extends Memcached_DataObject 255, false), new ColumnDef('verify_token', 'varchar', 32, true), + new ColumnDef('secret', 'varchar', + 64, true), new ColumnDef('sub_start', 'datetime', null, true), new ColumnDef('sub_end', 'datetime', @@ -119,7 +143,7 @@ class Feedinfo extends Memcached_DataObject function keys() { - return array('id' => 'P'); //? + return array_keys($this->keyTypes()); } /** @@ -133,7 +157,12 @@ class Feedinfo extends Memcached_DataObject function keyTypes() { - return $this->keys(); + return array('id' => 'K'); // @fixme we'll need a profile_id key at least + } + + function sequenceKey() + { + return array('id', true, false); } /** @@ -161,6 +190,10 @@ class Feedinfo extends Memcached_DataObject $feedinfo->query('BEGIN'); + // Awful hack! Awful hack! + $feedinfo->verify = common_good_rand(16); + $feedinfo->secret = common_good_rand(32); + try { $profile = $munger->profile(); $result = $profile->insert(); @@ -168,6 +201,21 @@ class Feedinfo extends Memcached_DataObject throw new FeedDBException($profile); } + $avatar = $munger->getAvatar(); + if ($avatar) { + // @fixme this should be better encapsulated + // ripped from oauthstore.php (for old OMB client) + $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); + copy($avatar, $temp_filename); + $imagefile = new ImageFile($profile->id, $temp_filename); + $filename = Avatar::filename($profile->id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + $profile->setOriginal($filename); + } + $feedinfo->profile_id = $profile->id; $result = $feedinfo->insert(); if (empty($result)) { @@ -191,27 +239,38 @@ class Feedinfo extends Memcached_DataObject */ public function subscribe() { + if (common_config('feedsub', 'nohub')) { + // Fake it! We're just testing remote feeds w/o hubs. + return true; + } // @fixme use the verification token #$token = md5(mt_rand() . ':' . $this->feeduri); #$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, 'hub.verify' => 'async', - //'hub.verify_token' => $token, + 'hub.verify_token' => $this->verify_token, + 'hub.secret' => $this->secret, //'hub.lease_seconds' => 0, 'hub.topic' => $this->feeduri); $client = new HTTPClient(); $response = $client->post($this->huburi, $headers, $post); - if ($response->getStatus() >= 200 && $response->getStatus() < 300) { - common_log(LOG_INFO, __METHOD__ . ': sub req ok'); + $status = $response->getStatus(); + if ($status == 202) { + common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback'); + return true; + } else if ($status == 204) { + common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified'); return true; + } else if ($status >= 200 && $status < 300) { + common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody()); + return false; } else { - common_log(LOG_INFO, __METHOD__ . ': sub req failed'); + common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody()); return false; } } catch (Exception $e) { @@ -227,10 +286,29 @@ class Feedinfo extends Memcached_DataObject * coming from a PuSH hub. * * @param string $xml source of Atom or RSS feed + * @param string $hmac X-Hub-Signature header, if present */ - public function postUpdates($xml) + public function postUpdates($xml, $hmac) { - common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $xml"); + common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml"); + + if ($this->secret) { + if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) { + $their_hmac = strtolower($matches[1]); + $our_hmac = sha1($xml . $this->secret); + if ($their_hmac !== $our_hmac) { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac"); + return; + } + } else { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'"); + return; + } + } else if ($hmac) { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'"); + return; + } + require_once "XML/Feed/Parser.php"; $feed = new XML_Feed_Parser($xml, false, false, true); $munger = new FeedMunger($feed); @@ -246,8 +324,7 @@ class Feedinfo extends Memcached_DataObject // @fixme this could explode horribly for multiple feeds on a blog. sigh $dupe = new Notice(); $dupe->uri = $notice->uri; - $dupe->find(); - if ($dupe->fetch()) { + if ($dupe->find(true)) { common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}"); continue; } diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php new file mode 100644 index 000000000..1769f6c94 --- /dev/null +++ b/plugins/OStatus/classes/HubSub.php @@ -0,0 +1,272 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * 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/>. + */ + +/** + * PuSH feed subscription record + * @package Hub + * @author Brion Vibber <brion@status.net> + */ +class HubSub extends Memcached_DataObject +{ + public $__table = 'hubsub'; + + public $hashkey; // sha1(topic . '|' . $callback); (topic, callback) key is too long for myisam in utf8 + public $topic; + public $callback; + public $secret; + public $verify_token; + public $challenge; + public $lease; + public $sub_start; + public $sub_end; + public $created; + + public /*static*/ function staticGet($topic, $callback) + { + return parent::staticGet(__CLASS__, 'hashkey', self::hashkey($topic, $callback)); + } + + protected static function hashkey($topic, $callback) + { + return sha1($topic . '|' . $callback); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('hashkey' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'topic' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'callback' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'secret' => DB_DATAOBJECT_STR, + 'verify_token' => DB_DATAOBJECT_STR, + 'challenge' => DB_DATAOBJECT_STR, + 'lease' => DB_DATAOBJECT_INT, + 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + static function schemaDef() + { + return array(new ColumnDef('hashkey', 'char', + /*size*/40, + /*nullable*/false, + /*key*/'PRI'), + new ColumnDef('topic', 'varchar', + /*size*/255, + /*nullable*/false, + /*key*/'KEY'), + new ColumnDef('callback', 'varchar', + 255, false), + new ColumnDef('secret', 'text', + null, true), + new ColumnDef('verify_token', 'text', + null, true), + new ColumnDef('challenge', 'varchar', + 32, true), + new ColumnDef('lease', 'int', + null, true), + new ColumnDef('sub_start', 'datetime', + null, true), + new ColumnDef('sub_end', 'datetime', + null, true), + new ColumnDef('created', 'datetime', + null, false)); + } + + function keys() + { + return array_keys($this->keyTypes()); + } + + function sequenceKeys() + { + return array(false, false, false); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keyTypes() + { + return array('hashkey' => 'K'); + } + + /** + * Validates a requested lease length, sets length plus + * subscription start & end dates. + * + * Does not save to database -- use before insert() or update(). + * + * @param int $length in seconds + */ + function setLease($length) + { + assert(is_int($length)); + + $min = 86400; + $max = 86400 * 30; + + if ($length == 0) { + // We want to garbage collect dead subscriptions! + $length = $max; + } elseif( $length < $min) { + $length = $min; + } else if ($length > $max) { + $length = $max; + } + + $this->lease = $length; + $this->start_sub = common_sql_now(); + $this->end_sub = common_sql_date(time() + $length); + } + + /** + * Send a verification ping to subscriber + * @param string $mode 'subscribe' or 'unsubscribe' + */ + function verify($mode) + { + assert($mode == 'subscribe' || $mode == 'unsubscribe'); + + // Is this needed? data object fun... + $clone = clone($this); + $clone->challenge = common_good_rand(16); + $clone->update($this); + $this->challenge = $clone->challenge; + unset($clone); + + $params = array('hub.mode' => $mode, + 'hub.topic' => $this->topic, + 'hub.challenge' => $this->challenge); + if ($mode == 'subscribe') { + $params['hub.lease_seconds'] = $this->lease; + } + if ($this->verify_token) { + $params['hub.verify_token'] = $this->verify_token; + } + $url = $this->callback . '?' . http_build_query($params, '', '&'); // @fixme ugly urls + + try { + $request = new HTTPClient(); + $response = $request->get($url); + $status = $response->getStatus(); + + if ($status >= 200 && $status < 300) { + $fail = false; + } else { + // @fixme how can we schedule a second attempt? + // Or should we? + $fail = "Returned HTTP $status"; + } + } catch (Exception $e) { + $fail = $e->getMessage(); + } + if ($fail) { + // @fixme how can we schedule a second attempt? + // or save a fail count? + // Or should we? + common_log(LOG_ERR, "Failed to verify $mode for $this->topic at $this->callback: $fail"); + return false; + } else { + if ($mode == 'subscribe') { + // Establish or renew the subscription! + // This seems unnecessary... dataobject fun! + $clone = clone($this); + $clone->challenge = null; + $clone->setLease($this->lease); + $clone->update($this); + unset($clone); + + $this->challenge = null; + $this->setLease($this->lease); + common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic for $this->lease seconds"); + } else if ($mode == 'unsubscribe') { + common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic"); + $this->delete(); + } + return true; + } + } + + /** + * Insert wrapper; transparently set the hash key from topic and callback columns. + * @return boolean success + */ + function insert() + { + $this->hashkey = self::hashkey($this->topic, $this->callback); + return parent::insert(); + } + + /** + * Send a 'fat ping' to the subscriber's callback endpoint + * containing the given Atom feed chunk. + * + * Determination of which items to send should be done at + * a higher level; don't just shove in a complete feed! + * + * @param string $atom well-formed Atom feed + */ + function push($atom) + { + $headers = array('Content-Type: application/atom+xml'); + if ($this->secret) { + $hmac = sha1($atom . $this->secret); + $headers[] = "X-Hub-Signature: sha1=$hmac"; + } else { + $hmac = '(none)'; + } + common_log(LOG_INFO, "About to push feed to $this->callback for $this->topic, HMAC $hmac"); + try { + $request = new HTTPClient(); + $request->setBody($atom); + $response = $request->post($this->callback, $headers); + + if ($response->isOk()) { + return true; + } + common_log(LOG_ERR, "Error sending PuSH content " . + "to $this->callback for $this->topic: " . + $response->getStatus()); + return false; + + } catch (Exception $e) { + common_log(LOG_ERR, "Error sending PuSH content " . + "to $this->callback for $this->topic: " . + $e->getMessage()); + return false; + } + } +} + diff --git a/plugins/FeedSub/extlib/README b/plugins/OStatus/extlib/README index 799b40c47..799b40c47 100644 --- a/plugins/FeedSub/extlib/README +++ b/plugins/OStatus/extlib/README diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser.php b/plugins/OStatus/extlib/XML/Feed/Parser.php index ffe8220a5..ffe8220a5 100755 --- a/plugins/FeedSub/extlib/XML/Feed/Parser.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php b/plugins/OStatus/extlib/XML/Feed/Parser/Atom.php index c7e218a1e..c7e218a1e 100644 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/Atom.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php b/plugins/OStatus/extlib/XML/Feed/Parser/AtomElement.php index 063ecb617..063ecb617 100755 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/AtomElement.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php b/plugins/OStatus/extlib/XML/Feed/Parser/Exception.php index 1e76e3f85..1e76e3f85 100755 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/Exception.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php index 07f38f911..07f38f911 100755 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS09Element.php index d41f36e8d..d41f36e8d 100755 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS09Element.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php index 60c9938ba..60c9938ba 100755 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php index 3cd1ef15d..3cd1ef15d 100755 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS11Element.php index 75918beda..75918beda 100755 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS11Element.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS1Element.php index 8e36d5a9b..8e36d5a9b 100755 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS1Element.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php index 0936bd2f5..0936bd2f5 100644 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS2Element.php index 6edf910dc..6edf910dc 100755 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS2Element.php diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/Type.php b/plugins/OStatus/extlib/XML/Feed/Parser/Type.php index 75052619b..75052619b 100644 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/Type.php +++ b/plugins/OStatus/extlib/XML/Feed/Parser/Type.php diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml b/plugins/OStatus/extlib/XML/Feed/samples/atom10-entryonly.xml index 02e1c5800..02e1c5800 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/atom10-entryonly.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml b/plugins/OStatus/extlib/XML/Feed/samples/atom10-example1.xml index d181d2b6f..d181d2b6f 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/atom10-example1.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml b/plugins/OStatus/extlib/XML/Feed/samples/atom10-example2.xml index 98abf9d54..98abf9d54 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/atom10-example2.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed b/plugins/OStatus/extlib/XML/Feed/samples/delicious.feed index 32f9fa493..32f9fa493 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed +++ b/plugins/OStatus/extlib/XML/Feed/samples/delicious.feed diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed b/plugins/OStatus/extlib/XML/Feed/samples/flickr.feed index 57e83af57..57e83af57 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed +++ b/plugins/OStatus/extlib/XML/Feed/samples/flickr.feed diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml b/plugins/OStatus/extlib/XML/Feed/samples/grwifi-atom.xml index c351d3c16..c351d3c16 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/grwifi-atom.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml b/plugins/OStatus/extlib/XML/Feed/samples/hoder.xml index 099463570..099463570 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/hoder.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml b/plugins/OStatus/extlib/XML/Feed/samples/illformed_atom10.xml index 612186897..612186897 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/illformed_atom10.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss091-complete.xml index b0a1fee2d..b0a1fee2d 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss091-complete.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss091-international.xml index cfe91691f..cfe91691f 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss091-international.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss091-simple.xml index f0964a227..f0964a227 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss091-simple.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss092-sample.xml index 5d75c352b..5d75c352b 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss092-sample.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss10-example1.xml index 0edecf58e..0edecf58e 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss10-example1.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss10-example2.xml index 26235f78f..26235f78f 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss10-example2.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss2sample.xml index 53483cc51..53483cc51 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss2sample.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml b/plugins/OStatus/extlib/XML/Feed/samples/sixapart-jp.xml index f8a04bba5..f8a04bba5 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml +++ b/plugins/OStatus/extlib/XML/Feed/samples/sixapart-jp.xml diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed b/plugins/OStatus/extlib/XML/Feed/samples/technorati.feed index 6274a32cd..6274a32cd 100755 --- a/plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed +++ b/plugins/OStatus/extlib/XML/Feed/samples/technorati.feed diff --git a/plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc b/plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc index e662d2626..e662d2626 100755 --- a/plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc +++ b/plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc diff --git a/plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc b/plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc index 725094788..725094788 100755 --- a/plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc +++ b/plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc diff --git a/plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc b/plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc index c8633766f..c8633766f 100755 --- a/plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc +++ b/plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc diff --git a/plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch b/plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch index c53bd9737..c53bd9737 100644 --- a/plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch +++ b/plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch diff --git a/plugins/FeedSub/images/24px-Feed-icon.svg.png b/plugins/OStatus/images/24px-Feed-icon.svg.png Binary files differindex 317225814..317225814 100644 --- a/plugins/FeedSub/images/24px-Feed-icon.svg.png +++ b/plugins/OStatus/images/24px-Feed-icon.svg.png diff --git a/plugins/FeedSub/images/48px-Feed-icon.svg.png b/plugins/OStatus/images/48px-Feed-icon.svg.png Binary files differindex bd1da4f91..bd1da4f91 100644 --- a/plugins/FeedSub/images/48px-Feed-icon.svg.png +++ b/plugins/OStatus/images/48px-Feed-icon.svg.png diff --git a/plugins/FeedSub/images/96px-Feed-icon.svg.png b/plugins/OStatus/images/96px-Feed-icon.svg.png Binary files differindex bf16571ec..bf16571ec 100644 --- a/plugins/FeedSub/images/96px-Feed-icon.svg.png +++ b/plugins/OStatus/images/96px-Feed-icon.svg.png diff --git a/plugins/FeedSub/images/README b/plugins/OStatus/images/README index d9379c23e..d9379c23e 100644 --- a/plugins/FeedSub/images/README +++ b/plugins/OStatus/images/README diff --git a/plugins/FeedSub/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php index 35edaca33..39985fc90 100644 --- a/plugins/FeedSub/feeddiscovery.php +++ b/plugins/OStatus/lib/feeddiscovery.php @@ -48,6 +48,18 @@ 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; @@ -64,7 +76,7 @@ class FeedDiscovery /** * @param string $url - * @param bool $htmlOk + * @param bool $htmlOk pass false here if you don't want to follow web pages. * @return string with validated URL * @throws FeedSubBadURLException * @throws FeedSubBadHtmlException @@ -156,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); @@ -169,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/FeedSub/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index f3618b8eb..cbaec6775 100644 --- a/plugins/FeedSub/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -30,8 +30,8 @@ class FeedSubPreviewNotice extends Notice function __construct($profile) { - //parent::__construct(); // uhhh? $this->profile = $profile; + $this->profile_id = 0; } function getProfile() @@ -56,14 +56,19 @@ class FeedSubPreviewProfile extends Profile { function getAvatar($width, $height=null) { - return new FeedSubPreviewAvatar($width, $height); + return new FeedSubPreviewAvatar($width, $height, $this->avatar); } } class FeedSubPreviewAvatar extends Avatar { + function __construct($width, $height, $remote) + { + $this->remoteImage = $remote; + } + function displayUrl() { - return common_path('plugins/FeedSub/images/48px-Feed-icon.svg.png'); + return $this->remoteImage; } } @@ -150,6 +155,23 @@ class FeedMunger return $this->getAtomLink($this->feed, array('rel' => 'hub')); } + /** + * Get an appropriate avatar image source URL, if available. + * @return mixed string or false + */ + function getAvatar() + { + $logo = $this->feed->logo; + if ($logo) { + return $logo; + } + $icon = $this->feed->icon; + if ($icon) { + return $icon; + } + return common_path('plugins/OStatus/images/48px-Feed-icon.svg.png'); + } + function profile($preview=false) { if ($preview) { @@ -164,6 +186,10 @@ class FeedMunger $profile->homepage = $this->getAltLink($this->feed); $profile->bio = $this->feed->description; $profile->profileurl = $this->getAltLink($this->feed); + + if ($preview) { + $profile->avatar = $this->getAvatar(); + } // @todo tags from categories // @todo lat/lon/location? @@ -186,6 +212,12 @@ class FeedMunger } $link = $this->getAltLink($entry); + if (empty($link)) { + if (preg_match('!^https?://!', $entry->id)) { + $link = $entry->id; + common_log(LOG_DEBUG, "No link on entry, using URL from id: $link"); + } + } $notice->uri = $link; $notice->url = $link; $notice->content = $this->noticeFromEntry($entry); @@ -193,44 +225,90 @@ class FeedMunger $notice->created = common_sql_date($entry->updated); // @fixme $notice->is_local = Notice::GATEWAY; $notice->source = 'feed'; - + + $location = $this->getLocation($entry); + if ($location) { + if ($location->location_id) { + $notice->location_ns = $location->location_ns; + $notice->location_id = $location->location_id; + } + $notice->lat = $location->lat; + $notice->lon = $location->lon; + } + return $notice; } /** + * @param feed item $entry + * @return mixed Location or false + */ + function getLocation($entry) + { + $dom = $entry->model; + $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point'); + + for ($i = 0; $i < $points->length; $i++) { + $point = trim($points->item(0)->textContent); + $coords = explode(' ', $point); + if (count($coords) == 2) { + list($lat, $lon) = $coords; + if (is_numeric($lat) && is_numeric($lon)) { + common_log(LOG_INFO, "Looking up location for $lat $lon from georss"); + return Location::fromLatLon($lat, $lon); + } + } + common_log(LOG_ERR, "Ignoring bogus georss:point value $point"); + } + + return false; + } + + /** * @param XML_Feed_Type $entry * @return string notice text, within post size limit */ function noticeFromEntry($entry) { + $max = Notice::maxContent(); + $ellipsis = "\xe2\x80\xa6"; // U+2026 HORIZONTAL ELLIPSIS $title = $entry->title; $link = $entry->link; - + // @todo We can get <category> 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; diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php new file mode 100644 index 000000000..126f1355f --- /dev/null +++ b/plugins/OStatus/lib/hubdistribqueuehandler.php @@ -0,0 +1,87 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * 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/>. + */ + +/** + * Send a PuSH subscription verification from our internal hub. + * Queue up final distribution for + * @package Hub + * @author Brion Vibber <brion@status.net> + */ +class HubDistribQueueHandler extends QueueHandler +{ + function transport() + { + return 'hubdistrib'; + } + + function handle($notice) + { + assert($notice instanceof Notice); + + // See if there's any PuSH subscriptions, including OStatus clients. + // @fixme handle group subscriptions as well + // http://identi.ca/api/statuses/user_timeline/1.atom + $feed = common_local_url('ApiTimelineUser', + array('id' => $notice->profile_id, + 'format' => 'atom')); + $sub = new HubSub(); + $sub->topic = $feed; + if ($sub->find()) { + common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $feed"); + $qm = QueueManager::get(); + $atom = $this->userFeedForNotice($notice); + while ($sub->fetch()) { + common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $feed"); + $data = array('sub' => clone($sub), + 'atom' => $atom); + $qm->enqueue($data, 'hubout'); + } + } else { + common_log(LOG_INFO, "No PuSH subscribers for $feed"); + } + } + + /** + * Build a single-item version of the sending user's Atom feed. + * @param Notice $notice + * @return string + */ + function userFeedForNotice($notice) + { + // @fixme this feels VERY hacky... + // should probably be a cleaner way to do it + + ob_start(); + $api = new ApiTimelineUserAction(); + $api->prepare(array('id' => $notice->profile_id, + 'format' => 'atom', + 'max_id' => $notice->id, + 'since_id' => $notice->id - 1)); + $api->showTimeline(); + $feed = ob_get_clean(); + + // ...and override the content-type back to something normal... eww! + // hope there's no other headers that got set while we weren't looking. + header('Content-Type: text/html; charset=utf-8'); + + common_log(LOG_DEBUG, $feed); + return $feed; + } +} + diff --git a/plugins/OStatus/lib/huboutqueuehandler.php b/plugins/OStatus/lib/huboutqueuehandler.php new file mode 100644 index 000000000..cb44ad2c4 --- /dev/null +++ b/plugins/OStatus/lib/huboutqueuehandler.php @@ -0,0 +1,52 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * 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/>. + */ + +/** + * Send a raw PuSH atom update from our internal hub. + * @package Hub + * @author Brion Vibber <brion@status.net> + */ +class HubOutQueueHandler extends QueueHandler +{ + function transport() + { + return 'hubout'; + } + + function handle($data) + { + $sub = $data['sub']; + $atom = $data['atom']; + + assert($sub instanceof HubSub); + assert(is_string($atom)); + + try { + $sub->push($atom); + } catch (Exception $e) { + common_log(LOG_ERR, "Failed PuSH to $sub->callback for $sub->topic: " . + $e->getMessage()); + // @fixme Reschedule a later delivery? + // Currently we have no way to do this other than 'send NOW' + } + + return true; + } +} + diff --git a/plugins/OStatus/lib/hubverifyqueuehandler.php b/plugins/OStatus/lib/hubverifyqueuehandler.php new file mode 100644 index 000000000..125d13a77 --- /dev/null +++ b/plugins/OStatus/lib/hubverifyqueuehandler.php @@ -0,0 +1,53 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * 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/>. + */ + +/** + * Send a PuSH subscription verification from our internal hub. + * @package Hub + * @author Brion Vibber <brion@status.net> + */ +class HubVerifyQueueHandler extends QueueHandler +{ + function transport() + { + return 'hubverify'; + } + + function handle($data) + { + $sub = $data['sub']; + $mode = $data['mode']; + + assert($sub instanceof HubSub); + assert($mode === 'subscribe' || $mode === 'unsubscribe'); + + common_log(LOG_INFO, __METHOD__ . ": $mode $sub->callback $sub->topic"); + try { + $sub->verify($mode); + } catch (Exception $e) { + common_log(LOG_ERR, "Failed PuSH $mode verify to $sub->callback for $sub->topic: " . + $e->getMessage()); + // @fixme schedule retry? + // @fixme just kill it? + } + + return true; + } +} + diff --git a/plugins/FeedSub/locale/FeedSub.po b/plugins/OStatus/locale/OStatus.po index dedc018e3..dedc018e3 100644 --- a/plugins/FeedSub/locale/FeedSub.po +++ b/plugins/OStatus/locale/OStatus.po diff --git a/plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po b/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po index f17dfa50a..f17dfa50a 100644 --- a/plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po +++ b/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po diff --git a/plugins/FeedSub/tests/FeedDiscoveryTest.php b/plugins/OStatus/tests/FeedDiscoveryTest.php index 1c5249701..1c5249701 100644 --- a/plugins/FeedSub/tests/FeedDiscoveryTest.php +++ b/plugins/OStatus/tests/FeedDiscoveryTest.php diff --git a/plugins/FeedSub/tests/FeedMungerTest.php b/plugins/OStatus/tests/FeedMungerTest.php index 0ce24c9fb..0ce24c9fb 100644 --- a/plugins/FeedSub/tests/FeedMungerTest.php +++ b/plugins/OStatus/tests/FeedMungerTest.php diff --git a/plugins/FeedSub/tests/gettext-speedtest.php b/plugins/OStatus/tests/gettext-speedtest.php index 8bbdf5e89..8bbdf5e89 100644 --- a/plugins/FeedSub/tests/gettext-speedtest.php +++ b/plugins/OStatus/tests/gettext-speedtest.php |