diff options
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/FeedSub/FeedSubPlugin.php | 115 | ||||
-rw-r--r-- | plugins/FeedSub/feedinfo.php | 268 | ||||
-rw-r--r-- | plugins/FeedSub/feedinfo.sql | 14 | ||||
-rw-r--r-- | plugins/MemcachePlugin.php | 18 | ||||
-rw-r--r-- | plugins/MobileProfile/MobileProfilePlugin.php | 10 | ||||
-rw-r--r-- | plugins/MobileProfile/mp-screen.css | 5 | ||||
-rw-r--r-- | plugins/OStatus/OStatusPlugin.php | 305 | ||||
-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) | 44 | ||||
-rw-r--r-- | plugins/OStatus/actions/hostmeta.php | 42 | ||||
-rw-r--r-- | plugins/OStatus/actions/ostatusinit.php | 140 | ||||
-rw-r--r-- | plugins/OStatus/actions/ostatussub.php | 226 | ||||
-rw-r--r-- | plugins/OStatus/actions/pushcallback.php (renamed from plugins/FeedSub/actions/feedsubcallback.php) | 52 | ||||
-rw-r--r-- | plugins/OStatus/actions/pushhub.php | 180 | ||||
-rw-r--r-- | plugins/OStatus/actions/salmon.php | 81 | ||||
-rw-r--r-- | plugins/OStatus/actions/webfinger.php | 77 | ||||
-rw-r--r-- | plugins/OStatus/classes/HubSub.php | 272 | ||||
-rw-r--r-- | plugins/OStatus/classes/Ostatus_profile.php | 936 | ||||
-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/js/ostatus.js | 60 | ||||
-rw-r--r-- | plugins/OStatus/lib/activity.php | 448 | ||||
-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) | 172 | ||||
-rw-r--r-- | plugins/OStatus/lib/hubdistribqueuehandler.php | 185 | ||||
-rw-r--r-- | plugins/OStatus/lib/huboutqueuehandler.php | 52 | ||||
-rw-r--r-- | plugins/OStatus/lib/hubverifyqueuehandler.php | 53 | ||||
-rw-r--r-- | plugins/OStatus/lib/salmon.php | 64 | ||||
-rw-r--r-- | plugins/OStatus/lib/webfinger.php | 143 | ||||
-rw-r--r-- | plugins/OStatus/lib/xrd.php | 183 | ||||
-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/ActivityParseTests.php | 147 | ||||
-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 | ||||
-rw-r--r-- | plugins/OStatus/theme/base/css/ostatus.css | 30 | ||||
-rw-r--r-- | plugins/OpenID/finishopenidlogin.php | 47 | ||||
-rw-r--r-- | plugins/PostDebug/PostDebugPlugin.php | 150 | ||||
-rw-r--r-- | plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php | 1 | ||||
-rw-r--r-- | plugins/Realtime/realtimeupdate.js | 12 | ||||
-rw-r--r-- | plugins/TwitterBridge/twitterauthorization.php | 10 | ||||
-rw-r--r-- | plugins/TwitterBridge/twitteroauthclient.php | 28 | ||||
-rw-r--r-- | plugins/UserFlag/UserFlagPlugin.php | 19 | ||||
-rw-r--r-- | plugins/UserFlag/clearflagform.php | 2 | ||||
-rw-r--r-- | plugins/UserFlag/icon_flag.gif | bin | 80 -> 0 bytes | |||
-rw-r--r-- | plugins/UserFlag/userflag.css | 4 |
84 files changed, 4076 insertions, 555 deletions
diff --git a/plugins/FeedSub/FeedSubPlugin.php b/plugins/FeedSub/FeedSubPlugin.php deleted file mode 100644 index e49e2a648..000000000 --- a/plugins/FeedSub/FeedSubPlugin.php +++ /dev/null @@ -1,115 +0,0 @@ -<?php -/* -StatusNet Plugin: 0.9 -Plugin Name: FeedSub -Plugin URI: http://status.net/wiki/Feed_subscription -Description: FeedSub allows subscribing to real-time updates from external feeds supporting PubHubSubbub protocol. -Version: 0.1 -Author: Brion Vibber <brion@status.net> -Author URI: http://status.net/ -*/ - -/* - * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2009, 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> - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - -define('FEEDSUB_SERVICE', 100); // fixme -- avoid hardcoding these? - -// We bundle the XML_Parse_Feed library... -set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib'); - -class FeedSubException extends Exception -{ -} - -class FeedSubPlugin extends Plugin -{ - /** - * Hook for RouterInitialized event. - * - * @param Net_URL_Mapper $m path-to-action mapper - * @return boolean hook return - */ - function onRouterInitialized($m) - { - $m->connect('feedsub/callback/:feed', - array('action' => 'feedsubcallback'), - array('feed' => '[0-9]+')); - $m->connect('settings/feedsub', - array('action' => 'feedsubsettings')); - return true; - } - - /** - * Add the feed settings page to the Connect Settings menu - * - * @param Action &$action The calling page - * - * @return boolean hook return - */ - function onEndConnectSettingsNav(&$action) - { - $action_name = $action->trimmed('action'); - - $action->menuItem(common_local_url('feedsubsettings'), - _m('Feeds'), - _m('Feed subscription options'), - $action_name === 'feedsubsettings'); - - return true; - } - - /** - * Automatically load the actions and libraries used by the plugin - * - * @param Class $cls the class - * - * @return boolean hook return - * - */ - function onAutoload($cls) - { - $base = dirname(__FILE__); - $lower = strtolower($cls); - $files = array("$base/$lower.php"); - if (substr($lower, -6) == 'action') { - $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php"; - } - foreach ($files as $file) { - if (file_exists($file)) { - include_once $file; - return false; - } - } - return true; - } - - function onCheckSchema() { - // warning: the autoincrement doesn't seem to set. - // alter table feedinfo change column id id int(11) not null auto_increment; - $schema = Schema::get(); - $schema->ensureTable('feedinfo', Feedinfo::schemaDef()); - return true; - } -} diff --git a/plugins/FeedSub/feedinfo.php b/plugins/FeedSub/feedinfo.php deleted file mode 100644 index b166bd6e1..000000000 --- a/plugins/FeedSub/feedinfo.php +++ /dev/null @@ -1,268 +0,0 @@ -<?php - -/* - -Subscription flow: - - $feedinfo->subscribe() - generate random verification token - save to verify_token - sends a sub request to the hub... - - feedsub/callback - hub sends confirmation back to us via GET - We verify the request, then echo back the challenge. - On our end, we save the time we subscribed and the lease expiration - - feedsub/callback - hub sends us updates via POST - ? - -*/ - -class FeedDBException extends FeedSubException -{ - public $obj; - - function __construct($obj) - { - parent::__construct('Database insert failure'); - $this->obj = $obj; - } -} - -class Feedinfo extends Memcached_DataObject -{ - public $__table = 'feedinfo'; - - public $id; - public $profile_id; - - public $feeduri; - public $homeuri; - public $huburi; - - // PuSH subscription data - public $verify_token; - public $sub_start; - public $sub_end; - - public $created; - public $lastupdate; - - - public /*static*/ function staticGet($k, $v=null) - { - return parent::staticGet(__CLASS__, $k, $v); - } - - /** - * 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('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - 'huburi' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - '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, - 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, - 'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); - } - - static function schemaDef() - { - return array(new ColumnDef('id', 'integer', - /*size*/ null, - /*nullable*/ false, - /*key*/ 'PRI', - /*default*/ '0', - /*extra*/ null, - /*auto_increment*/ true), - new ColumnDef('profile_id', 'integer', - null, false), - new ColumnDef('feeduri', 'varchar', - 255, false, 'UNI'), - new ColumnDef('homeuri', 'varchar', - 255, false), - new ColumnDef('huburi', 'varchar', - 255, false), - new ColumnDef('verify_token', 'varchar', - 32, true), - new ColumnDef('sub_start', 'datetime', - null, true), - new ColumnDef('sub_end', 'datetime', - null, true), - new ColumnDef('created', 'datetime', - null, false), - new ColumnDef('lastupdate', 'datetime', - null, 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 keys() - { - return array('id' => 'P'); //? - } - - /** - * return key definitions for Memcached_DataObject - * - * Our caching system uses the same key definitions, but uses a different - * method to get them. - * - * @return array key definitions - */ - - function keyTypes() - { - return $this->keys(); - } - - /** - * Fetch the StatusNet-side profile for this feed - * @return Profile - */ - public function getProfile() - { - return Profile::staticGet('id', $this->profile_id); - } - - /** - * @param FeedMunger $munger - * @return Feedinfo - */ - public static function ensureProfile($munger) - { - $feedinfo = $munger->feedinfo(); - - $current = self::staticGet('feeduri', $feedinfo->feeduri); - if ($current) { - // @fixme we should probably update info as necessary - return $current; - } - - $feedinfo->query('BEGIN'); - - try { - $profile = $munger->profile(); - $result = $profile->insert(); - if (empty($result)) { - throw new FeedDBException($profile); - } - - $feedinfo->profile_id = $profile->id; - $result = $feedinfo->insert(); - if (empty($result)) { - throw new FeedDBException($feedinfo); - } - - $feedinfo->query('COMMIT'); - } catch (FeedDBException $e) { - common_log_db_error($e->obj, 'INSERT', __FILE__); - $feedinfo->query('ROLLBACK'); - return false; - } - return $feedinfo; - } - - /** - * Send a subscription request to the hub for this feed. - * The hub will later send us a confirmation POST to /feedsub/callback. - * - * @return bool true on success, false on failure - */ - public function subscribe() - { - // @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)); - $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.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'); - return true; - } else { - common_log(LOG_INFO, __METHOD__ . ': sub req failed'); - return false; - } - } catch (Exception $e) { - // wtf! - common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri"); - return false; - } - } - - /** - * Read and post notices for updates from the feed. - * Currently assumes that all items in the feed are new, - * coming from a PuSH hub. - * - * @param string $xml source of Atom or RSS feed - */ - public function postUpdates($xml) - { - common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $xml"); - require_once "XML/Feed/Parser.php"; - $feed = new XML_Feed_Parser($xml, false, false, true); - $munger = new FeedMunger($feed); - - $hits = 0; - foreach ($feed as $index => $entry) { - // @fixme this might sort in wrong order if we get multiple updates - - $notice = $munger->notice($index); - $notice->profile_id = $this->profile_id; - - // Double-check for oldies - // @fixme this could explode horribly for multiple feeds on a blog. sigh - $dupe = new Notice(); - $dupe->uri = $notice->uri; - $dupe->find(); - if ($dupe->fetch()) { - common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}"); - continue; - } - - if (Event::handle('StartNoticeSave', array(&$notice))) { - $id = $notice->insert(); - Event::handle('EndNoticeSave', array($notice)); - } - $notice->addToInboxes(); - - common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\""); - $hits++; - } - if ($hits == 0) { - common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml"); - } - } -} 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 93a0583fe..c3ca5c135 100644 --- a/plugins/MemcachePlugin.php +++ b/plugins/MemcachePlugin.php @@ -115,7 +115,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 * @@ -128,7 +128,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; @@ -228,6 +228,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/MobileProfile/MobileProfilePlugin.php b/plugins/MobileProfile/MobileProfilePlugin.php index 5c913836d..cd2531fa7 100644 --- a/plugins/MobileProfile/MobileProfilePlugin.php +++ b/plugins/MobileProfile/MobileProfilePlugin.php @@ -240,6 +240,8 @@ class MobileProfilePlugin extends WAP20Plugin return true; } + $action->cssLink('css/display.css'); + if (file_exists(Theme::file('css/mp-screen.css'))) { $action->cssLink('css/mp-screen.css', null, 'screen'); } else { @@ -256,6 +258,14 @@ class MobileProfilePlugin extends WAP20Plugin } + function onStartShowUAStyles($action) { + if (!$this->serveMobile) { + return true; + } + + return false; + } + function onStartShowHeader($action) { if (!$this->serveMobile) { diff --git a/plugins/MobileProfile/mp-screen.css b/plugins/MobileProfile/mp-screen.css index 04fa5fb00..0fc801612 100644 --- a/plugins/MobileProfile/mp-screen.css +++ b/plugins/MobileProfile/mp-screen.css @@ -1,15 +1,12 @@ /** theme: mobile profile screen * * @package StatusNet - * @author Sarven Capadisli <csarven@status.net> + * @author Sarven Capadisli <csarven@status.net> * @copyright 2009 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ -@import url(../../theme/base/css/display.css); -@import url(../../theme/identica/css/display.css); - #wrap { min-width:0; max-width:100%; diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php new file mode 100644 index 000000000..b6c9fa1d4 --- /dev/null +++ b/plugins/OStatus/OStatusPlugin.php @@ -0,0 +1,305 @@ +<?php +/* +StatusNet Plugin: 0.9 +Plugin Name: FeedSub +Plugin URI: http://status.net/wiki/Feed_subscription +Description: FeedSub allows subscribing to real-time updates from external feeds supporting PubHubSubbub protocol. +Version: 0.1 +Author: Brion Vibber <brion@status.net> +Author URI: http://status.net/ +*/ + +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, 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> + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +define('FEEDSUB_SERVICE', 100); // fixme -- avoid hardcoding these? + +// We bundle the XML_Parse_Feed library... +set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib'); + +class FeedSubException extends Exception +{ +} + +class OStatusPlugin extends Plugin +{ + /** + * Hook for RouterInitialized event. + * + * @param Net_URL_Mapper $m path-to-action mapper + * @return boolean hook return + */ + function onRouterInitialized($m) + { + // Discovery actions + $m->connect('.well-known/host-meta', + array('action' => 'hostmeta')); + $m->connect('main/webfinger', + array('action' => 'webfinger')); + $m->connect('main/ostatus', + array('action' => 'ostatusinit')); + $m->connect('main/ostatus?nickname=:nickname', + array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+')); + $m->connect('main/ostatussub', + array('action' => 'ostatussub')); + $m->connect('main/ostatussub', + array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+')); + + // PuSH actions + $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')); + + // Salmon endpoint + $m->connect('main/salmon/user/:id', + array('action' => 'salmon'), + array('id' => '[0-9]+')); + $m->connect('main/salmon/group/:id', + array('action' => 'salmongroup'), + array('id' => '[0-9]+')); + return true; + } + + /** + * 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 and groups. + */ + function onStartApiAtom(AtomNoticeFeed $feed) + { + $id = null; + + if ($feed instanceof AtomUserNoticeFeed) { + $salmonAction = 'salmon'; + $id = $feed->getUser()->id; + } else if ($feed instanceof AtomGroupNoticeFeed) { + $salmonAction = 'salmongroup'; + $id = $feed->getGroup()->id; + } else { + return; + } + + if (!empty($id)) { + $hub = common_config('ostatus', 'hub'); + if (empty($hub)) { + // Updates will be handled through our internal PuSH hub. + $hub = common_local_url('pushhub'); + } + $feed->addLink($hub, array('rel' => 'hub')); + + // Also, we'll add in the salmon link + $salmon = common_local_url($salmonAction, array('id' => $id)); + $feed->addLink($salmon, array('rel' => 'salmon')); + } + } + + /** + * Add the feed settings page to the Connect Settings menu + * + * @param Action &$action The calling page + * + * @return boolean hook return + */ + function onEndConnectSettingsNav(&$action) + { + $action_name = $action->trimmed('action'); + + $action->menuItem(common_local_url('feedsubsettings'), + _m('Feeds'), + _m('Feed subscription options'), + $action_name === 'feedsubsettings'); + + return true; + } + + /** + * Automatically load the actions and libraries used by the plugin + * + * @param Class $cls the class + * + * @return boolean hook return + * + */ + function onAutoload($cls) + { + $base = dirname(__FILE__); + $lower = strtolower($cls); + $files = array("$base/classes/$cls.php", + "$base/lib/$lower.php"); + if (substr($lower, -6) == 'action') { + $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php"; + } + foreach ($files as $file) { + if (file_exists($file)) { + include_once $file; + return false; + } + } + return true; + } + + /** + * Add in an OStatus subscribe button + */ + function onStartProfileRemoteSubscribe($output, $profile) + { + $cur = common_current_user(); + + if (empty($cur)) { + // Add an OStatus subscribe + $output->elementStart('li', 'entity_subscribe'); + $url = common_local_url('ostatusinit', + array('nickname' => $profile->nickname)); + $output->element('a', array('href' => $url, + 'class' => 'entity_remote_subscribe'), + _m('Subscribe')); + + $output->elementEnd('li'); + } + + return false; + } + + /** + * Check if we've got remote replies to send via Salmon. + * + * @fixme push webfinger lookup & sending to a background queue + * @fixme also detect short-form name for remote subscribees where not ambiguous + */ + function onEndNoticeSave($notice) + { + $count = preg_match_all('/(\w+\.)*\w+@(\w+\.)*\w+(\w+\-\w+)*\.\w+/', $notice->content, $matches); + if ($count) { + foreach ($matches[0] as $webfinger) { + + // FIXME: look up locally first + + // Check to see if we've got an actual webfinger + $w = new Webfinger; + + $endpoint_uri = ''; + + $result = $w->lookup($webfinger); + if (empty($result)) { + continue; + } + + foreach ($result->links as $link) { + if ($link['rel'] == 'salmon') { + $endpoint_uri = $link['href']; + } + } + + if (empty($endpoint_uri)) { + continue; + } + + // FIXME: this needs to go out in a queue handler + + $xml = '<?xml version="1.0" encoding="UTF-8" ?>'; + $xml .= $notice->asAtomEntry(); + + $salmon = new Salmon(); + $salmon->post($endpoint_uri, $xml); + } + } + } + + /** + * Garbage collect unused feeds on unsubscribe + */ + function onEndUnsubscribe($user, $other) + { + $profile = Ostatus_profile::staticGet('profile_id', $other->id); + if ($feed) { + $sub = new Subscription(); + $sub->subscribed = $other->id; + $sub->limit(1); + if (!$sub->find(true)) { + common_log(LOG_INFO, "Unsubscribing from now-unused feed $feed->feeduri on hub $feed->huburi"); + $profile->unsubscribe(); + } + } + return true; + } + + /** + * Make sure necessary tables are filled out. + */ + function onCheckSchema() { + $schema = Schema::get(); + $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef()); + $schema->ensureTable('hubsub', HubSub::schemaDef()); + return true; + } + + function onEndShowStatusNetStyles($action) { + $action->cssLink(common_path('plugins/OStatus/theme/base/css/ostatus.css')); + return true; + } + + function onEndShowStatusNetScripts($action) { + $action->script(common_path('plugins/OStatus/js/ostatus.js')); + return true; + } + + function onStartNoticeSourceLink($notice, &$name, &$url, &$title) + { + if ($notice->source == 'ostatus') { + $bits = parse_url($notice->uri); + $domain = $bits['host']; + + $name = $domain; + $url = $notice->uri; + $title = sprintf(_m("Sent from %s via OStatus"), $domain); + return false; + } + } +} 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..6933c9bf2 100644 --- a/plugins/FeedSub/actions/feedsubsettings.php +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -182,9 +182,9 @@ class FeedSubSettingsAction extends ConnectSettingsAction } $this->munger = $discover->feedMunger(); - $this->feedinfo = $this->munger->feedInfo(); + $this->profile = $this->munger->ostatusProfile(); - if ($this->feedinfo->huburi == '') { + if ($this->profile->huburi == '' && !common_config('feedsub', 'nohub')) { $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); return false; } @@ -196,30 +196,44 @@ class FeedSubSettingsAction extends ConnectSettingsAction { if ($this->validateFeed()) { $this->preview = true; - $this->feedinfo = Feedinfo::ensureProfile($this->munger); + $this->profile = Ostatus_profile::ensureProfile($this->munger); + if (!$this->profile) { + throw new ServerException("Feed profile was not saved properly."); + } // If not already in use, subscribe to updates via the hub - if ($this->feedinfo->sub_start) { - common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); + if ($this->profile->sub_start) { + common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}"); } else { - $ok = $this->feedinfo->subscribe(); + $ok = $this->profile->subscribe(); common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); if (!$ok) { $this->showForm(_m('Feed subscription failed! Bad response from hub.')); return; } } - + // And subscribe the current user to the local profile $user = common_current_user(); - $profile = $this->feedinfo->getProfile(); - - if ($user->isSubscribed($profile)) { - $this->showForm(_m('Already subscribed!')); - } elseif ($user->subscribeTo($profile)) { - $this->showForm(_m('Feed subscribed!')); + + if ($this->profile->isGroup()) { + $group = $this->profile->localGroup(); + if ($user->isMember($group)) { + $this->showForm(_m('Already a member!')); + } elseif (Group_member::join($this->profile->group_id, $user->id)) { + $this->showForm(_m('Joined remote group!')); + } else { + $this->showForm(_m('Remote group join failed!')); + } } else { - $this->showForm(_m('Feed subscription failed!')); + $local = $this->profile->localProfile(); + if ($user->isSubscribed($local)) { + $this->showForm(_m('Already subscribed!')); + } elseif ($user->subscribeTo($local)) { + $this->showForm(_m('Feed subscribed!')); + } else { + $this->showForm(_m('Feed subscription failed!')); + } } } } @@ -234,7 +248,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction function previewFeed() { - $feedinfo = $this->munger->feedinfo(); + $profile = $this->munger->ostatusProfile(); $notice = $this->munger->notice(0, true); // preview if ($notice) { diff --git a/plugins/OStatus/actions/hostmeta.php b/plugins/OStatus/actions/hostmeta.php new file mode 100644 index 000000000..850b8a0fe --- /dev/null +++ b/plugins/OStatus/actions/hostmeta.php @@ -0,0 +1,42 @@ +<?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/>. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker <james@status.net> + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +class HostMetaAction extends Action +{ + + function handle() + { + parent::handle(); + + $w = new Webfinger(); + + + $domain = common_config('site', 'server'); + $url = common_local_url('webfinger'); + $url.= '?uri={uri}'; + print $w->getHostMeta($domain, $url); + } +} diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php new file mode 100644 index 000000000..d21774420 --- /dev/null +++ b/plugins/OStatus/actions/ostatusinit.php @@ -0,0 +1,140 @@ +<?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/>. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker <james@status.net> + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + + +class OStatusInitAction extends Action +{ + + var $nickname; + var $acct; + var $err; + + function prepare($args) + { + parent::prepare($args); + + if (common_logged_in()) { + $this->clientError(_('You can use the local subscription!')); + return false; + } + + $this->nickname = $this->trimmed('nickname'); + $this->acct = $this->trimmed('acct'); + + return true; + } + + function handle($args) + { + parent::handle($args); + + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + /* Use a session token for CSRF protection. */ + $token = $this->trimmed('token'); + if (!$token || $token != common_session_token()) { + $this->showForm(_('There was a problem with your session token. '. + 'Try again, please.')); + return; + } + $this->ostatusConnect(); + } else { + $this->showForm(); + } + } + + function showForm($err = null) + { + $this->err = $err; + if ($this->boolean('ajax')) { + header('Content-Type: text/xml;charset=utf-8'); + $this->xw->startDocument('1.0', 'UTF-8'); + $this->elementStart('html'); + $this->elementStart('head'); + $this->element('title', null, _('Subscribe to user')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->showContent(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + $this->showPage(); + } + } + + function showContent() + { + $this->elementStart('form', array('id' => 'form_ostatus_connect', + 'method' => 'post', + 'class' => 'form_settings', + 'action' => common_local_url('ostatusinit'))); + $this->elementStart('fieldset'); + $this->element('legend', null, sprintf(_('Subscribe to %s'), $this->nickname)); + $this->hidden('token', common_session_token()); + + $this->elementStart('ul', 'form_data'); + $this->elementStart('li', array('id' => 'ostatus_nickname')); + $this->input('nickname', _('User nickname'), $this->nickname, + _('Nickname of the user you want to follow')); + $this->elementEnd('li'); + $this->elementStart('li', array('id' => 'ostatus_profile')); + $this->input('acct', _('Profile Account'), $this->acct, + _('Your account id (i.e. user@identi.ca)')); + $this->elementEnd('li'); + $this->elementEnd('ul'); + $this->submit('submit', _('Subscribe')); + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + } + + function ostatusConnect() + { + $w = new Webfinger; + + $result = $w->lookup($this->acct); + foreach ($result->links as $link) { + if ($link['rel'] == 'http://ostatus.org/schema/1.0/subscribe') { + // We found a URL - let's redirect! + + $user = User::staticGet('nickname', $this->nickname); + + $feed_url = common_local_url('ApiTimelineUser', + array('id' => $user->id, + 'format' => 'atom')); + $url = $w->applyTemplate($link['template'], $feed_url); + + common_redirect($url, 303); + } + + } + + } + + function title() + { + return _('OStatus Connect'); + } + +} diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php new file mode 100644 index 000000000..239122501 --- /dev/null +++ b/plugins/OStatus/actions/ostatussub.php @@ -0,0 +1,226 @@ +<?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/>. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker <james@status.net> + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +class OStatusSubAction extends Action +{ + + protected $feedurl; + + function title() + { + return _m("OStatus Subscribe"); + } + + function handle($args) + { + if ($this->validateFeed()) { + $this->showForm(); + } + + return true; + + } + + function showForm($err = null) + { + $this->err = $err; + $this->showPage(); + } + + + function showContent() + { + $user = common_current_user(); + + $profile = $user->getProfile(); + + $fuser = null; + + $flink = Foreign_link::getByUserID($user->id, FEEDSUB_SERVICE); + + if (!empty($flink)) { + $fuser = $flink->getForeignUser(); + } + + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_settings_feedsub', + 'class' => 'form_settings', + 'action' => + common_local_url('feedsubsettings'))); + + $this->hidden('token', common_session_token()); + + $this->elementStart('fieldset', array('id' => 'settings_feeds')); + + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed')); + $this->elementEnd('li'); + $this->elementEnd('ul'); + + $this->submit('subscribe', _m('Subscribe')); + + $this->elementEnd('fieldset'); + + $this->elementEnd('form'); + + $this->previewFeed(); + } + + /** + * Handle posts to this form + * + * Based on the button that was pressed, muxes out to other functions + * to do the actual task requested. + * + * All sub-functions reload the form with a message -- success or failure. + * + * @return void + */ + + function handlePost() + { + // CSRF protection + $token = $this->trimmed('token'); + if (!$token || $token != common_session_token()) { + $this->showForm(_('There was a problem with your session token. '. + 'Try again, please.')); + return; + } + + if ($this->arg('subscribe')) { + $this->saveFeed(); + } else { + $this->showForm(_('Unexpected form submission.')); + } + } + + + /** + * Set up and add a feed + * + * @return boolean true if feed successfully read + * Sends you back to input form if not. + */ + function validateFeed() + { + $feedurl = $this->trimmed('feed'); + + if ($feedurl == '') { + $this->showForm(_m('Empty feed URL!')); + return; + } + $this->feedurl = $feedurl; + + // Get the canonical feed URI and check it + try { + $discover = new FeedDiscovery(); + $uri = $discover->discoverFromURL($feedurl); + } catch (FeedSubBadURLException $e) { + $this->showForm(_m('Invalid URL or could not reach server.')); + return false; + } catch (FeedSubBadResponseException $e) { + $this->showForm(_m('Cannot read feed; server returned error.')); + return false; + } catch (FeedSubEmptyException $e) { + $this->showForm(_m('Cannot read feed; server returned an empty page.')); + return false; + } catch (FeedSubBadHTMLException $e) { + $this->showForm(_m('Bad HTML, could not find feed link.')); + return false; + } catch (FeedSubNoFeedException $e) { + $this->showForm(_m('Could not find a feed linked from this URL.')); + return false; + } catch (FeedSubUnrecognizedTypeException $e) { + $this->showForm(_m('Not a recognized feed type.')); + return false; + } catch (FeedSubException $e) { + // Any new ones we forgot about + $this->showForm(_m('Bad feed URL.')); + return false; + } + + $this->munger = $discover->feedMunger(); + $this->profile = $this->munger->ostatusProfile(); + + if ($this->profile->huburi == '') { + $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); + return false; + } + + return true; + } + + function saveFeed() + { + if ($this->validateFeed()) { + $this->preview = true; + $this->profile = Ostatus_profile::ensureProfile($this->munger); + + // If not already in use, subscribe to updates via the hub + if ($this->profile->sub_start) { + common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}"); + } else { + $ok = $this->profile->subscribe(); + common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); + if (!$ok) { + $this->showForm(_m('Feed subscription failed! Bad response from hub.')); + return; + } + } + + // And subscribe the current user to the local profile + $user = common_current_user(); + $profile = $this->profile->getProfile(); + + if ($user->isSubscribed($profile)) { + $this->showForm(_m('Already subscribed!')); + } elseif ($user->subscribeTo($profile)) { + $this->showForm(_m('Feed subscribed!')); + } else { + $this->showForm(_m('Feed subscription failed!')); + } + } + } + + + function previewFeed() + { + $profile = $this->munger->ostatusProfile(); + $notice = $this->munger->notice(0, true); // preview + + if ($notice) { + $this->element('b', null, 'Preview of latest post from this feed:'); + + $item = new NoticeList($notice, $this); + $item->show(); + } else { + $this->element('b', null, 'No posts in this feed yet.'); + } + } + + +} diff --git a/plugins/FeedSub/actions/feedsubcallback.php b/plugins/OStatus/actions/pushcallback.php index 0c4280c1f..388c8f9c3 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() { @@ -48,13 +48,21 @@ class FeedSubCallbackAction extends Action throw new ServerException('Empty or invalid feed id', 400); } - $feedinfo = Feedinfo::staticGet('id', $feedid); - if (!$feedinfo) { - throw new ServerException('Unknown feed id ' . $feedid, 400); + $profile = Ostatus_profile::staticGet('id', $feedid); + if (!$profile) { + throw new ServerException('Unknown OStatus/PuSH 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); + + // @fixme Queue this to a background process; we should return + // as quickly as possible from a distribution POST. + $profile->postUpdates($post, $hmac); } /** @@ -73,28 +81,30 @@ class FeedSubCallbackAction extends Action throw new ServerException("Bogus hub callback: bad mode", 404); } - $feedinfo = Feedinfo::staticGet('feeduri', $topic); - if (!$feedinfo) { + $profile = Ostatus_profile::staticGet('feeduri', $topic); + if (!$profile) { 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); - #} - + if ($profile->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); + } + + if ($mode != $profile->sub_state) { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$profile->sub_state}\""); + throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 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); + if ($mode == 'subscribe') { + common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); + $profile->confirmSubscribe($lease_seconds); } else { - $feedinfo->sub_end = null; + common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic"); + $profile->confirmUnsubscribe(); } - $feedinfo->update(); - print $challenge; } } diff --git a/plugins/OStatus/actions/pushhub.php b/plugins/OStatus/actions/pushhub.php new file mode 100644 index 000000000..13ec09d52 --- /dev/null +++ b/plugins/OStatus/actions/pushhub.php @@ -0,0 +1,180 @@ +<?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('hub.', 'hub_', $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->verify_token = $this->arg('hub.verify_token', null); + $sub->secret = $this->arg('hub.secret', null); + if (strlen($sub->secret) > 200) { + throw new ClientException("hub.secret must be no longer than 200 chars", 400); + } + $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/salmon.php b/plugins/OStatus/actions/salmon.php new file mode 100644 index 000000000..c79d09c95 --- /dev/null +++ b/plugins/OStatus/actions/salmon.php @@ -0,0 +1,81 @@ +<?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/>. + */ + +/** + * @package OStatusPlugin + * @author James Walker <james@status.net> + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class SalmonAction extends Action +{ + var $user = null; + var $xml = null; + var $activity = null; + + function prepare($args) + { + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError(_('This method requires a POST.')); + } + + if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') { + $this->clientError(_('Salmon requires application/atom+xml')); + } + + $id = $this->trimmed('id'); + + if (!$id) { + $this->clientError(_('No ID.')); + } + + $this->user = User::staticGet($id); + + if (empty($this->user)) { + $this->clientError(_('No such user.')); + } + + $xml = file_get_contents('php://input'); + + $dom = DOMDocument::loadXML($xml); + + // XXX: check that document element is Atom entry + // XXX: check the signature + + $this->act = new Activity($dom->documentElement); + } + + function handle($args) + { + common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id); + + // TODO : Insert new $xml -> notice code + + switch ($this->act->verb) + { + case Activity::POST: + case Activity::SHARE: + case Activity::FAVORITE: + case Activity::FOLLOW: + } + } +} diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/webfinger.php new file mode 100644 index 000000000..75ba16638 --- /dev/null +++ b/plugins/OStatus/actions/webfinger.php @@ -0,0 +1,77 @@ +<?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/>. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker <james@status.net> + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +class WebfingerAction extends Action +{ + + public $uri; + + function prepare($args) + { + parent::prepare($args); + + $this->uri = $this->trimmed('uri'); + + return true; + } + + function handle() + { + $acct = Webfinger::normalize($this->uri); + + $xrd = new XRD(); + + list($nick, $domain) = explode('@', urldecode($acct)); + $nick = common_canonical_nickname($nick); + + $this->user = User::staticGet('nickname', $nick); + if (!$this->user) { + $this->clientError(_('No such user.'), 404); + return false; + } + + $xrd->subject = $this->uri; + $xrd->alias[] = common_profile_url($nick); + $xrd->links[] = array('rel' => 'http://webfinger.net/rel/profile-page', + 'type' => 'text/html', + 'href' => common_profile_url($nick)); + + $salmon_url = common_local_url('salmon', + array('id' => $this->user->id)); + + $xrd->links[] = array('rel' => 'salmon', + 'href' => $salmon_url); + + // TODO - finalize where the redirect should go on the publisher + $url = common_local_url('ostatussub') . '?feed={uri}'; + $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', + 'template' => $url ); + + header('Content-type: text/xml'); + print $xrd->toXML(); + } + +} diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php new file mode 100644 index 000000000..7071ee5b4 --- /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 = hash_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/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php new file mode 100644 index 000000000..be01cdfe1 --- /dev/null +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -0,0 +1,936 @@ +<?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> + */ + +/* +PuSH subscription flow: + + $profile->subscribe() + generate random verification token + save to verify_token + sends a sub request to the hub... + + main/push/callback + hub sends confirmation back to us via GET + We verify the request, then echo back the challenge. + On our end, we save the time we subscribed and the lease expiration + + main/push/callback + hub sends us updates via POST + +*/ + +class FeedDBException extends FeedSubException +{ + public $obj; + + function __construct($obj) + { + parent::__construct('Database insert failure'); + $this->obj = $obj; + } +} + +class Ostatus_profile extends Memcached_DataObject +{ + public $__table = 'ostatus_profile'; + + public $id; + public $profile_id; + public $group_id; + + public $feeduri; + public $homeuri; + + // PuSH subscription data + public $huburi; + public $secret; + public $verify_token; + public $sub_state; // subscribe, active, unsubscribe + public $sub_start; + public $sub_end; + + public $salmonuri; + + public $created; + public $lastupdate; + + public /*static*/ function staticGet($k, $v=null) + { + return parent::staticGet(__CLASS__, $k, $v); + } + + /** + * 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('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'profile_id' => DB_DATAOBJECT_INT, + 'group_id' => DB_DATAOBJECT_INT, + 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'huburi' => DB_DATAOBJECT_STR, + 'secret' => DB_DATAOBJECT_STR, + 'verify_token' => DB_DATAOBJECT_STR, + 'sub_state' => 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, + 'salmonuri' => DB_DATAOBJECT_STR, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + static function schemaDef() + { + return array(new ColumnDef('id', 'integer', + /*size*/ null, + /*nullable*/ false, + /*key*/ 'PRI', + /*default*/ '0', + /*extra*/ null, + /*auto_increment*/ true), + new ColumnDef('profile_id', 'integer', + null, true, 'UNI'), + new ColumnDef('group_id', 'integer', + null, true, 'UNI'), + new ColumnDef('feeduri', 'varchar', + 255, false, 'UNI'), + new ColumnDef('homeuri', 'varchar', + 255, false), + new ColumnDef('huburi', 'text', + null, true), + new ColumnDef('verify_token', 'varchar', + 32, true), + new ColumnDef('secret', 'varchar', + 64, true), + new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe')", + null, true), + new ColumnDef('sub_start', 'datetime', + null, true), + new ColumnDef('sub_end', 'datetime', + null, true), + new ColumnDef('salmonuri', 'text', + null, true), + new ColumnDef('created', 'datetime', + null, false), + new ColumnDef('lastupdate', 'datetime', + null, 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 keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. + * + * @return array key definitions + */ + + function keyTypes() + { + return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U'); + } + + function sequenceKey() + { + return array('id', true, false); + } + + /** + * Fetch the StatusNet-side profile for this feed + * @return Profile + */ + public function localProfile() + { + if ($this->profile_id) { + return Profile::staticGet('id', $this->profile_id); + } + return null; + } + + /** + * Fetch the StatusNet-side profile for this feed + * @return Profile + */ + public function localGroup() + { + if ($this->group_id) { + return User_group::staticGet('id', $this->group_id); + } + return null; + } + + /** + * @param FeedMunger $munger + * @param boolean $isGroup is this a group record? + * @return Ostatus_profile + */ + public static function ensureProfile($munger) + { + $profile = $munger->ostatusProfile(); + + $current = self::staticGet('feeduri', $profile->feeduri); + if ($current) { + // @fixme we should probably update info as necessary + return $current; + } + + $profile->query('BEGIN'); + + try { + $local = $munger->profile(); + + if ($profile->isGroup()) { + $group = new User_group(); + $group->nickname = $local->nickname . '@remote'; // @fixme + $group->fullname = $local->fullname; + $group->homepage = $local->homepage; + $group->location = $local->location; + $group->created = $local->created; + $group->insert(); + if (empty($result)) { + throw new FeedDBException($group); + } + $profile->group_id = $group->id; + } else { + $result = $local->insert(); + if (empty($result)) { + throw new FeedDBException($local); + } + $profile->profile_id = $local->id; + } + + $profile->created = common_sql_now(); + $profile->lastupdate = common_sql_now(); + $result = $profile->insert(); + if (empty($result)) { + throw new FeedDBException($profile); + } + + $profile->query('COMMIT'); + } catch (FeedDBException $e) { + common_log_db_error($e->obj, 'INSERT', __FILE__); + $profile->query('ROLLBACK'); + return false; + } + + $avatar = $munger->getAvatar(); + if ($avatar) { + try { + $profile->updateAvatar($avatar); + } catch (Exception $e) { + common_log(LOG_ERR, "Exception setting OStatus avatar: " . + $e->getMessage()); + } + } + + return $profile; + } + + /** + * Download and update given avatar image + * @param string $url + * @throws Exception in various failure cases + */ + public function updateAvatar($url) + { + // @fixme this should be better encapsulated + // ripped from oauthstore.php (for old OMB client) + $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); + copy($url, $temp_filename); + + // @fixme should we be using different ids? + $imagefile = new ImageFile($this->id, $temp_filename); + $filename = Avatar::filename($this->id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + if ($this->isGroup()) { + $group = $this->localGroup(); + $group->setOriginal($filename); + } else { + $profile = $this->localProfile(); + $profile->setOriginal($filename); + } + } + + /** + * Returns an XML string fragment with profile information as an + * Activity Streams noun object with the given element type. + * + * Assumes that 'activity' namespace has been previously defined. + * + * @param string $element one of 'actor', 'subject', 'object', 'target' + * @return string + */ + function asActivityNoun($element) + { + $xs = new XMLStringer(true); + + $avatarHref = Avatar::defaultImage(AVATAR_PROFILE_SIZE); + $avatarType = 'image/png'; + if ($this->isGroup()) { + $type = 'http://activitystrea.ms/schema/1.0/group'; + $self = $this->localGroup(); + + // @fixme put a standard getAvatar() interface on groups too + if ($self->homepage_logo) { + $avatarHref = $self->homepage_logo; + $map = array('png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif'); + $extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION); + if (isset($map[$extension])) { + $avatarType = $map[$extension]; + } + } + } else { + $type = 'http://activitystrea.ms/schema/1.0/person'; + $self = $this->localProfile(); + $avatar = $self->getAvatar(AVATAR_PROFILE_SIZE); + if ($avatar) { + $avatarHref = $avatar-> + $avatarType = $avatar->mediatype; + } + } + $xs->elementStart('activity:' . $element); + $xs->element( + 'activity:object-type', + null, + $type + ); + $xs->element( + 'id', + null, + $this->homeuri); // ? + $xs->element('title', null, $self->getBestName()); + + $xs->element( + 'link', array( + 'type' => $avatarType, + 'href' => $avatarHref + ), + '' + ); + + $xs->elementEnd('activity:' . $element); + + return $xs->getString(); + } + + /** + * Damn dirty hack! + */ + function isGroup() + { + return (strpos($this->feeduri, '/groups/') !== false); + } + + /** + * Send a subscription request to the hub for this feed. + * The hub will later send us a confirmation POST to /main/push/callback. + * + * @return bool true on success, false on failure + * @throws ServerException if feed state is not valid + */ + public function subscribe($mode='subscribe') + { + if ($this->sub_state != '') { + throw new ServerException("Attempting to start PuSH subscription to feed in state $this->sub_state"); + } + if (empty($this->huburi)) { + if (common_config('feedsub', 'nohub')) { + // Fake it! We're just testing remote feeds w/o hubs. + return true; + } else { + throw new ServerException("Attempting to start PuSH subscription for feed with no hub"); + } + } + + return $this->doSubscribe('subscribe'); + } + + /** + * Send a PuSH unsubscription request to the hub for this feed. + * The hub will later send us a confirmation POST to /main/push/callback. + * + * @return bool true on success, false on failure + * @throws ServerException if feed state is not valid + */ + public function unsubscribe() { + if ($this->sub_state != 'active') { + throw new ServerException("Attempting to end PuSH subscription to feed in state $this->sub_state"); + } + if (empty($this->huburi)) { + if (common_config('feedsub', 'nohub')) { + // Fake it! We're just testing remote feeds w/o hubs. + return true; + } else { + throw new ServerException("Attempting to end PuSH subscription for feed with no hub"); + } + } + + return $this->doSubscribe('unsubscribe'); + } + + protected function doSubscribe($mode) + { + $orig = clone($this); + $this->verify_token = common_good_rand(16); + if ($mode == 'subscribe') { + $this->secret = common_good_rand(32); + } + $this->sub_state = $mode; + $this->update($orig); + unset($orig); + + try { + $callback = common_local_url('pushcallback', array('feed' => $this->id)); + $headers = array('Content-Type: application/x-www-form-urlencoded'); + $post = array('hub.mode' => $mode, + 'hub.callback' => $callback, + 'hub.verify' => 'async', + '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); + $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_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody()); + return false; + } + } catch (Exception $e) { + // wtf! + common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri"); + + $orig = clone($this); + $this->verify_token = null; + $this->sub_state = null; + $this->update($orig); + unset($orig); + + return false; + } + } + + /** + * Save PuSH subscription confirmation. + * Sets approximate lease start and end times and finalizes state. + * + * @param int $lease_seconds provided hub.lease_seconds parameter, if given + */ + public function confirmSubscribe($lease_seconds=0) + { + $original = clone($this); + + $this->sub_state = 'active'; + $this->sub_start = common_sql_date(time()); + if ($lease_seconds > 0) { + $this->sub_end = common_sql_date(time() + $lease_seconds); + } else { + $this->sub_end = null; + } + $this->lastupdate = common_sql_date(); + + return $this->update($original); + } + + /** + * Save PuSH unsubscription confirmation. + * Wipes active PuSH sub info and resets state. + */ + public function confirmUnsubscribe() + { + $original = clone($this); + + $this->verify_token = null; + $this->secret = null; + $this->sub_state = null; + $this->sub_start = null; + $this->sub_end = null; + $this->lastupdate = common_sql_date(); + + return $this->update($original); + } + + /** + * Send an Activity Streams notification to the remote Salmon endpoint, + * if so configured. + * + * @param Profile $actor + * @param $verb eg Activity::SUBSCRIBE or Activity::JOIN + * @param $object object of the action; if null, the remote entity itself is assumed + */ + public function notify(Profile $actor, $verb, $object=null) + { + if ($object == null) { + $object = $this; + } + if ($this->salmonuri) { + $text = 'update'; // @fixme + $id = 'tag:' . common_config('site', 'server') . + ':' . $verb . + ':' . $actor->id . + ':' . time(); // @fixme + + $entry = new Atom10Entry(); + $entry->elementStart('entry'); + $entry->element('id', null, $id); + $entry->element('title', null, $text); + $entry->element('summary', null, $text); + $entry->element('published', null, common_date_w3dtf()); + + $entry->element('activity:verb', null, $verb); + $entry->raw($profile->asAtomAuthor()); + $entry->raw($profile->asActivityActor()); + $entry->raw($object->asActivityNoun('object')); + $entry->elmentEnd('entry'); + + $feed = $this->atomFeed($actor); + $feed->initFeed(); + $feed->addEntry($entry); + $feed->renderEntries(); + $feed->endFeed(); + + $xml = $feed->getString(); + common_log(LOG_INFO, "Posting to Salmon endpoint $salmon: $xml"); + + $salmon = new Salmon(); // ? + $salmon->post($this->salmonuri, $xml); + } + } + + function getBestName() + { + if ($this->isGroup()) { + return $this->localGroup()->getBestName(); + } else { + return $this->localProfile()->getBestName(); + } + } + + function atomFeed($actor) + { + $feed = new Atom10Feed(); + // @fixme should these be set up somewhere else? + $feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/'); + $feed->addNamesapce('thr', 'http://purl.org/syndication/thread/1.0'); + $feed->addNamespace('georss', 'http://www.georss.org/georss'); + $feed->addNamespace('ostatus', 'http://ostatus.org/schema/1.0'); + + $taguribase = common_config('integration', 'taguri'); + $feed->setId("tag:{$taguribase}:UserTimeline:{$actor->id}"); // ??? + + $feed->setTitle($actor->getBestName() . ' timeline'); // @fixme + $feed->setUpdated(time()); + $feed->setPublished(time()); + + $feed->addLink(common_url('ApiTimelineUser', + array('id' => $actor->id, + 'type' => 'atom')), + array('rel' => 'self', + 'type' => 'application/atom+xml')); + + $feed->addLink(common_url('userbyid', + array('id' => $actor->id)), + array('rel' => 'alternate', + 'type' => 'text/html')); + + return $feed; + } + + /** + * Read and post notices for updates from the feed. + * Currently assumes that all items in the feed are new, + * coming from a PuSH hub. + * + * @param string $post source of Atom or RSS feed + * @param string $hmac X-Hub-Signature header, if present + */ + public function postUpdates($post, $hmac) + { + common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $post"); + + if ($this->sub_state != 'active') { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH for inactive feed $this->feeduri (in state '$this->sub_state')"); + return; + } + + if ($post === '') { + common_log(LOG_ERR, __METHOD__ . ": ignoring empty post"); + return; + } + + if (!$this->validatePushSig($post, $hmac)) { + // Per spec we silently drop input with a bad sig, + // while reporting receipt to the server. + return; + } + + $feed = new DOMDocument(); + if (!$feed->loadXML($post)) { + // @fixme might help to include the err message + common_log(LOG_ERR, __METHOD__ . ": ignoring invalid XML"); + return; + } + + $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); + if ($entries->length == 0) { + common_log(LOG_ERR, __METHOD__ . ": no entries in feed update, ignoring"); + return; + } + + for ($i = 0; $i < $entries->length; $i++) { + $entry = $entries->item($i); + $this->processEntry($entry, $feed); + } + } + + /** + * Validate the given Atom chunk and HMAC signature against our + * shared secret that was set up at subscription time. + * + * If we don't have a shared secret, there should be no signature. + * If we we do, our the calculated HMAC should match theirs. + * + * @param string $post raw XML source as POSTed to us + * @param string $hmac X-Hub-Signature HTTP header value, or empty + * @return boolean true for a match + */ + protected function validatePushSig($post, $hmac) + { + if ($this->secret) { + if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) { + $their_hmac = strtolower($matches[1]); + $our_hmac = hash_hmac('sha1', $post, $this->secret); + if ($their_hmac === $our_hmac) { + return true; + } + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac"); + } else { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'"); + } + } else { + if (empty($hmac)) { + return true; + } else { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'"); + } + } + return false; + } + + /** + * Process a posted entry from this feed source. + * + * @param DOMElement $entry + * @param DOMElement $feed for context + */ + protected function processEntry($entry, $feed) + { + $activity = new Activity($entry, $feed); + + $debug = var_export($activity, true); + common_log(LOG_DEBUG, $debug); + + if ($activity->verb == ActivityVerb::POST) { + $this->processPost($activity); + } else { + common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb"); + } + } + + /** + * Process an incoming post activity from this remote feed. + * @param Activity $activity + */ + protected function processPost($activity) + { + if ($this->isGroup()) { + // @fixme validate these profiles in some way! + $oprofile = $this->ensureActorProfile($activity); + } else { + $actorUri = $this->getActorProfileURI($activity); + if ($actorUri == $this->homeuri) { + // @fixme check if profile info has changed and update it + } else { + // @fixme drop or reject the messages once we've got the canonical profile URI recorded sanely + common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->homeuri"); + //return; + } + $oprofile = $this; + } + + if ($activity->object->link) { + $sourceUri = $activity->object->link; + } else if (preg_match('!^https?://!', $activity->object->id)) { + $sourceUri = $activity->object->id; + } else { + common_log(LOG_INFO, "OStatus: ignoring post with no source link: id $activity->object->id"); + return; + } + + $dupe = Notice::staticGet('uri', $sourceUri); + if ($dupe) { + common_log(LOG_INFO, "OStatus: ignoring duplicate post: $noticeLink"); + return; + } + + // @fixme sanitize and save HTML content if available + $content = $activity->object->title; + + $params = array('is_local' => Notice::REMOTE_OMB, + 'uri' => $sourceUri); + + $location = $this->getEntryLocation($activity->entry); + if ($location) { + $params['lat'] = $location->lat; + $params['lon'] = $location->lon; + if ($location->location_id) { + $params['location_ns'] = $location->location_ns; + $params['location_id'] = $location->location_id; + } + } + + // @fixme save detailed ostatus source info + // @fixme ensure that groups get handled correctly + + $saved = Notice::saveNew($oprofile->localProfile()->id, + $content, + 'ostatus', + $params); + } + + /** + * Parse location given as a GeoRSS-simple point, if provided. + * http://www.georss.org/simple + * + * @param feed item $entry + * @return mixed Location or false + */ + function getLocation($dom) + { + $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point'); + + for ($i = 0; $i < $points->length; $i++) { + $point = $points->item(0)->textContent; + $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace" + $point = preg_replace('/\s+/', ' ', $point); + $point = trim($point); + $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; + } + + /** + * Get an appropriate avatar image source URL, if available. + * + * @param ActivityObject $actor + * @param DOMElement $feed + * @return string + */ + function getAvatar($actor, $feed) + { + $url = ''; + $icon = ''; + if ($actor->avatar) { + $url = trim($actor->avatar); + } + if (!$url) { + // Check <atom:logo> and <atom:icon> on the feed + $els = $feed->childNodes(); + if ($els && $els->length) { + for ($i = 0; $i < $els->length; $i++) { + $el = $els->item($i); + if ($el->namespaceURI == Activity::ATOM) { + if (empty($url) && $el->localName == 'logo') { + $url = trim($el->textContent); + break; + } + if (empty($icon) && $el->localName == 'icon') { + // Use as a fallback + $icon = trim($el->textContent); + } + } + } + } + if ($icon && !$url) { + $url = $icon; + } + } + if ($url) { + $opts = array('allowed_schemes' => array('http', 'https')); + if (Validate::uri($url, $opts)) { + return $url; + } + } + return common_path('plugins/OStatus/images/96px-Feed-icon.svg.png'); + } + + /** + * @fixme move off of ostatus_profile or static? + */ + function ensureActorProfile($activity) + { + $profile = $this->getActorProfile($activity); + if (!$profile) { + $profile = $this->createActorProfile($activity); + } + return $profile; + } + + /** + * @param Activity $activity + * @return mixed matching Ostatus_profile or false if none known + */ + function getActorProfile($activity) + { + $homeuri = $this->getActorProfileURI($activity); + return Ostatus_profile::staticGet('homeuri', $homeuri); + } + + /** + * @param Activity $activity + * @return string + * @throws ServerException + */ + function getActorProfileURI($activity) + { + $opts = array('allowed_schemes' => array('http', 'https')); + $actor = $activity->actor; + if ($actor->id && Validate::uri($actor->id, $opts)) { + return $actor->id; + } + if ($actor->link && Validate::uri($actor->link, $opts)) { + return $actor->link; + } + throw new ServerException("No author ID URI found"); + } + + /** + * + */ + function createActorProfile($activity) + { + $actor = $activity->actor(); + $homeuri = $this->getActivityProfileURI($activity); + $nickname = $this->getAuthorNick($activity); + $avatar = $this->getAvatar($actor, $feed); + + $profile = new Profile(); + $profile->nickname = $nickname; + $profile->fullname = $actor->displayName; + $profile->homepage = $actor->link; // @fixme + $profile->profileurl = $homeuri; + // @fixme bio + // @fixme tags/categories + // @fixme location? + // @todo tags from categories + // @todo lat/lon/location? + + $ok = $profile->insert(); + if ($ok) { + $this->updateAvatar($profile, $avatar); + } else { + throw new ServerException("Can't save local profile"); + } + + // @fixme either need to do feed discovery here + // or need to split out some of the feed stuff + // so we can leave it empty until later. + $oprofile = new Ostatus_profile(); + $oprofile->homeuri = $homeuri; + $oprofile->profile_id = $profile->id; + + $ok = $oprofile->insert(); + if ($ok) { + return $oprofile; + } else { + throw new ServerException("Can't save OStatus profile"); + } + } + + /** + * @fixme move this into Activity? + * @param Activity $activity + * @return string + */ + function getAuthorNick($activity) + { + // @fixme not technically part of the actor? + foreach (array($activity->entry, $activity->feed) as $source) { + $author = ActivityUtil::child($source, 'author', Activity::ATOM); + if ($author) { + $name = ActivityUtil::child($author, 'name', Activity::ATOM); + if ($name) { + return trim($name->textContent); + } + } + } + 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/OStatus/js/ostatus.js b/plugins/OStatus/js/ostatus.js new file mode 100644 index 000000000..671795558 --- /dev/null +++ b/plugins/OStatus/js/ostatus.js @@ -0,0 +1,60 @@ +SN.U.DialogBox = { + Subscribe: function(a) { + var f = a.parent().find('#form_ostatus_connect'); + if (f.length > 0) { + f.show(); + } + else { + $.ajax({ + type: 'GET', + dataType: 'xml', + url: a[0].href+'&ajax=1', + beforeSend: function(formData) { + a.addClass('processing'); + }, + error: function (xhr, textStatus, errorThrown) { + alert(errorThrown || textStatus); + }, + success: function(data, textStatus, xhr) { + if (typeof($('form', data)[0]) != 'undefined') { + a.after(document._importNode($('form', data)[0], true)); + + var form = a.parent().find('#form_ostatus_connect'); + + form + .addClass('dialogbox') + .append('<button class="close">×</button>'); + + form + .find('.submit') + .addClass('submit_dialogbox') + .removeClass('submit') + .bind('click', function() { + form.addClass('processing'); + }); + + form.find('button.close').click(function(){ + form.hide(); + + return false; + }); + + form.find('#acct').focus(); + } + + a.removeClass('processing'); + } + }); + } + } +}; + +SN.Init.Subscribe = function() { + $('.entity_subscribe a').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; }); +}; + +$(document).ready(function() { + if ($('.entity_subscribe .entity_remote_subscribe').length > 0) { + SN.Init.Subscribe(); + } +}); diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php new file mode 100644 index 000000000..f137946ab --- /dev/null +++ b/plugins/OStatus/lib/activity.php @@ -0,0 +1,448 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * An activity + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Utilities for turning DOMish things into Activityish things + * + * Some common functions that I didn't have the bandwidth to try to factor + * into some kind of reasonable superclass, so just dumped here. Might + * be useful to have an ActivityObject parent class or something. + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class ActivityUtils +{ + const ATOM = 'http://www.w3.org/2005/Atom'; + + const LINK = 'link'; + const REL = 'rel'; + const TYPE = 'type'; + const HREF = 'href'; + + /** + * Get the permalink for an Activity object + * + * @param DOMElement $element A DOM element + * + * @return string related link, if any + */ + + static function getPermalink($element) + { + return self::getLink($element, 'alternate', 'text/html'); + } + + /** + * Get the permalink for an Activity object + * + * @param DOMElement $element A DOM element + * + * @return string related link, if any + */ + + static function getLink($element, $rel, $type=null) + { + $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK); + + foreach ($links as $link) { + + $linkRel = $link->getAttribute(self::REL); + $linkType = $link->getAttribute(self::TYPE); + + if ($linkRel == $rel && + (is_null($type) || $linkType == $type)) { + return $link->getAttribute(self::HREF); + } + } + + return null; + } + + /** + * Gets the first child element with the given tag + * + * @param DOMElement $element element to pick at + * @param string $tag tag to look for + * @param string $namespace Namespace to look under + * + * @return DOMElement found element or null + */ + + static function child($element, $tag, $namespace=self::ATOM) + { + $els = $element->childNodes; + if (empty($els) || $els->length == 0) { + return null; + } else { + for ($i = 0; $i < $els->length; $i++) { + $el = $els->item($i); + if ($el->localName == $tag && $el->namespaceURI == $namespace) { + return $el; + } + } + } + } + + /** + * Grab the text content of a DOM element child of the current element + * + * @param DOMElement $element Element whose children we examine + * @param string $tag Tag to look up + * @param string $namespace Namespace to use, defaults to Atom + * + * @return string content of the child + */ + + static function childContent($element, $tag, $namespace=self::ATOM) + { + $el = self::child($element, $tag, $namespace); + + if (empty($el)) { + return null; + } else { + return $el->textContent; + } + } +} + +/** + * A noun-ish thing in the activity universe + * + * The activity streams spec talks about activity objects, while also having + * a tag activity:object, which is in fact an activity object. Aaaaaah! + * + * This is just a thing in the activity universe. Can be the subject, object, + * or indirect object (target!) of an activity verb. Rotten name, and I'm + * propagating it. *sigh* + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class ActivityObject +{ + const ARTICLE = 'http://activitystrea.ms/schema/1.0/article'; + const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry'; + const NOTE = 'http://activitystrea.ms/schema/1.0/note'; + const STATUS = 'http://activitystrea.ms/schema/1.0/status'; + const FILE = 'http://activitystrea.ms/schema/1.0/file'; + const PHOTO = 'http://activitystrea.ms/schema/1.0/photo'; + const ALBUM = 'http://activitystrea.ms/schema/1.0/photo-album'; + const PLAYLIST = 'http://activitystrea.ms/schema/1.0/playlist'; + const VIDEO = 'http://activitystrea.ms/schema/1.0/video'; + const AUDIO = 'http://activitystrea.ms/schema/1.0/audio'; + const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark'; + const PERSON = 'http://activitystrea.ms/schema/1.0/person'; + const GROUP = 'http://activitystrea.ms/schema/1.0/group'; + const PLACE = 'http://activitystrea.ms/schema/1.0/place'; + const COMMENT = 'http://activitystrea.ms/schema/1.0/comment'; + // ^^^^^^^^^^ tea! + + // Atom elements we snarf + + const TITLE = 'title'; + const SUMMARY = 'summary'; + const CONTENT = 'content'; + const ID = 'id'; + const SOURCE = 'source'; + + const NAME = 'name'; + const URI = 'uri'; + const EMAIL = 'email'; + + public $element; + public $type; + public $id; + public $title; + public $summary; + public $content; + public $link; + public $source; + + /** + * Constructor + * + * This probably needs to be refactored + * to generate a local class (ActivityPerson, ActivityFile, ...) + * based on the object type. + * + * @param DOMElement $element DOM thing to turn into an Activity thing + */ + + function __construct($element) + { + $this->element = $element; + + if ($element->tagName == 'author') { + + $this->type = self::PERSON; // XXX: is this fair? + $this->title = $this->_childContent($element, self::NAME); + $this->id = $this->_childContent($element, self::URI); + + if (empty($this->id)) { + $email = $this->_childContent($element, self::EMAIL); + if (!empty($email)) { + // XXX: acct: ? + $this->id = 'mailto:'.$email; + } + } + + } else { + + $this->type = $this->_childContent($element, Activity::OBJECTTYPE, + Activity::SPEC); + + if (empty($this->type)) { + $this->type = ActivityObject::NOTE; + } + + $this->id = $this->_childContent($element, self::ID); + $this->title = $this->_childContent($element, self::TITLE); + $this->summary = $this->_childContent($element, self::SUMMARY); + $this->content = $this->_childContent($element, self::CONTENT); + + $this->source = $this->_getSource($element); + + $this->link = ActivityUtils::getPermalink($element); + + // XXX: grab PoCo stuff + } + + // Some per-type attributes... + if ($this->type == self::PERSON || $this->type == self::GROUP) { + $this->displayName = $this->title; + + // @fixme we may have multiple avatars with different resolutions specified + $this->avatar = ActivityUtils::getLink($element, 'avatar'); + } + } + + private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM) + { + return ActivityUtils::childContent($element, $tag, $namespace); + } + + // Try to get a unique id for the source feed + + private function _getSource($element) + { + $sourceEl = ActivityUtils::child($element, 'source'); + + if (empty($sourceEl)) { + return null; + } else { + $href = ActivityUtils::getLink($sourceEl, 'self'); + if (!empty($href)) { + return $href; + } else { + return ActivityUtils::childContent($sourceEl, 'id'); + } + } + } +} + +/** + * Utility class to hold a bunch of constant defining default verb types + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class ActivityVerb +{ + const POST = 'http://activitystrea.ms/schema/1.0/post'; + const SHARE = 'http://activitystrea.ms/schema/1.0/share'; + const SAVE = 'http://activitystrea.ms/schema/1.0/save'; + const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite'; + const PLAY = 'http://activitystrea.ms/schema/1.0/play'; + const FOLLOW = 'http://activitystrea.ms/schema/1.0/follow'; + const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend'; + const JOIN = 'http://activitystrea.ms/schema/1.0/join'; + const TAG = 'http://activitystrea.ms/schema/1.0/tag'; +} + +/** + * An activity in the ActivityStrea.ms world + * + * An activity is kind of like a sentence: someone did something + * to something else. + * + * 'someone' is the 'actor'; 'did something' is the verb; + * 'something else' is the object. + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class Activity +{ + const SPEC = 'http://activitystrea.ms/spec/1.0/'; + const SCHEMA = 'http://activitystrea.ms/schema/1.0/'; + + const VERB = 'verb'; + const OBJECT = 'object'; + const ACTOR = 'actor'; + const SUBJECT = 'subject'; + const OBJECTTYPE = 'object-type'; + const CONTEXT = 'context'; + const TARGET = 'target'; + + const ATOM = 'http://www.w3.org/2005/Atom'; + + const AUTHOR = 'author'; + const PUBLISHED = 'published'; + const UPDATED = 'updated'; + + public $actor; // an ActivityObject + public $verb; // a string (the URL) + public $object; // an ActivityObject + public $target; // an ActivityObject + public $context; // an ActivityObject + public $time; // Time of the activity + public $link; // an ActivityObject + public $entry; // the source entry + public $feed; // the source feed + + /** + * Turns a regular old Atom <entry> into a magical activity + * + * @param DOMElement $entry Atom entry to poke at + * @param DOMElement $feed Atom feed, for context + */ + + function __construct($entry, $feed = null) + { + $this->entry = $entry; + $this->feed = $feed; + + $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM); + + if (!empty($pubEl)) { + $this->time = strtotime($pubEl->textContent); + } else { + // XXX technically an error; being liberal. Good idea...? + $updateEl = $this->_child($entry, self::UPDATED, self::ATOM); + if (!empty($updateEl)) { + $this->time = strtotime($updateEl->textContent); + } else { + $this->time = null; + } + } + + $this->link = ActivityUtils::getPermalink($entry); + + $verbEl = $this->_child($entry, self::VERB); + + if (!empty($verbEl)) { + $this->verb = trim($verbEl->textContent); + } else { + $this->verb = ActivityVerb::POST; + // XXX: do other implied stuff here + } + + $objectEl = $this->_child($entry, self::OBJECT); + + if (!empty($objectEl)) { + $this->object = new ActivityObject($objectEl); + } else { + $this->object = new ActivityObject($entry); + } + + $actorEl = $this->_child($entry, self::ACTOR); + + if (!empty($actorEl)) { + + $this->actor = new ActivityObject($actorEl); + + } else if (!empty($feed) && + $subjectEl = $this->_child($feed, self::SUBJECT)) { + + $this->actor = new ActivityObject($subjectEl); + + } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) { + + $this->actor = new ActivityObject($authorEl); + + } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR, + self::ATOM)) { + + $this->actor = new ActivityObject($authorEl); + } + + $contextEl = $this->_child($entry, self::CONTEXT); + + if (!empty($contextEl)) { + $this->context = new ActivityObject($contextEl); + } + + $targetEl = $this->_child($entry, self::TARGET); + + if (!empty($targetEl)) { + $this->target = new ActivityObject($targetEl); + } + } + + /** + * Returns an Atom <entry> based on this activity + * + * @return DOMElement Atom entry + */ + + function toAtomEntry() + { + return null; + } + + private function _child($element, $tag, $namespace=self::SPEC) + { + return ActivityUtils::child($element, $tag, $namespace); + } +}
\ No newline at end of file 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..e8c46de90 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; } } @@ -78,13 +83,17 @@ class FeedMunger $this->url = $url; } - function feedinfo() + function ostatusProfile() { - $feedinfo = new Feedinfo(); - $feedinfo->feeduri = $this->url; - $feedinfo->homeuri = $this->feed->link; - $feedinfo->huburi = $this->getHubLink(); - return $feedinfo; + $profile = new Ostatus_profile(); + $profile->feeduri = $this->url; + $profile->homeuri = $this->feed->link; + $profile->huburi = $this->getHubLink(); + $salmon = $this->getSalmonLink(); + if ($salmon) { + $profile->salmonuri = $salmon; + } + return $profile; } function getAtomLink($item, $attribs=array()) @@ -150,6 +159,33 @@ class FeedMunger return $this->getAtomLink($this->feed, array('rel' => 'hub')); } + function getSalmonLink() + { + return $this->getAtomLink($this->feed, array('rel' => 'salmon')); + } + + function getSelfLink() + { + return $this->getAtomLink($this->feed, array('rel' => 'self')); + } + + /** + * 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 +200,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? @@ -177,60 +217,132 @@ class FeedMunger if (!$entry) { return null; } - + if ($preview) { $notice = new FeedSubPreviewNotice($this->profile(true)); $notice->id = -1; } else { $notice = new Notice(); + $notice->profile_id = $this->profileIdForEntry($index); } $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); - $notice->rendered = common_render_content($notice->content, $notice); + $notice->rendered = common_render_content($notice->content, $notice); // @fixme this is failing on group posts $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; } + function profileIdForEntry($index=1) + { + // hack hack hack + // should get profile for this entry's author... + $feeduri = $this->getSelfLink(); + $remote = Ostatus_profile::staticGet('feeduri', $feeduri); + if ($remote) { + return $remote->profile_id; + } else { + throw new Exception("Can't find feed profile for $feeduri"); + } + } + + /** + * Parse location given as a GeoRSS-simple point, if provided. + * http://www.georss.org/simple + * + * @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 = $points->item(0)->textContent; + $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace" + $point = preg_replace('/\s+/', ' ', $point); + $point = trim($point); + $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..245a57f72 --- /dev/null +++ b/plugins/OStatus/lib/hubdistribqueuehandler.php @@ -0,0 +1,185 @@ +<?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); + + $this->pushUser($notice); + foreach ($notice->getGroups() as $group) { + $this->pushGroup($notice, $group->group_id); + } + return true; + } + + function pushUser($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')); + $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice); + } + + function pushGroup($notice, $group_id) + { + $feed = common_local_url('ApiTimelineGroup', + array('id' => $group_id, + 'format' => 'atom')); + $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id, $notice); + } + + /** + * @param string $feed URI to the feed + * @param callable $callback function to generate Atom feed update if needed + * any additional params are passed to the callback. + */ + function pushFeed($feed, $callback) + { + $hub = common_config('ostatus', 'hub'); + if ($hub) { + $this->pushFeedExternal($feed, $hub); + } + + $sub = new HubSub(); + $sub->topic = $feed; + if ($sub->find()) { + $args = array_slice(func_get_args(), 2); + $atom = call_user_func_array($callback, $args); + $this->pushFeedInternal($atom, $sub); + } else { + common_log(LOG_INFO, "No PuSH subscribers for $feed"); + } + return true; + } + + /** + * Ping external hub about this update. + * The hub will pull the feed and check for new items later. + * Not guaranteed safe in an environment with database replication. + * + * @param string $feed feed topic URI + * @param string $hub PuSH hub URI + * @fixme can consolidate pings for user & group posts + */ + function pushFeedExternal($feed, $hub) + { + $client = new HTTPClient(); + try { + $data = array('hub.mode' => 'publish', + 'hub.url' => $feed); + $response = $client->post($hub, array(), $data); + if ($response->getStatus() == 204) { + common_log(LOG_INFO, "PuSH ping to hub $hub for $feed ok"); + return true; + } else { + common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed with HTTP " . + $response->getStatus() . ': ' . + $response->getBody()); + } + } catch (Exception $e) { + common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed: " . $e->getMessage()); + return false; + } + } + + /** + * Queue up direct feed update pushes to subscribers on our internal hub. + * @param string $atom update feed, containing only new/changed items + * @param HubSub $sub open query of subscribers + */ + function pushFeedInternal($atom, $sub) + { + common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic"); + $qm = QueueManager::get(); + while ($sub->fetch()) { + common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $sub->topic"); + $data = array('sub' => clone($sub), + 'atom' => $atom); + $qm->enqueue($data, 'hubout'); + } + } + + /** + * 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; + } + + function groupFeedForNotice($group_id, $notice) + { + // @fixme this feels VERY hacky... + // should probably be a cleaner way to do it + + ob_start(); + $api = new ApiTimelineGroupAction(); + $args = array('id' => $group_id, + 'format' => 'atom', + 'max_id' => $notice->id, + 'since_id' => $notice->id - 1); + $api->prepare($args); + $api->handle($args); + $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..0791c7e5d --- /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? + return true; + } + + 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/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php new file mode 100644 index 000000000..8c77222a6 --- /dev/null +++ b/plugins/OStatus/lib/salmon.php @@ -0,0 +1,64 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * A sample module to show best practices for StatusNet plugins + * + * PHP version 5 + * + * 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 StatusNet + * @author James Walker <james@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class Salmon +{ + public function post($endpoint_uri, $xml) + { + if (empty($endpoint_uri)) { + return FALSE; + } + + $headers = array('Content-type: application/atom+xml'); + + try { + $client = new HTTPClient(); + $client->setBody($xml); + $response = $client->post($endpoint_uri, $headers); + } catch (HTTP_Request2_Exception $e) { + return false; + } + if ($response->getStatus() != 200) { + return false; + } + + } + + public function createMagicEnv($text, $userid) + { + + + } + + + public function verifyMagicEnv($env) + { + + + } +} diff --git a/plugins/OStatus/lib/webfinger.php b/plugins/OStatus/lib/webfinger.php new file mode 100644 index 000000000..417d54904 --- /dev/null +++ b/plugins/OStatus/lib/webfinger.php @@ -0,0 +1,143 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * A sample module to show best practices for StatusNet plugins + * + * PHP version 5 + * + * 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 StatusNet + * @author James Walker <james@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +define('WEBFINGER_SERVICE_REL_VALUE', 'lrdd'); + +/** + * Implement the webfinger protocol. + */ +class Webfinger +{ + /** + * Perform a webfinger lookup given an account. + */ + public function lookup($id) + { + $id = $this->normalize($id); + list($name, $domain) = explode('@', $id); + + $links = $this->getServiceLinks($domain); + if (!$links) { + return false; + } + + $services = array(); + foreach ($links as $link) { + if ($link['template']) { + return $this->getServiceDescription($link['template'], $id); + } + if ($link['href']) { + return $this->getServiceDescription($link['href'], $id); + } + } + } + + /** + * Normalize an account ID + */ + function normalize($id) + { + if (substr($id, 0, 7) == 'acct://') { + return substr($id, 7); + } else if (substr($id, 0, 5) == 'acct:') { + return substr($id, 5); + } + + return $id; + } + + function getServiceLinks($domain) + { + $url = 'http://'. $domain .'/.well-known/host-meta'; + $content = $this->fetchURL($url); + if (empty($content)) { + common_log(LOG_DEBUG, 'Error fetching host-meta'); + return false; + } + $result = XRD::parse($content); + + // Ensure that the host == domain (spec may include signing later) + if ($result->host != $domain) { + return false; + } + + $links = array(); + foreach ($result->links as $link) { + if ($link['rel'] == WEBFINGER_SERVICE_REL_VALUE) { + $links[] = $link; + } + + } + return $links; + } + + function getServiceDescription($template, $id) + { + $url = $this->applyTemplate($template, 'acct:' . $id); + + $content = $this->fetchURL($url); + + return XRD::parse($content); + } + + function fetchURL($url) + { + try { + $client = new HTTPClient(); + $response = $client->get($url); + } catch (HTTP_Request2_Exception $e) { + return false; + } + + if ($response->getStatus() != 200) { + return false; + } + + return $response->getBody(); + } + + function applyTemplate($template, $id) + { + $template = str_replace('{uri}', urlencode($id), $template); + + return $template; + } + + function getHostMeta($domain, $template) { + $xrd = new XRD(); + $xrd->host = $domain; + $xrd->links[] = array('rel' => 'lrdd', + 'template' => $template, + 'title' => array('Resource Descriptor')); + + return $xrd->toXML(); + } +} + + diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php new file mode 100644 index 000000000..16d27f8eb --- /dev/null +++ b/plugins/OStatus/lib/xrd.php @@ -0,0 +1,183 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * A sample module to show best practices for StatusNet plugins + * + * PHP version 5 + * + * 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 StatusNet + * @author James Walker <james@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + + +class XRD +{ + const XML_NS = 'http://www.w3.org/2000/xmlns/'; + + const XRD_NS = 'http://docs.oasis-open.org/ns/xri/xrd-1.0'; + + const HOST_META_NS = 'http://host-meta.net/xrd/1.0'; + + public $expires; + + public $subject; + + public $host; + + public $alias = array(); + + public $types = array(); + + public $links = array(); + + public static function parse($xml) + { + $xrd = new XRD(); + + $dom = new DOMDocument(); + $dom->loadXML($xml); + $xrd_element = $dom->getElementsByTagName('XRD')->item(0); + + // Check for host-meta host + $host = $xrd_element->getElementsByTagName('Host')->item(0)->nodeValue; + if ($host) { + $xrd->host = $host; + } + + // Loop through other elements + foreach ($xrd_element->childNodes as $node) { + switch ($node->tagName) { + case 'Expires': + $xrd->expires = $node->nodeValue; + break; + case 'Subject': + $xrd->subject = $node->nodeValue; + break; + + case 'Alias': + $xrd->alias[] = $node->nodeValue; + break; + + case 'Link': + $xrd->links[] = $xrd->parseLink($node); + break; + + case 'Type': + $xrd->types[] = $xrd->parseType($node); + break; + + } + } + return $xrd; + } + + public function toXML() + { + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + + $xrd_dom = $dom->createElementNS(XRD::XRD_NS, 'XRD'); + $dom->appendChild($xrd_dom); + + if ($this->host) { + $host_dom = $dom->createElement('hm:Host', $this->host); + $xrd_dom->setAttributeNS(XRD::XML_NS, 'xmlns:hm', XRD::HOST_META_NS); + $xrd_dom->appendChild($host_dom); + } + + if ($this->expires) { + $expires_dom = $dom->createElement('Expires', $this->expires); + $xrd_dom->appendChild($expires_dom); + } + + if ($this->subject) { + $subject_dom = $dom->createElement('Subject', $this->subject); + $xrd_dom->appendChild($subject_dom); + } + + foreach ($this->alias as $alias) { + $alias_dom = $dom->createElement('Alias', $alias); + $xrd_dom->appendChild($alias_dom); + } + + foreach ($this->types as $type) { + $type_dom = $dom->createElement('Type', $type); + $xrd_dom->appendChild($type_dom); + } + + foreach ($this->links as $link) { + $link_dom = $this->saveLink($dom, $link); + $xrd_dom->appendChild($link_dom); + } + + return $dom->saveXML(); + } + + function parseType($element) + { + return array(); + } + + function parseLink($element) + { + $link = array(); + $link['rel'] = $element->getAttribute('rel'); + $link['type'] = $element->getAttribute('type'); + $link['href'] = $element->getAttribute('href'); + $link['template'] = $element->getAttribute('template'); + foreach ($element->childNodes as $node) { + switch($node->tagName) { + case 'Title': + $link['title'][] = $node->nodeValue; + } + } + + return $link; + } + + function saveLink($doc, $link) + { + $link_element = $doc->createElement('Link'); + if ($link['rel']) { + $link_element->setAttribute('rel', $link['rel']); + } + if ($link['type']) { + $link_element->setAttribute('type', $link['type']); + } + if ($link['href']) { + $link_element->setAttribute('href', $link['href']); + } + if ($link['template']) { + $link_element->setAttribute('template', $link['template']); + } + + if (is_array($link['title'])) { + foreach($link['title'] as $title) { + $title = $doc->createElement('Title', $title); + $link_element->appendChild($title); + } + } + + + return $link_element; + } +} + 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/OStatus/tests/ActivityParseTests.php b/plugins/OStatus/tests/ActivityParseTests.php new file mode 100644 index 000000000..fa8bcdda2 --- /dev/null +++ b/plugins/OStatus/tests/ActivityParseTests.php @@ -0,0 +1,147 @@ +<?php + +if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { + print "This script must be run from the command line\n"; + exit(); +} + +// XXX: we should probably have some common source for this stuff + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); +define('STATUSNET', true); + +require_once INSTALLDIR . '/lib/common.php'; +require_once INSTALLDIR . '/plugins/OStatus/lib/activity.php'; + +class ActivityParseTests extends PHPUnit_Framework_TestCase +{ + public function testExample1() + { + global $_example1; + $dom = DOMDocument::loadXML($_example1); + $act = new Activity($dom->documentElement); + + $this->assertFalse(empty($act)); + $this->assertEquals($act->time, 1243860840); + $this->assertEquals($act->verb, ActivityVerb::POST); + } + + public function testExample3() + { + global $_example3; + $dom = DOMDocument::loadXML($_example3); + + $feed = $dom->documentElement; + + $entries = $feed->getElementsByTagName('entry'); + + $entry = $entries->item(0); + + $act = new Activity($entry, $feed); + + $this->assertFalse(empty($act)); + $this->assertEquals($act->time, 1071340202); + $this->assertEquals($act->link, 'http://example.org/2003/12/13/atom03.html'); + + $this->assertEquals($act->verb, ActivityVerb::POST); + + $this->assertFalse(empty($act->actor)); + $this->assertEquals($act->actor->type, ActivityObject::PERSON); + $this->assertEquals($act->actor->title, 'John Doe'); + $this->assertEquals($act->actor->id, 'mailto:johndoe@example.com'); + + $this->assertFalse(empty($act->object)); + $this->assertEquals($act->object->type, ActivityObject::NOTE); + $this->assertEquals($act->object->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a'); + $this->assertEquals($act->object->title, 'Atom-Powered Robots Run Amok'); + $this->assertEquals($act->object->summary, 'Some text.'); + $this->assertEquals($act->object->link, 'http://example.org/2003/12/13/atom03.html'); + + $this->assertTrue(empty($act->context)); + $this->assertTrue(empty($act->target)); + + $this->assertEquals($act->entry, $entry); + $this->assertEquals($act->feed, $feed); + } +} + +$_example1 = <<<EXAMPLE1 +<?xml version='1.0' encoding='UTF-8'?> +<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'> + <id>tag:versioncentral.example.org,2009:/commit/1643245</id> + <published>2009-06-01T12:54:00Z</published> + <title>Geraldine committed a change to yate</title> + <content type="xhtml">Geraldine just committed a change to yate on VersionCentral</content> + <link rel="alternate" type="text/html" + href="http://versioncentral.example.org/geraldine/yate/commit/1643245" /> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <activity:verb>http://versioncentral.example.org/activity/commit</activity:verb> + <activity:object> + <activity:object-type>http://versioncentral.example.org/activity/changeset</activity:object-type> + <id>tag:versioncentral.example.org,2009:/change/1643245</id> + <title>Punctuation Changeset</title> + <summary>Fixing punctuation because it makes it more readable.</summary> + <link rel="alternate" type="text/html" href="..." /> + </activity:object> +</entry> +EXAMPLE1; + +$_example2 = <<<EXAMPLE2 +<?xml version='1.0' encoding='UTF-8'?> +<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'> + <id>tag:photopanic.example.com,2008:activity01</id> + <title>Geraldine posted a Photo on PhotoPanic</title> + <published>2008-11-02T15:29:00Z</published> + <link rel="alternate" type="text/html" href="/geraldine/activities/1" /> + <activity:verb> + http://activitystrea.ms/schema/1.0/post + </activity:verb> + <activity:object> + <id>tag:photopanic.example.com,2008:photo01</id> + <title>My Cat</title> + <published>2008-11-02T15:29:00Z</published> + <link rel="alternate" type="text/html" href="/geraldine/photos/1" /> + <activity:object-type> + tag:atomactivity.example.com,2008:photo + </activity:object-type> + <source> + <title>Geraldine's Photos</title> + <link rel="self" type="application/atom+xml" href="/geraldine/photofeed.xml" /> + <link rel="alternate" type="text/html" href="/geraldine/" /> + </source> + </activity:object> + <content type="html"> + <p>Geraldine posted a Photo on PhotoPanic</p> + <img src="/geraldine/photo1.jpg"> + </content> +</entry> +EXAMPLE2; + +$_example3 = <<<EXAMPLE3 +<?xml version="1.0" encoding="utf-8"?> + +<feed xmlns="http://www.w3.org/2005/Atom"> + + <title>Example Feed</title> + <subtitle>A subtitle.</subtitle> + <link href="http://example.org/feed/" rel="self" /> + <link href="http://example.org/" /> + <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id> + <updated>2003-12-13T18:30:02Z</updated> + <author> + <name>John Doe</name> + <email>johndoe@example.com</email> + </author> + + <entry> + <title>Atom-Powered Robots Run Amok</title> + <link href="http://example.org/2003/12/13/atom03" /> + <link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/> + <link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/> + <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> + <updated>2003-12-13T18:30:02Z</updated> + <summary>Some text.</summary> + </entry> + +</feed> +EXAMPLE3; 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 diff --git a/plugins/OStatus/theme/base/css/ostatus.css b/plugins/OStatus/theme/base/css/ostatus.css new file mode 100644 index 000000000..9bc90a731 --- /dev/null +++ b/plugins/OStatus/theme/base/css/ostatus.css @@ -0,0 +1,30 @@ +/** theme: base for OStatus + * + * @package StatusNet + * @author Sarven Capadisli <csarven@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +#form_ostatus_connect.dialogbox { +width:70%; +background-image:none; +} +#form_ostatus_connect.dialogbox .form_data label { +width:34%; +} +#form_ostatus_connect.dialogbox .form_data input { +width:57%; +} +#form_ostatus_connect.dialogbox .form_data .form_guide { +margin-left:36%; +} + +#form_ostatus_connect.dialogbox #ostatus_nickname { +display:none; +} + +#form_ostatus_connect.dialogbox .submit_dialogbox { +min-width:96px; +} diff --git a/plugins/OpenID/finishopenidlogin.php b/plugins/OpenID/finishopenidlogin.php index d25ce696c..438a728d8 100644 --- a/plugins/OpenID/finishopenidlogin.php +++ b/plugins/OpenID/finishopenidlogin.php @@ -438,49 +438,7 @@ class FinishopenidloginAction extends Action function urlToNickname($openid) { - static $bad = array('query', 'user', 'password', 'port', 'fragment'); - - $parts = parse_url($openid); - - # If any of these parts exist, this won't work - - foreach ($bad as $badpart) { - if (array_key_exists($badpart, $parts)) { - return null; - } - } - - # We just have host and/or path - - # If it's just a host... - if (array_key_exists('host', $parts) && - (!array_key_exists('path', $parts) || strcmp($parts['path'], '/') == 0)) - { - $hostparts = explode('.', $parts['host']); - - # Try to catch common idiom of nickname.service.tld - - if ((count($hostparts) > 2) && - (strlen($hostparts[count($hostparts) - 2]) > 3) && # try to skip .co.uk, .com.au - (strcmp($hostparts[0], 'www') != 0)) - { - return $this->nicknamize($hostparts[0]); - } else { - # Do the whole hostname - return $this->nicknamize($parts['host']); - } - } else { - if (array_key_exists('path', $parts)) { - # Strip starting, ending slashes - $path = preg_replace('@/$@', '', $parts['path']); - $path = preg_replace('@^/@', '', $path); - if (strpos($path, '/') === false) { - return $this->nicknamize($path); - } - } - } - - return null; + return common_url_to_nickname($openid); } function xriToNickname($xri) @@ -510,7 +468,6 @@ class FinishopenidloginAction extends Action function nicknamize($str) { - $str = preg_replace('/\W/', '', $str); - return strtolower($str); + return common_nicknamize($str); } } diff --git a/plugins/PostDebug/PostDebugPlugin.php b/plugins/PostDebug/PostDebugPlugin.php new file mode 100644 index 000000000..48fe28eab --- /dev/null +++ b/plugins/PostDebug/PostDebugPlugin.php @@ -0,0 +1,150 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * Debugging helper plugin -- records detailed data on POSTs to log + * + * PHP version 5 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Sample + * @package StatusNet + * @author Brion Vibber <brionv@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class PostDebugPlugin extends Plugin +{ + /** + * Set to a directory to dump individual items instead of + * sending to the debug log + */ + public $dir=false; + + public function onArgsInitialize(&$args) + { + if (isset($_SERVER['REQUEST_METHOD']) && + $_SERVER['REQUEST_METHOD'] == 'POST') { + $this->doDebug(); + } + } + + public function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'PostDebug', + 'version' => STATUSNET_VERSION, + 'author' => 'Brion Vibber', + 'homepage' => 'http://status.net/wiki/Plugin:PostDebug', + 'rawdescription' => + _m('Debugging tool to record request details on POST.')); + return true; + } + + protected function doDebug() + { + $data = array('timestamp' => gmdate('r'), + 'remote_addr' => @$_SERVER['REMOTE_ADDR'], + 'url' => @$_SERVER['REQUEST_URI'], + 'have_session' => common_have_session(), + 'logged_in' => common_logged_in(), + 'is_real_login' => common_is_real_login(), + 'user' => common_logged_in() ? common_current_user()->nickname : null, + 'headers' => $this->getHttpHeaders(), + 'post_data' => $this->sanitizePostData($_POST)); + $this->saveDebug($data); + } + + protected function saveDebug($data) + { + $output = var_export($data, true); + if ($this->dir) { + $file = $this->dir . DIRECTORY_SEPARATOR . $this->logFileName(); + file_put_contents($file, $output); + } else { + common_log(LOG_DEBUG, "PostDebug: $output"); + } + } + + protected function logFileName() + { + $base = common_request_id(); + $base = preg_replace('/^(.+?) .*$/', '$1', $base); + $base = str_replace(':', '-', $base); + $base = rawurlencode($base); + return $base; + } + + protected function getHttpHeaders() + { + if (function_exists('getallheaders')) { + $headers = getallheaders(); + } else { + $headers = array(); + $prefix = 'HTTP_'; + $prefixLen = strlen($prefix); + foreach ($_SERVER as $key => $val) { + if (substr($key, 0, $prefixLen) == $prefix) { + $header = $this->normalizeHeader(substr($key, $prefixLen)); + $headers[$header] = $val; + } + } + } + foreach ($headers as $header => $val) { + if (strtolower($header) == 'cookie') { + $headers[$header] = $this->sanitizeCookies($val); + } + } + return $headers; + } + + protected function normalizeHeader($key) + { + return implode('-', + array_map('ucfirst', + explode("_", + strtolower($key)))); + } + + function sanitizeCookies($val) + { + $blacklist = array(session_name(), 'rememberme'); + foreach ($blacklist as $name) { + $val = preg_replace("/(^|;\s*)({$name}=)(.*?)(;|$)/", + "$1$2########$4", + $val); + } + return $val; + } + + function sanitizePostData($data) + { + $blacklist = array('password', 'confirm', 'token'); + foreach ($data as $key => $val) { + if (in_array($key, $blacklist)) { + $data[$key] = '########'; + } + } + return $data; + } + +} + diff --git a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php index 14d1608d3..fb4eff738 100644 --- a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php +++ b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php @@ -45,6 +45,7 @@ class PoweredByStatusNetPlugin extends Plugin { function onEndAddressData($action) { + $action->text(' '); $action->elementStart('span', 'poweredby'); $action->raw(sprintf(_m('powered by %s'), sprintf('<a href="http://status.net/">%s</a>', diff --git a/plugins/Realtime/realtimeupdate.js b/plugins/Realtime/realtimeupdate.js index 52151f9de..2e5851ae5 100644 --- a/plugins/Realtime/realtimeupdate.js +++ b/plugins/Realtime/realtimeupdate.js @@ -95,9 +95,7 @@ RealtimeUpdate = { $("#notices_primary .notice:first").css({display:"none"}); $("#notices_primary .notice:first").fadeIn(1000); - SN.U.FormXHR($('#'+noticeItemID+' .form_favor')); SN.U.NoticeReplyTo($('#'+noticeItemID)); - SN.U.FormXHR($('#'+noticeItemID+' .form_repeat')); SN.U.NoticeWithAttachment($('#'+noticeItemID)); }, @@ -136,7 +134,7 @@ RealtimeUpdate = { ni = "<li class=\"hentry notice\" id=\"notice-"+unique+"\">"+ "<div class=\"entry-title\">"+ "<span class=\"vcard author\">"+ - "<a href=\""+user['profile_url']+"\" class=\"url\">"+ + "<a href=\""+user['profile_url']+"\" class=\"url\" title=\""+user['name']+"\">"+ "<img src=\""+user['profile_image_url']+"\" class=\"avatar photo\" width=\"48\" height=\"48\" alt=\""+user['screen_name']+"\"/>"+ "<span class=\"nickname fn\">"+user['screen_name']+"</span>"+ "</a>"+ @@ -180,7 +178,7 @@ RealtimeUpdate = { ni = ni+"</div>"; - "</li>"; + ni = ni+"</li>"; return ni; }, @@ -211,10 +209,10 @@ RealtimeUpdate = { var rf; rf = "<form id=\"repeat-"+id+"\" class=\"form_repeat\" method=\"post\" action=\""+RealtimeUpdate._repeaturl+"\">"+ "<fieldset>"+ - "<legend>Favor this notice</legend>"+ + "<legend>Repeat this notice?</legend>"+ "<input name=\"token-"+id+"\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+ - "<input name=\"notice\" type=\"hidden\" id=\"notice-n"+id+"\" value=\""+id+"\"/>"+ - "<input type=\"submit\" id=\"repeat-submit-"+id+"\" name=\"repeat-submit-"+id+"\" class=\"submit\" value=\"Favor\" title=\"Repeat this notice\"/>"+ + "<input name=\"notice\" type=\"hidden\" id=\"notice-"+id+"\" value=\""+id+"\"/>"+ + "<input type=\"submit\" id=\"repeat-submit-"+id+"\" name=\"repeat-submit-"+id+"\" class=\"submit\" value=\"Yes\" title=\"Repeat this notice\"/>"+ "</fieldset>"+ "</form>"; diff --git a/plugins/TwitterBridge/twitterauthorization.php b/plugins/TwitterBridge/twitterauthorization.php index 6822d33dd..c154932bb 100644 --- a/plugins/TwitterBridge/twitterauthorization.php +++ b/plugins/TwitterBridge/twitterauthorization.php @@ -56,6 +56,7 @@ class TwitterauthorizationAction extends Action var $tw_fields = null; var $access_token = null; var $signin = null; + var $verifier = null; /** * Initialize class members. Looks for 'oauth_token' parameter. @@ -70,6 +71,7 @@ class TwitterauthorizationAction extends Action $this->signin = $this->boolean('signin'); $this->oauth_token = $this->arg('oauth_token'); + $this->verifier = $this->arg('oauth_verifier'); return true; } @@ -160,8 +162,7 @@ class TwitterauthorizationAction extends Action // Get a new request token and authorize it $client = new TwitterOAuthClient(); - $req_tok = - $client->getRequestToken(TwitterOAuthClient::$requestTokenURL); + $req_tok = $client->getRequestToken(); // Sock the request token away in the session temporarily @@ -171,7 +172,7 @@ class TwitterauthorizationAction extends Action $auth_link = $client->getAuthorizeLink($req_tok, $this->signin); } catch (OAuthClientException $e) { - $msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s', + $msg = sprintf('OAuth client error - code: %1s, msg: %2s', $e->getCode(), $e->getMessage()); $this->serverError(_m('Couldn\'t link your Twitter account.')); } @@ -187,7 +188,6 @@ class TwitterauthorizationAction extends Action */ function saveAccessToken() { - // Check to make sure Twitter returned the same request // token we sent them @@ -204,7 +204,7 @@ class TwitterauthorizationAction extends Action // Exchange the request token for an access token - $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL); + $atok = $client->getAccessToken($this->verifier); // Test the access token and get the user's Twitter info diff --git a/plugins/TwitterBridge/twitteroauthclient.php b/plugins/TwitterBridge/twitteroauthclient.php index 277e7ab40..ba45b533d 100644 --- a/plugins/TwitterBridge/twitteroauthclient.php +++ b/plugins/TwitterBridge/twitteroauthclient.php @@ -92,6 +92,19 @@ class TwitterOAuthClient extends OAuthClient } /** + * Gets a request token from Twitter + * + * @return OAuthToken $token the request token + */ + function getRequestToken() + { + return parent::getRequestToken( + self::$requestTokenURL, + common_local_url('twitterauthorization') + ); + } + + /** * Builds a link to Twitter's endpoint for authorizing a request token * * @param OAuthToken $request_token token to authorize @@ -108,6 +121,21 @@ class TwitterOAuthClient extends OAuthClient } /** + * Fetches an access token from Twitter + * + * @param string $verifier 1.0a verifier + * + * @return OAuthToken $token the access token + */ + function getAccessToken($verifier = null) + { + return parent::getAccessToken( + self::$accessTokenURL, + $verifier + ); + } + + /** * Calls Twitter's /account/verify_credentials API method * * @return mixed the Twitter user diff --git a/plugins/UserFlag/UserFlagPlugin.php b/plugins/UserFlag/UserFlagPlugin.php index a33869c19..ae3dfe036 100644 --- a/plugins/UserFlag/UserFlagPlugin.php +++ b/plugins/UserFlag/UserFlagPlugin.php @@ -183,21 +183,6 @@ class UserFlagPlugin extends Plugin } /** - * Add our plugin's CSS to page output - * - * @param Action $action action being shown - * - * @return boolean hook result - */ - - function onEndShowStatusNetStyles($action) - { - $action->cssLink(common_path('plugins/UserFlag/userflag.css'), - null, 'screen, projection, tv'); - return true; - } - - /** * Initialize any flagging buttons on the page * * @param Action $action action being shown @@ -208,8 +193,8 @@ class UserFlagPlugin extends Plugin function onEndShowScripts($action) { $action->inlineScript('if ($(".form_entity_flag").length > 0) { '. - 'SN.U.FormXHR($(".form_entity_flag")); '. - '}'); + '$(".form_entity_flag").bind("click", function() {'. + 'SN.U.FormXHR($(this)); return false; }); }'); return true; } diff --git a/plugins/UserFlag/clearflagform.php b/plugins/UserFlag/clearflagform.php index 5ad6055d3..eefd15c36 100644 --- a/plugins/UserFlag/clearflagform.php +++ b/plugins/UserFlag/clearflagform.php @@ -54,7 +54,7 @@ class ClearFlagForm extends ProfileActionForm function formClass() { - return 'form_entity_clearflag'; + return 'form_user_clearflag'; } /** diff --git a/plugins/UserFlag/icon_flag.gif b/plugins/UserFlag/icon_flag.gif Binary files differdeleted file mode 100644 index 68c8aee25..000000000 --- a/plugins/UserFlag/icon_flag.gif +++ /dev/null diff --git a/plugins/UserFlag/userflag.css b/plugins/UserFlag/userflag.css deleted file mode 100644 index 98da24cc9..000000000 --- a/plugins/UserFlag/userflag.css +++ /dev/null @@ -1,4 +0,0 @@ -.entity_flag input.submit, -.entity_flag p { -background:url(icon_flag.gif) 5px 5px no-repeat; -} |