diff options
Diffstat (limited to 'plugins')
122 files changed, 11129 insertions, 8232 deletions
diff --git a/plugins/Blacklist/BlacklistPlugin.php b/plugins/Blacklist/BlacklistPlugin.php index 84a2cb616..fb8f7306f 100644 --- a/plugins/Blacklist/BlacklistPlugin.php +++ b/plugins/Blacklist/BlacklistPlugin.php @@ -22,7 +22,7 @@ * @category Action * @package StatusNet * @author Evan Prodromou <evan@status.net> - * @copyright 2009 StatusNet Inc. + * @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/ */ @@ -47,6 +47,55 @@ class BlacklistPlugin extends Plugin public $nicknames = array(); public $urls = array(); + public $canAdmin = true; + + private $_nicknamePatterns = array(); + private $_urlPatterns = array(); + + /** + * Initialize the plugin + * + * @return void + */ + + function initialize() + { + $confNicknames = $this->_configArray('blacklist', 'nicknames'); + + $this->_nicknamePatterns = array_merge($this->nicknames, + $confNicknames); + + $confURLs = $this->_configArray('blacklist', 'urls'); + + $this->_urlPatterns = array_merge($this->urls, + $confURLs); + } + + /** + * Retrieve an array from configuration + * + * Carefully checks a section. + * + * @param string $section Configuration section + * @param string $setting Configuration setting + * + * @return array configuration values + */ + + function _configArray($section, $setting) + { + $config = common_config($section, $setting); + + if (empty($config)) { + return array(); + } else if (is_array($config)) { + return $config; + } else if (is_string($config)) { + return explode("\r\n", $config); + } else { + throw new Exception("Unknown data type for config $section + $setting"); + } + } /** * Hook registration to prevent blacklisted homepages or nicknames @@ -173,7 +222,8 @@ class BlacklistPlugin extends Plugin private function _checkUrl($url) { - foreach ($this->urls as $pattern) { + foreach ($this->_urlPatterns as $pattern) { + common_debug("Checking $url against $pattern"); if (preg_match("/$pattern/", $url)) { return false; } @@ -194,7 +244,8 @@ class BlacklistPlugin extends Plugin private function _checkNickname($nickname) { - foreach ($this->nicknames as $pattern) { + foreach ($this->_nicknamePatterns as $pattern) { + common_debug("Checking $nickname against $pattern"); if (preg_match("/$pattern/", $nickname)) { return false; } @@ -203,14 +254,191 @@ class BlacklistPlugin extends Plugin return true; } + /** + * Add our actions to the URL router + * + * @param Net_URL_Mapper $m URL mapper for this hit + * + * @return boolean hook return + */ + + function onRouterInitialized($m) + { + $m->connect('admin/blacklist', array('action' => 'blacklistadminpanel')); + return true; + } + + /** + * Auto-load our classes if called + * + * @param string $cls Class to load + * + * @return boolean hook return + */ + + function onAutoload($cls) + { + switch (strtolower($cls)) + { + case 'blacklistadminpanelaction': + $base = strtolower(mb_substr($cls, 0, -6)); + include_once INSTALLDIR.'/plugins/Blacklist/'.$base.'.php'; + return false; + default: + return true; + } + } + + /** + * Plugin version data + * + * @param array &$versions array of version blocks + * + * @return boolean hook value + */ + function onPluginVersion(&$versions) { $versions[] = array('name' => 'Blacklist', 'version' => self::VERSION, 'author' => 'Evan Prodromou', - 'homepage' => 'http://status.net/wiki/Plugin:Blacklist', + 'homepage' => + 'http://status.net/wiki/Plugin:Blacklist', 'description' => - _m('Keep a blacklist of forbidden nickname and URL patterns.')); + _m('Keep a blacklist of forbidden nickname '. + 'and URL patterns.')); + return true; + } + + /** + * Determines if our admin panel can be shown + * + * @param string $name name of the admin panel + * @param boolean &$isOK result + * + * @return boolean hook value + */ + + function onAdminPanelCheck($name, &$isOK) + { + if ($name == 'blacklist') { + $isOK = $this->canAdmin; + return false; + } + return true; } + + /** + * Add our tab to the admin panel + * + * @param Widget $nav Admin panel nav + * + * @return boolean hook value + */ + + function onEndAdminPanelNav($nav) + { + if (AdminPanelAction::canAdmin('blacklist')) { + + $action_name = $nav->action->trimmed('action'); + + $nav->out->menuItem(common_local_url('blacklistadminpanel'), + _('Blacklist'), + _('Blacklist configuration'), + $action_name == 'blacklistadminpanel', + 'nav_blacklist_admin_panel'); + } + + return true; + } + + function onEndDeleteUserForm($action, $user) + { + $cur = common_current_user(); + + if (empty($cur) || !$cur->hasRight(Right::CONFIGURESITE)) { + return; + } + + $profile = $user->getProfile(); + + if (empty($profile)) { + return; + } + + $action->elementStart('ul', 'form_data'); + $action->elementStart('li'); + $this->checkboxAndText($action, + 'blacklistnickname', + _('Add this nickname pattern to blacklist'), + 'blacklistnicknamepattern', + $this->patternizeNickname($user->nickname)); + $action->elementEnd('li'); + + if (!empty($profile->homepage)) { + $action->elementStart('li'); + $this->checkboxAndText($action, + 'blacklisthomepage', + _('Add this homepage pattern to blacklist'), + 'blacklisthomepagepattern', + $this->patternizeHomepage($profile->homepage)); + $action->elementEnd('li'); + } + + $action->elementEnd('ul'); + } + + function onEndDeleteUser($action, $user) + { + common_debug("Action args: " . print_r($action->args, true)); + + if ($action->boolean('blacklisthomepage')) { + $pattern = $action->trimmed('blacklisthomepagepattern'); + $confURLs = $this->_configArray('blacklist', 'urls'); + $confURLs[] = $pattern; + Config::save('blacklist', 'urls', implode("\r\n", $confURLs)); + } + + if ($action->boolean('blacklistnickname')) { + $pattern = $action->trimmed('blacklistnicknamepattern'); + $confNicknames = $this->_configArray('blacklist', 'nicknames'); + $confNicknames[] = $pattern; + Config::save('blacklist', 'nicknames', implode("\r\n", $confNicknames)); + } + + return true; + } + + function checkboxAndText($action, $checkID, $label, $textID, $value) + { + $action->element('input', array('name' => $checkID, + 'type' => 'checkbox', + 'class' => 'checkbox', + 'id' => $checkID)); + + $action->text(' '); + + $action->element('label', array('class' => 'checkbox', + 'for' => $checkID), + $label); + + $action->text(' '); + + $action->element('input', array('name' => $textID, + 'type' => 'text', + 'id' => $textID, + 'value' => $value)); + } + + function patternizeNickname($nickname) + { + return $nickname; + } + + function patternizeHomepage($homepage) + { + $hostname = parse_url($homepage, PHP_URL_HOST); + return $hostname; + } } diff --git a/plugins/Blacklist/blacklistadminpanel.php b/plugins/Blacklist/blacklistadminpanel.php new file mode 100644 index 000000000..98d07080d --- /dev/null +++ b/plugins/Blacklist/blacklistadminpanel.php @@ -0,0 +1,222 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Blacklist administration panel + * + * 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 Settings + * @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); +} + +/** + * Administer blacklist + * + * @category Admin + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class BlacklistadminpanelAction extends AdminPanelAction +{ + /** + * title of the admin panel + * + * @return string title + */ + + function title() + { + return _('Blacklist'); + } + + /** + * Panel instructions + * + * @return string instructions + */ + + function getInstructions() + { + return _('Blacklisted URLs and nicknames'); + } + + /** + * Show the actual form + * + * @return void + * + * @see BlacklistAdminPanelForm + */ + + function showForm() + { + $form = new BlacklistAdminPanelForm($this); + $form->show(); + return; + } + + /** + * Save the form settings + * + * @return void + */ + + function saveSettings() + { + static $settings = array( + 'blacklist' => array('nicknames', 'urls'), + ); + + $values = array(); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] = $this->trimmed("$section-$setting"); + } + } + + // This throws an exception on validation errors + + $this->validate($values); + + // assert(all values are valid); + + $config = new Config(); + + $config->query('BEGIN'); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + $config->query('COMMIT'); + + return; + } + + /** + * Validate the values + * + * @param array &$values 2d array of values to check + * + * @return boolean success flag + */ + + function validate(&$values) + { + return true; + } +} + +/** + * Admin panel form for blacklist panel + * + * @category Admin + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class BlacklistAdminPanelForm extends Form +{ + /** + * ID of the form + * + * @return string ID + */ + + function id() + { + return 'blacklistadminpanel'; + } + + /** + * Class of the form + * + * @return string class + */ + + function formClass() + { + return 'form_settings'; + } + + /** + * Action we post to + * + * @return string action URL + */ + + function action() + { + return common_local_url('blacklistadminpanel'); + } + + /** + * Show the form controls + * + * @return void + */ + + function formData() + { + $this->out->elementStart('ul', 'form_data'); + + $this->out->elementStart('li'); + $this->out->textarea('blacklist-nicknames', _m('Nicknames'), + common_config('blacklist', 'nicknames'), + _('Patterns of nicknames to block, one per line')); + $this->out->elementEnd('li'); + + $this->out->elementStart('li'); + $this->out->textarea('blacklist-urls', _m('URLs'), + common_config('blacklist', 'urls'), + _('Patterns of URLs to block, one per line')); + $this->out->elementEnd('li'); + + $this->out->elementEnd('ul'); + } + + /** + * Buttons for submitting + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', + _('Save'), + 'submit', + null, + _('Save site settings')); + } +} diff --git a/plugins/BlogspamNetPlugin.php b/plugins/BlogspamNetPlugin.php index 51236001a..d52e6006a 100644 --- a/plugins/BlogspamNetPlugin.php +++ b/plugins/BlogspamNetPlugin.php @@ -72,8 +72,10 @@ class BlogspamNetPlugin extends Plugin common_debug("Blogspamnet args = " . print_r($args, TRUE)); $requestBody = xmlrpc_encode_request('testComment', array($args)); - $request = HTTPClient::start(); - $httpResponse = $request->post($this->baseUrl, array('Content-Type: text/xml'), $requestBody); + $request = new HTTPClient($this->baseUrl, HTTPClient::METHOD_POST); + $request->setHeader('Content-Type', 'text/xml'); + $request->setBody($requestBody); + $httpResponse = $request->send(); $response = xmlrpc_decode($httpResponse->getBody()); if (xmlrpc_is_fault($response)) { @@ -118,7 +120,7 @@ class BlogspamNetPlugin extends Plugin $args['site'] = common_root_url(); $args['version'] = $this->userAgent(); - $args['options'] = "max-size=140,min-size=0,min-words=0,exclude=bayasian"; + $args['options'] = "max-size=" . common_config('site','textlimit') . ",min-size=0,min-words=0,exclude=bayasian"; return $args; } diff --git a/plugins/CasAuthentication/CasAuthenticationPlugin.php b/plugins/CasAuthentication/CasAuthenticationPlugin.php index 483b060ab..203e5fe42 100644 --- a/plugins/CasAuthentication/CasAuthenticationPlugin.php +++ b/plugins/CasAuthentication/CasAuthenticationPlugin.php @@ -137,6 +137,7 @@ class CasAuthenticationPlugin extends AuthenticationPlugin $casSettings['server']=$this->server; $casSettings['port']=$this->port; $casSettings['path']=$this->path; + $casSettings['takeOverLogin']=$this->takeOverLogin; } function onPluginVersion(&$versions) diff --git a/plugins/CasAuthentication/caslogin.php b/plugins/CasAuthentication/caslogin.php index 390a75d8b..a66774dc1 100644 --- a/plugins/CasAuthentication/caslogin.php +++ b/plugins/CasAuthentication/caslogin.php @@ -54,9 +54,18 @@ class CasloginAction extends Action // We don't have to return to it again common_set_returnto(null); } else { - $url = common_local_url('all', - array('nickname' => - $user->nickname)); + if(common_config('site', 'private') && $casSettings['takeOverLogin']) { + //SSO users expect to just go to the URL they entered + //if we don't have a returnto set, the user entered the + //main StatusNet url, so send them there. + $url = common_local_url('public'); + } else { + //With normal logins (regular form-based username/password), + //the user would expect to go to their home after logging in. + $url = common_local_url('public', + array('nickname' => + $user->nickname)); + } } common_redirect($url, 303); diff --git a/plugins/Facebook/FacebookPlugin.php b/plugins/Facebook/FacebookPlugin.php index 78c9054e1..443cb396f 100644 --- a/plugins/Facebook/FacebookPlugin.php +++ b/plugins/Facebook/FacebookPlugin.php @@ -22,7 +22,7 @@ * @category Plugin * @package StatusNet * @author Zach Copley <zach@status.net> - * @copyright 2009 StatusNet, Inc. + * @copyright 2009-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/ */ @@ -32,12 +32,12 @@ if (!defined('STATUSNET')) { } define("FACEBOOK_CONNECT_SERVICE", 3); -define('FACEBOOKPLUGIN_VERSION', '0.9'); require_once INSTALLDIR . '/plugins/Facebook/facebookutil.php'; /** - * Facebook plugin to add a StatusNet Facebook application + * Facebook plugin to add a StatusNet Facebook canvas application + * and allow registration and authentication via Facebook Connect * * @category Plugin * @package StatusNet @@ -49,6 +49,36 @@ require_once INSTALLDIR . '/plugins/Facebook/facebookutil.php'; class FacebookPlugin extends Plugin { + const VERSION = STATUSNET_VERSION; + + /** + * Initializer for the plugin. + */ + + function initialize() + { + // Allow the key and secret to be passed in + // Control panel will override + + if (isset($this->apikey)) { + $key = common_config('facebook', 'apikey'); + if (empty($key)) { + Config::save('facebook', 'apikey', $this->apikey); + } + } + + if (isset($this->secret)) { + $secret = common_config('facebook', 'secret'); + if (empty($secret)) { + Config::save( + 'facebook', + 'secret', + $this->secret + ); + } + } + } + /** * Add Facebook app actions to the router table * @@ -70,6 +100,7 @@ class FacebookPlugin extends Plugin array('action' => 'facebooksettings')); $m->connect('facebook/app/invite.php', array('action' => 'facebookinvite')); $m->connect('facebook/app/remove', array('action' => 'facebookremove')); + $m->connect('admin/facebook', array('action' => 'facebookadminpanel')); // Facebook Connect stuff @@ -98,6 +129,7 @@ class FacebookPlugin extends Plugin case 'FacebookinviteAction': case 'FacebookremoveAction': case 'FacebooksettingsAction': + case 'FacebookadminpanelAction': include_once INSTALLDIR . '/plugins/Facebook/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; @@ -123,6 +155,32 @@ class FacebookPlugin extends Plugin } /** + * Add a Facebook tab to the admin panels + * + * @param Widget $nav Admin panel nav + * + * @return boolean hook value + */ + + function onEndAdminPanelNav($nav) + { + if (AdminPanelAction::canAdmin('facebook')) { + + $action_name = $nav->action->trimmed('action'); + + $nav->out->menuItem( + common_local_url('facebookadminpanel'), + _m('Facebook'), + _m('Facebook integration configuration'), + $action_name == 'facebookadminpanel', + 'nav_facebook_admin_panel' + ); + } + + return true; + } + + /** * Override normal HTML output to force the content type to * text/html and add in xmlns:fb * @@ -359,8 +417,6 @@ class FacebookPlugin extends Plugin $connect = 'imsettings'; } else if (common_config('sms', 'enabled')) { $connect = 'smssettings'; - } else if (common_config('twitter', 'enabled')) { - $connect = 'twittersettings'; } if (!empty($user)) { @@ -525,15 +581,18 @@ class FacebookPlugin extends Plugin function onPluginVersion(&$versions) { - $versions[] = array('name' => 'Facebook', - 'version' => FACEBOOKPLUGIN_VERSION, - 'author' => 'Zach Copley', - 'homepage' => 'http://status.net/wiki/Plugin:Facebook', - 'rawdescription' => - _m('The Facebook plugin allows you to integrate ' . - 'your StatusNet instance with ' . - '<a href="http://facebook.com/">Facebook</a> ' . - 'and Facebook Connect.')); + $versions[] = array( + 'name' => 'Facebook', + 'version' => self::VERSION, + 'author' => 'Zach Copley', + 'homepage' => 'http://status.net/wiki/Plugin:Facebook', + 'rawdescription' => _m( + 'The Facebook plugin allows you to integrate ' . + 'your StatusNet instance with ' . + '<a href="http://facebook.com/">Facebook</a> ' . + 'and Facebook Connect.' + ) + ); return true; } diff --git a/plugins/Facebook/README b/plugins/Facebook/README index bf2f4a180..14c1d3241 100644 --- a/plugins/Facebook/README +++ b/plugins/Facebook/README @@ -1,6 +1,9 @@ -This plugin allows you to use Facebook Connect with StatusNet, provides a -Facebook application for your users, and allows them to update their -Facebook statuses from StatusNet. +Facebook Plugin +=============== + +This plugin allows you to use Facebook Connect with StatusNet, provides +a Facebook canvas application for your users, and allows them to update +their Facebook statuses from StatusNet. Facebook Connect ---------------- @@ -15,12 +18,12 @@ Facebook credentials. With Facebook Connect, your users can: Built-in Facebook Application ----------------------------- -The plugin also installs a StatusNet Facebook application that allows your -users to automatically update their Facebook statuses with their latest -notices, invite their friends to use the app (and thus your site), view -their notice timelines, and post notices -- all from within Facebook. The -application is built into the StatusNet Facebook plugin and runs on your -host. +The plugin also installs a StatusNet Facebook canvas application that +allows your users to automatically update their Facebook status with +their latest notices, invite their friends to use the app (and thus your +site), view their notice timelines and post notices -- all from within +Facebook. The application is built into the StatusNet Facebook plugin +and runs on your host. Quick setup instructions* ------------------------- @@ -29,13 +32,9 @@ Install the Facebook Developer application on Facebook: http://www.facebook.com/developers/ -Use it to create a new application and generate an API key and secret. Add a -Facebook app section of your config.php and copy in the key and secret, -e.g.: - - // Config section for the built-in Facebook application - $config['facebook']['apikey'] = 'APIKEY'; - $config['facebook']['secret'] = 'SECRET'; +Use it to create a new application and generate an API key and secret. +You will need the key and secret so cut-n-paste them into your text +editor or write them down. In Facebook's application editor, specify the following URLs for your app: @@ -67,11 +66,36 @@ can be left with default values. http://wiki.developers.facebook.com/index.php/Connect/Setting_Up_Your_Site http://wiki.developers.facebook.com/index.php/Creating_your_first_application -Finally you must activate the plugin by adding the following line to your -config.php: +Finally you must activate the plugin by adding it in your config.php +(this is where you'll need the API key and secret generated earlier): + + addPlugin( + 'Facebook', + array( + 'apikey' => 'YOUR_APIKEY', + 'secret' => 'YOUR_SECRET' + ) + ); + +Administration Panel +-------------------- + +As of StatusNet 0.9.0 you can alternatively specify the key and secret +via a Facebook administration panel from within StatusNet, in which case +you can just add: addPlugin('Facebook'); +to activate the plugin. + +NOTE: To enable the administration panel you'll need to add it to the +list of active administration panels, e.g.: + + $config['admin']['panels'][] = 'facebook'; + +and of course you'll need a user with the administrative role to access +it and input the API key and secret (see: scripts/userrole.php). + Testing It Out -------------- @@ -81,11 +105,11 @@ disconnect* to their Facebook accounts from it. To try out the plugin, fire up your browser and connect to: - http://SITE/PATH_TO_STATUSNET/main/facebooklogin + http://example.net/mublog/main/facebooklogin or, if you do not have fancy URLs turned on: - http://SITE/PATH_TO_STATUSNET/index.php/main/facebooklogin + http://example.net/mublog/index.php/main/facebooklogin You should see a page with a blue button that says: "Connect with Facebook" and you should be able to login or register. @@ -101,7 +125,7 @@ the app, you are given the option to update their Facebook status via StatusNet. * Note: Before a user can disconnect from Facebook, she must set a normal - StatusNet password. Otherwise, she might not be able to login in to her + StatusNet password. Otherwise, she might not be able to login in to her account in the future. This is usually only required for users who have used Facebook Connect to register their StatusNet account, and therefore haven't already set a local password. @@ -109,16 +133,20 @@ StatusNet. Offline Queue Handling ---------------------- -For larger sites needing better performance it's possible to enable queuing -and have users' notices posted to Facebook via a separate "offline" -FacebookQueueHandler (facebookqueuhandler.php in the Facebook plugin -directory), which will be started by the plugin along with their other -daemons when you run scripts/startdaemons.sh. See the StatusNet README for -more about queuing and daemons. +For larger sites needing better performance it's possible to enable +queuing and have users' notices posted to Facebook via a separate +"offline" process -- FacebookQueueHandler (facebookqueuhandler.php in +the Facebook plugin directory). It will run automatically if you have +enabled StatusNet's offline queueing subsystem. See the "Queues and +daemons" section in the StatusNet README for more about queuing. + TODO ---- +- Make Facebook Connect work for authentication for multi-site setups + (e.g.: *.status.net) +- Posting to Facebook user streams using only Facebook Connect - Invite Facebook friends to use your StatusNet installation via Facebook Connect - Auto-subscribe Facebook friends already using StatusNet @@ -126,4 +154,4 @@ TODO - Allow users to update their Facebook statuses once they have authenticated with Facebook Connect (no need for them to use the Facebook app if they don't want to). -- Re-design the whole thing to support multiple instances of StatusNet +- Import a user's Facebook updates into StatusNet diff --git a/plugins/Facebook/facebookadminpanel.php b/plugins/Facebook/facebookadminpanel.php new file mode 100644 index 000000000..ae1c7302f --- /dev/null +++ b/plugins/Facebook/facebookadminpanel.php @@ -0,0 +1,223 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Facebook integration administration panel + * + * 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 Settings + * @package StatusNet + * @author Zach Copley <zach@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/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Administer global Facebook integration settings + * + * @category Admin + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class FacebookadminpanelAction extends AdminPanelAction +{ + /** + * Returns the page title + * + * @return string page title + */ + + function title() + { + return _m('Facebook'); + } + + /** + * Instructions for using this form. + * + * @return string instructions + */ + + function getInstructions() + { + return _m('Facebook integration settings'); + } + + /** + * Show the Facebook admin panel form + * + * @return void + */ + + function showForm() + { + $form = new FacebookAdminPanelForm($this); + $form->show(); + return; + } + + /** + * Save settings from the form + * + * @return void + */ + + function saveSettings() + { + static $settings = array( + 'facebook' => array('apikey', 'secret'), + ); + + $values = array(); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] + = $this->trimmed($setting); + } + } + + // This throws an exception on validation errors + + $this->validate($values); + + // assert(all values are valid); + + $config = new Config(); + + $config->query('BEGIN'); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + $config->query('COMMIT'); + + return; + } + + function validate(&$values) + { + // Validate consumer key and secret (can't be too long) + + if (mb_strlen($values['facebook']['apikey']) > 255) { + $this->clientError( + _m("Invalid Facebook API key. Max length is 255 characters.") + ); + } + + if (mb_strlen($values['facebook']['secret']) > 255) { + $this->clientError( + _m("Invalid Facebook API secret. Max length is 255 characters.") + ); + } + } +} + +class FacebookAdminPanelForm extends AdminForm +{ + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'facebookadminpanel'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('facebookadminpanel'); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $this->out->elementStart( + 'fieldset', + array('id' => 'settings_facebook-application') + ); + $this->out->element('legend', null, _m('Facebook application settings')); + $this->out->elementStart('ul', 'form_data'); + + $this->li(); + $this->input( + 'apikey', + _m('API key'), + _m('API key provided by Facebook'), + 'facebook' + ); + $this->unli(); + + $this->li(); + $this->input( + 'secret', + _m('Secret'), + _m('API secret provided by Facebook'), + 'facebook' + ); + $this->unli(); + + $this->out->elementEnd('ul'); + $this->out->elementEnd('fieldset'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', _('Save'), 'submit', null, _('Save Facebook settings')); + } +} diff --git a/plugins/Facebook/locale/Facebook.po b/plugins/Facebook/locale/Facebook.po index 5b313c8c5..4bc00248c 100644 --- a/plugins/Facebook/locale/Facebook.po +++ b/plugins/Facebook/locale/Facebook.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-12-07 20:38-0800\n" +"POT-Creation-Date: 2010-03-01 14:58-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -59,63 +59,31 @@ msgstr "" msgid "Lost or forgotten password?" msgstr "" -#: facebookaction.php:386 facebookhome.php:248 +#: facebookaction.php:330 facebookhome.php:248 msgid "Pagination" msgstr "" -#: facebookaction.php:395 facebookhome.php:257 +#: facebookaction.php:339 facebookhome.php:257 msgid "After" msgstr "" -#: facebookaction.php:403 facebookhome.php:265 +#: facebookaction.php:347 facebookhome.php:265 msgid "Before" msgstr "" -#: facebookaction.php:421 +#: facebookaction.php:365 msgid "No notice content!" msgstr "" -#: facebookaction.php:427 +#: facebookaction.php:371 #, php-format msgid "That's too long. Max notice size is %d chars." msgstr "" -#: facebookaction.php:523 +#: facebookaction.php:430 msgid "Notices" msgstr "" -#: facebookutil.php:280 -#, php-format -msgid "Your %1$s Facebook application access has been disabled." -msgstr "" - -#: facebookutil.php:283 -#, php-format -msgid "" -"Hi, %1$s. We're sorry to inform you that we are unable to update your " -"Facebook status from %2$s, and have disabled the Facebook application for " -"your account. This may be because you have removed the Facebook " -"application's authorization, or have deleted your Facebook account. You can " -"re-enable the Facebook application and automatic status updating by re-" -"installing the %2$s Facebook application.\n" -"\n" -"Regards,\n" -"\n" -"%2$s" -msgstr "" - -#: FBConnectLogin.php:33 -msgid "Already logged in." -msgstr "" - -#: FBConnectLogin.php:41 -msgid "Login with your Facebook Account" -msgstr "" - -#: FBConnectLogin.php:55 -msgid "Facebook Login" -msgstr "" - #: facebookhome.php:111 msgid "Server error - couldn't get user!" msgstr "" @@ -149,50 +117,6 @@ msgstr "" msgid "Skip" msgstr "" -#: facebooksettings.php:74 -msgid "There was a problem saving your sync preferences!" -msgstr "" - -#: facebooksettings.php:76 -msgid "Sync preferences saved." -msgstr "" - -#: facebooksettings.php:99 -msgid "Automatically update my Facebook status with my notices." -msgstr "" - -#: facebooksettings.php:106 -msgid "Send \"@\" replies to Facebook." -msgstr "" - -#: facebooksettings.php:115 -msgid "Prefix" -msgstr "" - -#: facebooksettings.php:117 -msgid "A string to prefix notices with." -msgstr "" - -#: facebooksettings.php:123 -msgid "Save" -msgstr "" - -#: facebooksettings.php:133 -#, php-format -msgid "" -"If you would like %s to automatically update your Facebook status with your " -"latest notice, you need to give it permission." -msgstr "" - -#: facebooksettings.php:146 -#, php-format -msgid "Allow %s to update my Facebook status" -msgstr "" - -#: facebooksettings.php:156 -msgid "Sync preferences" -msgstr "" - #: facebookinvite.php:72 #, php-format msgid "Thanks for inviting your friends to use %s" @@ -221,61 +145,85 @@ msgstr "" msgid "Send invitations" msgstr "" -#: facebookremove.php:58 -msgid "Couldn't remove Facebook user." +#: FacebookPlugin.php:413 FacebookPlugin.php:433 +msgid "Facebook" msgstr "" -#: FBConnectSettings.php:56 FacebookPlugin.php:430 +#: FacebookPlugin.php:414 +msgid "Login or register using Facebook" +msgstr "" + +#: FacebookPlugin.php:434 FBConnectSettings.php:56 msgid "Facebook Connect Settings" msgstr "" -#: FBConnectSettings.php:67 -msgid "Manage how your account connects to Facebook" +#: FacebookPlugin.php:533 +msgid "" +"The Facebook plugin allows you to integrate your StatusNet instance with <a " +"href=\"http://facebook.com/\">Facebook</a> and Facebook Connect." msgstr "" -#: FBConnectSettings.php:92 -msgid "There is no Facebook user connected to this account." +#: facebookremove.php:58 +msgid "Couldn't remove Facebook user." msgstr "" -#: FBConnectSettings.php:100 -msgid "Connected Facebook user" +#: facebooksettings.php:74 +msgid "There was a problem saving your sync preferences!" msgstr "" -#: FBConnectSettings.php:119 -msgid "Disconnect my account from Facebook" +#: facebooksettings.php:76 +msgid "Sync preferences saved." msgstr "" -#: FBConnectSettings.php:124 -msgid "" -"Disconnecting your Faceboook would make it impossible to log in! Please " +#: facebooksettings.php:99 +msgid "Automatically update my Facebook status with my notices." msgstr "" -#: FBConnectSettings.php:128 -msgid "set a password" +#: facebooksettings.php:106 +msgid "Send \"@\" replies to Facebook." msgstr "" -#: FBConnectSettings.php:130 -msgid " first." +#: facebooksettings.php:115 +msgid "Prefix" msgstr "" -#: FBConnectSettings.php:142 -msgid "Disconnect" +#: facebooksettings.php:117 +msgid "A string to prefix notices with." msgstr "" -#: FBConnectSettings.php:164 FBConnectAuth.php:90 -msgid "There was a problem with your session token. Try again, please." +#: facebooksettings.php:123 +msgid "Save" msgstr "" -#: FBConnectSettings.php:178 -msgid "Couldn't delete link to Facebook." +#: facebooksettings.php:133 +#, php-format +msgid "" +"If you would like %s to automatically update your Facebook status with your " +"latest notice, you need to give it permission." msgstr "" -#: FBConnectSettings.php:194 -msgid "You have disconnected from Facebook." +#: facebooksettings.php:146 +#, php-format +msgid "Allow %s to update my Facebook status" msgstr "" -#: FBConnectSettings.php:197 -msgid "Not sure what you're trying to do." +#: facebooksettings.php:156 +msgid "Sync preferences" +msgstr "" + +#: facebookutil.php:285 +#, php-format +msgid "" +"Hi, %1$s. We're sorry to inform you that we are unable to update your " +"Facebook status from %2$s, and have disabled the Facebook application for " +"your account. This may be because you have removed the Facebook " +"application's authorization, or have deleted your Facebook account. You can " +"re-enable the Facebook application and automatic status updating by re-" +"installing the %2$s Facebook application.\n" +"\n" +"Regards,\n" +"\n" +"%2$s" msgstr "" #: FBConnectAuth.php:51 @@ -286,6 +234,10 @@ msgstr "" msgid "There is already a local user linked with this Facebook." msgstr "" +#: FBConnectAuth.php:90 FBConnectSettings.php:164 +msgid "There was a problem with your session token. Try again, please." +msgstr "" + #: FBConnectAuth.php:95 msgid "You can't register if you don't agree to the license." msgstr "" @@ -385,10 +337,59 @@ msgstr "" msgid "Invalid username or password." msgstr "" -#: FacebookPlugin.php:409 FacebookPlugin.php:429 -msgid "Facebook" +#: FBConnectLogin.php:33 +msgid "Already logged in." msgstr "" -#: FacebookPlugin.php:410 -msgid "Login or register using Facebook" +#: FBConnectLogin.php:41 +msgid "Login with your Facebook Account" +msgstr "" + +#: FBConnectLogin.php:55 +msgid "Facebook Login" +msgstr "" + +#: FBConnectSettings.php:67 +msgid "Manage how your account connects to Facebook" +msgstr "" + +#: FBConnectSettings.php:92 +msgid "There is no Facebook user connected to this account." +msgstr "" + +#: FBConnectSettings.php:100 +msgid "Connected Facebook user" +msgstr "" + +#: FBConnectSettings.php:119 +msgid "Disconnect my account from Facebook" +msgstr "" + +#: FBConnectSettings.php:124 +msgid "" +"Disconnecting your Faceboook would make it impossible to log in! Please " +msgstr "" + +#: FBConnectSettings.php:128 +msgid "set a password" +msgstr "" + +#: FBConnectSettings.php:130 +msgid " first." +msgstr "" + +#: FBConnectSettings.php:142 +msgid "Disconnect" +msgstr "" + +#: FBConnectSettings.php:178 +msgid "Couldn't delete link to Facebook." +msgstr "" + +#: FBConnectSettings.php:194 +msgid "You have disconnected from Facebook." +msgstr "" + +#: FBConnectSettings.php:197 +msgid "Not sure what you're trying to do." msgstr "" diff --git a/plugins/Gravatar/locale/Gravatar.po b/plugins/Gravatar/locale/Gravatar.po index 1df62b666..d7275b929 100644 --- a/plugins/Gravatar/locale/Gravatar.po +++ b/plugins/Gravatar/locale/Gravatar.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-12-11 16:27-0800\n" +"POT-Creation-Date: 2010-03-01 14:58-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -59,3 +59,9 @@ msgstr "" #: GravatarPlugin.php:177 msgid "Gravatar removed." msgstr "" + +#: GravatarPlugin.php:196 +msgid "" +"The Gravatar plugin allows users to use their <a href=\"http://www.gravatar." +"com/\">Gravatar</a> with StatusNet." +msgstr "" diff --git a/plugins/LdapAuthentication/LdapAuthenticationPlugin.php b/plugins/LdapAuthentication/LdapAuthenticationPlugin.php index 768f0fe7f..e0fd615dd 100644 --- a/plugins/LdapAuthentication/LdapAuthenticationPlugin.php +++ b/plugins/LdapAuthentication/LdapAuthenticationPlugin.php @@ -76,6 +76,32 @@ class LdapAuthenticationPlugin extends AuthenticationPlugin return false; } } + + function onEndShowPageNotice($action) + { + $name = $action->trimmed('action'); + $instr = false; + + switch ($name) + { + case 'register': + if($this->autoregistration) { + $instr = 'Have an LDAP account? Use your standard username and password.'; + } + break; + case 'login': + $instr = 'Have an LDAP account? Use your standard username and password.'; + break; + default: + return true; + } + + if($instr) { + $output = common_markup_to_html($instr); + $action->raw($output); + } + return true; + } //---interface implementation---// @@ -163,15 +189,14 @@ class LdapAuthenticationPlugin extends AuthenticationPlugin $entry = $this->ldap_get_user($username, $this->attributes); if(!$entry){ //this really shouldn't happen - return $username; + $nickname = $username; }else{ $nickname = $entry->getValue($this->attributes['nickname'],'single'); - if($nickname){ - return $nickname; - }else{ - return $username; + if(!$nickname){ + $nickname = $username; } } + return common_nicknamize($nickname); } //---utility functions---// @@ -199,8 +224,7 @@ class LdapAuthenticationPlugin extends AuthenticationPlugin $ldap->setErrorHandling(PEAR_ERROR_RETURN); $err=$ldap->bind(); if (Net_LDAP2::isError($err)) { - common_log(LOG_WARNING, 'Could not connect to LDAP server: '.$err->getMessage()); - return false; + throw new Exception('Could not connect to LDAP server: '.$err->getMessage()); } if($config == null) $this->default_ldap=$ldap; diff --git a/plugins/LdapAuthentication/README b/plugins/LdapAuthentication/README index 0460fb639..c188f2dbc 100644 --- a/plugins/LdapAuthentication/README +++ b/plugins/LdapAuthentication/README @@ -9,7 +9,10 @@ to the bottom of your config.php Settings ======== -provider_name*: a unique name for this authentication provider. +provider_name*: This is a identifier designated to the connection. + It's how StatusNet will refer to the authentication source. + For the most part, any name can be used, so long as each authentication source has a different identifier. + In most cases there will be only one authentication source used. authoritative (false): Set to true if LDAP's responses are authoritative (if authorative and LDAP fails, no other password checking will be done). autoregistration (false): Set to true if users should be automatically created diff --git a/plugins/LdapAuthorization/LdapAuthorizationPlugin.php b/plugins/LdapAuthorization/LdapAuthorizationPlugin.php index 7f48ce5e1..19aff42b8 100644 --- a/plugins/LdapAuthorization/LdapAuthorizationPlugin.php +++ b/plugins/LdapAuthorization/LdapAuthorizationPlugin.php @@ -167,7 +167,7 @@ class LdapAuthorizationPlugin extends AuthorizationPlugin $ldap->setErrorHandling(PEAR_ERROR_RETURN); $err=$ldap->bind(); if (Net_LDAP2::isError($err)) { - common_log(LOG_WARNING, 'Could not connect to LDAP server: '.$err->getMessage()); + throw new Exception('Could not connect to LDAP server: '.$err->getMessage()); return false; } if($config == null) $this->default_ldap=$ldap; @@ -185,6 +185,9 @@ class LdapAuthorizationPlugin extends AuthorizationPlugin if($ldap==null) { $ldap = $this->ldap_get_connection(); } + if(! $ldap) { + throw new Exception("Could not connect to LDAP"); + } $filter = Net_LDAP2_Filter::create($this->attributes['username'], 'equals', $username); $options = array( 'attributes' => $attributes diff --git a/plugins/LdapAuthorization/README b/plugins/LdapAuthorization/README index 44239d8e0..3a6d8d25e 100644 --- a/plugins/LdapAuthorization/README +++ b/plugins/LdapAuthorization/README @@ -11,7 +11,10 @@ You *cannot* use this plugin without the LDAP Authentication plugin Settings ======== -provider_name*: name of the LDAP authentication provider that this plugin works with. +provider_name*: This is a identifier designated to the connection. + It's how StatusNet will refer to the authentication source. + For the most part, any name can be used, so long as each authentication source has a different identifier. + In most cases there will be only one authentication source used. authoritative (false): should this plugin be authoritative for authorization? uniqueMember_attribute ('uniqueMember')*: the attribute of a group diff --git a/plugins/Mapstraction/locale/Mapstraction.po b/plugins/Mapstraction/locale/Mapstraction.po index c1c50bf50..1dd5dbbcc 100644 --- a/plugins/Mapstraction/locale/Mapstraction.po +++ b/plugins/Mapstraction/locale/Mapstraction.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-12-07 20:38-0800\n" +"POT-Creation-Date: 2010-03-01 14:58-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -34,15 +34,21 @@ msgstr "" msgid "User has no profile." msgstr "" -#: usermap.php:71 -#, php-format -msgid "%s map, page %d" -msgstr "" - -#: MapstractionPlugin.php:180 +#: MapstractionPlugin.php:182 msgid "Map" msgstr "" -#: MapstractionPlugin.php:191 +#: MapstractionPlugin.php:193 msgid "Full size" msgstr "" + +#: MapstractionPlugin.php:205 +msgid "" +"Show maps of users' and friends' notices with <a href=\"http://www." +"mapstraction.com/\">Mapstraction</a> JavaScript library." +msgstr "" + +#: usermap.php:71 +#, php-format +msgid "%s map, page %d" +msgstr "" diff --git a/plugins/Mapstraction/map.php b/plugins/Mapstraction/map.php index a33dfc736..b809c1b8e 100644 --- a/plugins/Mapstraction/map.php +++ b/plugins/Mapstraction/map.php @@ -142,8 +142,6 @@ class MapAction extends OwnerDesignAction // of refactoring from within a plugin, so I'm just abusing // the ApiAction method. Don't do this unless you're me! - require_once(INSTALLDIR.'/lib/api.php'); - $act = new ApiAction('/dev/null'); $arr = $act->twitterStatusArray($notice, true); diff --git a/plugins/MobileProfile/MobileProfilePlugin.php b/plugins/MobileProfile/MobileProfilePlugin.php index cd2531fa7..f788639ae 100644 --- a/plugins/MobileProfile/MobileProfilePlugin.php +++ b/plugins/MobileProfile/MobileProfilePlugin.php @@ -312,8 +312,6 @@ class MobileProfilePlugin extends WAP20Plugin $connect = 'imsettings'; } else if (common_config('sms', 'enabled')) { $connect = 'smssettings'; - } else if (common_config('twitter', 'enabled')) { - $connect = 'twittersettings'; } $action->elementStart('ul', array('id' => 'site_nav_global_primary')); diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 3686c0226..b69430ea7 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -1,17 +1,7 @@ <?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. + * 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 @@ -28,16 +18,13 @@ Author URI: http://status.net/ */ /** - * @package FeedSubPlugin + * @package OStatusPlugin * @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'); +set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/'); class FeedSubException extends Exception { @@ -56,8 +43,8 @@ class OStatusPlugin extends Plugin // Discovery actions $m->connect('.well-known/host-meta', array('action' => 'hostmeta')); - $m->connect('main/webfinger', - array('action' => 'webfinger')); + $m->connect('main/xrd', + array('action' => 'xrd')); $m->connect('main/ostatus', array('action' => 'ostatusinit')); $m->connect('main/ostatus?nickname=:nickname', @@ -73,15 +60,13 @@ class OStatusPlugin extends Plugin $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('action' => 'usersalmon'), array('id' => '[0-9]+')); $m->connect('main/salmon/group/:id', - array('action' => 'salmongroup'), + array('action' => 'groupsalmon'), array('id' => '[0-9]+')); return true; } @@ -93,9 +78,18 @@ class OStatusPlugin extends Plugin */ function onEndInitializeQueueManager(QueueManager $qm) { - $qm->connect('hubverify', 'HubVerifyQueueHandler'); - $qm->connect('hubdistrib', 'HubDistribQueueHandler'); + // Prepare outgoing distributions after notice save. + $qm->connect('ostatus', 'OStatusQueueHandler'); + + // Outgoing from our internal PuSH hub + $qm->connect('hubconf', 'HubConfQueueHandler'); $qm->connect('hubout', 'HubOutQueueHandler'); + + // Outgoing Salmon replies (when we don't need a return value) + $qm->connect('salmon', 'SalmonQueueHandler'); + + // Incoming from a foreign PuSH hub + $qm->connect('pushin', 'PushInQueueHandler'); return true; } @@ -104,29 +98,48 @@ class OStatusPlugin extends Plugin */ function onStartEnqueueNotice($notice, &$transports) { - $transports[] = 'hubdistrib'; + $transports[] = 'ostatus'; return true; } /** + * Add a link header for LRDD Discovery + */ + function onStartShowHTML($action) + { + if ($action instanceof ShowstreamAction) { + $acct = 'acct:'. $action->profile->nickname .'@'. common_config('site', 'server'); + $url = common_local_url('xrd'); + $url.= '?uri='. $acct; + + header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="application/xrd+xml"'); + } + } + + /** * Set up a PuSH hub link to our internal link for canonical timeline * Atom feeds for users and groups. */ - function onStartApiAtom(AtomNoticeFeed $feed) + function onStartApiAtom($feed) { $id = null; if ($feed instanceof AtomUserNoticeFeed) { - $salmonAction = 'salmon'; - $id = $feed->getUser()->id; + $salmonAction = 'usersalmon'; + $user = $feed->getUser(); + $id = $user->id; + $profile = $user->getProfile(); + $feed->setActivitySubject($profile->asActivityNoun('subject')); } else if ($feed instanceof AtomGroupNoticeFeed) { - $salmonAction = 'salmongroup'; - $id = $feed->getGroup()->id; + $salmonAction = 'groupsalmon'; + $group = $feed->getGroup(); + $id = $group->id; + $feed->setActivitySubject($group->asActivitySubject()); } else { - return; + return true; } - if (!empty($id)) { + if (!empty($id)) { $hub = common_config('ostatus', 'hub'); if (empty($hub)) { // Updates will be handled through our internal PuSH hub. @@ -136,25 +149,9 @@ class OStatusPlugin extends Plugin // Also, we'll add in the salmon link $salmon = common_local_url($salmonAction, array('id' => $id)); - $feed->addLink($salmon, array('rel' => 'salmon')); + $feed->addLink($salmon, array('rel' => Salmon::NS_REPLIES)); + $feed->addLink($salmon, array('rel' => Salmon::NS_MENTIONS)); } - } - - /** - * 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; } @@ -171,6 +168,12 @@ class OStatusPlugin extends Plugin { $base = dirname(__FILE__); $lower = strtolower($cls); + $map = array('activityverb' => 'activity', + 'activityobject' => 'activity', + 'activityutils' => 'activity'); + if (isset($map[$lower])) { + $lower = $map[$lower]; + } $files = array("$base/classes/$cls.php", "$base/lib/$lower.php"); if (substr($lower, -6) == 'action') { @@ -213,60 +216,88 @@ class OStatusPlugin extends Plugin * @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 = ''; + /** + * Find any explicit remote mentions. Accepted forms: + * Webfinger: @user@example.com + * Profile link: @example.com/mublog/user + * @param Profile $sender (os user?) + * @param string $text input markup text + * @param array &$mention in/out param: set of found mentions + * @return boolean hook return value + */ - $result = $w->lookup($webfinger); - if (empty($result)) { - continue; + function onEndFindMentions($sender, $text, &$mentions) + { + preg_match_all('!(?:^|\s+) + @( # Webfinger: + (?:\w+\.)*\w+ # user + @ # @ + (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ # domain + | # Profile: + (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ # domain + (?:/\w+)+ # /path1(/path2...) + )!x', + $text, + $wmatches, + PREG_OFFSET_CAPTURE); + + foreach ($wmatches[1] as $wmatch) { + $target = $wmatch[0]; + $oprofile = null; + + if (strpos($target, '/') === false) { + $this->log(LOG_INFO, "Checking Webfinger for address '$target'"); + try { + $oprofile = Ostatus_profile::ensureWebfinger($target); + } catch (Exception $e) { + $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage()); } - - foreach ($result->links as $link) { - if ($link['rel'] == 'salmon') { - $endpoint_uri = $link['href']; + } else { + $schemes = array('https', 'http'); + foreach ($schemes as $scheme) { + $url = "$scheme://$target"; + $this->log(LOG_INFO, "Checking profile address '$url'"); + try { + $oprofile = Ostatus_profile::ensureProfile($url); + if ($oprofile) { + continue; + } + } catch (Exception $e) { + $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage()); } } + } - if (empty($endpoint_uri)) { - continue; - } + if (empty($oprofile)) { + $this->log(LOG_INFO, "No Ostatus_profile found for address '$target'"); + } else { - // FIXME: this needs to go out in a queue handler + $this->log(LOG_INFO, "Ostatus_profile found for address '$target'"); - $xml = '<?xml version="1.0" encoding="UTF-8" ?>'; - $xml .= $notice->asAtomEntry(); + if ($oprofile->isGroup()) { + continue; + } + $profile = $oprofile->localProfile(); - $salmon = new Salmon(); - $salmon->post($endpoint_uri, $xml); + $pos = $wmatch[1]; + foreach ($mentions as $i => $other) { + // If we share a common prefix with a local user, override it! + if ($other['position'] == $pos) { + unset($mentions[$i]); + } + } + $mentions[] = array('mentioned' => array($profile), + 'text' => $target, + 'position' => $pos, + 'url' => $profile->profileurl); } } - } - /** - * 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; } @@ -276,7 +307,10 @@ class OStatusPlugin extends Plugin function onCheckSchema() { $schema = Schema::get(); $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef()); + $schema->ensureTable('ostatus_source', Ostatus_source::schemaDef()); + $schema->ensureTable('feedsub', FeedSub::schemaDef()); $schema->ensureTable('hubsub', HubSub::schemaDef()); + $schema->ensureTable('magicsig', Magicsig::schemaDef()); return true; } @@ -290,16 +324,459 @@ class OStatusPlugin extends Plugin return true; } + /** + * Override the "from ostatus" bit in notice lists to link to the + * original post and show the domain it came from. + * + * @param Notice in $notice + * @param string out &$name + * @param string out &$url + * @param string out &$title + * @return mixed hook return code + */ function onStartNoticeSourceLink($notice, &$name, &$url, &$title) { if ($notice->source == 'ostatus') { - $bits = parse_url($notice->uri); - $domain = $bits['host']; + if ($notice->url) { + $bits = parse_url($notice->url); + $domain = $bits['host']; + if (substr($domain, 0, 4) == 'www.') { + $name = substr($domain, 4); + } else { + $name = $domain; + } + + $url = $notice->url; + $title = sprintf(_m("Sent from %s via OStatus"), $domain); + return false; + } + } + } + + /** + * Send incoming PuSH feeds for OStatus endpoints in for processing. + * + * @param FeedSub $feedsub + * @param DOMDocument $feed + * @return mixed hook return code + */ + function onStartFeedSubReceive($feedsub, $feed) + { + $oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri); + if ($oprofile) { + $oprofile->processFeed($feed, 'push'); + } else { + common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri"); + } + } + + /** + * When about to subscribe to a remote user, start a server-to-server + * PuSH subscription if needed. If we can't establish that, abort. + * + * @fixme If something else aborts later, we could end up with a stray + * PuSH subscription. This is relatively harmless, though. + * + * @param Profile $subscriber + * @param Profile $other + * + * @return hook return code + * + * @throws Exception + */ + function onStartSubscribe($subscriber, $other) + { + $user = User::staticGet('id', $subscriber->id); + + if (empty($user)) { + return true; + } + + $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); + + if (empty($oprofile)) { + return true; + } + + if (!$oprofile->subscribe()) { + throw new Exception(_m('Could not set up remote subscription.')); + } + } + + /** + * Having established a remote subscription, send a notification to the + * remote OStatus profile's endpoint. + * + * @param Profile $subscriber + * @param Profile $other + * + * @return hook return code + * + * @throws Exception + */ + function onEndSubscribe($subscriber, $other) + { + $user = User::staticGet('id', $subscriber->id); + + if (empty($user)) { + return true; + } + + $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); + + if (empty($oprofile)) { + return true; + } + + $act = new Activity(); + + $act->verb = ActivityVerb::FOLLOW; + + $act->id = TagURI::mint('follow:%d:%d:%s', + $subscriber->id, + $other->id, + common_date_iso8601(time())); + + $act->time = time(); + $act->title = _("Follow"); + $act->content = sprintf(_("%s is now following %s."), + $subscriber->getBestName(), + $other->getBestName()); + + $act->actor = ActivityObject::fromProfile($subscriber); + $act->object = ActivityObject::fromProfile($other); + + $oprofile->notifyActivity($act, $subscriber); + + return true; + } + + /** + * Notify remote server and garbage collect unused feeds on unsubscribe. + * @fixme send these operations to background queues + * + * @param User $user + * @param Profile $other + * @return hook return value + */ + function onEndUnsubscribe($profile, $other) + { + $user = User::staticGet('id', $profile->id); + + if (empty($user)) { + return true; + } + + $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); + + if (empty($oprofile)) { + return true; + } + + // Drop the PuSH subscription if there are no other subscribers. + $oprofile->garbageCollect(); + + $act = new Activity(); + + $act->verb = ActivityVerb::UNFOLLOW; - $name = $domain; - $url = $notice->uri; - $title = sprintf(_m("Sent from %s via OStatus"), $domain); + $act->id = TagURI::mint('unfollow:%d:%d:%s', + $profile->id, + $other->id, + common_date_iso8601(time())); + + $act->time = time(); + $act->title = _("Unfollow"); + $act->content = sprintf(_("%s stopped following %s."), + $profile->getBestName(), + $other->getBestName()); + + $act->actor = ActivityObject::fromProfile($profile); + $act->object = ActivityObject::fromProfile($other); + + $oprofile->notifyActivity($act, $profile); + + return true; + } + + /** + * When one of our local users tries to join a remote group, + * notify the remote server. If the notification is rejected, + * deny the join. + * + * @param User_group $group + * @param User $user + * + * @return mixed hook return value + */ + + function onStartJoinGroup($group, $user) + { + $oprofile = Ostatus_profile::staticGet('group_id', $group->id); + if ($oprofile) { + if (!$oprofile->subscribe()) { + throw new Exception(_m('Could not set up remote group membership.')); + } + + $member = Profile::staticGet($user->id); + + $act = new Activity(); + $act->id = TagURI::mint('join:%d:%d:%s', + $member->id, + $group->id, + common_date_iso8601(time())); + + $act->actor = ActivityObject::fromProfile($member); + $act->verb = ActivityVerb::JOIN; + $act->object = $oprofile->asActivityObject(); + + $act->time = time(); + $act->title = _m("Join"); + $act->content = sprintf(_m("%s has joined group %s."), + $member->getBestName(), + $oprofile->getBestName()); + + if ($oprofile->notifyActivity($act, $member)) { + return true; + } else { + $oprofile->garbageCollect(); + throw new Exception(_m("Failed joining remote group.")); + } + } + } + + /** + * When one of our local users leaves a remote group, notify the remote + * server. + * + * @fixme Might be good to schedule a resend of the leave notification + * if it failed due to a transitory error. We've canceled the local + * membership already anyway, but if the remote server comes back up + * it'll be left with a stray membership record. + * + * @param User_group $group + * @param User $user + * + * @return mixed hook return value + */ + + function onEndLeaveGroup($group, $user) + { + $oprofile = Ostatus_profile::staticGet('group_id', $group->id); + if ($oprofile) { + // Drop the PuSH subscription if there are no other subscribers. + $oprofile->garbageCollect(); + + + $member = Profile::staticGet($user->id); + + $act = new Activity(); + $act->id = TagURI::mint('leave:%d:%d:%s', + $member->id, + $group->id, + common_date_iso8601(time())); + + $act->actor = ActivityObject::fromProfile($member); + $act->verb = ActivityVerb::LEAVE; + $act->object = $oprofile->asActivityObject(); + + $act->time = time(); + $act->title = _m("Leave"); + $act->content = sprintf(_m("%s has left group %s."), + $member->getBestName(), + $oprofile->getBestName()); + + $oprofile->notifyActivity($act, $member); + } + } + + /** + * Notify remote users when their notices get favorited. + * + * @param Profile or User $profile of local user doing the faving + * @param Notice $notice being favored + * @return hook return value + */ + + function onEndFavorNotice(Profile $profile, Notice $notice) + { + $user = User::staticGet('id', $profile->id); + + if (empty($user)) { + return true; + } + + $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id); + + if (empty($oprofile)) { + return true; + } + + $act = new Activity(); + + $act->verb = ActivityVerb::FAVORITE; + $act->id = TagURI::mint('favor:%d:%d:%s', + $profile->id, + $notice->id, + common_date_iso8601(time())); + + $act->time = time(); + $act->title = _("Favor"); + $act->content = sprintf(_("%s marked notice %s as a favorite."), + $profile->getBestName(), + $notice->uri); + + $act->actor = ActivityObject::fromProfile($profile); + $act->object = ActivityObject::fromNotice($notice); + + $oprofile->notifyActivity($act, $profile); + + return true; + } + + /** + * Notify remote users when their notices get de-favorited. + * + * @param Profile $profile Profile person doing the de-faving + * @param Notice $notice Notice being favored + * + * @return hook return value + */ + + function onEndDisfavorNotice(Profile $profile, Notice $notice) + { + $user = User::staticGet('id', $profile->id); + + if (empty($user)) { + return true; + } + + $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id); + + if (empty($oprofile)) { + return true; + } + + $act = new Activity(); + + $act->verb = ActivityVerb::UNFAVORITE; + $act->id = TagURI::mint('disfavor:%d:%d:%s', + $profile->id, + $notice->id, + common_date_iso8601(time())); + $act->time = time(); + $act->title = _("Disfavor"); + $act->content = sprintf(_("%s marked notice %s as no longer a favorite."), + $profile->getBestName(), + $notice->uri); + + $act->actor = ActivityObject::fromProfile($profile); + $act->object = ActivityObject::fromNotice($notice); + + $oprofile->notifyActivity($act, $profile); + + return true; + } + + function onStartGetProfileUri($profile, &$uri) + { + $oprofile = Ostatus_profile::staticGet('profile_id', $profile->id); + if (!empty($oprofile)) { + $uri = $oprofile->uri; + return false; + } + return true; + } + + function onStartUserGroupHomeUrl($group, &$url) + { + return $this->onStartUserGroupPermalink($group, $url); + } + + function onStartUserGroupPermalink($group, &$url) + { + $oprofile = Ostatus_profile::staticGet('group_id', $group->id); + if ($oprofile) { + // @fixme this should probably be in the user_group table + // @fixme this uri not guaranteed to be a profile page + $url = $oprofile->uri; return false; } } + + function onStartShowSubscriptionsContent($action) + { + $this->showEntityRemoteSubscribe($action); + + return true; + } + + function onStartShowAllContent($action) + { + $this->showEntityRemoteSubscribe($action); + + return true; + } + + function showEntityRemoteSubscribe($action) + { + $user = common_current_user(); + if ($user && ($user->id == $action->profile->id)) { + $action->elementStart('div', 'entity_actions'); + $action->elementStart('p', array('id' => 'entity_remote_subscribe', + 'class' => 'entity_subscribe')); + $action->element('a', array('href' => common_local_url('ostatussub'), + 'class' => 'entity_remote_subscribe') + , _m('Subscribe to remote user')); + $action->elementEnd('p'); + $action->elementEnd('div'); + } + } + + /** + * Ping remote profiles with updates to this profile. + * Salmon pings are queued for background processing. + */ + function onEndBroadcastProfile(Profile $profile) + { + $user = User::staticGet('id', $profile->id); + + // Find foreign accounts I'm subscribed to that support Salmon pings. + // + // @fixme we could run updates through the PuSH feed too, + // in which case we can skip Salmon pings to folks who + // are also subscribed to me. + $sql = "SELECT * FROM ostatus_profile " . + "WHERE profile_id IN " . + "(SELECT subscribed FROM subscription WHERE subscriber=%d) " . + "OR group_id IN " . + "(SELECT group_id FROM group_member WHERE profile_id=%d)"; + $oprofile = new Ostatus_profile(); + $oprofile->query(sprintf($sql, $profile->id, $profile->id)); + + if ($oprofile->N == 0) { + common_log(LOG_DEBUG, "No OStatus remote subscribees for $profile->nickname"); + return true; + } + + $act = new Activity(); + + $act->verb = ActivityVerb::UPDATE_PROFILE; + $act->id = TagURI::mint('update-profile:%d:%s', + $profile->id, + common_date_iso8601(time())); + $act->time = time(); + $act->title = _m("Profile update"); + $act->content = sprintf(_m("%s has updated their profile page."), + $profile->getBestName()); + + $act->actor = ActivityObject::fromProfile($profile); + $act->object = $act->actor; + + while ($oprofile->fetch()) { + $oprofile->notifyDeferred($act, $profile); + } + + return true; + } } diff --git a/plugins/OStatus/README b/plugins/OStatus/README index cbf3adbb9..3a98b7b25 100644 --- a/plugins/OStatus/README +++ b/plugins/OStatus/README @@ -2,23 +2,33 @@ Plugin to support importing updates from external RSS and Atom feeds into your t Uses PubSubHubbub for push feed updates; currently non-PuSH feeds cannot be subscribed. +Configuration options available: + +$config['ostatus']['hub'] + (default internal hub) + Set to URL of an external PuSH hub to use it instead of our internal hub. + +$config['ostatus']['hub_retries'] + (default 0) + Number of times to retry a PuSH send to consumers if using internal hub + + +For testing, shouldn't be used in production: + +$config['ostatus']['skip_signatures'] + (default use signatures) + Disable generation and validation of Salmon magicenv signatures + +$config['feedsub']['nohub'] + (default require hub) + Allow low-level feed subscription setup for feeds without hubs. + Not actually usable at this stage, OStatus will check for hubs too + and we have no polling backend. + + Todo: -* set feed icon avatar for actual profiles as well as for preview -* use channel image and/or favicon for avatar? -* garbage-collect subscriptions that are no longer being used -* administrative way to kill feeds? -* functional l10n -* clean up subscription form look and workflow -* use ajax for test/preview in subscription form -* rssCloud support? (Does anything use it that doesn't support PuSH as well?) -* possibly a polling daemon to support non-PuSH feeds? -* likely problems with multiple feeds from the same site, such as category feeds on a blog - (currently each feed would publish a separate notice on a separate profile, but pointing to the same post URI.) - (could use the local URI I guess, but that's so icky!) -* problems with Atom feeds that list <link rel="alternate" href="..."/> but don't have the type - (such as http://atomgen.appspot.com/feed/5 demo feed); currently it's not recognized and we end up with the feed's master URI -* make it easier to see what you're subscribed to and unsub from things -* saner treatment of fullname/nickname? +* fully functional l10n +* redo non-OStatus feed support +** rssCloud support? +** possibly a polling daemon to support non-PuSH feeds? * make use of tags/categories from feeds -* update feed profile data when it changes -* XML_Feed_Parser has major problems with category and link tags; consider replacing? diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php deleted file mode 100644 index 6933c9bf2..000000000 --- a/plugins/OStatus/actions/feedsubsettings.php +++ /dev/null @@ -1,269 +0,0 @@ -<?php -/* - * 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); } - -class FeedSubSettingsAction extends ConnectSettingsAction -{ - protected $feedurl; - protected $preview; - protected $munger; - - /** - * Title of the page - * - * @return string Title of the page - */ - - function title() - { - return _m('Feed subscriptions'); - } - - /** - * Instructions for use - * - * @return instructions for use - */ - - function getInstructions() - { - return _m('You can subscribe to feeds from other sites; ' . - 'updates will appear in your personal timeline.'); - } - - /** - * Content area of the page - * - * Shows a form for associating a Twitter account with this - * StatusNet account. Also lets the user set preferences. - * - * @return void - */ - - 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', array('id' => 'settings_twitter_login_button')); - $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed')); - $this->elementEnd('li'); - $this->elementEnd('ul'); - - if ($this->preview) { - $this->submit('subscribe', _m('Subscribe')); - } else { - $this->submit('validate', _m('Continue')); - } - - $this->elementEnd('fieldset'); - - $this->elementEnd('form'); - - if ($this->preview) { - $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('validate')) { - $this->validateAndPreview(); - } else 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 = trim($this->arg('feedurl')); - - 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 == '' && !common_config('feedsub', 'nohub')) { - $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 (!$this->profile) { - throw new ServerException("Feed profile was not saved properly."); - } - - // 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(); - - 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 { - $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!')); - } - } - } - } - - function validateAndPreview() - { - if ($this->validateFeed()) { - $this->preview = true; - $this->showForm(_m('Previewing feed:')); - } - } - - 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.'); - } - } - - function showScripts() - { - parent::showScripts(); - $this->autofocus('feedurl'); - } -} diff --git a/plugins/OStatus/actions/groupsalmon.php b/plugins/OStatus/actions/groupsalmon.php new file mode 100644 index 000000000..29377b5fa --- /dev/null +++ b/plugins/OStatus/actions/groupsalmon.php @@ -0,0 +1,188 @@ +<?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 GroupsalmonAction extends SalmonAction +{ + var $group = null; + + function prepare($args) + { + parent::prepare($args); + + $id = $this->trimmed('id'); + + if (!$id) { + $this->clientError(_('No ID.')); + } + + $this->group = User_group::staticGet('id', $id); + + if (empty($this->group)) { + $this->clientError(_('No such group.')); + } + + $oprofile = Ostatus_profile::staticGet('group_id', $id); + if ($oprofile) { + $this->clientError(_m("Can't accept remote posts for a remote group.")); + } + + return true; + } + + /** + * We've gotten a post event on the Salmon backchannel, probably a reply. + */ + + function handlePost() + { + switch ($this->act->object->type) { + case ActivityObject::ARTICLE: + case ActivityObject::BLOGENTRY: + case ActivityObject::NOTE: + case ActivityObject::STATUS: + case ActivityObject::COMMENT: + break; + default: + throw new ClientException("Can't handle that kind of post."); + } + + // Notice must be to the attention of this group + + $context = $this->act->context; + + if (empty($context->attention)) { + throw new ClientException("Not to the attention of anyone."); + } else { + $uri = common_local_url('groupbyid', array('id' => $this->group->id)); + if (!in_array($uri, $context->attention)) { + throw new ClientException("Not to the attention of this group."); + } + } + + $profile = $this->ensureProfile(); + $this->saveNotice(); + } + + /** + * We've gotten a follow/subscribe notification from a remote user. + * Save a subscription relationship for them. + */ + + /** + * Postel's law: consider a "follow" notification as a "join". + */ + function handleFollow() + { + $this->handleJoin(); + } + + /** + * Postel's law: consider an "unfollow" notification as a "leave". + */ + function handleUnfollow() + { + $this->handleLeave(); + } + + /** + * A remote user joined our group. + * @fixme move permission checks and event call into common code, + * currently we're doing the main logic in joingroup action + * and so have to repeat it here. + */ + + function handleJoin() + { + $oprofile = $this->ensureProfile(); + if (!$oprofile) { + $this->clientError(_m("Can't read profile to set up group membership.")); + } + if ($oprofile->isGroup()) { + $this->clientError(_m("Groups can't join groups.")); + } + + common_log(LOG_INFO, "Remote profile {$oprofile->uri} joining local group {$this->group->nickname}"); + $profile = $oprofile->localProfile(); + + if ($profile->isMember($this->group)) { + // Already a member; we'll take it silently to aid in resolving + // inconsistencies on the other side. + return true; + } + + if (Group_block::isBlocked($this->group, $profile)) { + $this->clientError(_('You have been blocked from that group by the admin.'), 403); + return false; + } + + try { + // @fixme that event currently passes a user from main UI + // Event should probably move into Group_member::join + // and take a Profile object. + // + //if (Event::handle('StartJoinGroup', array($this->group, $profile))) { + Group_member::join($this->group->id, $profile->id); + //Event::handle('EndJoinGroup', array($this->group, $profile)); + //} + } catch (Exception $e) { + $this->serverError(sprintf(_m('Could not join remote user %1$s to group %2$s.'), + $oprofile->uri, $this->group->nickname)); + } + } + + /** + * A remote user left our group. + */ + + function handleLeave() + { + $oprofile = $this->ensureProfile(); + if (!$oprofile) { + $this->clientError(_m("Can't read profile to cancel group membership.")); + } + if ($oprofile->isGroup()) { + $this->clientError(_m("Groups can't join groups.")); + } + + common_log(LOG_INFO, "Remote profile {$oprofile->uri} leaving local group {$this->group->nickname}"); + $profile = $oprofile->localProfile(); + + try { + // @fixme event needs to be refactored as above + //if (Event::handle('StartLeaveGroup', array($this->group, $profile))) { + Group_member::leave($this->group->id, $profile->id); + //Event::handle('EndLeaveGroup', array($this->group, $profile)); + //} + } catch (Exception $e) { + $this->serverError(sprintf(_m('Could not remove remote user %1$s from group %2$s.'), + $oprofile->uri, $this->group->nickname)); + return; + } + } + +} diff --git a/plugins/OStatus/actions/hostmeta.php b/plugins/OStatus/actions/hostmeta.php index 850b8a0fe..3d00b98ae 100644 --- a/plugins/OStatus/actions/hostmeta.php +++ b/plugins/OStatus/actions/hostmeta.php @@ -31,12 +31,18 @@ class HostMetaAction extends Action { parent::handle(); - $w = new Webfinger(); - - $domain = common_config('site', 'server'); - $url = common_local_url('webfinger'); + $url = common_local_url('xrd'); $url.= '?uri={uri}'; - print $w->getHostMeta($domain, $url); + + $xrd = new XRD(); + + $xrd = new XRD(); + $xrd->host = $domain; + $xrd->links[] = array('rel' => Discovery::LRDD_REL, + 'template' => $url, + 'title' => array('Resource Descriptor')); + + print $xrd->toXML(); } } diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php index d21774420..8ba8dcdcc 100644 --- a/plugins/OStatus/actions/ostatusinit.php +++ b/plugins/OStatus/actions/ostatusinit.php @@ -29,7 +29,7 @@ class OStatusInitAction extends Action { var $nickname; - var $acct; + var $profile; var $err; function prepare($args) @@ -37,12 +37,15 @@ class OStatusInitAction extends Action parent::prepare($args); if (common_logged_in()) { - $this->clientError(_('You can use the local subscription!')); + $this->clientError(_m('You can use the local subscription!')); return false; } - $this->nickname = $this->trimmed('nickname'); - $this->acct = $this->trimmed('acct'); + // Local user the remote wants to subscribe to + $this->nickname = $this->trimmed('nickname'); + + // Webfinger or profile URL of the remote user + $this->profile = $this->trimmed('profile'); return true; } @@ -55,7 +58,7 @@ class OStatusInitAction extends Action /* 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. '. + $this->showForm(_m('There was a problem with your session token. '. 'Try again, please.')); return; } @@ -73,7 +76,7 @@ class OStatusInitAction extends Action $this->xw->startDocument('1.0', 'UTF-8'); $this->elementStart('html'); $this->elementStart('head'); - $this->element('title', null, _('Subscribe to user')); + $this->element('title', null, _m('Subscribe to user')); $this->elementEnd('head'); $this->elementStart('body'); $this->showContent(); @@ -91,50 +94,81 @@ class OStatusInitAction extends Action 'class' => 'form_settings', 'action' => common_local_url('ostatusinit'))); $this->elementStart('fieldset'); - $this->element('legend', null, sprintf(_('Subscribe to %s'), $this->nickname)); + $this->element('legend', null, sprintf(_m('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->input('nickname', _m('User nickname'), $this->nickname, + _m('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->input('profile', _m('Profile Account'), $this->profile, + _m('Your account id (i.e. user@identi.ca)')); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('submit', _('Subscribe')); + $this->submit('submit', _m('Subscribe')); $this->elementEnd('fieldset'); $this->elementEnd('form'); } function ostatusConnect() { - $w = new Webfinger; + $opts = array('allowed_schemes' => array('http', 'https', 'acct')); + if (Validate::uri($this->profile, $opts)) { + $bits = parse_url($this->profile); + if ($bits['scheme'] == 'acct') { + $this->connectWebfinger($bits['path']); + } else { + $this->connectProfile($this->profile); + } + } elseif (strpos($this->profile, '@') !== false) { + $this->connectWebfinger($this->profile); + } else { + $this->clientError(_m("Must provide a remote profile.")); + } + } - $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! + function connectWebfinger($acct) + { + $disco = new Discovery; - $user = User::staticGet('nickname', $this->nickname); + $result = $disco->lookup($acct); + if (!$result) { + $this->clientError(_m("Couldn't look up OStatus account profile.")); + } + foreach ($result->links as $link) { + if ($link['rel'] == 'http://ostatus.org/schema/1.0/subscribe') { + // We found a URL - let's redirect! - $feed_url = common_local_url('ApiTimelineUser', - array('id' => $user->id, - 'format' => 'atom')); - $url = $w->applyTemplate($link['template'], $feed_url); + $user = User::staticGet('nickname', $this->nickname); + $target_profile = common_local_url('userbyid', array('id' => $user->id)); - common_redirect($url, 303); - } + $url = Discovery::applyTemplate($link['template'], $target_profile); + common_log(LOG_INFO, "Sending remote subscriber $acct to $url"); + common_redirect($url, 303); + } - } - + } + $this->clientError(_m("Couldn't confirm remote profile address.")); } - + + function connectProfile($subscriber_profile) + { + $user = User::staticGet('nickname', $this->nickname); + $target_profile = common_local_url('userbyid', array('id' => $user->id)); + + // @fixme hack hack! We should look up the remote sub URL from XRDS + $suburl = preg_replace('!^(.*)/(.*?)$!', '$1/main/ostatussub', $subscriber_profile); + $suburl .= '?profile=' . urlencode($target_profile); + + common_log(LOG_INFO, "Sending remote subscriber $subscriber_profile to $suburl"); + common_redirect($suburl, 303); + } + function title() { - return _('OStatus Connect'); + return _m('OStatus Connect'); } } diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index 239122501..e318701a2 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -1,7 +1,7 @@ <?php /* * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2010, StatusNet, Inc. + * 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 @@ -19,57 +19,44 @@ /** * @package OStatusPlugin - * @maintainer James Walker <james@status.net> + * @maintainer Brion Vibber <brion@status.net> */ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +/** + * Key UI methods: + * + * showInputForm() - form asking for a remote profile account or URL + * We end up back here on errors + * + * showPreviewForm() - surrounding form for preview-and-confirm + * previewUser() - display profile for a remote user + * previewGroup() - display profile for a remote group + * + * successUser() - redirects to subscriptions page on subscribe + * successGroup() - redirects to groups page on join + */ class OStatusSubAction extends Action { + protected $profile_uri; // provided acct: or URI of remote entity + protected $oprofile; // Ostatus_profile of remote entity, if valid - 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() + /** + * Show the initial form, when we haven't yet been given a valid + * remote profile. + */ + function showInputForm() { $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', + 'id' => 'form_ostatus_sub', 'class' => 'form_settings', 'action' => - common_local_url('feedsubsettings'))); + common_local_url('ostatussub'))); $this->hidden('token', common_session_token()); @@ -77,150 +64,440 @@ class OStatusSubAction extends Action $this->elementStart('ul', 'form_data'); $this->elementStart('li'); - $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed')); + $this->input('profile', + _m('Address or profile URL'), + $this->profile_uri, + _m('Enter the profile URL of a PubSubHubbub-enabled feed')); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('subscribe', _m('Subscribe')); + $this->submit('validate', _m('Continue')); $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. + * Show the preview-and-confirm form. We've got a valid remote + * profile and are ready to poke it! * - * @return void + * This controls the wrapper form; actual profile display will + * be in previewUser() or previewGroup() depending on the type. */ - - function handlePost() + function showPreviewForm() { - // 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.')); + if ($this->oprofile->isGroup()) { + $ok = $this->previewGroup(); + } else { + $ok = $this->previewUser(); + } + if (!$ok) { + // @fixme maybe provide a cancel button or link back? return; } - if ($this->arg('subscribe')) { - $this->saveFeed(); + $this->elementStart('div', 'entity_actions'); + $this->elementStart('ul'); + $this->elementStart('li', 'entity_subscribe'); + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_ostatus_sub', + 'class' => 'form_remote_authorize', + 'action' => + common_local_url('ostatussub'))); + $this->elementStart('fieldset'); + $this->hidden('token', common_session_token()); + $this->hidden('profile', $this->profile_uri); + if ($this->oprofile->isGroup()) { + $this->submit('submit', _m('Join'), 'submit', null, + _m('Join this group')); } else { - $this->showForm(_('Unexpected form submission.')); + $this->submit('submit', _m('Confirm'), 'submit', null, + _m('Subscribe to this user')); } + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + $this->elementEnd('li'); + $this->elementEnd('ul'); + $this->elementEnd('div'); + } + + /** + * Show a preview for a remote user's profile + * @return boolean true if we're ok to try subscribing + */ + function previewUser() + { + $oprofile = $this->oprofile; + $profile = $oprofile->localProfile(); + + $cur = common_current_user(); + if ($cur->isSubscribed($profile)) { + $this->element('div', array('class' => 'error'), + _m("You are already subscribed to this user.")); + $ok = false; + } else { + $ok = true; + } + + $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + $avatarUrl = $avatar ? $avatar->displayUrl() : false; + + $this->showEntity($profile, + $profile->profileurl, + $avatarUrl, + $profile->bio); + return $ok; + } + + /** + * Show a preview for a remote group's profile + * @return boolean true if we're ok to try joining + */ + function previewGroup() + { + $oprofile = $this->oprofile; + $group = $oprofile->localGroup(); + + $cur = common_current_user(); + if ($cur->isMember($group)) { + $this->element('div', array('class' => 'error'), + _m("You are already a member of this group.")); + $ok = false; + } else { + $ok = true; + } + + $this->showEntity($group, + $group->getProfileUrl(), + $group->homepage_logo, + $group->description); + return $ok; + } + + + function showEntity($entity, $profile, $avatar, $note) + { + $nickname = $entity->nickname; + $fullname = $entity->fullname; + $homepage = $entity->homepage; + $location = $entity->location; + + if (!$avatar) { + $avatar = Avatar::defaultImage(AVATAR_PROFILE_SIZE); + } + + $this->elementStart('div', 'entity_profile vcard'); + $this->elementStart('dl', 'entity_depiction'); + $this->element('dt', null, _('Photo')); + $this->elementStart('dd'); + $this->element('img', array('src' => $avatar, + 'class' => 'photo avatar', + 'width' => AVATAR_PROFILE_SIZE, + 'height' => AVATAR_PROFILE_SIZE, + 'alt' => $nickname)); + $this->elementEnd('dd'); + $this->elementEnd('dl'); + + $this->elementStart('dl', 'entity_nickname'); + $this->element('dt', null, _('Nickname')); + $this->elementStart('dd'); + $hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname'; + $this->elementStart('a', array('href' => $profile, + 'class' => 'url '.$hasFN)); + $this->raw($nickname); + $this->elementEnd('a'); + $this->elementEnd('dd'); + $this->elementEnd('dl'); + + if (!is_null($fullname)) { + $this->elementStart('dl', 'entity_fn'); + $this->elementStart('dd'); + $this->elementStart('span', 'fn'); + $this->raw($fullname); + $this->elementEnd('span'); + $this->elementEnd('dd'); + $this->elementEnd('dl'); + } + if (!is_null($location)) { + $this->elementStart('dl', 'entity_location'); + $this->element('dt', null, _('Location')); + $this->elementStart('dd', 'label'); + $this->raw($location); + $this->elementEnd('dd'); + $this->elementEnd('dl'); + } + + if (!is_null($homepage)) { + $this->elementStart('dl', 'entity_url'); + $this->element('dt', null, _('URL')); + $this->elementStart('dd'); + $this->elementStart('a', array('href' => $homepage, + 'class' => 'url')); + $this->raw($homepage); + $this->elementEnd('a'); + $this->elementEnd('dd'); + $this->elementEnd('dl'); + } + + if (!is_null($note)) { + $this->elementStart('dl', 'entity_note'); + $this->element('dt', null, _('Note')); + $this->elementStart('dd', 'note'); + $this->raw($note); + $this->elementEnd('dd'); + $this->elementEnd('dl'); + } + $this->elementEnd('div'); + } + + /** + * Redirect on successful remote user subscription + */ + function successUser() + { + $cur = common_current_user(); + $url = common_local_url('subscriptions', array('nickname' => $cur->nickname)); + common_redirect($url, 303); + } + + /** + * Redirect on successful remote group join + */ + function successGroup() + { + $cur = common_current_user(); + $url = common_local_url('usergroups', array('nickname' => $cur->nickname)); + common_redirect($url, 303); } - /** - * Set up and add a feed + * Pull data for a remote profile and check if it's valid. + * Fills out error UI string in $this->error + * Fills out $this->oprofile on success. * - * @return boolean true if feed successfully read - * Sends you back to input form if not. + * @return boolean */ function validateFeed() { - $feedurl = $this->trimmed('feed'); - - if ($feedurl == '') { - $this->showForm(_m('Empty feed URL!')); + $profile_uri = trim($this->arg('profile')); + + if ($profile_uri == '') { + $this->showForm(_m('Empty remote profile URL!')); return; } - $this->feedurl = $feedurl; - - // Get the canonical feed URI and check it + $this->profile_uri = $profile_uri; + try { - $discover = new FeedDiscovery(); - $uri = $discover->discoverFromURL($feedurl); + if (Validate::email($this->profile_uri)) { + $this->oprofile = Ostatus_profile::ensureWebfinger($this->profile_uri); + } else if (Validate::uri($this->profile_uri)) { + $this->oprofile = Ostatus_profile::ensureProfile($this->profile_uri); + } else { + $this->error = _m("Invalid address format."); + return false; + } + return true; } catch (FeedSubBadURLException $e) { - $this->showForm(_m('Invalid URL or could not reach server.')); - return false; + $this->error = _m('Invalid URL or could not reach server.'); } catch (FeedSubBadResponseException $e) { - $this->showForm(_m('Cannot read feed; server returned error.')); - return false; + $this->error = _m('Cannot read feed; server returned error.'); } catch (FeedSubEmptyException $e) { - $this->showForm(_m('Cannot read feed; server returned an empty page.')); - return false; + $this->error = _m('Cannot read feed; server returned an empty page.'); } catch (FeedSubBadHTMLException $e) { - $this->showForm(_m('Bad HTML, could not find feed link.')); - return false; + $this->error = _m('Bad HTML, could not find feed link.'); } catch (FeedSubNoFeedException $e) { - $this->showForm(_m('Could not find a feed linked from this URL.')); - return false; + $this->error = _m('Could not find a feed linked from this URL.'); } catch (FeedSubUnrecognizedTypeException $e) { - $this->showForm(_m('Not a recognized feed type.')); - return false; + $this->error = _m('Not a recognized feed type.'); } catch (FeedSubException $e) { // Any new ones we forgot about - $this->showForm(_m('Bad feed URL.')); - return false; + $this->error = sprintf(_m('Bad feed URL: %s %s'), get_class($e), $e->getMessage()); } - - $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; + return false; } + /** + * Attempt to finalize subscription. + * validateFeed must have been run first. + * + * Calls showForm on failure or successUser/successGroup on success. + */ function saveFeed() { - if ($this->validateFeed()) { - $this->preview = true; - $this->profile = Ostatus_profile::ensureProfile($this->munger); + // And subscribe the current user to the local profile + $user = common_current_user(); - // 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; + if ($this->oprofile->isGroup()) { + $group = $this->oprofile->localGroup(); + if ($user->isMember($group)) { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Already a member!')); + return; + } + if (Event::handle('StartJoinGroup', array($group, $user))) { + $ok = Group_member::join($this->oprofile->group_id, $user->id); + if ($ok) { + Event::handle('EndJoinGroup', array($group, $user)); + $this->successGroup(); + } else { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Remote group join failed!')); } + } else { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Remote group join aborted!')); } - - // And subscribe the current user to the local profile - $user = common_current_user(); - $profile = $this->profile->getProfile(); - - if ($user->isSubscribed($profile)) { + } else { + $local = $this->oprofile->localProfile(); + if ($user->isSubscribed($local)) { + // TRANS: OStatus remote subscription dialog error. $this->showForm(_m('Already subscribed!')); - } elseif ($user->subscribeTo($profile)) { - $this->showForm(_m('Feed subscribed!')); + } elseif ($this->oprofile->subscribeLocalToRemote($user)) { + $this->successUser(); } else { - $this->showForm(_m('Feed subscription failed!')); + // TRANS: OStatus remote subscription dialog error. + $this->showForm(_m('Remote subscription failed!')); + } + } + } + + function prepare($args) + { + parent::prepare($args); + + if (!common_logged_in()) { + // XXX: selfURL() didn't work. :< + common_set_returnto($_SERVER['REQUEST_URI']); + if (Event::handle('RedirectToLogin', array($this, null))) { + common_redirect(common_local_url('login'), 303); + } + return false; + } + + $this->profile_uri = $this->arg('profile'); + + return true; + } + + /** + * Handle the submission. + */ + function handle($args) + { + parent::handle($args); + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->handlePost(); + } else { + if ($this->arg('profile')) { + $this->validateFeed(); } + $this->showForm(); } } - - function previewFeed() + + /** + * Handle posts to this form + * + * @return void + */ + + function handlePost() { - $profile = $this->munger->ostatusProfile(); - $notice = $this->munger->notice(0, true); // preview + // 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 ($notice) { - $this->element('b', null, 'Preview of latest post from this feed:'); + if ($this->validateFeed()) { + if ($this->arg('submit')) { + $this->saveFeed(); + return; + } + } + $this->showForm(); + } - $item = new NoticeList($notice, $this); - $item->show(); + /** + * Show the appropriate form based on our input state. + */ + function showForm($err=null) + { + if ($err) { + $this->error = $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, _m('Subscribe to user')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->showContent(); + $this->elementEnd('body'); + $this->elementEnd('html'); } else { - $this->element('b', null, 'No posts in this feed yet.'); + $this->showPage(); + } + } + + /** + * Title of the page + * + * @return string Title of the page + */ + + function title() + { + // TRANS: Page title for OStatus remote subscription form + return _m('Authorize subscription'); + } + + /** + * Instructions for use + * + * @return instructions for use + */ + + function getInstructions() + { + return _m('You can subscribe to users from other supported sites. Paste their address or profile URI below:'); + } + + function showPageNotice() + { + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); } } + /** + * Content area of the page + * + * Shows a form for associating a remote OStatus account with this + * StatusNet account. + * + * @return void + */ + + function showContent() + { + if ($this->oprofile) { + $this->showPreviewForm(); + } else { + $this->showInputForm(); + } + } + function showScripts() + { + parent::showScripts(); + $this->autofocus('feedurl'); + } } diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index 388c8f9c3..9a2067b8c 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -29,6 +29,7 @@ class PushCallbackAction extends Action { function handle() { + StatusNet::setApi(true); // Minimize error messages to aid in debugging parent::handle(); if ($_SERVER['REQUEST_METHOD'] == 'POST') { $this->handlePost(); @@ -48,9 +49,9 @@ class PushCallbackAction extends Action throw new ServerException('Empty or invalid feed id', 400); } - $profile = Ostatus_profile::staticGet('id', $feedid); - if (!$profile) { - throw new ServerException('Unknown OStatus/PuSH feed id ' . $feedid, 400); + $feedsub = FeedSub::staticGet('id', $feedid); + if (!$feedsub) { + throw new ServerException('Unknown PuSH feed id ' . $feedid, 400); } $hmac = ''; @@ -60,13 +61,18 @@ class PushCallbackAction extends Action $post = file_get_contents('php://input'); - // @fixme Queue this to a background process; we should return + // Queue this to a background process; we should return // as quickly as possible from a distribution POST. - $profile->postUpdates($post, $hmac); + // If queues are disabled this'll process immediately. + $data = array('feedsub_id' => $feedsub->id, + 'post' => $post, + 'hmac' => $hmac); + $qm = QueueManager::get(); + $qm->enqueue($data, 'pushin'); } /** - * Handler for GET verification requests from the hub + * Handler for GET verification requests from the hub. */ function handleGet() { @@ -75,35 +81,41 @@ class PushCallbackAction extends Action $challenge = $this->arg('hub_challenge'); $lease_seconds = $this->arg('hub_lease_seconds'); $verify_token = $this->arg('hub_verify_token'); - + if ($mode != 'subscribe' && $mode != 'unsubscribe') { - common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\""); - throw new ServerException("Bogus hub callback: bad mode", 404); + throw new ClientException("Bad hub.mode $mode", 404); } - - $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); + + $feedsub = FeedSub::staticGet('uri', $topic); + if (!$feedsub) { + throw new ClientException("Bad hub.topic feed $topic", 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 ($feedsub->verify_token !== $verify_token) { + throw new ClientException("Bad hub.verify_token $token for $topic", 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); + if ($mode == 'subscribe') { + // We may get re-sub requests legitimately. + if ($feedsub->sub_state != 'subscribe' && $feedsub->sub_state != 'active') { + throw new ClientException("Unexpected subscribe request for $topic.", 404); + } + } else { + if ($feedsub->sub_state != 'unsubscribe') { + throw new ClientException("Unexpected unsubscribe request for $topic.", 404); + } } - // OK! if ($mode == 'subscribe') { - common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); - $profile->confirmSubscribe($lease_seconds); + if ($feedsub->sub_state == 'active') { + common_log(LOG_INFO, __METHOD__ . ': sub update confirmed'); + } else { + common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); + } + $feedsub->confirmSubscribe($lease_seconds); } else { common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic"); - $profile->confirmUnsubscribe(); + $feedsub->confirmUnsubscribe(); } print $challenge; } diff --git a/plugins/OStatus/actions/pushhub.php b/plugins/OStatus/actions/pushhub.php index 13ec09d52..842d65e7d 100644 --- a/plugins/OStatus/actions/pushhub.php +++ b/plugins/OStatus/actions/pushhub.php @@ -59,99 +59,121 @@ class PushHubAction extends Action $mode = $this->trimmed('hub.mode'); switch ($mode) { case "subscribe": - $this->subscribe(); - break; case "unsubscribe": - $this->unsubscribe(); + $this->subunsub($mode); break; case "publish": - throw new ServerException("Publishing outside feeds not supported.", 400); + throw new ClientException("Publishing outside feeds not supported.", 400); default: - throw new ServerException("Unrecognized mode '$mode'.", 400); + throw new ClientException("Unrecognized mode '$mode'.", 400); } } /** - * Process a PuSH feed subscription request. + * Process a request for a new or modified PuSH feed subscription. + * If asynchronous verification is requested, updates won't be saved immediately. * * HTTP return codes: * 202 Accepted - request saved and awaiting verification * 204 No Content - already subscribed - * 403 Forbidden - rejecting this (not specifically spec'd) + * 400 Bad Request - rejecting this (not specifically spec'd) */ - function subscribe() + function subunsub($mode) { - $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; + $topic = $this->argUrl('hub.topic'); + if (!$this->recognizedFeed($topic)) { + throw new ClientException("Unsupported hub.topic $topic; this hub only serves local user and group Atom feeds."); } - 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); + $verify = $this->arg('hub.verify'); // @fixme may be multiple + if ($verify != 'sync' && $verify != 'async') { + throw new ClientException("Invalid hub.verify $verify; must be sync or async."); } - $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? + $lease = $this->arg('hub.lease_seconds', null); + if ($mode == 'subscribe' && $lease != '' && !preg_match('/^\d+$/', $lease)) { + throw new ClientException("Invalid hub.lease $lease; must be empty or positive integer."); + } - common_log(LOG_DEBUG, __METHOD__ . ': inserting'); - $ok = $sub->insert(); - - if (!$ok) { - throw new ServerException("Failed to save subscription record", 500); + $token = $this->arg('hub.verify_token', null); + + $secret = $this->arg('hub.secret', null); + if ($secret != '' && strlen($secret) >= 200) { + throw new ClientException("Invalid hub.secret $secret; must be under 200 bytes."); } - // @fixme check errors ;) + $sub = HubSub::staticGet($topic, $callback); + if (!$sub) { + // Creating a new one! + $sub = new HubSub(); + $sub->topic = $topic; + $sub->callback = $callback; + } + if ($mode == 'subscribe') { + if ($secret) { + $sub->secret = $secret; + } + if ($lease) { + $sub->setLease(intval($lease)); + } + } - $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'); + if (!common_config('queue', 'enabled')) { + // Won't be able to background it. + $verify = 'sync'; + } + if ($verify == 'async') { + $sub->scheduleVerify($mode, $token); + header('HTTP/1.1 202 Accepted'); + } else { + $sub->verify($mode, $token); + header('HTTP/1.1 204 No Content'); + } } /** - * Process a PuSH feed unsubscription request. + * Check whether the given URL represents one of our canonical + * user or group Atom feeds. * - * HTTP return codes: - * 202 Accepted - request saved and awaiting verification - * 204 No Content - already subscribed - * 400 Bad Request - invalid params or rejected feed + * @param string $feed URL + * @return boolean true if it matches */ - function unsubscribe() + function recognizedFeed($feed) { - $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"); + $matches = array(); + if (preg_match('!/(\d+)\.atom$!', $feed, $matches)) { + $id = $matches[1]; + $params = array('id' => $id, 'format' => 'atom'); + $userFeed = common_local_url('ApiTimelineUser', $params); + $groupFeed = common_local_url('ApiTimelineGroup', $params); + + if ($feed == $userFeed) { + $user = User::staticGet('id', $id); + if (!$user) { + throw new ClientException("Invalid hub.topic $feed; user doesn't exist."); + } else { + return true; + } } - } else { - throw new ServerException("Failed PuSH unsubscription: not subscribed! $feed for $callback"); + if ($feed == $groupFeed) { + $user = User_group::staticGet('id', $id); + if (!$user) { + throw new ClientException("Invalid hub.topic $feed; group doesn't exist."); + } else { + return true; + } + } + common_log(LOG_DEBUG, "Not a user or group feed? $feed $userFeed $groupFeed"); } + common_log(LOG_DEBUG, "LOST $feed"); + return false; } /** * Grab and validate a URL from POST parameters. - * @throws ServerException for malformed or non-http/https URLs + * @throws ClientException for malformed or non-http/https URLs */ protected function argUrl($arg) { @@ -161,7 +183,7 @@ class PushHubAction extends Action if (Validate::uri($url, $params)) { return $url; } else { - throw new ServerException("Invalid URL passed for $arg: '$url'", 400); + throw new ClientException("Invalid URL passed for $arg: '$url'"); } } diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php deleted file mode 100644 index c79d09c95..000000000 --- a/plugins/OStatus/actions/salmon.php +++ /dev/null @@ -1,81 +0,0 @@ -<?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/usersalmon.php b/plugins/OStatus/actions/usersalmon.php new file mode 100644 index 000000000..c8a16e06f --- /dev/null +++ b/plugins/OStatus/actions/usersalmon.php @@ -0,0 +1,212 @@ +<?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 UsersalmonAction extends SalmonAction +{ + function prepare($args) + { + parent::prepare($args); + + $id = $this->trimmed('id'); + + if (!$id) { + $this->clientError(_('No ID.')); + } + + $this->user = User::staticGet('id', $id); + + if (empty($this->user)) { + $this->clientError(_('No such user.')); + } + + return true; + } + + /** + * We've gotten a post event on the Salmon backchannel, probably a reply. + * + * @todo validate if we need to handle this post, then call into + * ostatus_profile's general incoming-post handling. + */ + function handlePost() + { + common_log(LOG_INFO, "Received post of '{$this->act->object->id}' from '{$this->act->actor->id}'"); + + switch ($this->act->object->type) { + case ActivityObject::ARTICLE: + case ActivityObject::BLOGENTRY: + case ActivityObject::NOTE: + case ActivityObject::STATUS: + case ActivityObject::COMMENT: + break; + default: + throw new ClientException("Can't handle that kind of post."); + } + + // Notice must either be a) in reply to a notice by this user + // or b) to the attention of this user + + $context = $this->act->context; + + if (!empty($context->replyToID)) { + $notice = Notice::staticGet('uri', $context->replyToID); + if (empty($notice)) { + throw new ClientException("In reply to unknown notice"); + } + if ($notice->profile_id != $this->user->id) { + throw new ClientException("In reply to a notice not by this user"); + } + } else if (!empty($context->attention)) { + if (!in_array($this->user->uri, $context->attention)) { + common_log(LOG_ERR, "{$this->user->uri} not in attention list (".implode(',', $context->attention).")"); + throw new ClientException("To the attention of user(s) not including this one!"); + } + } else { + throw new ClientException("Not to anyone in reply to anything!"); + } + + $existing = Notice::staticGet('uri', $this->act->object->id); + + if (!empty($existing)) { + common_log(LOG_ERR, "Not saving notice '{$existing->uri}'; already exists."); + return; + } + + $this->saveNotice(); + } + + /** + * We've gotten a follow/subscribe notification from a remote user. + * Save a subscription relationship for them. + */ + + function handleFollow() + { + $oprofile = $this->ensureProfile(); + if ($oprofile) { + common_log(LOG_INFO, "Setting up subscription from remote {$oprofile->uri} to local {$this->user->nickname}"); + Subscription::start($oprofile->localProfile(), + $this->user->getProfile()); + } else { + common_log(LOG_INFO, "Can't set up subscription from remote; missing profile."); + } + } + + /** + * We've gotten an unfollow/unsubscribe notification from a remote user. + * Check if we have a subscription relationship for them and kill it. + * + * @fixme probably catch exceptions on fail? + */ + function handleUnfollow() + { + $oprofile = $this->ensureProfile(); + if ($oprofile) { + common_log(LOG_INFO, "Canceling subscription from remote {$oprofile->uri} to local {$this->user->nickname}"); + Subscription::cancel($oprofile->localProfile(), $this->user->getProfile()); + } else { + common_log(LOG_ERR, "Can't cancel subscription from remote, didn't find the profile"); + } + } + + /** + * Remote user likes one of our posts. + * Confirm the post is ours, and save a local favorite event. + */ + + function handleFavorite() + { + $notice = $this->getNotice($this->act->object); + $profile = $this->ensureProfile()->localProfile(); + + $old = Fave::pkeyGet(array('user_id' => $profile->id, + 'notice_id' => $notice->id)); + + if (!empty($old)) { + throw new ClientException("We already know that's a fave!"); + } + + if (!Fave::addNew($profile, $notice)) { + throw new ClientException("Could not save new favorite."); + } + } + + /** + * Remote user doesn't like one of our posts after all! + * Confirm the post is ours, and save a local favorite event. + */ + function handleUnfavorite() + { + $notice = $this->getNotice($this->act->object); + $profile = $this->ensureProfile()->localProfile(); + + $fave = Fave::pkeyGet(array('user_id' => $profile->id, + 'notice_id' => $notice->id)); + if (empty($fave)) { + throw new ClientException("Notice wasn't favorited!"); + } + + $fave->delete(); + } + + /** + * @param ActivityObject $object + * @return Notice + * @throws ClientException on invalid input + */ + function getNotice($object) + { + if (!$object) { + throw new ClientException("Can't favorite/unfavorite without an object."); + } + + switch ($object->type) { + case ActivityObject::ARTICLE: + case ActivityObject::BLOGENTRY: + case ActivityObject::NOTE: + case ActivityObject::STATUS: + case ActivityObject::COMMENT: + break; + default: + throw new ClientException("Can't handle that kind of object for liking/faving."); + } + + $notice = Notice::staticGet('uri', $object->id); + + if (empty($notice)) { + throw new ClientException("Notice with ID $object->id unknown."); + } + + if ($notice->profile_id != $this->user->id) { + throw new ClientException("Notice with ID $object->id not posted by $this->user->id."); + } + + return $notice; + } + +} diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/webfinger.php deleted file mode 100644 index 75ba16638..000000000 --- a/plugins/OStatus/actions/webfinger.php +++ /dev/null @@ -1,77 +0,0 @@ -<?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/actions/xrd.php b/plugins/OStatus/actions/xrd.php new file mode 100644 index 000000000..f574b60ee --- /dev/null +++ b/plugins/OStatus/actions/xrd.php @@ -0,0 +1,113 @@ +<?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 XrdAction extends Action +{ + + public $uri; + + function prepare($args) + { + parent::prepare($args); + + $this->uri = $this->trimmed('uri'); + + return true; + } + + function handle() + { + $acct = Discovery::normalize($this->uri); + + $xrd = new XRD(); + + list($nick, $domain) = explode('@', substr(urldecode($acct), 5)); + $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' => Discovery::PROFILEPAGE, + 'type' => 'text/html', + 'href' => common_profile_url($nick)); + + $xrd->links[] = array('rel' => Discovery::UPDATESFROM, + 'href' => common_local_url('ApiTimelineUser', + array('id' => $this->user->id, + 'format' => 'atom')), + 'type' => 'application/atom+xml'); + + // hCard + $xrd->links[] = array('rel' => Discovery::HCARD, + 'type' => 'text/html', + 'href' => common_local_url('hcard', array('nickname' => $nick))); + + // XFN + $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', + 'type' => 'text/html', + 'href' => common_profile_url($nick)); + // FOAF + $xrd->links[] = array('rel' => 'describedby', + 'type' => 'application/rdf+xml', + 'href' => common_local_url('foaf', + array('nickname' => $nick))); + + // Salmon + $salmon_url = common_local_url('usersalmon', + array('id' => $this->user->id)); + + $xrd->links[] = array('rel' => Salmon::NS_REPLIES, + 'href' => $salmon_url); + + $xrd->links[] = array('rel' => Salmon::NS_MENTIONS, + 'href' => $salmon_url); + + // Get this user's keypair + $magickey = Magicsig::staticGet('user_id', $this->user->id); + if (!$magickey) { + // No keypair yet, let's generate one. + $magickey = new Magicsig(); + $magickey->generate($this->user->id); + } + + $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, + 'href' => 'data:application/magic-public-key;'. $magickey->toString(false)); + + // TODO - finalize where the redirect should go on the publisher + $url = common_local_url('ostatussub') . '?profile={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/FeedSub.php b/plugins/OStatus/classes/FeedSub.php new file mode 100644 index 000000000..b848b6b1d --- /dev/null +++ b/plugins/OStatus/classes/FeedSub.php @@ -0,0 +1,452 @@ +<?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 OStatusPlugin + * @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; + } +} + +/** + * FeedSub handles low-level PubHubSubbub (PuSH) subscriptions. + * Higher-level behavior building OStatus stuff on top is handled + * under Ostatus_profile. + */ +class FeedSub extends Memcached_DataObject +{ + public $__table = 'feedsub'; + + public $id; + public $feeduri; + + // PuSH subscription data + public $huburi; + public $secret; + public $verify_token; + public $sub_state; // subscribe, active, unsubscribe, inactive + public $sub_start; + public $sub_end; + public $last_update; + + public $created; + public $modified; + + 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, + 'uri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'huburi' => DB_DATAOBJECT_STR, + 'secret' => DB_DATAOBJECT_STR, + 'verify_token' => DB_DATAOBJECT_STR, + 'sub_state' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'last_update' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'modified' => 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('uri', 'varchar', + 255, false, 'UNI'), + new ColumnDef('huburi', 'text', + null, true), + new ColumnDef('verify_token', 'text', + null, true), + new ColumnDef('secret', 'text', + null, true), + new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe','inactive')", + null, false), + new ColumnDef('sub_start', 'datetime', + null, true), + new ColumnDef('sub_end', 'datetime', + null, true), + new ColumnDef('last_update', 'datetime', + null, false), + new ColumnDef('created', 'datetime', + null, false), + new ColumnDef('modified', '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', 'uri' => '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 string $feeduri + * @return FeedSub + * @throws FeedSubException if feed is invalid or lacks PuSH setup + */ + public static function ensureFeed($feeduri) + { + $current = self::staticGet('uri', $feeduri); + if ($current) { + return $current; + } + + $discover = new FeedDiscovery(); + $discover->discoverFromFeedURL($feeduri); + + $huburi = $discover->getAtomLink('hub'); + if (!$huburi) { + throw new FeedSubNoHubException(); + } + + $feedsub = new FeedSub(); + $feedsub->uri = $feeduri; + $feedsub->huburi = $huburi; + $feedsub->sub_state = 'inactive'; + + $feedsub->created = common_sql_now(); + $feedsub->modified = common_sql_now(); + + $result = $feedsub->insert(); + if (empty($result)) { + throw new FeedDBException($feedsub); + } + + return $feedsub; + } + + /** + * 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 && $this->sub_state != 'inactive') { + 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' => 'sync', + 'hub.verify_token' => $this->verify_token, + 'hub.secret' => $this->secret, + 'hub.topic' => $this->uri); + $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->uri"); + + $orig = clone($this); + $this->verify_token = ''; + $this->sub_state = 'inactive'; + $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->modified = common_sql_now(); + + return $this->update($original); + } + + /** + * Save PuSH unsubscription confirmation. + * Wipes active PuSH sub info and resets state. + */ + public function confirmUnsubscribe() + { + $original = clone($this); + + // @fixme these should all be null, but DB_DataObject doesn't save null values...????? + $this->verify_token = ''; + $this->secret = ''; + $this->sub_state = ''; + $this->sub_start = ''; + $this->sub_end = ''; + $this->modified = common_sql_now(); + + return $this->update($original); + } + + /** + * Accept updates from a PuSH feed. If validated, this object and the + * feed (as a DOMDocument) will be passed to the StartFeedSubHandleFeed + * and EndFeedSubHandleFeed events for processing. + * + * Not guaranteed to be running in an immediate POST context; may be run + * from a queue handler. + * + * Side effects: the feedsub record's lastupdate field will be updated + * to the current time (not published time) if we got a legit update. + * + * @param string $post source of Atom or RSS feed + * @param string $hmac X-Hub-Signature header, if present + */ + public function receive($post, $hmac) + { + common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->uri\"! $hmac $post"); + + if ($this->sub_state != 'active') { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH for inactive feed $this->uri (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; + } + + $orig = clone($this); + $this->last_update = common_sql_now(); + $this->update($orig); + + Event::handle('StartFeedSubReceive', array($this, $feed)); + Event::handle('EndFeedSubReceive', array($this, $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; + } + +} diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php index 7071ee5b4..3120a70f9 100644 --- a/plugins/OStatus/classes/HubSub.php +++ b/plugins/OStatus/classes/HubSub.php @@ -30,12 +30,11 @@ class HubSub extends Memcached_DataObject public $topic; public $callback; public $secret; - public $verify_token; - public $challenge; public $lease; public $sub_start; public $sub_end; public $created; + public $modified; public /*static*/ function staticGet($topic, $callback) { @@ -62,12 +61,11 @@ class HubSub extends Memcached_DataObject '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); + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); } static function schemaDef() @@ -84,10 +82,6 @@ class HubSub extends Memcached_DataObject 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', @@ -95,6 +89,8 @@ class HubSub extends Memcached_DataObject new ColumnDef('sub_end', 'datetime', null, true), new ColumnDef('created', 'datetime', + null, false), + new ColumnDef('modified', 'datetime', null, false)); } @@ -103,7 +99,7 @@ class HubSub extends Memcached_DataObject return array_keys($this->keyTypes()); } - function sequenceKeys() + function sequenceKey() { return array(false, false, false); } @@ -152,84 +148,132 @@ class HubSub extends Memcached_DataObject } /** - * Send a verification ping to subscriber + * Schedule a future verification ping to the subscriber. + * If queues are disabled, will be immediate. + * * @param string $mode 'subscribe' or 'unsubscribe' + * @param string $token hub.verify_token value, if provided by client */ - function verify($mode) + function scheduleVerify($mode, $token=null, $retries=null) { - assert($mode == 'subscribe' || $mode == 'unsubscribe'); + if ($retries === null) { + $retries = intval(common_config('ostatus', 'hub_retries')); + } + $data = array('sub' => clone($this), + 'mode' => $mode, + 'token' => $token, + 'retries' => $retries); + $qm = QueueManager::get(); + $qm->enqueue($data, 'hubconf'); + } - // Is this needed? data object fun... - $clone = clone($this); - $clone->challenge = common_good_rand(16); - $clone->update($this); - $this->challenge = $clone->challenge; - unset($clone); + /** + * Send a verification ping to subscriber, and if confirmed apply the changes. + * This may create, update, or delete the database record. + * + * @param string $mode 'subscribe' or 'unsubscribe' + * @param string $token hub.verify_token value, if provided by client + * @throws ClientException on failure + */ + function verify($mode, $token=null) + { + assert($mode == 'subscribe' || $mode == 'unsubscribe'); + $challenge = common_good_rand(32); $params = array('hub.mode' => $mode, 'hub.topic' => $this->topic, - 'hub.challenge' => $this->challenge); + 'hub.challenge' => $challenge); if ($mode == 'subscribe') { $params['hub.lease_seconds'] = $this->lease; } - if ($this->verify_token) { - $params['hub.verify_token'] = $this->verify_token; + if ($token !== null) { + $params['hub.verify_token'] = $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(); + // Any existing query string parameters must be preserved + $url = $this->callback; + if (strpos('?', $url) !== false) { + $url .= '&'; + } else { + $url .= '?'; } - 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; + $url .= http_build_query($params, '', '&'); + + $request = new HTTPClient(); + $response = $request->get($url); + $status = $response->getStatus(); + + if ($status >= 200 && $status < 300) { + common_log(LOG_INFO, "Verified $mode of $this->callback:$this->topic"); } 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); + throw new ClientException("Hub subscriber verification returned HTTP $status"); + } - $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(); + $old = HubSub::staticGet($this->topic, $this->callback); + if ($mode == 'subscribe') { + if ($old) { + $this->update($old); + } else { + $ok = $this->insert(); + } + } else if ($mode == 'unsubscribe') { + if ($old) { + $old->delete(); + } else { + // That's ok, we're already unsubscribed. } - return true; } } /** * Insert wrapper; transparently set the hash key from topic and callback columns. - * @return boolean success + * @return mixed success */ function insert() { $this->hashkey = self::hashkey($this->topic, $this->callback); + $this->created = common_sql_now(); + $this->modified = common_sql_now(); return parent::insert(); } /** + * Update wrapper; transparently update modified column. + * @return boolean success + */ + function update($old=null) + { + $this->modified = common_sql_now(); + return parent::update($old); + } + + /** + * Schedule delivery of a 'fat ping' to the subscriber's callback + * endpoint. If queues are disabled, this will run immediately. + * + * @param string $atom well-formed Atom feed + * @param int $retries optional count of retries if POST fails; defaults to hub_retries from config or 0 if unset + */ + function distribute($atom, $retries=null) + { + if ($retries === null) { + $retries = intval(common_config('ostatus', 'hub_retries')); + } + + // We dare not clone() as when the clone is discarded it'll + // destroy the result data for the parent query. + // @fixme use clone() again when it's safe to copy an + // individual item from a multi-item query again. + $sub = HubSub::staticGet($this->topic, $this->callback); + $data = array('sub' => $sub, + 'atom' => $atom, + 'retries' => $retries); + common_log(LOG_INFO, "Queuing PuSH: $this->topic to $this->callback"); + $qm = QueueManager::get(); + $qm->enqueue($data, 'hubout'); + } + + /** * Send a 'fat ping' to the subscriber's callback endpoint * containing the given Atom feed chunk. * @@ -237,6 +281,7 @@ class HubSub extends Memcached_DataObject * a higher level; don't just shove in a complete feed! * * @param string $atom well-formed Atom feed + * @throws Exception (HTTP or general) */ function push($atom) { @@ -248,24 +293,18 @@ class HubSub extends Memcached_DataObject $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; + $request = new HTTPClient(); + $request->setBody($atom); + $response = $request->post($this->callback, $headers); - } catch (Exception $e) { - common_log(LOG_ERR, "Error sending PuSH content " . - "to $this->callback for $this->topic: " . - $e->getMessage()); - return false; + if ($response->isOk()) { + return true; + } else { + throw new Exception("Callback returned status: " . + $response->getStatus() . + "; body: " . + trim($response->getBody())); } } } diff --git a/plugins/OStatus/classes/Magicsig.php b/plugins/OStatus/classes/Magicsig.php new file mode 100644 index 000000000..5a46aeeb6 --- /dev/null +++ b/plugins/OStatus/classes/Magicsig.php @@ -0,0 +1,233 @@ +<?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/ + */ + +require_once 'Crypt/RSA.php'; + +class Magicsig extends Memcached_DataObject +{ + + const PUBLICKEYREL = 'magic-public-key'; + + public $__table = 'magicsig'; + + public $user_id; + public $keypair; + public $alg; + + private $_rsa; + + public function __construct($alg = 'RSA-SHA256') + { + $this->alg = $alg; + } + + public /*static*/ function staticGet($k, $v=null) + { + $obj = parent::staticGet(__CLASS__, $k, $v); + if (!empty($obj)) { + return Magicsig::fromString($obj->keypair); + } + + return $obj; + } + + + function table() + { + return array( + 'user_id' => DB_DATAOBJECT_INT, + 'keypair' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'alg' => DB_DATAOBJECT_STR + ); + } + + static function schemaDef() + { + return array(new ColumnDef('user_id', 'integer', + null, true, 'PRI'), + new ColumnDef('keypair', 'varchar', + 255, false), + new ColumnDef('alg', 'varchar', + 64, false)); + } + + + function keys() + { + return array_keys($this->keyTypes()); + } + + function keyTypes() + { + return array('user_id' => 'K'); + } + + function sequenceKey() { + return array(false, false, false); + } + + function insert() + { + $this->keypair = $this->toString(); + + return parent::insert(); + } + + public function generate($user_id, $key_length = 512) + { + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + + $keypair = new Crypt_RSA_KeyPair($key_length); + $params['public_key'] = $keypair->getPublicKey(); + $params['private_key'] = $keypair->getPrivateKey(); + + $this->_rsa = new Crypt_RSA($params); + PEAR::popErrorHandling(); + + $this->user_id = $user_id; + $this->insert(); + } + + + public function toString($full_pair = true) + { + $public_key = $this->_rsa->_public_key; + $private_key = $this->_rsa->_private_key; + + $mod = base64_url_encode($public_key->getModulus()); + $exp = base64_url_encode($public_key->getExponent()); + $private_exp = ''; + if ($full_pair && $private_key->getExponent()) { + $private_exp = '.' . base64_url_encode($private_key->getExponent()); + } + + return 'RSA.' . $mod . '.' . $exp . $private_exp; + } + + public static function fromString($text) + { + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + + $magic_sig = new Magicsig(); + + // remove whitespace + $text = preg_replace('/\s+/', '', $text); + + // parse components + if (!preg_match('/RSA\.([^\.]+)\.([^\.]+)(.([^\.]+))?/', $text, $matches)) { + return false; + } + + $mod = base64_url_decode($matches[1]); + $exp = base64_url_decode($matches[2]); + if (!empty($matches[4])) { + $private_exp = base64_url_decode($matches[4]); + } else { + $private_exp = false; + } + + $params['public_key'] = new Crypt_RSA_KEY($mod, $exp, 'public'); + if ($params['public_key']->isError()) { + $error = $params['public_key']->getLastError(); + common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage()); + return false; + } + if ($private_exp) { + $params['private_key'] = new Crypt_RSA_KEY($mod, $private_exp, 'private'); + if ($params['private_key']->isError()) { + $error = $params['private_key']->getLastError(); + common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage()); + return false; + } + } + + $magic_sig->_rsa = new Crypt_RSA($params); + PEAR::popErrorHandling(); + + return $magic_sig; + } + + public function getName() + { + return $this->alg; + } + + public function getHash() + { + switch ($this->alg) { + + case 'RSA-SHA256': + return 'magicsig_sha256'; + } + + } + + public function sign($bytes) + { + $hash = $this->getHash(); + $sig = $this->_rsa->createSign($bytes, null, $hash); + if ($this->_rsa->isError()) { + $error = $this->_rsa->getLastError(); + common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage()); + return false; + } + + return $sig; + } + + public function verify($signed_bytes, $signature) + { + $hash = $this->getHash(); + $result = $this->_rsa->validateSign($signed_bytes, $signature, null, $hash); + if ($this->_rsa->isError()) { + $error = $this->keypair->getLastError(); + common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage()); + return false; + } + return $result; + } + +} + +// Define a sha256 function for hashing +// (Crypt_RSA should really be updated to use hash() ) +function magicsig_sha256($bytes) +{ + return hash('sha256', $bytes); +} + +function base64_url_encode($input) +{ + return strtr(base64_encode($input), '+/', '-_'); +} + +function base64_url_decode($input) +{ + return base64_decode(strtr($input, '-_', '+/')); +}
\ No newline at end of file diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index be01cdfe1..a33e95d93 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -18,62 +18,25 @@ */ /** - * @package FeedSubPlugin + * @package OStatusPlugin * @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 $uri; + 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 $avatar; // remote URL of the last avatar we saved public $created; - public $lastupdate; + public $modified; public /*static*/ function staticGet($k, $v=null) { @@ -91,56 +54,33 @@ class Ostatus_profile extends Memcached_DataObject function table() { - return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + return array('uri' => DB_DATAOBJECT_STR + 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, + 'feeduri' => DB_DATAOBJECT_STR, 'salmonuri' => DB_DATAOBJECT_STR, + 'avatar' => 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); + 'modified' => 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), + return array(new ColumnDef('uri', 'varchar', + 255, false, 'PRI'), 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), + 255, true, 'UNI'), new ColumnDef('salmonuri', 'text', null, true), + new ColumnDef('avatar', 'text', + null, true), new ColumnDef('created', 'datetime', null, false), - new ColumnDef('lastupdate', 'datetime', + new ColumnDef('modified', 'datetime', null, false)); } @@ -169,12 +109,12 @@ class Ostatus_profile extends Memcached_DataObject function keyTypes() { - return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U'); + return array('uri' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U'); } function sequenceKey() { - return array('id', true, false); + return array(false, false, false); } /** @@ -202,97 +142,17 @@ class Ostatus_profile extends Memcached_DataObject } /** - * @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 + * Returns an ActivityObject describing this remote user or group profile. + * Can then be used to generate Atom chunks. + * + * @return ActivityObject */ - public function updateAvatar($url) + function asActivityObject() { - // @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); + return ActivityObject::fromGroup($this->localGroup()); } else { - $profile = $this->localProfile(); - $profile->setOriginal($filename); + return ActivityObject::fromProfile($this->localProfile()); } } @@ -302,71 +162,82 @@ class Ostatus_profile extends Memcached_DataObject * * Assumes that 'activity' namespace has been previously defined. * + * @fixme replace with wrappers on asActivityObject when it's got everything. + * * @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]; - } - } + $noun = ActivityObject::fromGroup($this->localGroup()); + return $noun->asString('activity:' . $element); } 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; - } + $noun = ActivityObject::fromProfile($this->localProfile()); + return $noun->asString('activity:' . $element); } - $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 - ), - '' - ); + /** + * @return boolean true if this is a remote group + */ + function isGroup() + { + if ($this->profile_id && !$this->group_id) { + return false; + } else if ($this->group_id && !$this->profile_id) { + return true; + } else if ($this->group_id && $this->profile_id) { + throw new ServerException("Invalid ostatus_profile state: both group and profile IDs set for $this->uri"); + } else { + throw new ServerException("Invalid ostatus_profile state: both group and profile IDs empty for $this->uri"); + } + } - $xs->elementEnd('activity:' . $element); + /** + * Subscribe a local user to this remote user. + * PuSH subscription will be started if necessary, and we'll + * send a Salmon notification to the remote server if available + * notifying them of the sub. + * + * @param User $user + * @return boolean success + * @throws FeedException + */ + public function subscribeLocalToRemote(User $user) + { + if ($this->isGroup()) { + throw new ServerException("Can't subscribe to a remote group"); + } - return $xs->getString(); + if ($this->subscribe()) { + if ($user->subscribeTo($this->localProfile())) { + $this->notify($user->getProfile(), ActivityVerb::FOLLOW, $this); + return true; + } + } + return false; } /** - * Damn dirty hack! + * Mark this remote profile as subscribing to the given local user, + * and send appropriate notifications to the user. + * + * This will generally be in response to a subscription notification + * from a foreign site to our local Salmon response channel. + * + * @param User $user + * @return boolean success */ - function isGroup() + public function subscribeRemoteToLocal(User $user) { - return (strpos($this->feeduri, '/groups/') !== false); + if ($this->isGroup()) { + throw new ServerException("Remote groups can't subscribe to local users"); + } + + Subscription::start($this->localProfile(), $user->getProfile()); + + return true; } /** @@ -376,21 +247,16 @@ class Ostatus_profile extends Memcached_DataObject * @return bool true on success, false on failure * @throws ServerException if feed state is not valid */ - public function subscribe($mode='subscribe') + public function subscribe() { - if ($this->sub_state != '') { - throw new ServerException("Attempting to start PuSH subscription to feed in state $this->sub_state"); + $feedsub = FeedSub::ensureFeed($this->feeduri); + if ($feedsub->sub_state == 'active' || $feedsub->sub_state == 'subscribe') { + return true; + } else if ($feedsub->sub_state == '' || $feedsub->sub_state == 'inactive') { + return $feedsub->subscribe(); + } else if ('unsubscribe') { + throw new FeedSubException("Unsub is pending, can't subscribe..."); } - 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'); } /** @@ -401,196 +267,160 @@ class Ostatus_profile extends Memcached_DataObject * @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"); + $feedsub = FeedSub::staticGet('uri', $this->feeduri); + if (!$feedsub) { + return true; } - 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; + if ($feedsub->sub_state == 'active') { + return $feedsub->unsubscribe(); + } else if ($feedsub->sub_state == '' || $feedsub->sub_state == 'inactive' || $feedsub->sub_state == 'unsubscribe') { + return true; + } else if ($feedsub->sub_state == 'subscribe') { + throw new FeedSubException("Feed is awaiting subscription, can't unsub..."); } } /** - * Save PuSH subscription confirmation. - * Sets approximate lease start and end times and finalizes state. + * Check if this remote profile has any active local subscriptions, and + * if not drop the PuSH subscription feed. * - * @param int $lease_seconds provided hub.lease_seconds parameter, if given + * @return boolean */ - public function confirmSubscribe($lease_seconds=0) + public function garbageCollect() { - $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); + if ($this->isGroup()) { + $members = $this->localGroup()->getMembers(0, 1); + $count = $members->N; } else { - $this->sub_end = null; + $count = $this->localProfile()->subscriberCount(); + } + if ($count == 0) { + common_log(LOG_INFO, "Unsubscribing from now-unused remote feed $this->feeduri"); + $this->unsubscribe(); + return true; + } else { + return false; } - $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 + * @param Profile $actor Actor who did the activity + * @param string $verb Activity::SUBSCRIBE or Activity::JOIN + * @param Object $object object of the action; must define asActivityNoun($tag) */ - public function notify(Profile $actor, $verb, $object=null) + public function notify($actor, $verb, $object=null) { + if (!($actor instanceof Profile)) { + $type = gettype($actor); + if ($type == 'object') { + $type = get_class($actor); + } + throw new ServerException("Invalid actor passed to " . __METHOD__ . ": " . $type); + } 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'); + + $text = 'update'; + $id = TagURI::mint('%s:%s:%s', + $verb, + $actor->getURI(), + common_date_iso8601(time())); + + // @fixme consolidate all these NS settings somewhere + $attributes = array('xmlns' => Activity::ATOM, + 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', + 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', + 'xmlns:georss' => 'http://www.georss.org/georss', + 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0', + 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0', + 'xmlns:media' => 'http://purl.org/syndication/atommedia'); + + $entry = new XMLStringer(); + $entry->elementStart('entry', $attributes); $entry->element('id', null, $id); $entry->element('title', null, $text); $entry->element('summary', null, $text); - $entry->element('published', null, common_date_w3dtf()); + $entry->element('published', null, common_date_w3dtf(common_sql_now())); $entry->element('activity:verb', null, $verb); - $entry->raw($profile->asAtomAuthor()); - $entry->raw($profile->asActivityActor()); + $entry->raw($actor->asAtomAuthor()); + $entry->raw($actor->asActivityActor()); $entry->raw($object->asActivityNoun('object')); - $entry->elmentEnd('entry'); + $entry->elementEnd('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"); + $xml = $entry->getString(); + common_log(LOG_INFO, "Posting to Salmon endpoint $this->salmonuri: $xml"); $salmon = new Salmon(); // ? - $salmon->post($this->salmonuri, $xml); + return $salmon->post($this->salmonuri, $xml, $actor); } + return false; } - function getBestName() + /** + * Send a Salmon notification ping immediately, and confirm that we got + * an acceptable response from the remote site. + * + * @param mixed $entry XML string, Notice, or Activity + * @return boolean success + */ + public function notifyActivity($entry, $actor) { - if ($this->isGroup()) { - return $this->localGroup()->getBestName(); - } else { - return $this->localProfile()->getBestName(); + if ($this->salmonuri) { + $salmon = new Salmon(); + return $salmon->post($this->salmonuri, $this->notifyPrepXml($entry), $actor); } + + return false; } - function atomFeed($actor) + /** + * Queue a Salmon notification for later. If queues are disabled we'll + * send immediately but won't get the return value. + * + * @param mixed $entry XML string, Notice, or Activity + * @return boolean success + */ + public function notifyDeferred($entry, $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}"); // ??? + if ($this->salmonuri) { + $data = array('salmonuri' => $this->salmonuri, + 'entry' => $this->notifyPrepXml($entry), + 'actor' => $actor->id); - $feed->setTitle($actor->getBestName() . ' timeline'); // @fixme - $feed->setUpdated(time()); - $feed->setPublished(time()); + $qm = QueueManager::get(); + return $qm->enqueue($data, 'salmon'); + } - $feed->addLink(common_url('ApiTimelineUser', - array('id' => $actor->id, - 'type' => 'atom')), - array('rel' => 'self', - 'type' => 'application/atom+xml')); + return false; + } - $feed->addLink(common_url('userbyid', - array('id' => $actor->id)), - array('rel' => 'alternate', - 'type' => 'text/html')); + protected function notifyPrepXml($entry) + { + $preamble = '<?xml version="1.0" encoding="UTF-8" ?' . '>'; + if (is_string($entry)) { + return $entry; + } else if ($entry instanceof Activity) { + return $preamble . $entry->asString(true); + } else if ($entry instanceof Notice) { + return $preamble . $entry->asAtomEntry(true, true); + } else { + throw new ServerException("Invalid type passed to Ostatus_profile::notify; must be XML string or Activity entry"); + } + } - return $feed; + function getBestName() + { + if ($this->isGroup()) { + return $this->localGroup()->getBestName(); + } else { + return $this->localProfile()->getBestName(); + } } /** @@ -598,36 +428,10 @@ class Ostatus_profile extends Memcached_DataObject * 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 + * @param DOMDocument $feed */ - public function postUpdates($post, $hmac) + public function processFeed($feed, $source) { - 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"); @@ -636,59 +440,22 @@ class Ostatus_profile extends Memcached_DataObject for ($i = 0; $i < $entries->length; $i++) { $entry = $entries->item($i); - $this->processEntry($entry, $feed); + $this->processEntry($entry, $feed, $source); } } /** - * 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) + public function processEntry($entry, $feed, $source) { $activity = new Activity($entry, $feed); - $debug = var_export($activity, true); - common_log(LOG_DEBUG, $debug); - if ($activity->verb == ActivityVerb::POST) { - $this->processPost($activity); + $this->processPost($activity, $source); } else { common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb"); } @@ -697,91 +464,385 @@ class Ostatus_profile extends Memcached_DataObject /** * Process an incoming post activity from this remote feed. * @param Activity $activity + * @param string $method 'push' or 'salmon' + * @return mixed saved Notice or false + * @fixme break up this function, it's getting nasty long */ - protected function processPost($activity) + public function processPost($activity, $method) { if ($this->isGroup()) { + // A group feed will contain posts from multiple authors. // @fixme validate these profiles in some way! - $oprofile = $this->ensureActorProfile($activity); + $oprofile = self::ensureActorProfile($activity); + if ($oprofile->isGroup()) { + // Groups can't post notices in StatusNet. + common_log(LOG_WARNING, "OStatus: skipping post with group listed as author: $oprofile->uri in feed from $this->uri"); + return false; + } } else { - $actorUri = $this->getActorProfileURI($activity); - if ($actorUri == $this->homeuri) { - // @fixme check if profile info has changed and update it + // Individual user feeds may contain only posts from themselves. + // Authorship is validated against the profile URI on upper layers, + // through PuSH setup or Salmon signature checks. + $actorUri = self::getActorProfileURI($activity); + if ($actorUri == $this->uri) { + // Check if profile info has changed and update it + $this->updateFromActivityObject($activity->actor); } 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; + common_log(LOG_WARNING, "OStatus: skipping post with bad author: got $actorUri expected $this->uri"); + return false; } $oprofile = $this; } + // The id URI will be used as a unique identifier for for the notice, + // protecting against duplicate saves. It isn't required to be a URL; + // tag: URIs for instance are found in Google Buzz feeds. + $sourceUri = $activity->object->id; + $dupe = Notice::staticGet('uri', $sourceUri); + if ($dupe) { + common_log(LOG_INFO, "OStatus: ignoring duplicate post: $sourceUri"); + return false; + } + + // We'll also want to save a web link to the original notice, if provided. + $sourceUrl = null; if ($activity->object->link) { - $sourceUri = $activity->object->link; + $sourceUrl = $activity->object->link; + } else if ($activity->link) { + $sourceUrl = $activity->link; } else if (preg_match('!^https?://!', $activity->object->id)) { - $sourceUri = $activity->object->id; + $sourceUrl = $activity->object->id; + } + + // Get (safe!) HTML and text versions of the content + $rendered = $this->purify($activity->object->content); + $content = html_entity_decode(strip_tags($rendered)); + + $shortened = common_shorten_links($content); + + // If it's too long, try using the summary, and make the + // HTML an attachment. + + $attachment = null; + + if (Notice::contentTooLong($shortened)) { + $attachment = $this->saveHTMLFile($activity->object->title, $rendered); + $summary = $activity->object->summary; + if (empty($summary)) { + $summary = $content; + } + $shortSummary = common_shorten_links($summary); + if (Notice::contentTooLong($shortSummary)) { + $url = common_shorten_url(common_local_url('attachment', + array('attachment' => $attachment->id))); + $shortSummary = substr($shortSummary, + 0, + Notice::maxContent() - (mb_strlen($url) + 2)); + $shortSummary .= '… ' . $url; + $content = $shortSummary; + $rendered = common_render_text($content); + } + } + + $options = array('is_local' => Notice::REMOTE_OMB, + 'url' => $sourceUrl, + 'uri' => $sourceUri, + 'rendered' => $rendered, + 'replies' => array(), + 'groups' => array(), + 'tags' => array()); + + // Check for optional attributes... + + if (!empty($activity->time)) { + $options['created'] = common_sql_date($activity->time); + } + + if ($activity->context) { + // Any individual or group attn: targets? + $replies = $activity->context->attention; + $options['groups'] = $this->filterReplies($oprofile, $replies); + $options['replies'] = $replies; + + // Maintain direct reply associations + // @fixme what about conversation ID? + if (!empty($activity->context->replyToID)) { + $orig = Notice::staticGet('uri', + $activity->context->replyToID); + if (!empty($orig)) { + $options['reply_to'] = $orig->id; + } + } + + $location = $activity->context->location; + if ($location) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + if ($location->location_id) { + $options['location_ns'] = $location->location_ns; + $options['location_id'] = $location->location_id; + } + } + } + + // Atom categories <-> hashtags + foreach ($activity->categories as $cat) { + if ($cat->term) { + $term = common_canonical_tag($cat->term); + if ($term) { + $options['tags'][] = $term; + } + } + } + + try { + $saved = Notice::saveNew($oprofile->profile_id, + $content, + 'ostatus', + $options); + if ($saved) { + Ostatus_source::saveNew($saved, $this, $method); + if (!empty($attachment)) { + File_to_post::processNew($attachment->id, $saved->id); + } + } + } catch (Exception $e) { + common_log(LOG_ERR, "OStatus save of remote message $sourceUri failed: " . $e->getMessage()); + throw $e; + } + common_log(LOG_INFO, "OStatus saved remote message $sourceUri as notice id $saved->id"); + return $saved; + } + + /** + * Clean up HTML + */ + protected function purify($html) + { + require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php'; + $config = array('safe' => 1); + return htmLawed($html, $config); + } + + /** + * Filters a list of recipient ID URIs to just those for local delivery. + * @param Ostatus_profile local profile of sender + * @param array in/out &$attention_uris set of URIs, will be pruned on output + * @return array of group IDs + */ + protected function filterReplies($sender, &$attention_uris) + { + common_log(LOG_DEBUG, "Original reply recipients: " . implode(', ', $attention_uris)); + $groups = array(); + $replies = array(); + foreach ($attention_uris as $recipient) { + // Is the recipient a local user? + $user = User::staticGet('uri', $recipient); + if ($user) { + // @fixme sender verification, spam etc? + $replies[] = $recipient; + continue; + } + + // Is the recipient a remote group? + $oprofile = Ostatus_profile::staticGet('uri', $recipient); + if ($oprofile) { + if ($oprofile->isGroup()) { + // Deliver to local members of this remote group. + // @fixme sender verification? + $groups[] = $oprofile->group_id; + } else { + common_log(LOG_DEBUG, "Skipping reply to remote profile $recipient"); + } + continue; + } + + // Is the recipient a local group? + // @fixme we need a uri on user_group + // $group = User_group::staticGet('uri', $recipient); + $template = common_local_url('groupbyid', array('id' => '31337')); + $template = preg_quote($template, '/'); + $template = str_replace('31337', '(\d+)', $template); + if (preg_match("/$template/", $recipient, $matches)) { + $id = $matches[1]; + $group = User_group::staticGet('id', $id); + if ($group) { + // Deliver to all members of this local group if allowed. + $profile = $sender->localProfile(); + if ($profile->isMember($group)) { + $groups[] = $group->id; + } else { + common_log(LOG_DEBUG, "Skipping reply to local group $group->nickname as sender $profile->id is not a member"); + } + continue; + } else { + common_log(LOG_DEBUG, "Skipping reply to bogus group $recipient"); + } + } + + common_log(LOG_DEBUG, "Skipping reply to unrecognized profile $recipient"); + + } + $attention_uris = $replies; + common_log(LOG_DEBUG, "Local reply recipients: " . implode(', ', $replies)); + common_log(LOG_DEBUG, "Local group recipients: " . implode(', ', $groups)); + return $groups; + } + + /** + * @param string $profile_url + * @return Ostatus_profile + * @throws FeedSubException + */ + public static function ensureProfile($profile_uri, $hints=array()) + { + // Get the canonical feed URI and check it + $discover = new FeedDiscovery(); + if (isset($hints['feedurl'])) { + $feeduri = $hints['feedurl']; + $feeduri = $discover->discoverFromFeedURL($feeduri); } else { - common_log(LOG_INFO, "OStatus: ignoring post with no source link: id $activity->object->id"); - return; + $feeduri = $discover->discoverFromURL($profile_uri); + $hints['feedurl'] = $feeduri; } - $dupe = Notice::staticGet('uri', $sourceUri); - if ($dupe) { - common_log(LOG_INFO, "OStatus: ignoring duplicate post: $noticeLink"); - return; + $huburi = $discover->getAtomLink('hub'); + $hints['hub'] = $huburi; + $salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES); + $hints['salmon'] = $salmonuri; + + if (!$huburi) { + // We can only deal with folks with a PuSH hub + throw new FeedSubNoHubException(); } - // @fixme sanitize and save HTML content if available - $content = $activity->object->title; + // Try to get a profile from the feed activity:subject + + $feedEl = $discover->feed->documentElement; + + $subject = ActivityUtils::child($feedEl, Activity::SUBJECT, Activity::SPEC); + + if (!empty($subject)) { + $subjObject = new ActivityObject($subject); + return self::ensureActivityObjectProfile($subjObject, $hints); + } + + // Otherwise, try the feed author + + $author = ActivityUtils::child($feedEl, Activity::AUTHOR, Activity::ATOM); + + if (!empty($author)) { + $authorObject = new ActivityObject($author); + return self::ensureActivityObjectProfile($authorObject, $hints); + } + + // Sheesh. Not a very nice feed! Let's try fingerpoken in the + // entries. + + $entries = $discover->feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); - $params = array('is_local' => Notice::REMOTE_OMB, - 'uri' => $sourceUri); + if (!empty($entries) && $entries->length > 0) { + + $entry = $entries->item(0); + + $actor = ActivityUtils::child($entry, Activity::ACTOR, Activity::SPEC); + + if (!empty($actor)) { + $actorObject = new ActivityObject($actor); + return self::ensureActivityObjectProfile($actorObject, $hints); - $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; + } + + $author = ActivityUtils::child($entry, Activity::AUTHOR, Activity::ATOM); + + if (!empty($author)) { + $authorObject = new ActivityObject($author); + return self::ensureActivityObjectProfile($authorObject, $hints); } } - // @fixme save detailed ostatus source info - // @fixme ensure that groups get handled correctly + // XXX: make some educated guesses here - $saved = Notice::saveNew($oprofile->localProfile()->id, - $content, - 'ostatus', - $params); + throw new FeedSubException("Can't find enough profile information to make a feed."); } /** - * Parse location given as a GeoRSS-simple point, if provided. - * http://www.georss.org/simple * - * @param feed item $entry - * @return mixed Location or false + * Download and update given avatar image + * @param string $url + * @throws Exception in various failure cases */ - 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); + protected function updateAvatar($url) + { + if ($url == $this->avatar) { + // We've already got this one. + return; + } + + if ($this->isGroup()) { + $self = $this->localGroup(); + } else { + $self = $this->localProfile(); + } + if (!$self) { + throw new ServerException(sprintf( + _m("Tried to update avatar for unsaved remote profile %s"), + $this->uri)); + } + + // @fixme this should be better encapsulated + // ripped from oauthstore.php (for old OMB client) + $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); + if (!copy($url, $temp_filename)) { + throw new ServerException(sprintf(_m("Unable to fetch avatar from %s"), $url)); + } + + if ($this->isGroup()) { + $id = $this->group_id; + } else { + $id = $this->profile_id; + } + // @fixme should we be using different ids? + $imagefile = new ImageFile($id, $temp_filename); + $filename = Avatar::filename($id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + $self->setOriginal($filename); + + $orig = clone($this); + $this->avatar = $url; + $this->update($orig); + } + + /** + * Pull avatar URL from ActivityObject or profile hints + * + * @param ActivityObject $object + * @param array $hints + * @return mixed URL string or false + */ + + protected static function getActivityObjectAvatar($object, $hints=array()) + { + if ($object->avatarLinks) { + $best = false; + // Take the exact-size avatar, or the largest avatar, or the first avatar if all sizeless + foreach ($object->avatarLinks as $avatar) { + if ($avatar->width == AVATAR_PROFILE_SIZE && $avatar->height = AVATAR_PROFILE_SIZE) { + // Exact match! + $best = $avatar; + break; + } + if (!$best || $avatar->width > $best->width) { + $best = $avatar; } } - common_log(LOG_ERR, "Ignoring bogus georss:point value $point"); + return $best->url; + } else if (array_key_exists('avatar', $hints)) { + return $hints['avatar']; } - return false; } @@ -792,7 +853,8 @@ class Ostatus_profile extends Memcached_DataObject * @param DOMElement $feed * @return string */ - function getAvatar($actor, $feed) + + protected static function getAvatar($actor, $feed) { $url = ''; $icon = ''; @@ -831,13 +893,27 @@ class Ostatus_profile extends Memcached_DataObject } /** - * @fixme move off of ostatus_profile or static? + * Fetch, or build if necessary, an Ostatus_profile for the actor + * in a given Activity Streams activity. + * + * @param Activity $activity + * @param string $feeduri if we already know the canonical feed URI! + * @param string $salmonuri if we already know the salmon return channel URI + * @return Ostatus_profile */ - function ensureActorProfile($activity) + + public static function ensureActorProfile($activity, $hints=array()) { - $profile = $this->getActorProfile($activity); - if (!$profile) { - $profile = $this->createActorProfile($activity); + return self::ensureActivityObjectProfile($activity->actor, $hints); + } + + public static function ensureActivityObjectProfile($object, $hints=array()) + { + $profile = self::getActivityObjectProfile($object); + if ($profile) { + $profile->updateFromActivityObject($object, $hints); + } else { + $profile = self::createActivityObjectProfile($object, $hints); } return $profile; } @@ -846,10 +922,20 @@ class Ostatus_profile extends Memcached_DataObject * @param Activity $activity * @return mixed matching Ostatus_profile or false if none known */ - function getActorProfile($activity) + public static function getActorProfile($activity) { - $homeuri = $this->getActorProfileURI($activity); - return Ostatus_profile::staticGet('homeuri', $homeuri); + return self::getActivityObjectProfile($activity->actor); + } + + protected static function getActivityObjectProfile($object) + { + $uri = self::getActivityObjectProfileURI($object); + return Ostatus_profile::staticGet('uri', $uri); + } + + protected static function getActorProfileURI($activity) + { + return self::getActivityObjectProfileURI($activity->actor); } /** @@ -857,56 +943,110 @@ class Ostatus_profile extends Memcached_DataObject * @return string * @throws ServerException */ - function getActorProfileURI($activity) + protected static function getActivityObjectProfileURI($object) { $opts = array('allowed_schemes' => array('http', 'https')); - $actor = $activity->actor; - if ($actor->id && Validate::uri($actor->id, $opts)) { - return $actor->id; + if ($object->id && Validate::uri($object->id, $opts)) { + return $object->id; } - if ($actor->link && Validate::uri($actor->link, $opts)) { - return $actor->link; + if ($object->link && Validate::uri($object->link, $opts)) { + return $object->link; } throw new ServerException("No author ID URI found"); } /** + * @fixme validate stuff somewhere + */ + + /** + * Create local ostatus_profile and profile/user_group entries for + * the provided remote user or group. * + * @param ActivityObject $object + * @param array $hints + * + * @return Ostatus_profile */ - 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? + protected static function createActivityObjectProfile($object, $hints=array()) + { + $homeuri = $object->id; + $discover = false; - $ok = $profile->insert(); - if ($ok) { - $this->updateAvatar($profile, $avatar); + if (!$homeuri) { + common_log(LOG_DEBUG, __METHOD__ . " empty actor profile URI: " . var_export($activity, true)); + throw new ServerException("No profile URI"); + } + + if (array_key_exists('feedurl', $hints)) { + $feeduri = $hints['feedurl']; } else { - throw new ServerException("Can't save local profile"); + $discover = new FeedDiscovery(); + $feeduri = $discover->discoverFromURL($homeuri); + } + + if (array_key_exists('salmon', $hints)) { + $salmonuri = $hints['salmon']; + } else { + if (!$discover) { + $discover = new FeedDiscovery(); + $discover->discoverFromFeedURL($hints['feedurl']); + } + $salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES); + } + + if (array_key_exists('hub', $hints)) { + $huburi = $hints['hub']; + } else { + if (!$discover) { + $discover = new FeedDiscovery(); + $discover->discoverFromFeedURL($hints['feedurl']); + } + $huburi = $discover->getAtomLink('hub'); + } + + if (!$huburi) { + // We can only deal with folks with a PuSH hub + throw new FeedSubNoHubException(); } - // @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; + + $oprofile->uri = $homeuri; + $oprofile->feeduri = $feeduri; + $oprofile->salmonuri = $salmonuri; + + $oprofile->created = common_sql_now(); + $oprofile->modified = common_sql_now(); + + if ($object->type == ActivityObject::PERSON) { + $profile = new Profile(); + $profile->created = common_sql_now(); + self::updateProfile($profile, $object, $hints); + + $oprofile->profile_id = $profile->insert(); + if (!$oprofile->profile_id) { + throw new ServerException("Can't save local profile"); + } + } else { + $group = new User_group(); + $group->uri = $homeuri; + $group->created = common_sql_now(); + self::updateGroup($group, $object, $hints); + + $oprofile->group_id = $group->insert(); + if (!$oprofile->group_id) { + throw new ServerException("Can't save local profile"); + } + } $ok = $oprofile->insert(); + if ($ok) { + $avatar = self::getActivityObjectAvatar($object, $hints); + if ($avatar) { + $oprofile->updateAvatar($avatar); + } return $oprofile; } else { throw new ServerException("Can't save OStatus profile"); @@ -914,23 +1054,450 @@ class Ostatus_profile extends Memcached_DataObject } /** - * @fixme move this into Activity? - * @param Activity $activity - * @return string + * Save any updated profile information to our local copy. + * @param ActivityObject $object + * @param array $hints */ - 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); - } + public function updateFromActivityObject($object, $hints=array()) + { + if ($this->isGroup()) { + $group = $this->localGroup(); + self::updateGroup($group, $object, $hints); + } else { + $profile = $this->localProfile(); + self::updateProfile($profile, $object, $hints); + } + $avatar = self::getActivityObjectAvatar($object, $hints); + if ($avatar) { + $this->updateAvatar($avatar); + } + } + + protected static function updateProfile($profile, $object, $hints=array()) + { + $orig = clone($profile); + + $profile->nickname = self::getActivityObjectNickname($object, $hints); + + if (!empty($object->title)) { + $profile->fullname = $object->title; + } else if (array_key_exists('fullname', $hints)) { + $profile->fullname = $hints['fullname']; + } + + if (!empty($object->link)) { + $profile->profileurl = $object->link; + } else if (array_key_exists('profileurl', $hints)) { + $profile->profileurl = $hints['profileurl']; + } else if (Validate::uri($object->id, array('allowed_schemes' => array('http', 'https')))) { + $profile->profileurl = $object->id; + } + + $profile->bio = self::getActivityObjectBio($object, $hints); + $profile->location = self::getActivityObjectLocation($object, $hints); + $profile->homepage = self::getActivityObjectHomepage($object, $hints); + + if (!empty($object->geopoint)) { + $location = ActivityContext::locationFromPoint($object->geopoint); + if (!empty($location)) { + $profile->lat = $location->lat; + $profile->lon = $location->lon; } } - return false; + + // @fixme tags/categories + // @todo tags from categories + + if ($profile->id) { + common_log(LOG_DEBUG, "Updating OStatus profile $profile->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true)); + $profile->update($orig); + } } + protected static function updateGroup($group, $object, $hints=array()) + { + $orig = clone($group); + + $group->nickname = self::getActivityObjectNickname($object, $hints); + $group->fullname = $object->title; + + if (!empty($object->link)) { + $group->mainpage = $object->link; + } else if (array_key_exists('profileurl', $hints)) { + $group->mainpage = $hints['profileurl']; + } + + // @todo tags from categories + $group->description = self::getActivityObjectBio($object, $hints); + $group->location = self::getActivityObjectLocation($object, $hints); + $group->homepage = self::getActivityObjectHomepage($object, $hints); + + if ($group->id) { + common_log(LOG_DEBUG, "Updating OStatus group $group->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true)); + $group->update($orig); + } + } + + protected static function getActivityObjectHomepage($object, $hints=array()) + { + $homepage = null; + $poco = $object->poco; + + if (!empty($poco)) { + $url = $poco->getPrimaryURL(); + if ($url && $url->type == 'homepage') { + $homepage = $url->value; + } + } + + // @todo Try for a another PoCo URL? + + return $homepage; + } + + protected static function getActivityObjectLocation($object, $hints=array()) + { + $location = null; + + if (!empty($object->poco) && + isset($object->poco->address->formatted)) { + $location = $object->poco->address->formatted; + } else if (array_key_exists('location', $hints)) { + $location = $hints['location']; + } + + if (!empty($location)) { + if (mb_strlen($location) > 255) { + $location = mb_substr($note, 0, 255 - 3) . ' … '; + } + } + + // @todo Try to find location some othe way? Via goerss point? + + return $location; + } + + protected static function getActivityObjectBio($object, $hints=array()) + { + $bio = null; + + if (!empty($object->poco)) { + $note = $object->poco->note; + } else if (array_key_exists('bio', $hints)) { + $note = $hints['bio']; + } + + if (!empty($note)) { + if (Profile::bioTooLong($note)) { + // XXX: truncate ok? + $bio = mb_substr($note, 0, Profile::maxBio() - 3) . ' … '; + } else { + $bio = $note; + } + } + + // @todo Try to get bio info some other way? + + return $bio; + } + + protected static function getActivityObjectNickname($object, $hints=array()) + { + if ($object->poco) { + if (!empty($object->poco->preferredUsername)) { + return common_nicknamize($object->poco->preferredUsername); + } + } + + if (!empty($object->nickname)) { + return common_nicknamize($object->nickname); + } + + if (array_key_exists('nickname', $hints)) { + return $hints['nickname']; + } + + // Try the definitive ID + + $nickname = self::nicknameFromURI($object->id); + + // Try a Webfinger if one was passed (way) down + + if (empty($nickname)) { + if (array_key_exists('webfinger', $hints)) { + $nickname = self::nicknameFromURI($hints['webfinger']); + } + } + + // Try the name + + if (empty($nickname)) { + $nickname = common_nicknamize($object->title); + } + + return $nickname; + } + + protected static function nicknameFromURI($uri) + { + preg_match('/(\w+):/', $uri, $matches); + + $protocol = $matches[1]; + + switch ($protocol) { + case 'acct': + case 'mailto': + if (preg_match("/^$protocol:(.*)?@.*\$/", $uri, $matches)) { + return common_canonical_nickname($matches[1]); + } + return null; + case 'http': + return common_url_to_nickname($uri); + break; + default: + return null; + } + } + + public static function ensureWebfinger($addr) + { + // First, try the cache + + $uri = self::cacheGet(sprintf('ostatus_profile:webfinger:%s', $addr)); + + if ($uri !== false) { + if (is_null($uri)) { + return null; + } + $oprofile = Ostatus_profile::staticGet('uri', $uri); + if (!empty($oprofile)) { + return $oprofile; + } + } + + // First, look it up + + $oprofile = Ostatus_profile::staticGet('uri', 'acct:'.$addr); + + if (!empty($oprofile)) { + self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); + return $oprofile; + } + + // Now, try some discovery + + $disco = new Discovery(); + + try { + $result = $disco->lookup($addr); + } catch (Exception $e) { + self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), null); + return null; + } + + foreach ($result->links as $link) { + switch ($link['rel']) { + case Discovery::PROFILEPAGE: + $profileUrl = $link['href']; + break; + case Salmon::NS_REPLIES: + $salmonEndpoint = $link['href']; + break; + case Discovery::UPDATESFROM: + $feedUrl = $link['href']; + break; + case Discovery::HCARD: + $hcardUrl = $link['href']; + break; + default: + common_log(LOG_NOTICE, "Don't know what to do with rel = '{$link['rel']}'"); + break; + } + } + + $hints = array('webfinger' => $addr, + 'profileurl' => $profileUrl, + 'feedurl' => $feedUrl, + 'salmon' => $salmonEndpoint); + + if (isset($hcardUrl)) { + $hcardHints = self::slurpHcard($hcardUrl); + // Note: Webfinger > hcard + $hints = array_merge($hcardHints, $hints); + } + + // If we got a feed URL, try that + + if (isset($feedUrl)) { + try { + common_log(LOG_INFO, "Discovery on acct:$addr with feed URL $feedUrl"); + $oprofile = self::ensureProfile($feedUrl, $hints); + self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); + return $oprofile; + } catch (Exception $e) { + common_log(LOG_WARNING, "Failed creating profile from feed URL '$feedUrl': " . $e->getMessage()); + // keep looking + } + } + + // If we got a profile page, try that! + + if (isset($profileUrl)) { + try { + common_log(LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl"); + $oprofile = self::ensureProfile($profileUrl, $hints); + self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); + return $oprofile; + } catch (Exception $e) { + common_log(LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage()); + // keep looking + } + } + + // XXX: try hcard + // XXX: try FOAF + + if (isset($salmonEndpoint)) { + + // An account URL, a salmon endpoint, and a dream? Not much to go + // on, but let's give it a try + + $uri = 'acct:'.$addr; + + $profile = new Profile(); + + $profile->nickname = self::nicknameFromUri($uri); + $profile->created = common_sql_now(); + + if (isset($profileUrl)) { + $profile->profileurl = $profileUrl; + } + + $profile_id = $profile->insert(); + + if (!$profile_id) { + common_log_db_error($profile, 'INSERT', __FILE__); + throw new Exception("Couldn't save profile for '$addr'"); + } + + $oprofile = new Ostatus_profile(); + + $oprofile->uri = $uri; + $oprofile->salmonuri = $salmonEndpoint; + $oprofile->profile_id = $profile_id; + $oprofile->created = common_sql_now(); + + if (isset($feedUrl)) { + $profile->feeduri = $feedUrl; + } + + $result = $oprofile->insert(); + + if (!$result) { + common_log_db_error($oprofile, 'INSERT', __FILE__); + throw new Exception("Couldn't save ostatus_profile for '$addr'"); + } + + self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); + return $oprofile; + } + + return null; + } + + function saveHTMLFile($title, $rendered) + { + $final = sprintf("<!DOCTYPE html>\n<html><head><title>%s</title></head>". + '<body><div>%s</div></body></html>', + htmlspecialchars($title), + $rendered); + + $filename = File::filename($this->localProfile(), + 'ostatus', // ignored? + 'text/html'); + + $filepath = File::path($filename); + + file_put_contents($filepath, $final); + + $file = new File; + + $file->filename = $filename; + $file->url = File::url($filename); + $file->size = filesize($filepath); + $file->date = time(); + $file->mimetype = 'text/html'; + + $file_id = $file->insert(); + + if ($file_id === false) { + common_log_db_error($file, "INSERT", __FILE__); + throw new ServerException(_('Could not store HTML content of long post as file.')); + } + + return $file; + } + + protected static function slurpHcard($url) + { + set_include_path(get_include_path() . PATH_SEPARATOR . INSTALLDIR . '/plugins/OStatus/extlib/hkit/'); + require_once('hkit.class.php'); + + $h = new hKit; + + // Google Buzz hcards need to be tidied. Probably others too. + + $h->tidy_mode = 'proxy'; // 'proxy', 'exec', 'php' or 'none' + + // Get by URL + $hcards = $h->getByURL('hcard', $url); + + if (empty($hcards)) { + return array(); + } + + // @fixme more intelligent guess on multi-hcard pages + $hcard = $hcards[0]; + + $hints = array(); + + $hints['profileurl'] = $url; + + if (array_key_exists('nickname', $hcard)) { + $hints['nickname'] = $hcard['nickname']; + } + + if (array_key_exists('fn', $hcard)) { + $hints['fullname'] = $hcard['fn']; + } else if (array_key_exists('n', $hcard)) { + $hints['fullname'] = implode(' ', $hcard['n']); + } + + if (array_key_exists('photo', $hcard)) { + $hints['avatar'] = $hcard['photo']; + } + + if (array_key_exists('note', $hcard)) { + $hints['bio'] = $hcard['note']; + } + + if (array_key_exists('adr', $hcard)) { + if (is_string($hcard['adr'])) { + $hints['location'] = $hcard['adr']; + } else if (is_array($hcard['adr'])) { + $hints['location'] = implode(' ', $hcard['adr']); + } + } + + if (array_key_exists('url', $hcard)) { + if (is_string($hcard['url'])) { + $hints['homepage'] = $hcard['url']; + } else if (is_array($hcard['adr'])) { + // HACK get the last one; that's how our hcards look + $hints['homepage'] = $hcard['url'][count($hcard['url'])-1]; + } + } + + return $hints; + } } diff --git a/plugins/OStatus/classes/Ostatus_source.php b/plugins/OStatus/classes/Ostatus_source.php new file mode 100644 index 000000000..e6ce7d442 --- /dev/null +++ b/plugins/OStatus/classes/Ostatus_source.php @@ -0,0 +1,114 @@ +<?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 Brion Vibber <brion@status.net> + */ + +class Ostatus_source extends Memcached_DataObject +{ + public $__table = 'ostatus_source'; + + public $notice_id; // notice we're referring to + public $profile_uri; // uri of the ostatus_profile this came through -- may be a group feed + public $method; // push or salmon + + 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('notice_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'profile_uri' => DB_DATAOBJECT_STR, + 'method' => DB_DATAOBJECT_STR); + } + + static function schemaDef() + { + return array(new ColumnDef('notice_id', 'integer', + null, false, 'PRI'), + new ColumnDef('profile_uri', 'varchar', + 255, false), + new ColumnDef('method', "ENUM('push','salmon')", + 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('notice_id' => 'K'); + } + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Save a remote notice source record; this helps indicate how trusted we are. + * @param string $method + */ + public static function saveNew(Notice $notice, Ostatus_profile $oprofile, $method) + { + $osource = new Ostatus_source(); + $osource->notice_id = $notice->id; + $osource->profile_uri = $oprofile->uri; + $osource->method = $method; + if ($osource->insert()) { + return true; + } else { + common_log_db_error($osource, 'INSERT', __FILE__); + return false; + } + } +} diff --git a/plugins/OStatus/extlib/Crypt/RSA.php b/plugins/OStatus/extlib/Crypt/RSA.php new file mode 100644 index 000000000..16dfa54d4 --- /dev/null +++ b/plugins/OStatus/extlib/Crypt/RSA.php @@ -0,0 +1,524 @@ +<?php +/** + * Crypt_RSA allows to do following operations: + * - key pair generation + * - encryption and decryption + * - signing and sign validation + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005, 2006 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version 1.2.0b + * @link http://pear.php.net/package/Crypt_RSA + */ + +/** + * RSA error handling facilities + */ +require_once 'Crypt/RSA/ErrorHandler.php'; + +/** + * loader for math wrappers + */ +require_once 'Crypt/RSA/MathLoader.php'; + +/** + * helper class for mange single key + */ +require_once 'Crypt/RSA/Key.php'; + +/** + * helper class for manage key pair + */ +require_once 'Crypt/RSA/KeyPair.php'; + +/** + * Crypt_RSA class, derived from Crypt_RSA_ErrorHandler + * + * Provides the following functions: + * - setParams($params) - sets parameters of current object + * - encrypt($plain_data, $key = null) - encrypts data + * - decrypt($enc_data, $key = null) - decrypts data + * - createSign($doc, $private_key = null) - signs document by private key + * - validateSign($doc, $signature, $public_key = null) - validates signature of document + * + * Example usage: + * // creating an error handler + * $error_handler = create_function('$obj', 'echo "error: ", $obj->getMessage(), "\n"'); + * + * // 1024-bit key pair generation + * $key_pair = new Crypt_RSA_KeyPair(1024); + * + * // check consistence of Crypt_RSA_KeyPair object + * $error_handler($key_pair); + * + * // creating Crypt_RSA object + * $rsa_obj = new Crypt_RSA; + * + * // check consistence of Crypt_RSA object + * $error_handler($rsa_obj); + * + * // set error handler on Crypt_RSA object ( see Crypt/RSA/ErrorHandler.php for details ) + * $rsa_obj->setErrorHandler($error_handler); + * + * // encryption (usually using public key) + * $enc_data = $rsa_obj->encrypt($plain_data, $key_pair->getPublicKey()); + * + * // decryption (usually using private key) + * $plain_data = $rsa_obj->decrypt($enc_data, $key_pair->getPrivateKey()); + * + * // signing + * $signature = $rsa_obj->createSign($document, $key_pair->getPrivateKey()); + * + * // signature checking + * $is_valid = $rsa_obj->validateSign($document, $signature, $key_pair->getPublicKey()); + * + * // signing many documents by one private key + * $rsa_obj = new Crypt_RSA(array('private_key' => $key_pair->getPrivateKey())); + * // check consistence of Crypt_RSA object + * $error_handler($rsa_obj); + * // set error handler ( see Crypt/RSA/ErrorHandler.php for details ) + * $rsa_obj->setErrorHandler($error_handler); + * // sign many documents + * $sign_1 = $rsa_obj->sign($doc_1); + * $sign_2 = $rsa_obj->sign($doc_2); + * //... + * $sign_n = $rsa_obj->sign($doc_n); + * + * // changing default hash function, which is used for sign + * // creating/validation + * $rsa_obj->setParams(array('hash_func' => 'md5')); + * + * // using factory() method instead of constructor (it returns PEAR_Error object on failure) + * $rsa_obj = &Crypt_RSA::factory(); + * if (PEAR::isError($rsa_obj)) { + * echo "error: ", $rsa_obj->getMessage(), "\n"; + * } + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005, 2006 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @link http://pear.php.net/package/Crypt_RSA + * @version @package_version@ + * @access public + */ +class Crypt_RSA extends Crypt_RSA_ErrorHandler +{ + /** + * Reference to math wrapper, which is used to + * manipulate large integers in RSA algorithm. + * + * @var object of Crypt_RSA_Math_* class + * @access private + */ + var $_math_obj; + + /** + * key for encryption, which is used by encrypt() method + * + * @var object of Crypt_RSA_KEY class + * @access private + */ + var $_enc_key; + + /** + * key for decryption, which is used by decrypt() method + * + * @var object of Crypt_RSA_KEY class + * @access private + */ + var $_dec_key; + + /** + * public key, which is used by validateSign() method + * + * @var object of Crypt_RSA_KEY class + * @access private + */ + var $_public_key; + + /** + * private key, which is used by createSign() method + * + * @var object of Crypt_RSA_KEY class + * @access private + */ + var $_private_key; + + /** + * name of hash function, which is used by validateSign() + * and createSign() methods. Default hash function is SHA-1 + * + * @var string + * @access private + */ + var $_hash_func = 'sha1'; + + /** + * Crypt_RSA constructor. + * + * @param array $params + * Optional associative array of parameters, such as: + * enc_key, dec_key, private_key, public_key, hash_func. + * See setParams() method for more detailed description of + * these parameters. + * @param string $wrapper_name + * Name of math wrapper, which will be used to + * perform different operations with big integers. + * See contents of Crypt/RSA/Math folder for examples of wrappers. + * Read docs/Crypt_RSA/docs/math_wrappers.txt for details. + * @param string $error_handler name of error handler function + * + * @access public + */ + function Crypt_RSA($params = null, $wrapper_name = 'default', $error_handler = '') + { + // set error handler + $this->setErrorHandler($error_handler); + // try to load math wrapper + $obj = &Crypt_RSA_MathLoader::loadWrapper($wrapper_name); + if ($this->isError($obj)) { + // error during loading of math wrapper + // Crypt_RSA object is partially constructed. + $this->pushError($obj); + return; + } + $this->_math_obj = &$obj; + + if (!is_null($params)) { + if (!$this->setParams($params)) { + // error in Crypt_RSA::setParams() function + return; + } + } + } + + /** + * Crypt_RSA factory. + * + * @param array $params + * Optional associative array of parameters, such as: + * enc_key, dec_key, private_key, public_key, hash_func. + * See setParams() method for more detailed description of + * these parameters. + * @param string $wrapper_name + * Name of math wrapper, which will be used to + * perform different operations with big integers. + * See contents of Crypt/RSA/Math folder for examples of wrappers. + * Read docs/Crypt_RSA/docs/math_wrappers.txt for details. + * @param string $error_handler name of error handler function + * + * @return object new Crypt_RSA object on success or PEAR_Error object on failure + * @access public + */ + function &factory($params = null, $wrapper_name = 'default', $error_handler = '') + { + $obj = &new Crypt_RSA($params, $wrapper_name, $error_handler); + if ($obj->isError()) { + // error during creating a new object. Retrurn PEAR_Error object + return $obj->getLastError(); + } + // object created successfully. Return it + return $obj; + } + + /** + * Accepts any combination of available parameters as associative array: + * enc_key - encryption key for encrypt() method + * dec_key - decryption key for decrypt() method + * public_key - key for validateSign() method + * private_key - key for createSign() method + * hash_func - name of hash function, which will be used to create and validate sign + * + * @param array $params + * associative array of permitted parameters (see above) + * + * @return bool true on success or false on error + * @access public + */ + function setParams($params) + { + if (!is_array($params)) { + $this->pushError('parameters must be passed to function as associative array', CRYPT_RSA_ERROR_WRONG_PARAMS); + return false; + } + + if (isset($params['enc_key'])) { + if (Crypt_RSA_Key::isValid($params['enc_key'])) { + $this->_enc_key = $params['enc_key']; + } + else { + $this->pushError('wrong encryption key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY); + return false; + } + } + if (isset($params['dec_key'])) { + if (Crypt_RSA_Key::isValid($params['dec_key'])) { + $this->_dec_key = $params['dec_key']; + } + else { + $this->pushError('wrong decryption key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY); + return false; + } + } + if (isset($params['private_key'])) { + if (Crypt_RSA_Key::isValid($params['private_key'])) { + if ($params['private_key']->getKeyType() != 'private') { + $this->pushError('private key must have "private" attribute', CRYPT_RSA_ERROR_WRONG_KEY_TYPE); + return false; + } + $this->_private_key = $params['private_key']; + } + else { + $this->pushError('wrong private key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY); + return false; + } + } + if (isset($params['public_key'])) { + if (Crypt_RSA_Key::isValid($params['public_key'])) { + if ($params['public_key']->getKeyType() != 'public') { + $this->pushError('public key must have "public" attribute', CRYPT_RSA_ERROR_WRONG_KEY_TYPE); + return false; + } + $this->_public_key = $params['public_key']; + } + else { + $this->pushError('wrong public key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY); + return false; + } + } + if (isset($params['hash_func'])) { + if (!function_exists($params['hash_func'])) { + $this->pushError('cannot find hash function with name [' . $params['hash_func'] . ']', CRYPT_RSA_ERROR_WRONG_HASH_FUNC); + return false; + } + $this->_hash_func = $params['hash_func']; + } + return true; // all ok + } + + /** + * Ecnrypts $plain_data by the key $this->_enc_key or $key. + * + * @param string $plain_data data, which must be encrypted + * @param object $key encryption key (object of Crypt_RSA_Key class) + * @return mixed + * encrypted data as string on success or false on error + * + * @access public + */ + function encrypt($plain_data, $key = null) + { + $enc_data = $this->encryptBinary($plain_data, $key); + if ($enc_data !== false) { + return base64_encode($enc_data); + } + // error during encripting data + return false; + } + + /** + * Ecnrypts $plain_data by the key $this->_enc_key or $key. + * + * @param string $plain_data data, which must be encrypted + * @param object $key encryption key (object of Crypt_RSA_Key class) + * @return mixed + * encrypted data as binary string on success or false on error + * + * @access public + */ + function encryptBinary($plain_data, $key = null) + { + if (is_null($key)) { + // use current encryption key + $key = $this->_enc_key; + } + else if (!Crypt_RSA_Key::isValid($key)) { + $this->pushError('invalid encryption key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY); + return false; + } + + // append tail \x01 to plain data. It needs for correctly decrypting of data + $plain_data .= "\x01"; + + $plain_data = $this->_math_obj->bin2int($plain_data); + $exp = $this->_math_obj->bin2int($key->getExponent()); + $modulus = $this->_math_obj->bin2int($key->getModulus()); + + // divide plain data into chunks + $data_len = $this->_math_obj->bitLen($plain_data); + $chunk_len = $key->getKeyLength() - 1; + $block_len = (int) ceil($chunk_len / 8); + $curr_pos = 0; + $enc_data = ''; + while ($curr_pos < $data_len) { + $tmp = $this->_math_obj->subint($plain_data, $curr_pos, $chunk_len); + $enc_data .= str_pad( + $this->_math_obj->int2bin($this->_math_obj->powmod($tmp, $exp, $modulus)), + $block_len, + "\0" + ); + $curr_pos += $chunk_len; + } + return $enc_data; + } + + /** + * Decrypts $enc_data by the key $this->_dec_key or $key. + * + * @param string $enc_data encrypted data as string + * @param object $key decryption key (object of RSA_Crypt_Key class) + * @return mixed + * decrypted data as string on success or false on error + * + * @access public + */ + function decrypt($enc_data, $key = null) + { + $enc_data = base64_decode($enc_data); + return $this->decryptBinary($enc_data, $key); + } + + /** + * Decrypts $enc_data by the key $this->_dec_key or $key. + * + * @param string $enc_data encrypted data as binary string + * @param object $key decryption key (object of RSA_Crypt_Key class) + * @return mixed + * decrypted data as string on success or false on error + * + * @access public + */ + function decryptBinary($enc_data, $key = null) + { + if (is_null($key)) { + // use current decryption key + $key = $this->_dec_key; + } + else if (!Crypt_RSA_Key::isValid($key)) { + $this->pushError('invalid decryption key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY); + return false; + } + + $exp = $this->_math_obj->bin2int($key->getExponent()); + $modulus = $this->_math_obj->bin2int($key->getModulus()); + + $data_len = strlen($enc_data); + $chunk_len = $key->getKeyLength() - 1; + $block_len = (int) ceil($chunk_len / 8); + $curr_pos = 0; + $bit_pos = 0; + $plain_data = $this->_math_obj->bin2int("\0"); + while ($curr_pos < $data_len) { + $tmp = $this->_math_obj->bin2int(substr($enc_data, $curr_pos, $block_len)); + $tmp = $this->_math_obj->powmod($tmp, $exp, $modulus); + $plain_data = $this->_math_obj->bitOr($plain_data, $tmp, $bit_pos); + $bit_pos += $chunk_len; + $curr_pos += $block_len; + } + $result = $this->_math_obj->int2bin($plain_data); + + // delete tail, containing of \x01 + $tail = ord($result{strlen($result) - 1}); + if ($tail != 1) { + $this->pushError("Error tail of decrypted text = {$tail}. Expected 1", CRYPT_RSA_ERROR_WRONG_TAIL); + return false; + } + return substr($result, 0, -1); + } + + /** + * Creates sign for document $document, using $this->_private_key or $private_key + * as private key and $this->_hash_func or $hash_func as hash function. + * + * @param string $document document, which must be signed + * @param object $private_key private key (object of Crypt_RSA_Key type) + * @param string $hash_func name of hash function, which will be used during signing + * @return mixed + * signature of $document as string on success or false on error + * + * @access public + */ + function createSign($document, $private_key = null, $hash_func = null) + { + // check private key + if (is_null($private_key)) { + $private_key = $this->_private_key; + } + else if (!Crypt_RSA_Key::isValid($private_key)) { + $this->pushError('invalid private key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY); + return false; + } + if ($private_key->getKeyType() != 'private') { + $this->pushError('signing key must be private', CRYPT_RSA_ERROR_NEED_PRV_KEY); + return false; + } + + // check hash_func + if (is_null($hash_func)) { + $hash_func = $this->_hash_func; + } + if (!function_exists($hash_func)) { + $this->pushError("cannot find hash function with name [$hash_func]", CRYPT_RSA_ERROR_WRONG_HASH_FUNC); + return false; + } + + return $this->encrypt($hash_func($document), $private_key); + } + + /** + * Validates $signature for document $document with public key $this->_public_key + * or $public_key and hash function $this->_hash_func or $hash_func. + * + * @param string $document document, signature of which must be validated + * @param string $signature signature, which must be validated + * @param object $public_key public key (object of Crypt_RSA_Key class) + * @param string $hash_func hash function, which will be used during validating signature + * @return mixed + * true, if signature of document is valid + * false, if signature of document is invalid + * null on error + * + * @access public + */ + function validateSign($document, $signature, $public_key = null, $hash_func = null) + { + // check public key + if (is_null($public_key)) { + $public_key = $this->_public_key; + } + else if (!Crypt_RSA_Key::isValid($public_key)) { + $this->pushError('invalid public key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY); + return null; + } + if ($public_key->getKeyType() != 'public') { + $this->pushError('validating key must be public', CRYPT_RSA_ERROR_NEED_PUB_KEY); + return null; + } + + // check hash_func + if (is_null($hash_func)) { + $hash_func = $this->_hash_func; + } + if (!function_exists($hash_func)) { + $this->pushError("cannot find hash function with name [$hash_func]", CRYPT_RSA_ERROR_WRONG_HASH_FUNC); + return null; + } + + return $hash_func($document) == $this->decrypt($signature, $public_key); + } +} + +?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/Crypt/RSA/ErrorHandler.php b/plugins/OStatus/extlib/Crypt/RSA/ErrorHandler.php new file mode 100644 index 000000000..8f39741e0 --- /dev/null +++ b/plugins/OStatus/extlib/Crypt/RSA/ErrorHandler.php @@ -0,0 +1,234 @@ +<?php +/** + * Crypt_RSA allows to do following operations: + * - key pair generation + * - encryption and decryption + * - signing and sign validation + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: ErrorHandler.php,v 1.4 2009/01/05 08:30:29 clockwerx Exp $ + * @link http://pear.php.net/package/Crypt_RSA + */ + +/** + * uses PEAR's error handling + */ +require_once 'PEAR.php'; + +/** + * cannot load required extension for math wrapper + */ +define('CRYPT_RSA_ERROR_NO_EXT', 1); + +/** + * cannot load any math wrappers. + * Possible reasons: + * - there is no any wrappers (they must exist in Crypt/RSA/Math folder ) + * - all available wrappers are incorrect (read docs/Crypt_RSA/docs/math_wrappers.txt ) + * - cannot load any extension, required by available wrappers + */ +define('CRYPT_RSA_ERROR_NO_WRAPPERS', 2); + +/** + * cannot find file, containing requested math wrapper + */ +define('CRYPT_RSA_ERROR_NO_FILE', 3); + +/** + * cannot find math wrapper class in the math wrapper file + */ +define('CRYPT_RSA_ERROR_NO_CLASS', 4); + +/** + * invalid key type passed to function (it must be 'public' or 'private') + */ +define('CRYPT_RSA_ERROR_WRONG_KEY_TYPE', 5); + +/** + * key modulus must be greater than key exponent + */ +define('CRYPT_RSA_ERROR_EXP_GE_MOD', 6); + +/** + * missing $key_len parameter in Crypt_RSA_KeyPair::generate($key_len) function + */ +define('CRYPT_RSA_ERROR_MISSING_KEY_LEN', 7); + +/** + * wrong key object passed to function (it must be an object of Crypt_RSA_Key class) + */ +define('CRYPT_RSA_ERROR_WRONG_KEY', 8); + +/** + * wrong name of hash function passed to Crypt_RSA::setParams() function + */ +define('CRYPT_RSA_ERROR_WRONG_HASH_FUNC', 9); + +/** + * key, used for signing, must be private + */ +define('CRYPT_RSA_ERROR_NEED_PRV_KEY', 10); + +/** + * key, used for sign validating, must be public + */ +define('CRYPT_RSA_ERROR_NEED_PUB_KEY', 11); + +/** + * parameters must be passed to function as associative array + */ +define('CRYPT_RSA_ERROR_WRONG_PARAMS', 12); + +/** + * error tail of decrypted text. Maybe, wrong decryption key? + */ +define('CRYPT_RSA_ERROR_WRONG_TAIL', 13); + +/** + * Crypt_RSA_ErrorHandler class. + * + * This class is used as base for Crypt_RSA, Crypt_RSA_Key + * and Crypt_RSA_KeyPair classes. + * + * It provides following functions: + * - isError() - returns true, if list contains errors, else returns false + * - getErrorList() - returns error list + * - getLastError() - returns last error from error list or false, if list is empty + * - pushError($errstr) - pushes $errstr into the error list + * - setErrorHandler($new_error_handler) - sets error handler function + * - getErrorHandler() - returns name of error handler function + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: @package_version@ + * @link http://pear.php.net/package/Crypt_RSA + * @access public + */ +class Crypt_RSA_ErrorHandler +{ + /** + * array of error objects, pushed by $this->pushError() + * + * @var array + * @access private + */ + var $_errors = array(); + + /** + * name of error handler - function, which calls on $this->pushError() call + * + * @var string + * @access private + */ + var $_error_handler = ''; + + /** + * Returns true if list of errors is not empty, else returns false + * + * @param mixed $err Check if the object is an error + * + * @return bool true, if list of errors is not empty or $err is PEAR_Error object, else false + * @access public + */ + function isError($err = null) + { + return is_null($err) ? (sizeof($this->_errors) > 0) : PEAR::isError($err); + } + + /** + * Returns list of all errors, pushed to error list by $this->pushError() + * + * @return array list of errors (usually it contains objects of PEAR_Error class) + * @access public + */ + function getErrorList() + { + return $this->_errors; + } + + /** + * Returns last error from errors list or false, if list is empty + * + * @return mixed + * last error from errors list (usually it is PEAR_Error object) + * or false, if list is empty. + * + * @access public + */ + function getLastError() + { + $len = sizeof($this->_errors); + return $len ? $this->_errors[$len - 1] : false; + } + + /** + * pushes error object $error to the error list + * + * @param string $errstr error string + * @param int $errno error number + * + * @return bool true on success, false on error + * @access public + */ + function pushError($errstr, $errno = 0) + { + $this->_errors[] = PEAR::raiseError($errstr, $errno); + + if ($this->_error_handler != '') { + // call user defined error handler + $func = $this->_error_handler; + $func($this); + } + return true; + } + + /** + * sets error handler to function with name $func_name. + * Function $func_name must accept one parameter - current + * object, which triggered error. + * + * @param string $func_name name of error handler function + * + * @return bool true on success, false on error + * @access public + */ + function setErrorHandler($func_name = '') + { + if ($func_name == '') { + $this->_error_handler = ''; + } + if (!function_exists($func_name)) { + return false; + } + $this->_error_handler = $func_name; + return true; + } + + /** + * returns name of current error handler, or null if there is no error handler + * + * @return mixed error handler name as string or null, if there is no error handler + * @access public + */ + function getErrorHandler() + { + return $this->_error_handler; + } +} + +?> diff --git a/plugins/OStatus/extlib/Crypt/RSA/Key.php b/plugins/OStatus/extlib/Crypt/RSA/Key.php new file mode 100644 index 000000000..659530229 --- /dev/null +++ b/plugins/OStatus/extlib/Crypt/RSA/Key.php @@ -0,0 +1,315 @@ +<?php +/** + * Crypt_RSA allows to do following operations: + * - key pair generation + * - encryption and decryption + * - signing and sign validation + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: Key.php,v 1.6 2009/01/05 08:30:29 clockwerx Exp $ + * @link http://pear.php.net/package/Crypt_RSA + */ + +/** + * RSA error handling facilities + */ +require_once 'Crypt/RSA/ErrorHandler.php'; + +/** + * loader for RSA math wrappers + */ +require_once 'Crypt/RSA/MathLoader.php'; + +/** + * Crypt_RSA_Key class, derived from Crypt_RSA_ErrorHandler + * + * Provides the following functions: + * - getKeyLength() - returns bit key length + * - getExponent() - returns key exponent as binary string + * - getModulus() - returns key modulus as binary string + * - getKeyType() - returns type of the key (public or private) + * - toString() - returns serialized key as string + * - fromString($key_str) - static function; returns key, unserialized from string + * - isValid($key) - static function for validating of $key + * + * Example usage: + * // create new 1024-bit key pair + * $key_pair = new Crypt_RSA_KeyPair(1024); + * + * // get public key (its class is Crypt_RSA_Key) + * $key = $key_pair->getPublicKey(); + * + * // get key length + * $len = $key->getKeyLength(); + * + * // get modulus as string + * $modulus = $key->getModulus(); + * + * // get exponent as string + * $exponent = $key->getExponent(); + * + * // get string represenation of key (use it instead of serialization of Crypt_RSA_Key object) + * $key_in_str = $key->toString(); + * + * // restore key object from string using 'BigInt' math wrapper + * $key = Crypt_RSA_Key::fromString($key_in_str, 'BigInt'); + * + * // error check + * if ($key->isError()) { + * echo "error while unserializing key object:\n"; + * $erorr = $key->getLastError(); + * echo $error->getMessage(), "\n"; + * } + * + * // validate key + * if (Crypt_RSA_Key::isValid($key)) echo 'valid key'; + * else echo 'invalid key'; + * + * // using factory() method instead of constructor (it returns PEAR_Error object on failure) + * $rsa_obj = &Crypt_RSA_Key::factory($modulus, $exp, $key_type); + * if (PEAR::isError($rsa_obj)) { + * echo "error: ", $rsa_obj->getMessage(), "\n"; + * } + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: @package_version@ + * @link http://pear.php.net/package/Crypt_RSA + * @access public + */ +class Crypt_RSA_Key extends Crypt_RSA_ErrorHandler +{ + /** + * Reference to math wrapper object, which is used to + * manipulate large integers in RSA algorithm. + * + * @var object of Crypt_RSA_Math_* class + * @access private + */ + var $_math_obj; + + /** + * shared modulus + * + * @var string + * @access private + */ + var $_modulus; + + /** + * exponent + * + * @var string + * @access private + */ + var $_exp; + + /** + * key type (private or public) + * + * @var string + * @access private + */ + var $_key_type; + + /** + * key length in bits + * + * @var int + * @access private + */ + var $_key_len; + + /** + * Crypt_RSA_Key constructor. + * + * You should pass in the name of math wrapper, which will be used to + * perform different operations with big integers. + * See contents of Crypt/RSA/Math folder for examples of wrappers. + * Read docs/Crypt_RSA/docs/math_wrappers.txt for details. + * + * @param string $modulus key modulus + * @param string $exp key exponent + * @param string $key_type type of the key (public or private) + * @param string $wrapper_name wrapper to use + * @param string $error_handler name of error handler function + * + * @access public + */ + function Crypt_RSA_Key($modulus, $exp, $key_type, $wrapper_name = 'default', $error_handler = '') + { + // set error handler + $this->setErrorHandler($error_handler); + // try to load math wrapper $wrapper_name + $obj = &Crypt_RSA_MathLoader::loadWrapper($wrapper_name); + if ($this->isError($obj)) { + // error during loading of math wrapper + $this->pushError($obj); // push error object into error list + return; + } + $this->_math_obj = &$obj; + + $this->_modulus = $modulus; + $this->_exp = $exp; + + if (!in_array($key_type, array('private', 'public'))) { + $this->pushError('invalid key type. It must be private or public', CRYPT_RSA_ERROR_WRONG_KEY_TYPE); + return; + } + $this->_key_type = $key_type; + + /* check length of modulus & exponent ( abs(modulus) > abs(exp) ) */ + $mod_num = $this->_math_obj->bin2int($this->_modulus); + $exp_num = $this->_math_obj->bin2int($this->_exp); + + if ($this->_math_obj->cmpAbs($mod_num, $exp_num) <= 0) { + $this->pushError('modulus must be greater than exponent', CRYPT_RSA_ERROR_EXP_GE_MOD); + return; + } + + // determine key length + $this->_key_len = $this->_math_obj->bitLen($mod_num); + } + + /** + * Crypt_RSA_Key factory. + * + * @param string $modulus key modulus + * @param string $exp key exponent + * @param string $key_type type of the key (public or private) + * @param string $wrapper_name wrapper to use + * @param string $error_handler name of error handler function + * + * @return object new Crypt_RSA_Key object on success or PEAR_Error object on failure + * @access public + */ + function factory($modulus, $exp, $key_type, $wrapper_name = 'default', $error_handler = '') + { + $obj = new Crypt_RSA_Key($modulus, $exp, $key_type, $wrapper_name, $error_handler); + if ($obj->isError()) { + // error during creating a new object. Retrurn PEAR_Error object + return $obj->getLastError(); + } + // object created successfully. Return it + return $obj; + } + + /** + * Calculates bit length of the key + * + * @return int bit length of key + * @access public + */ + function getKeyLength() + { + return $this->_key_len; + } + + /** + * Returns modulus part of the key as binary string, + * which can be used to construct new Crypt_RSA_Key object. + * + * @return string modulus as binary string + * @access public + */ + function getModulus() + { + return $this->_modulus; + } + + /** + * Returns exponent part of the key as binary string, + * which can be used to construct new Crypt_RSA_Key object. + * + * @return string exponent as binary string + * @access public + */ + function getExponent() + { + return $this->_exp; + } + + /** + * Returns key type (public, private) + * + * @return string key type (public, private) + * @access public + */ + function getKeyType() + { + return $this->_key_type; + } + + /** + * Returns string representation of key + * + * @return string key, serialized to string + * @access public + */ + function toString() + { + return base64_encode( + serialize( + array($this->_modulus, $this->_exp, $this->_key_type) + ) + ); + } + + /** + * Returns Crypt_RSA_Key object, unserialized from + * string representation of key. + * + * optional parameter $wrapper_name - is the name of math wrapper, + * which will be used during unserialization of this object. + * + * This function can be called statically: + * $key = Crypt_RSA_Key::fromString($key_in_string, 'BigInt'); + * + * @param string $key_str RSA key, serialized into string + * @param string $wrapper_name optional math wrapper name + * + * @return object key as Crypt_RSA_Key object + * @access public + * @static + */ + function fromString($key_str, $wrapper_name = 'default') + { + list($modulus, $exponent, $key_type) = unserialize(base64_decode($key_str)); + $obj = new Crypt_RSA_Key($modulus, $exponent, $key_type, $wrapper_name); + return $obj; + } + + /** + * Validates key + * This function can be called statically: + * $is_valid = Crypt_RSA_Key::isValid($key) + * + * Returns true, if $key is valid Crypt_RSA key, else returns false + * + * @param object $key Crypt_RSA_Key object for validating + * + * @return bool true if $key is valid, else false + * @access public + */ + function isValid($key) + { + return (is_object($key) && strtolower(get_class($key)) === strtolower(__CLASS__)); + } +} + +?> diff --git a/plugins/OStatus/extlib/Crypt/RSA/KeyPair.php b/plugins/OStatus/extlib/Crypt/RSA/KeyPair.php new file mode 100644 index 000000000..ecc0b7dc7 --- /dev/null +++ b/plugins/OStatus/extlib/Crypt/RSA/KeyPair.php @@ -0,0 +1,804 @@ +<?php +/** + * Crypt_RSA allows to do following operations: + * - key pair generation + * - encryption and decryption + * - signing and sign validation + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: KeyPair.php,v 1.7 2009/01/05 08:30:29 clockwerx Exp $ + * @link http://pear.php.net/package/Crypt_RSA + */ + +/** + * RSA error handling facilities + */ +require_once 'Crypt/RSA/ErrorHandler.php'; + +/** + * loader for RSA math wrappers + */ +require_once 'Crypt/RSA/MathLoader.php'; + +/** + * helper class for single key managing + */ +require_once 'Crypt/RSA/Key.php'; + +/** + * Crypt_RSA_KeyPair class, derived from Crypt_RSA_ErrorHandler + * + * Provides the following functions: + * - generate($key) - generates new key pair + * - getPublicKey() - returns public key + * - getPrivateKey() - returns private key + * - getKeyLength() - returns bit key length + * - setRandomGenerator($func_name) - sets random generator to $func_name + * - fromPEMString($str) - retrieves keypair from PEM-encoded string + * - toPEMString() - stores keypair to PEM-encoded string + * - isEqual($keypair2) - compares current keypair to $keypair2 + * + * Example usage: + * // create new 1024-bit key pair + * $key_pair = new Crypt_RSA_KeyPair(1024); + * + * // error check + * if ($key_pair->isError()) { + * echo "error while initializing Crypt_RSA_KeyPair object:\n"; + * $erorr = $key_pair->getLastError(); + * echo $error->getMessage(), "\n"; + * } + * + * // get public key + * $public_key = $key_pair->getPublicKey(); + * + * // get private key + * $private_key = $key_pair->getPrivateKey(); + * + * // generate new 512-bit key pair + * $key_pair->generate(512); + * + * // error check + * if ($key_pair->isError()) { + * echo "error while generating key pair:\n"; + * $erorr = $key_pair->getLastError(); + * echo $error->getMessage(), "\n"; + * } + * + * // get key pair length + * $length = $key_pair->getKeyLength(); + * + * // set random generator to $func_name, where $func_name + * // consists name of random generator function. See comments + * // before setRandomGenerator() method for details + * $key_pair->setRandomGenerator($func_name); + * + * // error check + * if ($key_pair->isError()) { + * echo "error while changing random generator:\n"; + * $erorr = $key_pair->getLastError(); + * echo $error->getMessage(), "\n"; + * } + * + * // using factory() method instead of constructor (it returns PEAR_Error object on failure) + * $rsa_obj = &Crypt_RSA_KeyPair::factory($key_len); + * if (PEAR::isError($rsa_obj)) { + * echo "error: ", $rsa_obj->getMessage(), "\n"; + * } + * + * // read key pair from PEM-encoded string: + * $str = "-----BEGIN RSA PRIVATE KEY-----" + * . "MCsCAQACBHr5LDkCAwEAAQIEBc6jbQIDAOCfAgMAjCcCAk3pAgJMawIDAL41" + * . "-----END RSA PRIVATE KEY-----"; + * $keypair = Crypt_RSA_KeyPair::fromPEMString($str); + * + * // read key pair from .pem file 'private.pem': + * $str = file_get_contents('private.pem'); + * $keypair = Crypt_RSA_KeyPair::fromPEMString($str); + * + * // generate and write 1024-bit key pair to .pem file 'private_new.pem' + * $keypair = new Crypt_RSA_KeyPair(1024); + * $str = $keypair->toPEMString(); + * file_put_contents('private_new.pem', $str); + * + * // compare $keypair1 to $keypair2 + * if ($keypair1->isEqual($keypair2)) { + * echo "keypair1 = keypair2\n"; + * } + * else { + * echo "keypair1 != keypair2\n"; + * } + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: @package_version@ + * @link http://pear.php.net/package/Crypt_RSA + * @access public + */ +class Crypt_RSA_KeyPair extends Crypt_RSA_ErrorHandler +{ + /** + * Reference to math wrapper object, which is used to + * manipulate large integers in RSA algorithm. + * + * @var object of Crypt_RSA_Math_* class + * @access private + */ + var $_math_obj; + + /** + * length of each key in the key pair + * + * @var int + * @access private + */ + var $_key_len; + + /** + * public key + * + * @var object of Crypt_RSA_KEY class + * @access private + */ + var $_public_key; + + /** + * private key + * + * @var object of Crypt_RSA_KEY class + * @access private + */ + var $_private_key; + + /** + * name of function, which is used as random generator + * + * @var string + * @access private + */ + var $_random_generator; + + /** + * RSA keypair attributes [version, n, e, d, p, q, dmp1, dmq1, iqmp] as associative array + * + * @var array + * @access private + */ + var $_attrs; + + /** + * Returns names of keypair attributes from $this->_attrs array + * + * @return array Array of keypair attributes names + * @access private + */ + function _get_attr_names() + { + return array('version', 'n', 'e', 'd', 'p', 'q', 'dmp1', 'dmq1', 'iqmp'); + } + + /** + * Parses ASN.1 string [$str] starting form position [$pos]. + * Returns tag and string value of parsed object. + * + * @param string $str + * @param int &$pos + * @param Crypt_RSA_ErrorHandler &$err_handler + * + * @return mixed Array('tag' => ..., 'str' => ...) on success, false on error + * @access private + */ + function _ASN1Parse($str, &$pos, &$err_handler) + { + $max_pos = strlen($str); + if ($max_pos < 2) { + $err_handler->pushError("ASN.1 string too short"); + return false; + } + + // get ASN.1 tag value + $tag = ord($str[$pos++]) & 0x1f; + if ($tag == 0x1f) { + $tag = 0; + do { + $n = ord($str[$pos++]); + $tag <<= 7; + $tag |= $n & 0x7f; + } while (($n & 0x80) && $pos < $max_pos); + } + if ($pos >= $max_pos) { + $err_handler->pushError("ASN.1 string too short"); + return false; + } + + // get ASN.1 object length + $len = ord($str[$pos++]); + if ($len & 0x80) { + $n = $len & 0x1f; + $len = 0; + while ($n-- && $pos < $max_pos) { + $len <<= 8; + $len |= ord($str[$pos++]); + } + } + if ($pos >= $max_pos || $len > $max_pos - $pos) { + $err_handler->pushError("ASN.1 string too short"); + return false; + } + + // get string value of ASN.1 object + $str = substr($str, $pos, $len); + + return array( + 'tag' => $tag, + 'str' => $str, + ); + } + + /** + * Parses ASN.1 sting [$str] starting from position [$pos]. + * Returns string representation of number, which can be passed + * in bin2int() function of math wrapper. + * + * @param string $str + * @param int &$pos + * @param Crypt_RSA_ErrorHandler &$err_handler + * + * @return mixed string representation of parsed number on success, false on error + * @access private + */ + function _ASN1ParseInt($str, &$pos, &$err_handler) + { + $tmp = Crypt_RSA_KeyPair::_ASN1Parse($str, $pos, $err_handler); + if ($err_handler->isError()) { + return false; + } + if ($tmp['tag'] != 0x02) { + $errstr = sprintf("wrong ASN tag value: 0x%02x. Expected 0x02 (INTEGER)", $tmp['tag']); + $err_handler->pushError($errstr); + return false; + } + $pos += strlen($tmp['str']); + + return strrev($tmp['str']); + } + + /** + * Constructs ASN.1 string from tag $tag and object $str + * + * @param string $str ASN.1 object string + * @param int $tag ASN.1 tag value + * @param bool $is_constructed + * @param bool $is_private + * + * @return ASN.1-encoded string + * @access private + */ + function _ASN1Store($str, $tag, $is_constructed = false, $is_private = false) + { + $out = ''; + + // encode ASN.1 tag value + $tag_ext = ($is_constructed ? 0x20 : 0) | ($is_private ? 0xc0 : 0); + if ($tag < 0x1f) { + $out .= chr($tag | $tag_ext); + } else { + $out .= chr($tag_ext | 0x1f); + $tmp = chr($tag & 0x7f); + $tag >>= 7; + while ($tag) { + $tmp .= chr(($tag & 0x7f) | 0x80); + $tag >>= 7; + } + $out .= strrev($tmp); + } + + // encode ASN.1 object length + $len = strlen($str); + if ($len < 0x7f) { + $out .= chr($len); + } else { + $tmp = ''; + $n = 0; + while ($len) { + $tmp .= chr($len & 0xff); + $len >>= 8; + $n++; + } + $out .= chr($n | 0x80); + $out .= strrev($tmp); + } + + return $out . $str; + } + + /** + * Constructs ASN.1 string from binary representation of big integer + * + * @param string $str binary representation of big integer + * + * @return ASN.1-encoded string + * @access private + */ + function _ASN1StoreInt($str) + { + $str = strrev($str); + return Crypt_RSA_KeyPair::_ASN1Store($str, 0x02); + } + + /** + * Crypt_RSA_KeyPair constructor. + * + * Wrapper: name of math wrapper, which will be used to + * perform different operations with big integers. + * See contents of Crypt/RSA/Math folder for examples of wrappers. + * Read docs/Crypt_RSA/docs/math_wrappers.txt for details. + * + * @param int $key_len bit length of key pair, which will be generated in constructor + * @param string $wrapper_name wrapper name + * @param string $error_handler name of error handler function + * @param callback $random_generator function which will be used as random generator + * + * @access public + */ + function Crypt_RSA_KeyPair($key_len, $wrapper_name = 'default', $error_handler = '', $random_generator = null) + { + // set error handler + $this->setErrorHandler($error_handler); + // try to load math wrapper + $obj = &Crypt_RSA_MathLoader::loadWrapper($wrapper_name); + if ($this->isError($obj)) { + // error during loading of math wrapper + $this->pushError($obj); + return; + } + $this->_math_obj = &$obj; + + // set random generator + if (!$this->setRandomGenerator($random_generator)) { + // error in setRandomGenerator() function + return; + } + + if (is_array($key_len)) { + // ugly BC hack - it is possible to pass RSA private key attributes [version, n, e, d, p, q, dmp1, dmq1, iqmp] + // as associative array instead of key length to Crypt_RSA_KeyPair constructor + $rsa_attrs = $key_len; + + // convert attributes to big integers + $attr_names = $this->_get_attr_names(); + foreach ($attr_names as $attr) { + if (!isset($rsa_attrs[$attr])) { + $this->pushError("missing required RSA attribute [$attr]"); + return; + } + ${$attr} = $this->_math_obj->bin2int($rsa_attrs[$attr]); + } + + // check primality of p and q + if (!$this->_math_obj->isPrime($p)) { + $this->pushError("[p] must be prime"); + return; + } + if (!$this->_math_obj->isPrime($q)) { + $this->pushError("[q] must be prime"); + return; + } + + // check n = p * q + $n1 = $this->_math_obj->mul($p, $q); + if ($this->_math_obj->cmpAbs($n, $n1)) { + $this->pushError("n != p * q"); + return; + } + + // check e * d = 1 mod (p-1) * (q-1) + $p1 = $this->_math_obj->dec($p); + $q1 = $this->_math_obj->dec($q); + $p1q1 = $this->_math_obj->mul($p1, $q1); + $ed = $this->_math_obj->mul($e, $d); + $one = $this->_math_obj->mod($ed, $p1q1); + if (!$this->_math_obj->isOne($one)) { + $this->pushError("e * d != 1 mod (p-1)*(q-1)"); + return; + } + + // check dmp1 = d mod (p-1) + $dmp = $this->_math_obj->mod($d, $p1); + if ($this->_math_obj->cmpAbs($dmp, $dmp1)) { + $this->pushError("dmp1 != d mod (p-1)"); + return; + } + + // check dmq1 = d mod (q-1) + $dmq = $this->_math_obj->mod($d, $q1); + if ($this->_math_obj->cmpAbs($dmq, $dmq1)) { + $this->pushError("dmq1 != d mod (q-1)"); + return; + } + + // check iqmp = 1/q mod p + $q1 = $this->_math_obj->invmod($iqmp, $p); + if ($this->_math_obj->cmpAbs($q, $q1)) { + $this->pushError("iqmp != 1/q mod p"); + return; + } + + // try to create public key object + $public_key = &new Crypt_RSA_Key($rsa_attrs['n'], $rsa_attrs['e'], 'public', $wrapper_name, $error_handler); + if ($public_key->isError()) { + // error during creating public object + $this->pushError($public_key->getLastError()); + return; + } + + // try to create private key object + $private_key = &new Crypt_RSA_Key($rsa_attrs['n'], $rsa_attrs['d'], 'private', $wrapper_name, $error_handler); + if ($private_key->isError()) { + // error during creating private key object + $this->pushError($private_key->getLastError()); + return; + } + + $this->_public_key = $public_key; + $this->_private_key = $private_key; + $this->_key_len = $public_key->getKeyLength(); + $this->_attrs = $rsa_attrs; + } else { + // generate key pair + if (!$this->generate($key_len)) { + // error during generating key pair + return; + } + } + } + + /** + * Crypt_RSA_KeyPair factory. + * + * Wrapper - Name of math wrapper, which will be used to + * perform different operations with big integers. + * See contents of Crypt/RSA/Math folder for examples of wrappers. + * Read docs/Crypt_RSA/docs/math_wrappers.txt for details. + * + * @param int $key_len bit length of key pair, which will be generated in constructor + * @param string $wrapper_name wrapper name + * @param string $error_handler name of error handler function + * @param callback $random_generator function which will be used as random generator + * + * @return object new Crypt_RSA_KeyPair object on success or PEAR_Error object on failure + * @access public + */ + function &factory($key_len, $wrapper_name = 'default', $error_handler = '', $random_generator = null) + { + $obj = &new Crypt_RSA_KeyPair($key_len, $wrapper_name, $error_handler, $random_generator); + if ($obj->isError()) { + // error during creating a new object. Return PEAR_Error object + return $obj->getLastError(); + } + // object created successfully. Return it + return $obj; + } + + /** + * Generates new Crypt_RSA key pair with length $key_len. + * If $key_len is missed, use an old key length from $this->_key_len + * + * @param int $key_len bit length of key pair, which will be generated + * + * @return bool true on success or false on error + * @access public + */ + function generate($key_len = null) + { + if (is_null($key_len)) { + // use an old key length + $key_len = $this->_key_len; + if (is_null($key_len)) { + $this->pushError('missing key_len parameter', CRYPT_RSA_ERROR_MISSING_KEY_LEN); + return false; + } + } + + // minimal key length is 8 bit ;) + if ($key_len < 8) { + $key_len = 8; + } + // store key length in the _key_len property + $this->_key_len = $key_len; + + // set [e] to 0x10001 (65537) + $e = $this->_math_obj->bin2int("\x01\x00\x01"); + + // generate [p], [q] and [n] + $p_len = intval(($key_len + 1) / 2); + $q_len = $key_len - $p_len; + $p1 = $q1 = 0; + do { + // generate prime number [$p] with length [$p_len] with the following condition: + // GCD($e, $p - 1) = 1 + do { + $p = $this->_math_obj->getPrime($p_len, $this->_random_generator); + $p1 = $this->_math_obj->dec($p); + $tmp = $this->_math_obj->GCD($e, $p1); + } while (!$this->_math_obj->isOne($tmp)); + // generate prime number [$q] with length [$q_len] with the following conditions: + // GCD($e, $q - 1) = 1 + // $q != $p + do { + $q = $this->_math_obj->getPrime($q_len, $this->_random_generator); + $q1 = $this->_math_obj->dec($q); + $tmp = $this->_math_obj->GCD($e, $q1); + } while (!$this->_math_obj->isOne($tmp) && !$this->_math_obj->cmpAbs($q, $p)); + // if (p < q), then exchange them + if ($this->_math_obj->cmpAbs($p, $q) < 0) { + $tmp = $p; + $p = $q; + $q = $tmp; + $tmp = $p1; + $p1 = $q1; + $q1 = $tmp; + } + // calculate n = p * q + $n = $this->_math_obj->mul($p, $q); + } while ($this->_math_obj->bitLen($n) != $key_len); + + // calculate d = 1/e mod (p - 1) * (q - 1) + $pq = $this->_math_obj->mul($p1, $q1); + $d = $this->_math_obj->invmod($e, $pq); + + // calculate dmp1 = d mod (p - 1) + $dmp1 = $this->_math_obj->mod($d, $p1); + + // calculate dmq1 = d mod (q - 1) + $dmq1 = $this->_math_obj->mod($d, $q1); + + // calculate iqmp = 1/q mod p + $iqmp = $this->_math_obj->invmod($q, $p); + + // store RSA keypair attributes + $this->_attrs = array( + 'version' => "\x00", + 'n' => $this->_math_obj->int2bin($n), + 'e' => $this->_math_obj->int2bin($e), + 'd' => $this->_math_obj->int2bin($d), + 'p' => $this->_math_obj->int2bin($p), + 'q' => $this->_math_obj->int2bin($q), + 'dmp1' => $this->_math_obj->int2bin($dmp1), + 'dmq1' => $this->_math_obj->int2bin($dmq1), + 'iqmp' => $this->_math_obj->int2bin($iqmp), + ); + + $n = $this->_attrs['n']; + $e = $this->_attrs['e']; + $d = $this->_attrs['d']; + + // try to create public key object + $obj = &new Crypt_RSA_Key($n, $e, 'public', $this->_math_obj->getWrapperName(), $this->_error_handler); + if ($obj->isError()) { + // error during creating public object + $this->pushError($obj->getLastError()); + return false; + } + $this->_public_key = &$obj; + + // try to create private key object + $obj = &new Crypt_RSA_Key($n, $d, 'private', $this->_math_obj->getWrapperName(), $this->_error_handler); + if ($obj->isError()) { + // error during creating private key object + $this->pushError($obj->getLastError()); + return false; + } + $this->_private_key = &$obj; + + return true; // key pair successfully generated + } + + /** + * Returns public key from the pair + * + * @return object public key object of class Crypt_RSA_Key + * @access public + */ + function getPublicKey() + { + return $this->_public_key; + } + + /** + * Returns private key from the pair + * + * @return object private key object of class Crypt_RSA_Key + * @access public + */ + function getPrivateKey() + { + return $this->_private_key; + } + + /** + * Sets name of random generator function for key generation. + * If parameter is skipped, then sets to default random generator. + * + * Random generator function must return integer with at least 8 lower + * significant bits, which will be used as random values. + * + * @param string $random_generator name of random generator function + * + * @return bool true on success or false on error + * @access public + */ + function setRandomGenerator($random_generator = null) + { + static $default_random_generator = null; + + if (is_string($random_generator)) { + // set user's random generator + if (!function_exists($random_generator)) { + $this->pushError("can't find random generator function with name [{$random_generator}]"); + return false; + } + $this->_random_generator = $random_generator; + } else { + // set default random generator + $this->_random_generator = is_null($default_random_generator) ? + ($default_random_generator = create_function('', '$a=explode(" ",microtime());return(int)($a[0]*1000000);')) : + $default_random_generator; + } + return true; + } + + /** + * Returns length of each key in the key pair + * + * @return int bit length of each key in key pair + * @access public + */ + function getKeyLength() + { + return $this->_key_len; + } + + /** + * Retrieves RSA keypair from PEM-encoded string, containing RSA private key. + * Example of such string: + * -----BEGIN RSA PRIVATE KEY----- + * MCsCAQACBHtvbSECAwEAAQIEeYrk3QIDAOF3AgMAjCcCAmdnAgJMawIDALEk + * -----END RSA PRIVATE KEY----- + * + * Wrapper: Name of math wrapper, which will be used to + * perform different operations with big integers. + * See contents of Crypt/RSA/Math folder for examples of wrappers. + * Read docs/Crypt_RSA/docs/math_wrappers.txt for details. + * + * @param string $str PEM-encoded string + * @param string $wrapper_name Wrapper name + * @param string $error_handler name of error handler function + * + * @return Crypt_RSA_KeyPair object on success, PEAR_Error object on error + * @access public + * @static + */ + function &fromPEMString($str, $wrapper_name = 'default', $error_handler = '') + { + if (isset($this)) { + if ($wrapper_name == 'default') { + $wrapper_name = $this->_math_obj->getWrapperName(); + } + if ($error_handler == '') { + $error_handler = $this->_error_handler; + } + } + $err_handler = &new Crypt_RSA_ErrorHandler; + $err_handler->setErrorHandler($error_handler); + + // search for base64-encoded private key + if (!preg_match('/-----BEGIN RSA PRIVATE KEY-----([^-]+)-----END RSA PRIVATE KEY-----/', $str, $matches)) { + $err_handler->pushError("can't find RSA private key in the string [{$str}]"); + return $err_handler->getLastError(); + } + + // parse private key. It is ASN.1-encoded + $str = base64_decode($matches[1]); + $pos = 0; + $tmp = Crypt_RSA_KeyPair::_ASN1Parse($str, $pos, $err_handler); + if ($err_handler->isError()) { + return $err_handler->getLastError(); + } + if ($tmp['tag'] != 0x10) { + $errstr = sprintf("wrong ASN tag value: 0x%02x. Expected 0x10 (SEQUENCE)", $tmp['tag']); + $err_handler->pushError($errstr); + return $err_handler->getLastError(); + } + + // parse ASN.1 SEQUENCE for RSA private key + $attr_names = Crypt_RSA_KeyPair::_get_attr_names(); + $n = sizeof($attr_names); + $rsa_attrs = array(); + for ($i = 0; $i < $n; $i++) { + $tmp = Crypt_RSA_KeyPair::_ASN1ParseInt($str, $pos, $err_handler); + if ($err_handler->isError()) { + return $err_handler->getLastError(); + } + $attr = $attr_names[$i]; + $rsa_attrs[$attr] = $tmp; + } + + // create Crypt_RSA_KeyPair object. + $keypair = &new Crypt_RSA_KeyPair($rsa_attrs, $wrapper_name, $error_handler); + if ($keypair->isError()) { + return $keypair->getLastError(); + } + + return $keypair; + } + + /** + * converts keypair to PEM-encoded string, which can be stroed in + * .pem compatible files, contianing RSA private key. + * + * @return string PEM-encoded keypair on success, false on error + * @access public + */ + function toPEMString() + { + // store RSA private key attributes into ASN.1 string + $str = ''; + $attr_names = $this->_get_attr_names(); + $n = sizeof($attr_names); + $rsa_attrs = $this->_attrs; + for ($i = 0; $i < $n; $i++) { + $attr = $attr_names[$i]; + if (!isset($rsa_attrs[$attr])) { + $this->pushError("Cannot find value for ASN.1 attribute [$attr]"); + return false; + } + $tmp = $rsa_attrs[$attr]; + $str .= Crypt_RSA_KeyPair::_ASN1StoreInt($tmp); + } + + // prepend $str by ASN.1 SEQUENCE (0x10) header + $str = Crypt_RSA_KeyPair::_ASN1Store($str, 0x10, true); + + // encode and format PEM string + $str = base64_encode($str); + $str = chunk_split($str, 64, "\n"); + return "-----BEGIN RSA PRIVATE KEY-----\n$str-----END RSA PRIVATE KEY-----\n"; + } + + /** + * Compares keypairs in Crypt_RSA_KeyPair objects $this and $key_pair + * + * @param Crypt_RSA_KeyPair $key_pair keypair to compare + * + * @return bool true, if keypair stored in $this equal to keypair stored in $key_pair + * @access public + */ + function isEqual($key_pair) + { + $attr_names = $this->_get_attr_names(); + foreach ($attr_names as $attr) { + if ($this->_attrs[$attr] != $key_pair->_attrs[$attr]) { + return false; + } + } + return true; + } +} + +?> diff --git a/plugins/OStatus/extlib/Crypt/RSA/Math/BCMath.php b/plugins/OStatus/extlib/Crypt/RSA/Math/BCMath.php new file mode 100644 index 000000000..646ff6710 --- /dev/null +++ b/plugins/OStatus/extlib/Crypt/RSA/Math/BCMath.php @@ -0,0 +1,482 @@ +<?php +/** + * Crypt_RSA allows to do following operations: + * - key pair generation + * - encryption and decryption + * - signing and sign validation + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2006 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version 1.2.0b + * @link http://pear.php.net/package/Crypt_RSA + */ + +/** + * Crypt_RSA_Math_BCMath class. + * + * Provides set of math functions, which are used by Crypt_RSA package + * This class is a wrapper for PHP BCMath extension. + * See http://php.net/manual/en/ref.bc.php for details. + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005, 2006 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @link http://pear.php.net/package/Crypt_RSA + * @version @package_version@ + * @access public + */ +class Crypt_RSA_Math_BCMath +{ + /** + * error description + * + * @var string + * @access public + */ + var $errstr = ''; + + /** + * Performs Miller-Rabin primality test for number $num + * with base $base. Returns true, if $num is strong pseudoprime + * by base $base. Else returns false. + * + * @param string $num + * @param string $base + * @return bool + * @access private + */ + function _millerTest($num, $base) + { + if (!bccomp($num, '1')) { + // 1 is not prime ;) + return false; + } + $tmp = bcsub($num, '1'); + + $zero_bits = 0; + while (!bccomp(bcmod($tmp, '2'), '0')) { + $zero_bits++; + $tmp = bcdiv($tmp, '2'); + } + + $tmp = $this->powmod($base, $tmp, $num); + if (!bccomp($tmp, '1')) { + // $num is probably prime + return true; + } + + while ($zero_bits--) { + if (!bccomp(bcadd($tmp, '1'), $num)) { + // $num is probably prime + return true; + } + $tmp = $this->powmod($tmp, '2', $num); + } + // $num is composite + return false; + } + + /** + * Crypt_RSA_Math_BCMath constructor. + * Checks an existance of PHP BCMath extension. + * On failure saves error description in $this->errstr + * + * @access public + */ + function Crypt_RSA_Math_BCMath() + { + if (!extension_loaded('bcmath')) { + if (!@dl('bcmath.' . PHP_SHLIB_SUFFIX) && !@dl('php_bcmath.' . PHP_SHLIB_SUFFIX)) { + // cannot load BCMath extension. Set error string + $this->errstr = 'Crypt_RSA package requires the BCMath extension. See http://php.net/manual/en/ref.bc.php for details'; + return; + } + } + } + + /** + * Transforms binary representation of large integer into its native form. + * + * Example of transformation: + * $str = "\x12\x34\x56\x78\x90"; + * $num = 0x9078563412; + * + * @param string $str + * @return string + * @access public + */ + function bin2int($str) + { + $result = '0'; + $n = strlen($str); + do { + $result = bcadd(bcmul($result, '256'), ord($str{--$n})); + } while ($n > 0); + return $result; + } + + /** + * Transforms large integer into binary representation. + * + * Example of transformation: + * $num = 0x9078563412; + * $str = "\x12\x34\x56\x78\x90"; + * + * @param string $num + * @return string + * @access public + */ + function int2bin($num) + { + $result = ''; + do { + $result .= chr(bcmod($num, '256')); + $num = bcdiv($num, '256'); + } while (bccomp($num, '0')); + return $result; + } + + /** + * Calculates pow($num, $pow) (mod $mod) + * + * @param string $num + * @param string $pow + * @param string $mod + * @return string + * @access public + */ + function powmod($num, $pow, $mod) + { + if (function_exists('bcpowmod')) { + // bcpowmod is only available under PHP5 + return bcpowmod($num, $pow, $mod); + } + + // emulate bcpowmod + $result = '1'; + do { + if (!bccomp(bcmod($pow, '2'), '1')) { + $result = bcmod(bcmul($result, $num), $mod); + } + $num = bcmod(bcpow($num, '2'), $mod); + $pow = bcdiv($pow, '2'); + } while (bccomp($pow, '0')); + return $result; + } + + /** + * Calculates $num1 * $num2 + * + * @param string $num1 + * @param string $num2 + * @return string + * @access public + */ + function mul($num1, $num2) + { + return bcmul($num1, $num2); + } + + /** + * Calculates $num1 % $num2 + * + * @param string $num1 + * @param string $num2 + * @return string + * @access public + */ + function mod($num1, $num2) + { + return bcmod($num1, $num2); + } + + /** + * Compares abs($num1) to abs($num2). + * Returns: + * -1, if abs($num1) < abs($num2) + * 0, if abs($num1) == abs($num2) + * 1, if abs($num1) > abs($num2) + * + * @param string $num1 + * @param string $num2 + * @return int + * @access public + */ + function cmpAbs($num1, $num2) + { + return bccomp($num1, $num2); + } + + /** + * Tests $num on primality. Returns true, if $num is strong pseudoprime. + * Else returns false. + * + * @param string $num + * @return bool + * @access private + */ + function isPrime($num) + { + static $primes = null; + static $primes_cnt = 0; + if (is_null($primes)) { + // generate all primes up to 10000 + $primes = array(); + for ($i = 0; $i < 10000; $i++) { + $primes[] = $i; + } + $primes[0] = $primes[1] = 0; + for ($i = 2; $i < 100; $i++) { + while (!$primes[$i]) { + $i++; + } + $j = $i; + for ($j += $i; $j < 10000; $j += $i) { + $primes[$j] = 0; + } + } + $j = 0; + for ($i = 0; $i < 10000; $i++) { + if ($primes[$i]) { + $primes[$j++] = $primes[$i]; + } + } + $primes_cnt = $j; + } + + // try to divide number by small primes + for ($i = 0; $i < $primes_cnt; $i++) { + if (bccomp($num, $primes[$i]) <= 0) { + // number is prime + return true; + } + if (!bccomp(bcmod($num, $primes[$i]), '0')) { + // number divides by $primes[$i] + return false; + } + } + + /* + try Miller-Rabin's probable-primality test for first + 7 primes as bases + */ + for ($i = 0; $i < 7; $i++) { + if (!$this->_millerTest($num, $primes[$i])) { + // $num is composite + return false; + } + } + // $num is strong pseudoprime + return true; + } + + /** + * Generates prime number with length $bits_cnt + * using $random_generator as random generator function. + * + * @param int $bits_cnt + * @param string $rnd_generator + * @access public + */ + function getPrime($bits_cnt, $random_generator) + { + $bytes_n = intval($bits_cnt / 8); + $bits_n = $bits_cnt % 8; + do { + $str = ''; + for ($i = 0; $i < $bytes_n; $i++) { + $str .= chr(call_user_func($random_generator) & 0xff); + } + $n = call_user_func($random_generator) & 0xff; + $n |= 0x80; + $n >>= 8 - $bits_n; + $str .= chr($n); + $num = $this->bin2int($str); + + // search for the next closest prime number after [$num] + if (!bccomp(bcmod($num, '2'), '0')) { + $num = bcadd($num, '1'); + } + while (!$this->isPrime($num)) { + $num = bcadd($num, '2'); + } + } while ($this->bitLen($num) != $bits_cnt); + return $num; + } + + /** + * Calculates $num - 1 + * + * @param string $num + * @return string + * @access public + */ + function dec($num) + { + return bcsub($num, '1'); + } + + /** + * Returns true, if $num is equal to one. Else returns false + * + * @param string $num + * @return bool + * @access public + */ + function isOne($num) + { + return !bccomp($num, '1'); + } + + /** + * Finds greatest common divider (GCD) of $num1 and $num2 + * + * @param string $num1 + * @param string $num2 + * @return string + * @access public + */ + function GCD($num1, $num2) + { + do { + $tmp = bcmod($num1, $num2); + $num1 = $num2; + $num2 = $tmp; + } while (bccomp($num2, '0')); + return $num1; + } + + /** + * Finds inverse number $inv for $num by modulus $mod, such as: + * $inv * $num = 1 (mod $mod) + * + * @param string $num + * @param string $mod + * @return string + * @access public + */ + function invmod($num, $mod) + { + $x = '1'; + $y = '0'; + $num1 = $mod; + do { + $tmp = bcmod($num, $num1); + $q = bcdiv($num, $num1); + $num = $num1; + $num1 = $tmp; + + $tmp = bcsub($x, bcmul($y, $q)); + $x = $y; + $y = $tmp; + } while (bccomp($num1, '0')); + if (bccomp($x, '0') < 0) { + $x = bcadd($x, $mod); + } + return $x; + } + + /** + * Returns bit length of number $num + * + * @param string $num + * @return int + * @access public + */ + function bitLen($num) + { + $tmp = $this->int2bin($num); + $bit_len = strlen($tmp) * 8; + $tmp = ord($tmp{strlen($tmp) - 1}); + if (!$tmp) { + $bit_len -= 8; + } + else { + while (!($tmp & 0x80)) { + $bit_len--; + $tmp <<= 1; + } + } + return $bit_len; + } + + /** + * Calculates bitwise or of $num1 and $num2, + * starting from bit $start_pos for number $num1 + * + * @param string $num1 + * @param string $num2 + * @param int $start_pos + * @return string + * @access public + */ + function bitOr($num1, $num2, $start_pos) + { + $start_byte = intval($start_pos / 8); + $start_bit = $start_pos % 8; + $tmp1 = $this->int2bin($num1); + + $num2 = bcmul($num2, 1 << $start_bit); + $tmp2 = $this->int2bin($num2); + if ($start_byte < strlen($tmp1)) { + $tmp2 |= substr($tmp1, $start_byte); + $tmp1 = substr($tmp1, 0, $start_byte) . $tmp2; + } + else { + $tmp1 = str_pad($tmp1, $start_byte, "\0") . $tmp2; + } + return $this->bin2int($tmp1); + } + + /** + * Returns part of number $num, starting at bit + * position $start with length $length + * + * @param string $num + * @param int start + * @param int length + * @return string + * @access public + */ + function subint($num, $start, $length) + { + $start_byte = intval($start / 8); + $start_bit = $start % 8; + $byte_length = intval($length / 8); + $bit_length = $length % 8; + if ($bit_length) { + $byte_length++; + } + $num = bcdiv($num, 1 << $start_bit); + $tmp = substr($this->int2bin($num), $start_byte, $byte_length); + $tmp = str_pad($tmp, $byte_length, "\0"); + $tmp = substr_replace($tmp, $tmp{$byte_length - 1} & chr(0xff >> (8 - $bit_length)), $byte_length - 1, 1); + return $this->bin2int($tmp); + } + + /** + * Returns name of current wrapper + * + * @return string name of current wrapper + * @access public + */ + function getWrapperName() + { + return 'BCMath'; + } +} + +?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/Crypt/RSA/Math/BigInt.php b/plugins/OStatus/extlib/Crypt/RSA/Math/BigInt.php new file mode 100644 index 000000000..b7ac24cb6 --- /dev/null +++ b/plugins/OStatus/extlib/Crypt/RSA/Math/BigInt.php @@ -0,0 +1,313 @@ +<?php +/** + * Crypt_RSA allows to do following operations: + * - key pair generation + * - encryption and decryption + * - signing and sign validation + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005, 2006 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version 1.2.0b + * @link http://pear.php.net/package/Crypt_RSA + */ + +/** + * Crypt_RSA_Math_BigInt class. + * + * Provides set of math functions, which are used by Crypt_RSA package + * This class is a wrapper for big_int PECL extension, + * which could be loaded from http://pecl.php.net/packages/big_int + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005, 2006 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @link http://pear.php.net/package/Crypt_RSA + * @version @package_version@ + * @access public + */ +class Crypt_RSA_Math_BigInt +{ + /** + * error description + * + * @var string + * @access public + */ + var $errstr = ''; + + /** + * Crypt_RSA_Math_BigInt constructor. + * Checks an existance of big_int PECL math package. + * This package is available at http://pecl.php.net/packages/big_int + * On failure saves error description in $this->errstr + * + * @access public + */ + function Crypt_RSA_Math_BigInt() + { + if (!extension_loaded('big_int')) { + if (!@dl('big_int.' . PHP_SHLIB_SUFFIX) && !@dl('php_big_int.' . PHP_SHLIB_SUFFIX)) { + // cannot load big_int extension + $this->errstr = 'Crypt_RSA package requires big_int PECL package. ' . + 'It is available at http://pecl.php.net/packages/big_int'; + return; + } + } + + // check version of big_int extension ( Crypt_RSA requires version 1.0.2 and higher ) + if (!in_array('bi_info', get_extension_funcs('big_int'))) { + // there is no bi_info() function in versions, older than 1.0.2 + $this->errstr = 'Crypt_RSA package requires big_int package version 1.0.2 and higher'; + } + } + + /** + * Transforms binary representation of large integer into its native form. + * + * Example of transformation: + * $str = "\x12\x34\x56\x78\x90"; + * $num = 0x9078563412; + * + * @param string $str + * @return big_int resource + * @access public + */ + function bin2int($str) + { + return bi_unserialize($str); + } + + /** + * Transforms large integer into binary representation. + * + * Example of transformation: + * $num = 0x9078563412; + * $str = "\x12\x34\x56\x78\x90"; + * + * @param big_int resource $num + * @return string + * @access public + */ + function int2bin($num) + { + return bi_serialize($num); + } + + /** + * Calculates pow($num, $pow) (mod $mod) + * + * @param big_int resource $num + * @param big_int resource $pow + * @param big_int resource $mod + * @return big_int resource + * @access public + */ + function powmod($num, $pow, $mod) + { + return bi_powmod($num, $pow, $mod); + } + + /** + * Calculates $num1 * $num2 + * + * @param big_int resource $num1 + * @param big_int resource $num2 + * @return big_int resource + * @access public + */ + function mul($num1, $num2) + { + return bi_mul($num1, $num2); + } + + /** + * Calculates $num1 % $num2 + * + * @param string $num1 + * @param string $num2 + * @return string + * @access public + */ + function mod($num1, $num2) + { + return bi_mod($num1, $num2); + } + + /** + * Compares abs($num1) to abs($num2). + * Returns: + * -1, if abs($num1) < abs($num2) + * 0, if abs($num1) == abs($num2) + * 1, if abs($num1) > abs($num2) + * + * @param big_int resource $num1 + * @param big_int resource $num2 + * @return int + * @access public + */ + function cmpAbs($num1, $num2) + { + return bi_cmp_abs($num1, $num2); + } + + /** + * Tests $num on primality. Returns true, if $num is strong pseudoprime. + * Else returns false. + * + * @param string $num + * @return bool + * @access private + */ + function isPrime($num) + { + return bi_is_prime($num) ? true : false; + } + + /** + * Generates prime number with length $bits_cnt + * using $random_generator as random generator function. + * + * @param int $bits_cnt + * @param string $rnd_generator + * @access public + */ + function getPrime($bits_cnt, $random_generator) + { + $bytes_n = intval($bits_cnt / 8); + $bits_n = $bits_cnt % 8; + do { + $str = ''; + for ($i = 0; $i < $bytes_n; $i++) { + $str .= chr(call_user_func($random_generator) & 0xff); + } + $n = call_user_func($random_generator) & 0xff; + $n |= 0x80; + $n >>= 8 - $bits_n; + $str .= chr($n); + $num = $this->bin2int($str); + + // search for the next closest prime number after [$num] + $num = bi_next_prime($num); + } while ($this->bitLen($num) != $bits_cnt); + return $num; + } + + /** + * Calculates $num - 1 + * + * @param big_int resource $num + * @return big_int resource + * @access public + */ + function dec($num) + { + return bi_dec($num); + } + + /** + * Returns true, if $num is equal to 1. Else returns false + * + * @param big_int resource $num + * @return bool + * @access public + */ + function isOne($num) + { + return bi_is_one($num); + } + + /** + * Finds greatest common divider (GCD) of $num1 and $num2 + * + * @param big_int resource $num1 + * @param big_int resource $num2 + * @return big_int resource + * @access public + */ + function GCD($num1, $num2) + { + return bi_gcd($num1, $num2); + } + + /** + * Finds inverse number $inv for $num by modulus $mod, such as: + * $inv * $num = 1 (mod $mod) + * + * @param big_int resource $num + * @param big_int resource $mod + * @return big_int resource + * @access public + */ + function invmod($num, $mod) + { + return bi_invmod($num, $mod); + } + + /** + * Returns bit length of number $num + * + * @param big_int resource $num + * @return int + * @access public + */ + function bitLen($num) + { + return bi_bit_len($num); + } + + /** + * Calculates bitwise or of $num1 and $num2, + * starting from bit $start_pos for number $num1 + * + * @param big_int resource $num1 + * @param big_int resource $num2 + * @param int $start_pos + * @return big_int resource + * @access public + */ + function bitOr($num1, $num2, $start_pos) + { + return bi_or($num1, $num2, $start_pos); + } + + /** + * Returns part of number $num, starting at bit + * position $start with length $length + * + * @param big_int resource $num + * @param int start + * @param int length + * @return big_int resource + * @access public + */ + function subint($num, $start, $length) + { + return bi_subint($num, $start, $length); + } + + /** + * Returns name of current wrapper + * + * @return string name of current wrapper + * @access public + */ + function getWrapperName() + { + return 'BigInt'; + } +} + +?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/Crypt/RSA/Math/GMP.php b/plugins/OStatus/extlib/Crypt/RSA/Math/GMP.php new file mode 100644 index 000000000..54e4c34fc --- /dev/null +++ b/plugins/OStatus/extlib/Crypt/RSA/Math/GMP.php @@ -0,0 +1,361 @@ +<?php +/** + * Crypt_RSA allows to do following operations: + * - key pair generation + * - encryption and decryption + * - signing and sign validation + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005, 2006 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version 1.2.0b + * @link http://pear.php.net/package/Crypt_RSA + */ + +/** + * Crypt_RSA_Math_GMP class. + * + * Provides set of math functions, which are used by Crypt_RSA package + * This class is a wrapper for PHP GMP extension. + * See http://php.net/gmp for details. + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright 2005, 2006 Alexander Valyalkin + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @link http://pear.php.net/package/Crypt_RSA + * @version @package_version@ + * @access public + */ +class Crypt_RSA_Math_GMP +{ + /** + * error description + * + * @var string + * @access public + */ + var $errstr = ''; + + /** + * Crypt_RSA_Math_GMP constructor. + * Checks an existance of PHP GMP package. + * See http://php.net/gmp for details. + * + * On failure saves error description in $this->errstr + * + * @access public + */ + function Crypt_RSA_Math_GMP() + { + if (!extension_loaded('gmp')) { + if (!@dl('gmp.' . PHP_SHLIB_SUFFIX) && !@dl('php_gmp.' . PHP_SHLIB_SUFFIX)) { + // cannot load GMP extension + $this->errstr = 'Crypt_RSA package requires PHP GMP package. ' . + 'See http://php.net/gmp for details'; + return; + } + } + } + + /** + * Transforms binary representation of large integer into its native form. + * + * Example of transformation: + * $str = "\x12\x34\x56\x78\x90"; + * $num = 0x9078563412; + * + * @param string $str + * @return gmp resource + * @access public + */ + function bin2int($str) + { + $result = 0; + $n = strlen($str); + do { + // dirty hack: GMP returns FALSE, when second argument equals to int(0). + // so, it must be converted to string '0' + $result = gmp_add(gmp_mul($result, 256), strval(ord($str{--$n}))); + } while ($n > 0); + return $result; + } + + /** + * Transforms large integer into binary representation. + * + * Example of transformation: + * $num = 0x9078563412; + * $str = "\x12\x34\x56\x78\x90"; + * + * @param gmp resource $num + * @return string + * @access public + */ + function int2bin($num) + { + $result = ''; + do { + $result .= chr(gmp_intval(gmp_mod($num, 256))); + $num = gmp_div($num, 256); + } while (gmp_cmp($num, 0)); + return $result; + } + + /** + * Calculates pow($num, $pow) (mod $mod) + * + * @param gmp resource $num + * @param gmp resource $pow + * @param gmp resource $mod + * @return gmp resource + * @access public + */ + function powmod($num, $pow, $mod) + { + return gmp_powm($num, $pow, $mod); + } + + /** + * Calculates $num1 * $num2 + * + * @param gmp resource $num1 + * @param gmp resource $num2 + * @return gmp resource + * @access public + */ + function mul($num1, $num2) + { + return gmp_mul($num1, $num2); + } + + /** + * Calculates $num1 % $num2 + * + * @param string $num1 + * @param string $num2 + * @return string + * @access public + */ + function mod($num1, $num2) + { + return gmp_mod($num1, $num2); + } + + /** + * Compares abs($num1) to abs($num2). + * Returns: + * -1, if abs($num1) < abs($num2) + * 0, if abs($num1) == abs($num2) + * 1, if abs($num1) > abs($num2) + * + * @param gmp resource $num1 + * @param gmp resource $num2 + * @return int + * @access public + */ + function cmpAbs($num1, $num2) + { + return gmp_cmp($num1, $num2); + } + + /** + * Tests $num on primality. Returns true, if $num is strong pseudoprime. + * Else returns false. + * + * @param string $num + * @return bool + * @access private + */ + function isPrime($num) + { + return gmp_prob_prime($num) ? true : false; + } + + /** + * Generates prime number with length $bits_cnt + * using $random_generator as random generator function. + * + * @param int $bits_cnt + * @param string $rnd_generator + * @access public + */ + function getPrime($bits_cnt, $random_generator) + { + $bytes_n = intval($bits_cnt / 8); + $bits_n = $bits_cnt % 8; + do { + $str = ''; + for ($i = 0; $i < $bytes_n; $i++) { + $str .= chr(call_user_func($random_generator) & 0xff); + } + $n = call_user_func($random_generator) & 0xff; + $n |= 0x80; + $n >>= 8 - $bits_n; + $str .= chr($n); + $num = $this->bin2int($str); + + // search for the next closest prime number after [$num] + if (!gmp_cmp(gmp_mod($num, '2'), '0')) { + $num = gmp_add($num, '1'); + } + while (!gmp_prob_prime($num)) { + $num = gmp_add($num, '2'); + } + } while ($this->bitLen($num) != $bits_cnt); + return $num; + } + + /** + * Calculates $num - 1 + * + * @param gmp resource $num + * @return gmp resource + * @access public + */ + function dec($num) + { + return gmp_sub($num, 1); + } + + /** + * Returns true, if $num is equal to one. Else returns false + * + * @param gmp resource $num + * @return bool + * @access public + */ + function isOne($num) + { + return !gmp_cmp($num, 1); + } + + /** + * Finds greatest common divider (GCD) of $num1 and $num2 + * + * @param gmp resource $num1 + * @param gmp resource $num2 + * @return gmp resource + * @access public + */ + function GCD($num1, $num2) + { + return gmp_gcd($num1, $num2); + } + + /** + * Finds inverse number $inv for $num by modulus $mod, such as: + * $inv * $num = 1 (mod $mod) + * + * @param gmp resource $num + * @param gmp resource $mod + * @return gmp resource + * @access public + */ + function invmod($num, $mod) + { + return gmp_invert($num, $mod); + } + + /** + * Returns bit length of number $num + * + * @param gmp resource $num + * @return int + * @access public + */ + function bitLen($num) + { + $tmp = $this->int2bin($num); + $bit_len = strlen($tmp) * 8; + $tmp = ord($tmp{strlen($tmp) - 1}); + if (!$tmp) { + $bit_len -= 8; + } + else { + while (!($tmp & 0x80)) { + $bit_len--; + $tmp <<= 1; + } + } + return $bit_len; + } + + /** + * Calculates bitwise or of $num1 and $num2, + * starting from bit $start_pos for number $num1 + * + * @param gmp resource $num1 + * @param gmp resource $num2 + * @param int $start_pos + * @return gmp resource + * @access public + */ + function bitOr($num1, $num2, $start_pos) + { + $start_byte = intval($start_pos / 8); + $start_bit = $start_pos % 8; + $tmp1 = $this->int2bin($num1); + + $num2 = gmp_mul($num2, 1 << $start_bit); + $tmp2 = $this->int2bin($num2); + if ($start_byte < strlen($tmp1)) { + $tmp2 |= substr($tmp1, $start_byte); + $tmp1 = substr($tmp1, 0, $start_byte) . $tmp2; + } + else { + $tmp1 = str_pad($tmp1, $start_byte, "\0") . $tmp2; + } + return $this->bin2int($tmp1); + } + + /** + * Returns part of number $num, starting at bit + * position $start with length $length + * + * @param gmp resource $num + * @param int start + * @param int length + * @return gmp resource + * @access public + */ + function subint($num, $start, $length) + { + $start_byte = intval($start / 8); + $start_bit = $start % 8; + $byte_length = intval($length / 8); + $bit_length = $length % 8; + if ($bit_length) { + $byte_length++; + } + $num = gmp_div($num, 1 << $start_bit); + $tmp = substr($this->int2bin($num), $start_byte, $byte_length); + $tmp = str_pad($tmp, $byte_length, "\0"); + $tmp = substr_replace($tmp, $tmp{$byte_length - 1} & chr(0xff >> (8 - $bit_length)), $byte_length - 1, 1); + return $this->bin2int($tmp); + } + + /** + * Returns name of current wrapper + * + * @return string name of current wrapper + * @access public + */ + function getWrapperName() + { + return 'GMP'; + } +} + +?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/Crypt/RSA/MathLoader.php b/plugins/OStatus/extlib/Crypt/RSA/MathLoader.php new file mode 100644 index 000000000..de6c94642 --- /dev/null +++ b/plugins/OStatus/extlib/Crypt/RSA/MathLoader.php @@ -0,0 +1,135 @@ +<?php +/** + * Crypt_RSA allows to do following operations: + * - key pair generation + * - encryption and decryption + * - signing and sign validation + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to version 3.0 of the PHP license + * that is available through the world-wide-web at the following URI: + * http://www.php.net/license/3_0.txt. If you did not receive a copy of + * the PHP License and are unable to obtain it through the web, please + * send a note to license@php.net so we can mail you a copy immediately. + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright Alexander Valyalkin 2005 + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version CVS: $Id: MathLoader.php,v 1.5 2009/01/05 08:30:29 clockwerx Exp $ + * @link http://pear.php.net/package/Crypt_RSA + */ + +/** + * RSA error handling facilities + */ +require_once 'Crypt/RSA/ErrorHandler.php'; + +/** + * Crypt_RSA_MathLoader class. + * + * Provides static function: + * - loadWrapper($wrapper_name) - loads RSA math wrapper with name $wrapper_name + * or most suitable wrapper if $wrapper_name == 'default' + * + * Example usage: + * // load BigInt wrapper + * $big_int_wrapper = Crypt_RSA_MathLoader::loadWrapper('BigInt'); + * + * // load BCMath wrapper + * $bcmath_wrapper = Crypt_RSA_MathLoader::loadWrapper('BCMath'); + * + * // load the most suitable wrapper + * $bcmath_wrapper = Crypt_RSA_MathLoader::loadWrapper(); + * + * @category Encryption + * @package Crypt_RSA + * @author Alexander Valyalkin <valyala@gmail.com> + * @copyright Alexander Valyalkin 2005 + * @license http://www.php.net/license/3_0.txt PHP License 3.0 + * @version Release: @package_version@ + * @link http://pear.php.net/package/Crypt_RSA + * @access public + */ +class Crypt_RSA_MathLoader +{ + /** + * Loads RSA math wrapper with name $wrapper_name. + * Implemented wrappers can be found at Crypt/RSA/Math folder. + * Read docs/Crypt_RSA/docs/math_wrappers.txt for details + * + * This is a static function: + * // load BigInt wrapper + * $big_int_wrapper = &Crypt_RSA_MathLoader::loadWrapper('BigInt'); + * + * // load BCMath wrapper + * $bcmath_wrapper = &Crypt_RSA_MathLoader::loadWrapper('BCMath'); + * + * @param string $wrapper_name Name of wrapper + * + * @return object + * Reference to object of wrapper with name $wrapper_name on success + * or PEAR_Error object on error + * + * @access public + */ + function loadWrapper($wrapper_name = 'default') + { + static $math_objects = array(); + // ordered by performance. GMP is the fastest math library, BCMath - the slowest. + static $math_wrappers = array('GMP', 'BigInt', 'BCMath',); + + if (isset($math_objects[$wrapper_name])) { + /* + wrapper with name $wrapper_name is already loaded and created. + Return reference to existing copy of wrapper + */ + return $math_objects[$wrapper_name]; + } + + $err_handler = new Crypt_RSA_ErrorHandler(); + + if ($wrapper_name === 'default') { + // try to load the most suitable wrapper + $n = sizeof($math_wrappers); + for ($i = 0; $i < $n; $i++) { + $obj = Crypt_RSA_MathLoader::loadWrapper($math_wrappers[$i]); + if (!$err_handler->isError($obj)) { + // wrapper for $math_wrappers[$i] successfully loaded + // register it as default wrapper and return reference to it + return $math_objects['default'] = $obj; + } + } + // can't load any wrapper + $err_handler->pushError("can't load any wrapper for existing math libraries", CRYPT_RSA_ERROR_NO_WRAPPERS); + return $err_handler->getLastError(); + } + + $class_name = 'Crypt_RSA_Math_' . $wrapper_name; + $class_filename = dirname(__FILE__) . '/Math/' . $wrapper_name . '.php'; + + if (!is_file($class_filename)) { + $err_handler->pushError("can't find file [{$class_filename}] for RSA math wrapper [{$wrapper_name}]", CRYPT_RSA_ERROR_NO_FILE); + return $err_handler->getLastError(); + } + + include_once $class_filename; + if (!class_exists($class_name)) { + $err_handler->pushError("can't find class [{$class_name}] in file [{$class_filename}]", CRYPT_RSA_ERROR_NO_CLASS); + return $err_handler->getLastError(); + } + + // create and return wrapper object on success or PEAR_Error object on error + $obj = new $class_name; + if ($obj->errstr) { + // cannot load required extension for math wrapper + $err_handler->pushError($obj->errstr, CRYPT_RSA_ERROR_NO_EXT); + return $err_handler->getLastError(); + } + return $math_objects[$wrapper_name] = $obj; + } +} + +?> diff --git a/plugins/OStatus/extlib/README b/plugins/OStatus/extlib/README deleted file mode 100644 index 799b40c47..000000000 --- a/plugins/OStatus/extlib/README +++ /dev/null @@ -1,9 +0,0 @@ -XML_Feed_Parser 1.0.3 is not currently actively maintained, and has -a nasty bug which breaks getting the feed target link from WordPress -feeds and possibly others that are RSS2-formatted but include an -<atom:link> self-link element as well. - -Patch from this bug report is included: -http://pear.php.net/bugs/bug.php?id=16416 - -If upgrading, be sure that fix is included with the future upgrade! diff --git a/plugins/OStatus/extlib/XML/Feed/Parser.php b/plugins/OStatus/extlib/XML/Feed/Parser.php deleted file mode 100755 index ffe8220a5..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser.php +++ /dev/null @@ -1,351 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * Key gateway class for XML_Feed_Parser package - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL - * @version CVS: $Id: Parser.php,v 1.24 2006/08/15 13:04:00 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/** - * XML_Feed_Parser_Type is an abstract class required by all of our - * feed types. It makes sense to load it here to keep the other files - * clean. - */ -require_once 'XML/Feed/Parser/Type.php'; - -/** - * We will throw exceptions when errors occur. - */ -require_once 'XML/Feed/Parser/Exception.php'; - -/** - * This is the core of the XML_Feed_Parser package. It identifies feed types - * and abstracts access to them. It is an iterator, allowing for easy access - * to the entire feed. - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - */ -class XML_Feed_Parser implements Iterator -{ - /** - * This is where we hold the feed object - * @var Object - */ - private $feed; - - /** - * To allow for extensions, we make a public reference to the feed model - * @var DOMDocument - */ - public $model; - - /** - * A map between entry ID and offset - * @var array - */ - protected $idMappings = array(); - - /** - * A storage space for Namespace URIs. - * @var array - */ - private $feedNamespaces = array( - 'rss2' => array( - 'http://backend.userland.com/rss', - 'http://backend.userland.com/rss2', - 'http://blogs.law.harvard.edu/tech/rss')); - /** - * Detects feed types and instantiate appropriate objects. - * - * Our constructor takes care of detecting feed types and instantiating - * appropriate classes. For now we're going to treat Atom 0.3 as Atom 1.0 - * but raise a warning. I do not intend to introduce full support for - * Atom 0.3 as it has been deprecated, but others are welcome to. - * - * @param string $feed XML serialization of the feed - * @param bool $strict Whether or not to validate the feed - * @param bool $suppressWarnings Trigger errors for deprecated feed types? - * @param bool $tidy Whether or not to try and use the tidy library on input - */ - function __construct($feed, $strict = false, $suppressWarnings = false, $tidy = false) - { - $this->model = new DOMDocument; - if (! $this->model->loadXML($feed)) { - if (extension_loaded('tidy') && $tidy) { - $tidy = new tidy; - $tidy->parseString($feed, - array('input-xml' => true, 'output-xml' => true)); - $tidy->cleanRepair(); - if (! $this->model->loadXML((string) $tidy)) { - throw new XML_Feed_Parser_Exception('Invalid input: this is not ' . - 'valid XML'); - } - } else { - throw new XML_Feed_Parser_Exception('Invalid input: this is not valid XML'); - } - - } - - /* detect feed type */ - $doc_element = $this->model->documentElement; - $error = false; - - switch (true) { - case ($doc_element->namespaceURI == 'http://www.w3.org/2005/Atom'): - require_once 'XML/Feed/Parser/Atom.php'; - require_once 'XML/Feed/Parser/AtomElement.php'; - $class = 'XML_Feed_Parser_Atom'; - break; - case ($doc_element->namespaceURI == 'http://purl.org/atom/ns#'): - require_once 'XML/Feed/Parser/Atom.php'; - require_once 'XML/Feed/Parser/AtomElement.php'; - $class = 'XML_Feed_Parser_Atom'; - $error = 'Atom 0.3 deprecated, using 1.0 parser which won\'t provide ' . - 'all options'; - break; - case ($doc_element->namespaceURI == 'http://purl.org/rss/1.0/' || - ($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1 - && $doc_element->childNodes->item(1)->namespaceURI == - 'http://purl.org/rss/1.0/')): - require_once 'XML/Feed/Parser/RSS1.php'; - require_once 'XML/Feed/Parser/RSS1Element.php'; - $class = 'XML_Feed_Parser_RSS1'; - break; - case ($doc_element->namespaceURI == 'http://purl.org/rss/1.1/' || - ($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1 - && $doc_element->childNodes->item(1)->namespaceURI == - 'http://purl.org/rss/1.1/')): - require_once 'XML/Feed/Parser/RSS11.php'; - require_once 'XML/Feed/Parser/RSS11Element.php'; - $class = 'XML_Feed_Parser_RSS11'; - break; - case (($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1 - && $doc_element->childNodes->item(1)->namespaceURI == - 'http://my.netscape.com/rdf/simple/0.9/') || - $doc_element->namespaceURI == 'http://my.netscape.com/rdf/simple/0.9/'): - require_once 'XML/Feed/Parser/RSS09.php'; - require_once 'XML/Feed/Parser/RSS09Element.php'; - $class = 'XML_Feed_Parser_RSS09'; - break; - case ($doc_element->tagName == 'rss' and - $doc_element->hasAttribute('version') && - $doc_element->getAttribute('version') == 0.91): - $error = 'RSS 0.91 has been superceded by RSS2.0. Using RSS2.0 parser.'; - require_once 'XML/Feed/Parser/RSS2.php'; - require_once 'XML/Feed/Parser/RSS2Element.php'; - $class = 'XML_Feed_Parser_RSS2'; - break; - case ($doc_element->tagName == 'rss' and - $doc_element->hasAttribute('version') && - $doc_element->getAttribute('version') == 0.92): - $error = 'RSS 0.92 has been superceded by RSS2.0. Using RSS2.0 parser.'; - require_once 'XML/Feed/Parser/RSS2.php'; - require_once 'XML/Feed/Parser/RSS2Element.php'; - $class = 'XML_Feed_Parser_RSS2'; - break; - case (in_array($doc_element->namespaceURI, $this->feedNamespaces['rss2']) - || $doc_element->tagName == 'rss'): - if (! $doc_element->hasAttribute('version') || - $doc_element->getAttribute('version') != 2) { - $error = 'RSS version not specified. Parsing as RSS2.0'; - } - require_once 'XML/Feed/Parser/RSS2.php'; - require_once 'XML/Feed/Parser/RSS2Element.php'; - $class = 'XML_Feed_Parser_RSS2'; - break; - default: - throw new XML_Feed_Parser_Exception('Feed type unknown'); - break; - } - - if (! $suppressWarnings && ! empty($error)) { - trigger_error($error, E_USER_WARNING); - } - - /* Instantiate feed object */ - $this->feed = new $class($this->model, $strict); - } - - /** - * Proxy to allow feed element names to be used as method names - * - * For top-level feed elements we will provide access using methods or - * attributes. This function simply passes on a request to the appropriate - * feed type object. - * - * @param string $call - the method being called - * @param array $attributes - */ - function __call($call, $attributes) - { - $attributes = array_pad($attributes, 5, false); - list($a, $b, $c, $d, $e) = $attributes; - return $this->feed->$call($a, $b, $c, $d, $e); - } - - /** - * Proxy to allow feed element names to be used as attribute names - * - * To allow variable-like access to feed-level data we use this - * method. It simply passes along to __call() which in turn passes - * along to the relevant object. - * - * @param string $val - the name of the variable required - */ - function __get($val) - { - return $this->feed->$val; - } - - /** - * Provides iteration functionality. - * - * Of course we must be able to iterate... This function simply increases - * our internal counter. - */ - function next() - { - if (isset($this->current_item) && - $this->current_item <= $this->feed->numberEntries - 1) { - ++$this->current_item; - } else if (! isset($this->current_item)) { - $this->current_item = 0; - } else { - return false; - } - } - - /** - * Return XML_Feed_Type object for current element - * - * @return XML_Feed_Parser_Type Object - */ - function current() - { - return $this->getEntryByOffset($this->current_item); - } - - /** - * For iteration -- returns the key for the current stage in the array. - * - * @return int - */ - function key() - { - return $this->current_item; - } - - /** - * For iteration -- tells whether we have reached the - * end. - * - * @return bool - */ - function valid() - { - return $this->current_item < $this->feed->numberEntries; - } - - /** - * For iteration -- resets the internal counter to the beginning. - */ - function rewind() - { - $this->current_item = 0; - } - - /** - * Provides access to entries by ID if one is specified in the source feed. - * - * As well as allowing the items to be iterated over we want to allow - * users to be able to access a specific entry. This is one of two ways of - * doing that, the other being by offset. This method can be quite slow - * if dealing with a large feed that hasn't yet been processed as it - * instantiates objects for every entry until it finds the one needed. - * - * @param string $id Valid ID for the given feed format - * @return XML_Feed_Parser_Type|false - */ - function getEntryById($id) - { - if (isset($this->idMappings[$id])) { - return $this->getEntryByOffset($this->idMappings[$id]); - } - - /* - * Since we have not yet encountered that ID, let's go through all the - * remaining entries in order till we find it. - * This is a fairly slow implementation, but it should work. - */ - return $this->feed->getEntryById($id); - } - - /** - * Retrieve entry by numeric offset, starting from zero. - * - * As well as allowing the items to be iterated over we want to allow - * users to be able to access a specific entry. This is one of two ways of - * doing that, the other being by ID. - * - * @param int $offset The position of the entry within the feed, starting from 0 - * @return XML_Feed_Parser_Type|false - */ - function getEntryByOffset($offset) - { - if ($offset < $this->feed->numberEntries) { - if (isset($this->feed->entries[$offset])) { - return $this->feed->entries[$offset]; - } else { - try { - $this->feed->getEntryByOffset($offset); - } catch (Exception $e) { - return false; - } - $id = $this->feed->entries[$offset]->getID(); - $this->idMappings[$id] = $offset; - return $this->feed->entries[$offset]; - } - } else { - return false; - } - } - - /** - * Retrieve version details from feed type class. - * - * @return void - * @author James Stewart - */ - function version() - { - return $this->feed->version; - } - - /** - * Returns a string representation of the feed. - * - * @return String - **/ - function __toString() - { - return $this->feed->__toString(); - } -} -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/Atom.php b/plugins/OStatus/extlib/XML/Feed/Parser/Atom.php deleted file mode 100644 index c7e218a1e..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/Atom.php +++ /dev/null @@ -1,365 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * Atom feed class for XML_Feed_Parser - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1 - * @version CVS: $Id: Atom.php,v 1.29 2008/03/30 22:00:36 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ -*/ - -/** - * This is the class that determines how we manage Atom 1.0 feeds - * - * How we deal with constructs: - * date - return as unix datetime for use with the 'date' function unless specified otherwise - * text - return as is. optional parameter will give access to attributes - * person - defaults to name, but parameter based access - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - */ -class XML_Feed_Parser_Atom extends XML_Feed_Parser_Type -{ - /** - * The URI of the RelaxNG schema used to (optionally) validate the feed - * @var string - */ - private $relax = 'atom.rnc'; - - /** - * We're likely to use XPath, so let's keep it global - * @var DOMXPath - */ - public $xpath; - - /** - * When performing XPath queries we will use this prefix - * @var string - */ - private $xpathPrefix = '//'; - - /** - * The feed type we are parsing - * @var string - */ - public $version = 'Atom 1.0'; - - /** - * The class used to represent individual items - * @var string - */ - protected $itemClass = 'XML_Feed_Parser_AtomElement'; - - /** - * The element containing entries - * @var string - */ - protected $itemElement = 'entry'; - - /** - * Here we map those elements we're not going to handle individually - * to the constructs they are. The optional second parameter in the array - * tells the parser whether to 'fall back' (not apt. at the feed level) or - * fail if the element is missing. If the parameter is not set, the function - * will simply return false and leave it to the client to decide what to do. - * @var array - */ - protected $map = array( - 'author' => array('Person'), - 'contributor' => array('Person'), - 'icon' => array('Text'), - 'logo' => array('Text'), - 'id' => array('Text', 'fail'), - 'rights' => array('Text'), - 'subtitle' => array('Text'), - 'title' => array('Text', 'fail'), - 'updated' => array('Date', 'fail'), - 'link' => array('Link'), - 'generator' => array('Text'), - 'category' => array('Category')); - - /** - * Here we provide a few mappings for those very special circumstances in - * which it makes sense to map back to the RSS2 spec. Key is RSS2 version - * value is an array consisting of the equivalent in atom and any attributes - * needed to make the mapping. - * @var array - */ - protected $compatMap = array( - 'guid' => array('id'), - 'links' => array('link'), - 'tags' => array('category'), - 'contributors' => array('contributor')); - - /** - * Our constructor does nothing more than its parent. - * - * @param DOMDocument $xml A DOM object representing the feed - * @param bool (optional) $string Whether or not to validate this feed - */ - function __construct(DOMDocument $model, $strict = false) - { - $this->model = $model; - - if ($strict) { - if (! $this->model->relaxNGValidateSource($this->relax)) { - throw new XML_Feed_Parser_Exception('Failed required validation'); - } - } - - $this->xpath = new DOMXPath($this->model); - $this->xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom'); - $this->numberEntries = $this->count('entry'); - } - - /** - * Implement retrieval of an entry based on its ID for atom feeds. - * - * This function uses XPath to get the entry based on its ID. If DOMXPath::evaluate - * is available, we also use that to store a reference to the entry in the array - * used by getEntryByOffset so that method does not have to seek out the entry - * if it's requested that way. - * - * @param string $id any valid Atom ID. - * @return XML_Feed_Parser_AtomElement - */ - function getEntryById($id) - { - if (isset($this->idMappings[$id])) { - return $this->entries[$this->idMappings[$id]]; - } - - $entries = $this->xpath->query("//atom:entry[atom:id='$id']"); - - if ($entries->length > 0) { - $xmlBase = $entries->item(0)->baseURI; - $entry = new $this->itemClass($entries->item(0), $this, $xmlBase); - - if (in_array('evaluate', get_class_methods($this->xpath))) { - $offset = $this->xpath->evaluate("count(preceding-sibling::atom:entry)", $entries->item(0)); - $this->entries[$offset] = $entry; - } - - $this->idMappings[$id] = $entry; - - return $entry; - } - - } - - /** - * Retrieves data from a person construct. - * - * Get a person construct. We default to the 'name' element but allow - * access to any of the elements. - * - * @param string $method The name of the person construct we want - * @param array $arguments An array which we hope gives a 'param' - * @return string|false - */ - protected function getPerson($method, $arguments) - { - $offset = empty($arguments[0]) ? 0 : $arguments[0]; - $parameter = empty($arguments[1]['param']) ? 'name' : $arguments[1]['param']; - $section = $this->model->getElementsByTagName($method); - - if ($parameter == 'url') { - $parameter = 'uri'; - } - - if ($section->length <= $offset) { - return false; - } - - $param = $section->item($offset)->getElementsByTagName($parameter); - if ($param->length == 0) { - return false; - } - return $param->item(0)->nodeValue; - } - - /** - * Retrieves an element's content where that content is a text construct. - * - * Get a text construct. When calling this method, the two arguments - * allowed are 'offset' and 'attribute', so $parser->subtitle() would - * return the content of the element, while $parser->subtitle(false, 'type') - * would return the value of the type attribute. - * - * @todo Clarify overlap with getContent() - * @param string $method The name of the text construct we want - * @param array $arguments An array which we hope gives a 'param' - * @return string - */ - protected function getText($method, $arguments) - { - $offset = empty($arguments[0]) ? 0: $arguments[0]; - $attribute = empty($arguments[1]) ? false : $arguments[1]; - $tags = $this->model->getElementsByTagName($method); - - if ($tags->length <= $offset) { - return false; - } - - $content = $tags->item($offset); - - if (! $content->hasAttribute('type')) { - $content->setAttribute('type', 'text'); - } - $type = $content->getAttribute('type'); - - if (! empty($attribute) and - ! ($method == 'generator' and $attribute == 'name')) { - if ($content->hasAttribute($attribute)) { - return $content->getAttribute($attribute); - } else if ($attribute == 'href' and $content->hasAttribute('uri')) { - return $content->getAttribute('uri'); - } - return false; - } - - return $this->parseTextConstruct($content); - } - - /** - * Extract content appropriately from atom text constructs - * - * Because of different rules applied to the content element and other text - * constructs, they are deployed as separate functions, but they share quite - * a bit of processing. This method performs the core common process, which is - * to apply the rules for different mime types in order to extract the content. - * - * @param DOMNode $content the text construct node to be parsed - * @return String - * @author James Stewart - **/ - protected function parseTextConstruct(DOMNode $content) - { - if ($content->hasAttribute('type')) { - $type = $content->getAttribute('type'); - } else { - $type = 'text'; - } - - if (strpos($type, 'text/') === 0) { - $type = 'text'; - } - - switch ($type) { - case 'text': - case 'html': - return $content->textContent; - break; - case 'xhtml': - $container = $content->getElementsByTagName('div'); - if ($container->length == 0) { - return false; - } - $contents = $container->item(0); - if ($contents->hasChildNodes()) { - /* Iterate through, applying xml:base and store the result */ - $result = ''; - foreach ($contents->childNodes as $node) { - $result .= $this->traverseNode($node); - } - return $result; - } - break; - case preg_match('@^[a-zA-Z]+/[a-zA-Z+]*xml@i', $type) > 0: - return $content; - break; - case 'application/octet-stream': - default: - return base64_decode(trim($content->nodeValue)); - break; - } - return false; - } - /** - * Get a category from the entry. - * - * A feed or entry can have any number of categories. A category can have the - * attributes term, scheme and label. - * - * @param string $method The name of the text construct we want - * @param array $arguments An array which we hope gives a 'param' - * @return string - */ - function getCategory($method, $arguments) - { - $offset = empty($arguments[0]) ? 0: $arguments[0]; - $attribute = empty($arguments[1]) ? 'term' : $arguments[1]; - $categories = $this->model->getElementsByTagName('category'); - if ($categories->length <= $offset) { - $category = $categories->item($offset); - if ($category->hasAttribute($attribute)) { - return $category->getAttribute($attribute); - } - } - return false; - } - - /** - * This element must be present at least once with rel="feed". This element may be - * present any number of further times so long as there is no clash. If no 'rel' is - * present and we're asked for one, we follow the example of the Universal Feed - * Parser and presume 'alternate'. - * - * @param int $offset the position of the link within the container - * @param string $attribute the attribute name required - * @param array an array of attributes to search by - * @return string the value of the attribute - */ - function getLink($offset = 0, $attribute = 'href', $params = false) - { - if (is_array($params) and !empty($params)) { - $terms = array(); - $alt_predicate = ''; - $other_predicate = ''; - - foreach ($params as $key => $value) { - if ($key == 'rel' && $value == 'alternate') { - $alt_predicate = '[not(@rel) or @rel="alternate"]'; - } else { - $terms[] = "@$key='$value'"; - } - } - if (!empty($terms)) { - $other_predicate = '[' . join(' and ', $terms) . ']'; - } - $query = $this->xpathPrefix . 'atom:link' . $alt_predicate . $other_predicate; - $links = $this->xpath->query($query); - } else { - $links = $this->model->getElementsByTagName('link'); - } - if ($links->length > $offset) { - if ($links->item($offset)->hasAttribute($attribute)) { - $value = $links->item($offset)->getAttribute($attribute); - if ($attribute == 'href') { - $value = $this->addBase($value, $links->item($offset)); - } - return $value; - } else if ($attribute == 'rel') { - return 'alternate'; - } - } - return false; - } -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/AtomElement.php b/plugins/OStatus/extlib/XML/Feed/Parser/AtomElement.php deleted file mode 100755 index 063ecb617..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/AtomElement.php +++ /dev/null @@ -1,261 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * AtomElement class for XML_Feed_Parser package - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1 - * @version CVS: $Id: AtomElement.php,v 1.19 2007/03/26 12:43:11 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/** - * This class provides support for atom entries. It will usually be called by - * XML_Feed_Parser_Atom with which it shares many methods. - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - */ -class XML_Feed_Parser_AtomElement extends XML_Feed_Parser_Atom -{ - /** - * This will be a reference to the parent object for when we want - * to use a 'fallback' rule - * @var XML_Feed_Parser_Atom - */ - protected $parent; - - /** - * When performing XPath queries we will use this prefix - * @var string - */ - private $xpathPrefix = ''; - - /** - * xml:base values inherited by the element - * @var string - */ - protected $xmlBase; - - /** - * Here we provide a few mappings for those very special circumstances in - * which it makes sense to map back to the RSS2 spec or to manage other - * compatibilities (eg. with the Univeral Feed Parser). Key is the other version's - * name for the command, value is an array consisting of the equivalent in our atom - * api and any attributes needed to make the mapping. - * @var array - */ - protected $compatMap = array( - 'guid' => array('id'), - 'links' => array('link'), - 'tags' => array('category'), - 'contributors' => array('contributor')); - - /** - * Our specific element map - * @var array - */ - protected $map = array( - 'author' => array('Person', 'fallback'), - 'contributor' => array('Person'), - 'id' => array('Text', 'fail'), - 'published' => array('Date'), - 'updated' => array('Date', 'fail'), - 'title' => array('Text', 'fail'), - 'rights' => array('Text', 'fallback'), - 'summary' => array('Text'), - 'content' => array('Content'), - 'link' => array('Link'), - 'enclosure' => array('Enclosure'), - 'category' => array('Category')); - - /** - * Store useful information for later. - * - * @param DOMElement $element - this item as a DOM element - * @param XML_Feed_Parser_Atom $parent - the feed of which this is a member - */ - function __construct(DOMElement $element, $parent, $xmlBase = '') - { - $this->model = $element; - $this->parent = $parent; - $this->xmlBase = $xmlBase; - $this->xpathPrefix = "//atom:entry[atom:id='" . $this->id . "']/"; - $this->xpath = $this->parent->xpath; - } - - /** - * Provides access to specific aspects of the author data for an atom entry - * - * Author data at the entry level is more complex than at the feed level. - * If atom:author is not present for the entry we need to look for it in - * an atom:source child of the atom:entry. If it's not there either, then - * we look to the parent for data. - * - * @param array - * @return string - */ - function getAuthor($arguments) - { - /* Find out which part of the author data we're looking for */ - if (isset($arguments['param'])) { - $parameter = $arguments['param']; - } else { - $parameter = 'name'; - } - - $test = $this->model->getElementsByTagName('author'); - if ($test->length > 0) { - $item = $test->item(0); - return $item->getElementsByTagName($parameter)->item(0)->nodeValue; - } - - $source = $this->model->getElementsByTagName('source'); - if ($source->length > 0) { - $test = $this->model->getElementsByTagName('author'); - if ($test->length > 0) { - $item = $test->item(0); - return $item->getElementsByTagName($parameter)->item(0)->nodeValue; - } - } - return $this->parent->getAuthor($arguments); - } - - /** - * Returns the content of the content element or info on a specific attribute - * - * This element may or may not be present. It cannot be present more than - * once. It may have a 'src' attribute, in which case there's no content - * If not present, then the entry must have link with rel="alternate". - * If there is content we return it, if not and there's a 'src' attribute - * we return the value of that instead. The method can take an 'attribute' - * argument, in which case we return the value of that attribute if present. - * eg. $item->content("type") will return the type of the content. It is - * recommended that all users check the type before getting the content to - * ensure that their script is capable of handling the type of returned data. - * (data carried in the content element can be either 'text', 'html', 'xhtml', - * or any standard MIME type). - * - * @return string|false - */ - protected function getContent($method, $arguments = array()) - { - $attribute = empty($arguments[0]) ? false : $arguments[0]; - $tags = $this->model->getElementsByTagName('content'); - - if ($tags->length == 0) { - return false; - } - - $content = $tags->item(0); - - if (! $content->hasAttribute('type')) { - $content->setAttribute('type', 'text'); - } - if (! empty($attribute)) { - return $content->getAttribute($attribute); - } - - $type = $content->getAttribute('type'); - - if (! empty($attribute)) { - if ($content->hasAttribute($attribute)) - { - return $content->getAttribute($attribute); - } - return false; - } - - if ($content->hasAttribute('src')) { - return $content->getAttribute('src'); - } - - return $this->parseTextConstruct($content); - } - - /** - * For compatibility, this method provides a mapping to access enclosures. - * - * The Atom spec doesn't provide for an enclosure element, but it is - * generally supported using the link element with rel='enclosure'. - * - * @param string $method - for compatibility with our __call usage - * @param array $arguments - for compatibility with our __call usage - * @return array|false - */ - function getEnclosure($method, $arguments = array()) - { - $offset = isset($arguments[0]) ? $arguments[0] : 0; - $query = "//atom:entry[atom:id='" . $this->getText('id', false) . - "']/atom:link[@rel='enclosure']"; - - $encs = $this->parent->xpath->query($query); - if ($encs->length > $offset) { - try { - if (! $encs->item($offset)->hasAttribute('href')) { - return false; - } - $attrs = $encs->item($offset)->attributes; - $length = $encs->item($offset)->hasAttribute('length') ? - $encs->item($offset)->getAttribute('length') : false; - return array( - 'url' => $attrs->getNamedItem('href')->value, - 'type' => $attrs->getNamedItem('type')->value, - 'length' => $length); - } catch (Exception $e) { - return false; - } - } - return false; - } - - /** - * Get details of this entry's source, if available/relevant - * - * Where an atom:entry is taken from another feed then the aggregator - * is supposed to include an atom:source element which replicates at least - * the atom:id, atom:title, and atom:updated metadata from the original - * feed. Atom:source therefore has a very similar structure to atom:feed - * and if we find it we will return it as an XML_Feed_Parser_Atom object. - * - * @return XML_Feed_Parser_Atom|false - */ - function getSource() - { - $test = $this->model->getElementsByTagName('source'); - if ($test->length == 0) { - return false; - } - $source = new XML_Feed_Parser_Atom($test->item(0)); - } - - /** - * Get the entry as an XML string - * - * Return an XML serialization of the feed, should it be required. Most - * users however, will already have a serialization that they used when - * instantiating the object. - * - * @return string XML serialization of element - */ - function __toString() - { - $simple = simplexml_import_dom($this->model); - return $simple->asXML(); - } -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/Exception.php b/plugins/OStatus/extlib/XML/Feed/Parser/Exception.php deleted file mode 100755 index 1e76e3f85..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/Exception.php +++ /dev/null @@ -1,42 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * Keeps the exception class for XML_Feed_Parser. - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL - * @version CVS: $Id: Exception.php,v 1.3 2005/11/07 01:52:35 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/** - * We are extending PEAR_Exception - */ -require_once 'PEAR/Exception.php'; - -/** - * XML_Feed_Parser_Exception is a simple extension of PEAR_Exception, existing - * to help with identification of the source of exceptions. - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - */ -class XML_Feed_Parser_Exception extends PEAR_Exception -{ - -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php deleted file mode 100755 index 07f38f911..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php +++ /dev/null @@ -1,214 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * RSS0.9 class for XML_Feed_Parser - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1 - * @version CVS: $Id: RSS09.php,v 1.5 2006/07/26 21:18:46 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/** - * This class handles RSS0.9 feeds. - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - * @todo Find a Relax NG URI we can use - */ -class XML_Feed_Parser_RSS09 extends XML_Feed_Parser_Type -{ - /** - * The URI of the RelaxNG schema used to (optionally) validate the feed - * @var string - */ - private $relax = ''; - - /** - * We're likely to use XPath, so let's keep it global - * @var DOMXPath - */ - protected $xpath; - - /** - * The feed type we are parsing - * @var string - */ - public $version = 'RSS 0.9'; - - /** - * The class used to represent individual items - * @var string - */ - protected $itemClass = 'XML_Feed_Parser_RSS09Element'; - - /** - * The element containing entries - * @var string - */ - protected $itemElement = 'item'; - - /** - * Here we map those elements we're not going to handle individually - * to the constructs they are. The optional second parameter in the array - * tells the parser whether to 'fall back' (not apt. at the feed level) or - * fail if the element is missing. If the parameter is not set, the function - * will simply return false and leave it to the client to decide what to do. - * @var array - */ - protected $map = array( - 'title' => array('Text'), - 'link' => array('Text'), - 'description' => array('Text'), - 'image' => array('Image'), - 'textinput' => array('TextInput')); - - /** - * Here we map some elements to their atom equivalents. This is going to be - * quite tricky to pull off effectively (and some users' methods may vary) - * but is worth trying. The key is the atom version, the value is RSS2. - * @var array - */ - protected $compatMap = array( - 'title' => array('title'), - 'link' => array('link'), - 'subtitle' => array('description')); - - /** - * We will be working with multiple namespaces and it is useful to - * keep them together - * @var array - */ - protected $namespaces = array( - 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); - - /** - * Our constructor does nothing more than its parent. - * - * @todo RelaxNG validation - * @param DOMDocument $xml A DOM object representing the feed - * @param bool (optional) $string Whether or not to validate this feed - */ - function __construct(DOMDocument $model, $strict = false) - { - $this->model = $model; - - $this->xpath = new DOMXPath($model); - foreach ($this->namespaces as $key => $value) { - $this->xpath->registerNamespace($key, $value); - } - $this->numberEntries = $this->count('item'); - } - - /** - * Included for compatibility -- will not work with RSS 0.9 - * - * This is not something that will work with RSS0.9 as it does not have - * clear restrictions on the global uniqueness of IDs. - * - * @param string $id any valid ID. - * @return false - */ - function getEntryById($id) - { - return false; - } - - /** - * Get details of the image associated with the feed. - * - * @return array|false an array simply containing the child elements - */ - protected function getImage() - { - $images = $this->model->getElementsByTagName('image'); - if ($images->length > 0) { - $image = $images->item(0); - $details = array(); - if ($image->hasChildNodes()) { - $details = array( - 'title' => $image->getElementsByTagName('title')->item(0)->value, - 'link' => $image->getElementsByTagName('link')->item(0)->value, - 'url' => $image->getElementsByTagName('url')->item(0)->value); - } else { - $details = array('title' => false, - 'link' => false, - 'url' => $image->attributes->getNamedItem('resource')->nodeValue); - } - $details = array_merge($details, - array('description' => false, 'height' => false, 'width' => false)); - if (! empty($details)) { - return $details; - } - } - return false; - } - - /** - * The textinput element is little used, but in the interests of - * completeness we will support it. - * - * @return array|false - */ - protected function getTextInput() - { - $inputs = $this->model->getElementsByTagName('textinput'); - if ($inputs->length > 0) { - $input = $inputs->item(0); - $results = array(); - $results['title'] = isset( - $input->getElementsByTagName('title')->item(0)->value) ? - $input->getElementsByTagName('title')->item(0)->value : null; - $results['description'] = isset( - $input->getElementsByTagName('description')->item(0)->value) ? - $input->getElementsByTagName('description')->item(0)->value : null; - $results['name'] = isset( - $input->getElementsByTagName('name')->item(0)->value) ? - $input->getElementsByTagName('name')->item(0)->value : null; - $results['link'] = isset( - $input->getElementsByTagName('link')->item(0)->value) ? - $input->getElementsByTagName('link')->item(0)->value : null; - if (empty($results['link']) && - $input->attributes->getNamedItem('resource')) { - $results['link'] = $input->attributes->getNamedItem('resource')->nodeValue; - } - if (! empty($results)) { - return $results; - } - } - return false; - } - - /** - * Get details of a link from the feed. - * - * In RSS1 a link is a text element but in order to ensure that we resolve - * URLs properly we have a special function for them. - * - * @return string - */ - function getLink($offset = 0, $attribute = 'href', $params = false) - { - $links = $this->model->getElementsByTagName('link'); - if ($links->length <= $offset) { - return false; - } - $link = $links->item($offset); - return $this->addBase($link->nodeValue, $link); - } -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS09Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS09Element.php deleted file mode 100755 index d41f36e8d..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/RSS09Element.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * RSS0.9 Element class for XML_Feed_Parser - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1 - * @version CVS: $Id: RSS09Element.php,v 1.4 2006/06/30 17:41:56 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/* - * This class provides support for RSS 0.9 entries. It will usually be called by - * XML_Feed_Parser_RSS09 with which it shares many methods. - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - */ -class XML_Feed_Parser_RSS09Element extends XML_Feed_Parser_RSS09 -{ - /** - * This will be a reference to the parent object for when we want - * to use a 'fallback' rule - * @var XML_Feed_Parser_RSS09 - */ - protected $parent; - - /** - * Our specific element map - * @var array - */ - protected $map = array( - 'title' => array('Text'), - 'link' => array('Link')); - - /** - * Store useful information for later. - * - * @param DOMElement $element - this item as a DOM element - * @param XML_Feed_Parser_RSS1 $parent - the feed of which this is a member - */ - function __construct(DOMElement $element, $parent, $xmlBase = '') - { - $this->model = $element; - $this->parent = $parent; - } -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php deleted file mode 100755 index 60c9938ba..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php +++ /dev/null @@ -1,277 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * RSS1 class for XML_Feed_Parser - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1 - * @version CVS: $Id: RSS1.php,v 1.10 2006/07/27 13:52:05 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/** - * This class handles RSS1.0 feeds. - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - * @todo Find a Relax NG URI we can use - */ -class XML_Feed_Parser_RSS1 extends XML_Feed_Parser_Type -{ - /** - * The URI of the RelaxNG schema used to (optionally) validate the feed - * @var string - */ - private $relax = 'rss10.rnc'; - - /** - * We're likely to use XPath, so let's keep it global - * @var DOMXPath - */ - protected $xpath; - - /** - * The feed type we are parsing - * @var string - */ - public $version = 'RSS 1.0'; - - /** - * The class used to represent individual items - * @var string - */ - protected $itemClass = 'XML_Feed_Parser_RSS1Element'; - - /** - * The element containing entries - * @var string - */ - protected $itemElement = 'item'; - - /** - * Here we map those elements we're not going to handle individually - * to the constructs they are. The optional second parameter in the array - * tells the parser whether to 'fall back' (not apt. at the feed level) or - * fail if the element is missing. If the parameter is not set, the function - * will simply return false and leave it to the client to decide what to do. - * @var array - */ - protected $map = array( - 'title' => array('Text'), - 'link' => array('Text'), - 'description' => array('Text'), - 'image' => array('Image'), - 'textinput' => array('TextInput'), - 'updatePeriod' => array('Text'), - 'updateFrequency' => array('Text'), - 'updateBase' => array('Date'), - 'rights' => array('Text'), # dc:rights - 'description' => array('Text'), # dc:description - 'creator' => array('Text'), # dc:creator - 'publisher' => array('Text'), # dc:publisher - 'contributor' => array('Text'), # dc:contributor - 'date' => array('Date') # dc:contributor - ); - - /** - * Here we map some elements to their atom equivalents. This is going to be - * quite tricky to pull off effectively (and some users' methods may vary) - * but is worth trying. The key is the atom version, the value is RSS2. - * @var array - */ - protected $compatMap = array( - 'title' => array('title'), - 'link' => array('link'), - 'subtitle' => array('description'), - 'author' => array('creator'), - 'updated' => array('date')); - - /** - * We will be working with multiple namespaces and it is useful to - * keep them together - * @var array - */ - protected $namespaces = array( - 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'rss' => 'http://purl.org/rss/1.0/', - 'dc' => 'http://purl.org/rss/1.0/modules/dc/', - 'content' => 'http://purl.org/rss/1.0/modules/content/', - 'sy' => 'http://web.resource.org/rss/1.0/modules/syndication/'); - - /** - * Our constructor does nothing more than its parent. - * - * @param DOMDocument $xml A DOM object representing the feed - * @param bool (optional) $string Whether or not to validate this feed - */ - function __construct(DOMDocument $model, $strict = false) - { - $this->model = $model; - if ($strict) { - $validate = $this->model->relaxNGValidate(self::getSchemaDir . - DIRECTORY_SEPARATOR . $this->relax); - if (! $validate) { - throw new XML_Feed_Parser_Exception('Failed required validation'); - } - } - - $this->xpath = new DOMXPath($model); - foreach ($this->namespaces as $key => $value) { - $this->xpath->registerNamespace($key, $value); - } - $this->numberEntries = $this->count('item'); - } - - /** - * Allows retrieval of an entry by ID where the rdf:about attribute is used - * - * This is not really something that will work with RSS1 as it does not have - * clear restrictions on the global uniqueness of IDs. We will employ the - * _very_ hit and miss method of selecting entries based on the rdf:about - * attribute. If DOMXPath::evaluate is available, we also use that to store - * a reference to the entry in the array used by getEntryByOffset so that - * method does not have to seek out the entry if it's requested that way. - * - * @param string $id any valid ID. - * @return XML_Feed_Parser_RSS1Element - */ - function getEntryById($id) - { - if (isset($this->idMappings[$id])) { - return $this->entries[$this->idMappings[$id]]; - } - - $entries = $this->xpath->query("//rss:item[@rdf:about='$id']"); - if ($entries->length > 0) { - $classname = $this->itemClass; - $entry = new $classname($entries->item(0), $this); - if (in_array('evaluate', get_class_methods($this->xpath))) { - $offset = $this->xpath->evaluate("count(preceding-sibling::rss:item)", $entries->item(0)); - $this->entries[$offset] = $entry; - } - $this->idMappings[$id] = $entry; - return $entry; - } - return false; - } - - /** - * Get details of the image associated with the feed. - * - * @return array|false an array simply containing the child elements - */ - protected function getImage() - { - $images = $this->model->getElementsByTagName('image'); - if ($images->length > 0) { - $image = $images->item(0); - $details = array(); - if ($image->hasChildNodes()) { - $details = array( - 'title' => $image->getElementsByTagName('title')->item(0)->value, - 'link' => $image->getElementsByTagName('link')->item(0)->value, - 'url' => $image->getElementsByTagName('url')->item(0)->value); - } else { - $details = array('title' => false, - 'link' => false, - 'url' => $image->attributes->getNamedItem('resource')->nodeValue); - } - $details = array_merge($details, array('description' => false, 'height' => false, 'width' => false)); - if (! empty($details)) { - return $details; - } - } - return false; - } - - /** - * The textinput element is little used, but in the interests of - * completeness we will support it. - * - * @return array|false - */ - protected function getTextInput() - { - $inputs = $this->model->getElementsByTagName('textinput'); - if ($inputs->length > 0) { - $input = $inputs->item(0); - $results = array(); - $results['title'] = isset( - $input->getElementsByTagName('title')->item(0)->value) ? - $input->getElementsByTagName('title')->item(0)->value : null; - $results['description'] = isset( - $input->getElementsByTagName('description')->item(0)->value) ? - $input->getElementsByTagName('description')->item(0)->value : null; - $results['name'] = isset( - $input->getElementsByTagName('name')->item(0)->value) ? - $input->getElementsByTagName('name')->item(0)->value : null; - $results['link'] = isset( - $input->getElementsByTagName('link')->item(0)->value) ? - $input->getElementsByTagName('link')->item(0)->value : null; - if (empty($results['link']) and - $input->attributes->getNamedItem('resource')) { - $results['link'] = - $input->attributes->getNamedItem('resource')->nodeValue; - } - if (! empty($results)) { - return $results; - } - } - return false; - } - - /** - * Employs various techniques to identify the author - * - * Dublin Core provides the dc:creator, dc:contributor, and dc:publisher - * elements for defining authorship in RSS1. We will try each of those in - * turn in order to simulate the atom author element and will return it - * as text. - * - * @return array|false - */ - function getAuthor() - { - $options = array('creator', 'contributor', 'publisher'); - foreach ($options as $element) { - $test = $this->model->getElementsByTagName($element); - if ($test->length > 0) { - return $test->item(0)->value; - } - } - return false; - } - - /** - * Retrieve a link - * - * In RSS1 a link is a text element but in order to ensure that we resolve - * URLs properly we have a special function for them. - * - * @return string - */ - function getLink($offset = 0, $attribute = 'href', $params = false) - { - $links = $this->model->getElementsByTagName('link'); - if ($links->length <= $offset) { - return false; - } - $link = $links->item($offset); - return $this->addBase($link->nodeValue, $link); - } -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php deleted file mode 100755 index 3cd1ef15d..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php +++ /dev/null @@ -1,276 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * RSS1.1 class for XML_Feed_Parser - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1 - * @version CVS: $Id: RSS11.php,v 1.6 2006/07/27 13:52:05 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/** - * This class handles RSS1.1 feeds. RSS1.1 is documented at: - * http://inamidst.com/rss1.1/ - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - * @todo Support for RDF:List - * @todo Ensure xml:lang is accessible to users - */ -class XML_Feed_Parser_RSS11 extends XML_Feed_Parser_Type -{ - /** - * The URI of the RelaxNG schema used to (optionally) validate the feed - * @var string - */ - private $relax = 'rss11.rnc'; - - /** - * We're likely to use XPath, so let's keep it global - * @var DOMXPath - */ - protected $xpath; - - /** - * The feed type we are parsing - * @var string - */ - public $version = 'RSS 1.0'; - - /** - * The class used to represent individual items - * @var string - */ - protected $itemClass = 'XML_Feed_Parser_RSS1Element'; - - /** - * The element containing entries - * @var string - */ - protected $itemElement = 'item'; - - /** - * Here we map those elements we're not going to handle individually - * to the constructs they are. The optional second parameter in the array - * tells the parser whether to 'fall back' (not apt. at the feed level) or - * fail if the element is missing. If the parameter is not set, the function - * will simply return false and leave it to the client to decide what to do. - * @var array - */ - protected $map = array( - 'title' => array('Text'), - 'link' => array('Text'), - 'description' => array('Text'), - 'image' => array('Image'), - 'updatePeriod' => array('Text'), - 'updateFrequency' => array('Text'), - 'updateBase' => array('Date'), - 'rights' => array('Text'), # dc:rights - 'description' => array('Text'), # dc:description - 'creator' => array('Text'), # dc:creator - 'publisher' => array('Text'), # dc:publisher - 'contributor' => array('Text'), # dc:contributor - 'date' => array('Date') # dc:contributor - ); - - /** - * Here we map some elements to their atom equivalents. This is going to be - * quite tricky to pull off effectively (and some users' methods may vary) - * but is worth trying. The key is the atom version, the value is RSS2. - * @var array - */ - protected $compatMap = array( - 'title' => array('title'), - 'link' => array('link'), - 'subtitle' => array('description'), - 'author' => array('creator'), - 'updated' => array('date')); - - /** - * We will be working with multiple namespaces and it is useful to - * keep them together. We will retain support for some common RSS1.0 modules - * @var array - */ - protected $namespaces = array( - 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'rss' => 'http://purl.org/net/rss1.1#', - 'dc' => 'http://purl.org/rss/1.0/modules/dc/', - 'content' => 'http://purl.org/rss/1.0/modules/content/', - 'sy' => 'http://web.resource.org/rss/1.0/modules/syndication/'); - - /** - * Our constructor does nothing more than its parent. - * - * @param DOMDocument $xml A DOM object representing the feed - * @param bool (optional) $string Whether or not to validate this feed - */ - function __construct(DOMDocument $model, $strict = false) - { - $this->model = $model; - - if ($strict) { - $validate = $this->model->relaxNGValidate(self::getSchemaDir . - DIRECTORY_SEPARATOR . $this->relax); - if (! $validate) { - throw new XML_Feed_Parser_Exception('Failed required validation'); - } - } - - $this->xpath = new DOMXPath($model); - foreach ($this->namespaces as $key => $value) { - $this->xpath->registerNamespace($key, $value); - } - $this->numberEntries = $this->count('item'); - } - - /** - * Attempts to identify an element by ID given by the rdf:about attribute - * - * This is not really something that will work with RSS1.1 as it does not have - * clear restrictions on the global uniqueness of IDs. We will employ the - * _very_ hit and miss method of selecting entries based on the rdf:about - * attribute. Please note that this is even more hit and miss with RSS1.1 than - * with RSS1.0 since RSS1.1 does not require the rdf:about attribute for items. - * - * @param string $id any valid ID. - * @return XML_Feed_Parser_RSS1Element - */ - function getEntryById($id) - { - if (isset($this->idMappings[$id])) { - return $this->entries[$this->idMappings[$id]]; - } - - $entries = $this->xpath->query("//rss:item[@rdf:about='$id']"); - if ($entries->length > 0) { - $classname = $this->itemClass; - $entry = new $classname($entries->item(0), $this); - return $entry; - } - return false; - } - - /** - * Get details of the image associated with the feed. - * - * @return array|false an array simply containing the child elements - */ - protected function getImage() - { - $images = $this->model->getElementsByTagName('image'); - if ($images->length > 0) { - $image = $images->item(0); - $details = array(); - if ($image->hasChildNodes()) { - $details = array( - 'title' => $image->getElementsByTagName('title')->item(0)->value, - 'url' => $image->getElementsByTagName('url')->item(0)->value); - if ($image->getElementsByTagName('link')->length > 0) { - $details['link'] = - $image->getElementsByTagName('link')->item(0)->value; - } - } else { - $details = array('title' => false, - 'link' => false, - 'url' => $image->attributes->getNamedItem('resource')->nodeValue); - } - $details = array_merge($details, - array('description' => false, 'height' => false, 'width' => false)); - if (! empty($details)) { - return $details; - } - } - return false; - } - - /** - * The textinput element is little used, but in the interests of - * completeness we will support it. - * - * @return array|false - */ - protected function getTextInput() - { - $inputs = $this->model->getElementsByTagName('textinput'); - if ($inputs->length > 0) { - $input = $inputs->item(0); - $results = array(); - $results['title'] = isset( - $input->getElementsByTagName('title')->item(0)->value) ? - $input->getElementsByTagName('title')->item(0)->value : null; - $results['description'] = isset( - $input->getElementsByTagName('description')->item(0)->value) ? - $input->getElementsByTagName('description')->item(0)->value : null; - $results['name'] = isset( - $input->getElementsByTagName('name')->item(0)->value) ? - $input->getElementsByTagName('name')->item(0)->value : null; - $results['link'] = isset( - $input->getElementsByTagName('link')->item(0)->value) ? - $input->getElementsByTagName('link')->item(0)->value : null; - if (empty($results['link']) and - $input->attributes->getNamedItem('resource')) { - $results['link'] = $input->attributes->getNamedItem('resource')->nodeValue; - } - if (! empty($results)) { - return $results; - } - } - return false; - } - - /** - * Attempts to discern authorship - * - * Dublin Core provides the dc:creator, dc:contributor, and dc:publisher - * elements for defining authorship in RSS1. We will try each of those in - * turn in order to simulate the atom author element and will return it - * as text. - * - * @return array|false - */ - function getAuthor() - { - $options = array('creator', 'contributor', 'publisher'); - foreach ($options as $element) { - $test = $this->model->getElementsByTagName($element); - if ($test->length > 0) { - return $test->item(0)->value; - } - } - return false; - } - - /** - * Retrieve a link - * - * In RSS1 a link is a text element but in order to ensure that we resolve - * URLs properly we have a special function for them. - * - * @return string - */ - function getLink($offset = 0, $attribute = 'href', $params = false) - { - $links = $this->model->getElementsByTagName('link'); - if ($links->length <= $offset) { - return false; - } - $link = $links->item($offset); - return $this->addBase($link->nodeValue, $link); - } -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS11Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS11Element.php deleted file mode 100755 index 75918beda..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/RSS11Element.php +++ /dev/null @@ -1,151 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * RSS1 Element class for XML_Feed_Parser - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1 - * @version CVS: $Id: RSS11Element.php,v 1.4 2006/06/30 17:41:56 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/* - * This class provides support for RSS 1.1 entries. It will usually be called by - * XML_Feed_Parser_RSS11 with which it shares many methods. - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - */ -class XML_Feed_Parser_RSS11Element extends XML_Feed_Parser_RSS11 -{ - /** - * This will be a reference to the parent object for when we want - * to use a 'fallback' rule - * @var XML_Feed_Parser_RSS1 - */ - protected $parent; - - /** - * Our specific element map - * @var array - */ - protected $map = array( - 'id' => array('Id'), - 'title' => array('Text'), - 'link' => array('Link'), - 'description' => array('Text'), # or dc:description - 'category' => array('Category'), - 'rights' => array('Text'), # dc:rights - 'creator' => array('Text'), # dc:creator - 'publisher' => array('Text'), # dc:publisher - 'contributor' => array('Text'), # dc:contributor - 'date' => array('Date'), # dc:date - 'content' => array('Content') - ); - - /** - * Here we map some elements to their atom equivalents. This is going to be - * quite tricky to pull off effectively (and some users' methods may vary) - * but is worth trying. The key is the atom version, the value is RSS1. - * @var array - */ - protected $compatMap = array( - 'content' => array('content'), - 'updated' => array('lastBuildDate'), - 'published' => array('pubdate'), - 'subtitle' => array('description'), - 'updated' => array('date'), - 'author' => array('creator'), - 'contributor' => array('contributor') - ); - - /** - * Store useful information for later. - * - * @param DOMElement $element - this item as a DOM element - * @param XML_Feed_Parser_RSS1 $parent - the feed of which this is a member - */ - function __construct(DOMElement $element, $parent, $xmlBase = '') - { - $this->model = $element; - $this->parent = $parent; - } - - /** - * If an rdf:about attribute is specified, return that as an ID - * - * There is no established way of showing an ID for an RSS1 entry. We will - * simulate it using the rdf:about attribute of the entry element. This cannot - * be relied upon for unique IDs but may prove useful. - * - * @return string|false - */ - function getId() - { - if ($this->model->attributes->getNamedItem('about')) { - return $this->model->attributes->getNamedItem('about')->nodeValue; - } - return false; - } - - /** - * Return the entry's content - * - * The official way to include full content in an RSS1 entry is to use - * the content module's element 'encoded'. Often, however, the 'description' - * element is used instead. We will offer that as a fallback. - * - * @return string|false - */ - function getContent() - { - $options = array('encoded', 'description'); - foreach ($options as $element) { - $test = $this->model->getElementsByTagName($element); - if ($test->length == 0) { - continue; - } - if ($test->item(0)->hasChildNodes()) { - $value = ''; - foreach ($test->item(0)->childNodes as $child) { - if ($child instanceof DOMText) { - $value .= $child->nodeValue; - } else { - $simple = simplexml_import_dom($child); - $value .= $simple->asXML(); - } - } - return $value; - } else if ($test->length > 0) { - return $test->item(0)->nodeValue; - } - } - return false; - } - - /** - * How RSS1.1 should support for enclosures is not clear. For now we will return - * false. - * - * @return false - */ - function getEnclosure() - { - return false; - } -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS1Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS1Element.php deleted file mode 100755 index 8e36d5a9b..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/RSS1Element.php +++ /dev/null @@ -1,116 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * RSS1 Element class for XML_Feed_Parser - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1 - * @version CVS: $Id: RSS1Element.php,v 1.6 2006/06/30 17:41:56 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/* - * This class provides support for RSS 1.0 entries. It will usually be called by - * XML_Feed_Parser_RSS1 with which it shares many methods. - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - */ -class XML_Feed_Parser_RSS1Element extends XML_Feed_Parser_RSS1 -{ - /** - * This will be a reference to the parent object for when we want - * to use a 'fallback' rule - * @var XML_Feed_Parser_RSS1 - */ - protected $parent; - - /** - * Our specific element map - * @var array - */ - protected $map = array( - 'id' => array('Id'), - 'title' => array('Text'), - 'link' => array('Link'), - 'description' => array('Text'), # or dc:description - 'category' => array('Category'), - 'rights' => array('Text'), # dc:rights - 'creator' => array('Text'), # dc:creator - 'publisher' => array('Text'), # dc:publisher - 'contributor' => array('Text'), # dc:contributor - 'date' => array('Date'), # dc:date - 'content' => array('Content') - ); - - /** - * Here we map some elements to their atom equivalents. This is going to be - * quite tricky to pull off effectively (and some users' methods may vary) - * but is worth trying. The key is the atom version, the value is RSS1. - * @var array - */ - protected $compatMap = array( - 'content' => array('content'), - 'updated' => array('lastBuildDate'), - 'published' => array('pubdate'), - 'subtitle' => array('description'), - 'updated' => array('date'), - 'author' => array('creator'), - 'contributor' => array('contributor') - ); - - /** - * Store useful information for later. - * - * @param DOMElement $element - this item as a DOM element - * @param XML_Feed_Parser_RSS1 $parent - the feed of which this is a member - */ - function __construct(DOMElement $element, $parent, $xmlBase = '') - { - $this->model = $element; - $this->parent = $parent; - } - - /** - * If an rdf:about attribute is specified, return it as an ID - * - * There is no established way of showing an ID for an RSS1 entry. We will - * simulate it using the rdf:about attribute of the entry element. This cannot - * be relied upon for unique IDs but may prove useful. - * - * @return string|false - */ - function getId() - { - if ($this->model->attributes->getNamedItem('about')) { - return $this->model->attributes->getNamedItem('about')->nodeValue; - } - return false; - } - - /** - * How RSS1 should support for enclosures is not clear. For now we will return - * false. - * - * @return false - */ - function getEnclosure() - { - return false; - } -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php deleted file mode 100644 index 0936bd2f5..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php +++ /dev/null @@ -1,335 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * Class representing feed-level data for an RSS2 feed - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1 - * @version CVS: $Id: RSS2.php,v 1.12 2008/03/08 18:16:45 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/** - * This class handles RSS2 feeds. - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - */ -class XML_Feed_Parser_RSS2 extends XML_Feed_Parser_Type -{ - /** - * The URI of the RelaxNG schema used to (optionally) validate the feed - * @var string - */ - private $relax = 'rss20.rnc'; - - /** - * We're likely to use XPath, so let's keep it global - * @var DOMXPath - */ - protected $xpath; - - /** - * The feed type we are parsing - * @var string - */ - public $version = 'RSS 2.0'; - - /** - * The class used to represent individual items - * @var string - */ - protected $itemClass = 'XML_Feed_Parser_RSS2Element'; - - /** - * The element containing entries - * @var string - */ - protected $itemElement = 'item'; - - /** - * Here we map those elements we're not going to handle individually - * to the constructs they are. The optional second parameter in the array - * tells the parser whether to 'fall back' (not apt. at the feed level) or - * fail if the element is missing. If the parameter is not set, the function - * will simply return false and leave it to the client to decide what to do. - * @var array - */ - protected $map = array( - 'ttl' => array('Text'), - 'pubDate' => array('Date'), - 'lastBuildDate' => array('Date'), - 'title' => array('Text'), - 'link' => array('Link'), - 'description' => array('Text'), - 'language' => array('Text'), - 'copyright' => array('Text'), - 'managingEditor' => array('Text'), - 'webMaster' => array('Text'), - 'category' => array('Text'), - 'generator' => array('Text'), - 'docs' => array('Text'), - 'ttl' => array('Text'), - 'image' => array('Image'), - 'skipDays' => array('skipDays'), - 'skipHours' => array('skipHours')); - - /** - * Here we map some elements to their atom equivalents. This is going to be - * quite tricky to pull off effectively (and some users' methods may vary) - * but is worth trying. The key is the atom version, the value is RSS2. - * @var array - */ - protected $compatMap = array( - 'title' => array('title'), - 'rights' => array('copyright'), - 'updated' => array('lastBuildDate'), - 'subtitle' => array('description'), - 'date' => array('pubDate'), - 'author' => array('managingEditor')); - - protected $namespaces = array( - 'dc' => 'http://purl.org/rss/1.0/modules/dc/', - 'content' => 'http://purl.org/rss/1.0/modules/content/'); - - /** - * Our constructor does nothing more than its parent. - * - * @param DOMDocument $xml A DOM object representing the feed - * @param bool (optional) $string Whether or not to validate this feed - */ - function __construct(DOMDocument $model, $strict = false) - { - $this->model = $model; - - if ($strict) { - if (! $this->model->relaxNGValidate($this->relax)) { - throw new XML_Feed_Parser_Exception('Failed required validation'); - } - } - - $this->xpath = new DOMXPath($this->model); - foreach ($this->namespaces as $key => $value) { - $this->xpath->registerNamespace($key, $value); - } - $this->numberEntries = $this->count('item'); - } - - /** - * Retrieves an entry by ID, if the ID is specified with the guid element - * - * This is not really something that will work with RSS2 as it does not have - * clear restrictions on the global uniqueness of IDs. But we can emulate - * it by allowing access based on the 'guid' element. If DOMXPath::evaluate - * is available, we also use that to store a reference to the entry in the array - * used by getEntryByOffset so that method does not have to seek out the entry - * if it's requested that way. - * - * @param string $id any valid ID. - * @return XML_Feed_Parser_RSS2Element - */ - function getEntryById($id) - { - if (isset($this->idMappings[$id])) { - return $this->entries[$this->idMappings[$id]]; - } - - $entries = $this->xpath->query("//item[guid='$id']"); - if ($entries->length > 0) { - $entry = new $this->itemElement($entries->item(0), $this); - if (in_array('evaluate', get_class_methods($this->xpath))) { - $offset = $this->xpath->evaluate("count(preceding-sibling::item)", $entries->item(0)); - $this->entries[$offset] = $entry; - } - $this->idMappings[$id] = $entry; - return $entry; - } - } - - /** - * Get a category from the element - * - * The category element is a simple text construct which can occur any number - * of times. We allow access by offset or access to an array of results. - * - * @param string $call for compatibility with our overloading - * @param array $arguments - arg 0 is the offset, arg 1 is whether to return as array - * @return string|array|false - */ - function getCategory($call, $arguments = array()) - { - $categories = $this->model->getElementsByTagName('category'); - $offset = empty($arguments[0]) ? 0 : $arguments[0]; - $array = empty($arguments[1]) ? false : true; - if ($categories->length <= $offset) { - return false; - } - if ($array) { - $list = array(); - foreach ($categories as $category) { - array_push($list, $category->nodeValue); - } - return $list; - } - return $categories->item($offset)->nodeValue; - } - - /** - * Get details of the image associated with the feed. - * - * @return array|false an array simply containing the child elements - */ - protected function getImage() - { - $images = $this->xpath->query("//image"); - if ($images->length > 0) { - $image = $images->item(0); - $desc = $image->getElementsByTagName('description'); - $description = $desc->length ? $desc->item(0)->nodeValue : false; - $heigh = $image->getElementsByTagName('height'); - $height = $heigh->length ? $heigh->item(0)->nodeValue : false; - $widt = $image->getElementsByTagName('width'); - $width = $widt->length ? $widt->item(0)->nodeValue : false; - return array( - 'title' => $image->getElementsByTagName('title')->item(0)->nodeValue, - 'link' => $image->getElementsByTagName('link')->item(0)->nodeValue, - 'url' => $image->getElementsByTagName('url')->item(0)->nodeValue, - 'description' => $description, - 'height' => $height, - 'width' => $width); - } - return false; - } - - /** - * The textinput element is little used, but in the interests of - * completeness... - * - * @return array|false - */ - function getTextInput() - { - $inputs = $this->model->getElementsByTagName('input'); - if ($inputs->length > 0) { - $input = $inputs->item(0); - return array( - 'title' => $input->getElementsByTagName('title')->item(0)->value, - 'description' => - $input->getElementsByTagName('description')->item(0)->value, - 'name' => $input->getElementsByTagName('name')->item(0)->value, - 'link' => $input->getElementsByTagName('link')->item(0)->value); - } - return false; - } - - /** - * Utility function for getSkipDays and getSkipHours - * - * This is a general function used by both getSkipDays and getSkipHours. It simply - * returns an array of the values of the children of the appropriate tag. - * - * @param string $tagName The tag name (getSkipDays or getSkipHours) - * @return array|false - */ - protected function getSkips($tagName) - { - $hours = $this->model->getElementsByTagName($tagName); - if ($hours->length == 0) { - return false; - } - $skipHours = array(); - foreach($hours->item(0)->childNodes as $hour) { - if ($hour instanceof DOMElement) { - array_push($skipHours, $hour->nodeValue); - } - } - return $skipHours; - } - - /** - * Retrieve skipHours data - * - * The skiphours element provides a list of hours on which this feed should - * not be checked. We return an array of those hours (integers, 24 hour clock) - * - * @return array - */ - function getSkipHours() - { - return $this->getSkips('skipHours'); - } - - /** - * Retrieve skipDays data - * - * The skipdays element provides a list of days on which this feed should - * not be checked. We return an array of those days. - * - * @return array - */ - function getSkipDays() - { - return $this->getSkips('skipDays'); - } - - /** - * Return content of the little-used 'cloud' element - * - * The cloud element is rarely used. It is designed to provide some details - * of a location to update the feed. - * - * @return array an array of the attributes of the element - */ - function getCloud() - { - $cloud = $this->model->getElementsByTagName('cloud'); - if ($cloud->length == 0) { - return false; - } - $cloudData = array(); - foreach ($cloud->item(0)->attributes as $attribute) { - $cloudData[$attribute->name] = $attribute->value; - } - return $cloudData; - } - - /** - * Get link URL - * - * In RSS2 a link is a text element but in order to ensure that we resolve - * URLs properly we have a special function for them. We maintain the - * parameter used by the atom getLink method, though we only use the offset - * parameter. - * - * @param int $offset The position of the link within the feed. Starts from 0 - * @param string $attribute The attribute of the link element required - * @param array $params An array of other parameters. Not used. - * @return string - */ - function getLink($offset, $attribute = 'href', $params = array()) - { - $xPath = new DOMXPath($this->model); - $links = $xPath->query('//link'); - - if ($links->length <= $offset) { - return false; - } - $link = $links->item($offset); - return $this->addBase($link->nodeValue, $link); - } -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS2Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS2Element.php deleted file mode 100755 index 6edf910dc..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/RSS2Element.php +++ /dev/null @@ -1,171 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * Class representing entries in an RSS2 feed. - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1 - * @version CVS: $Id: RSS2Element.php,v 1.11 2006/07/26 21:18:47 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/** - * This class provides support for RSS 2.0 entries. It will usually be - * called by XML_Feed_Parser_RSS2 with which it shares many methods. - * - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - * @package XML_Feed_Parser - */ -class XML_Feed_Parser_RSS2Element extends XML_Feed_Parser_RSS2 -{ - /** - * This will be a reference to the parent object for when we want - * to use a 'fallback' rule - * @var XML_Feed_Parser_RSS2 - */ - protected $parent; - - /** - * Our specific element map - * @var array - */ - protected $map = array( - 'title' => array('Text'), - 'guid' => array('Guid'), - 'description' => array('Text'), - 'author' => array('Text'), - 'comments' => array('Text'), - 'enclosure' => array('Enclosure'), - 'pubDate' => array('Date'), - 'source' => array('Source'), - 'link' => array('Text'), - 'content' => array('Content')); - - /** - * Here we map some elements to their atom equivalents. This is going to be - * quite tricky to pull off effectively (and some users' methods may vary) - * but is worth trying. The key is the atom version, the value is RSS2. - * @var array - */ - protected $compatMap = array( - 'id' => array('guid'), - 'updated' => array('lastBuildDate'), - 'published' => array('pubdate'), - 'guidislink' => array('guid', 'ispermalink'), - 'summary' => array('description')); - - /** - * Store useful information for later. - * - * @param DOMElement $element - this item as a DOM element - * @param XML_Feed_Parser_RSS2 $parent - the feed of which this is a member - */ - function __construct(DOMElement $element, $parent, $xmlBase = '') - { - $this->model = $element; - $this->parent = $parent; - } - - /** - * Get the value of the guid element, if specified - * - * guid is the closest RSS2 has to atom's ID. It is usually but not always a - * URI. The one attribute that RSS2 can posess is 'ispermalink' which specifies - * whether the guid is itself dereferencable. Use of guid is not obligatory, - * but is advisable. To get the guid you would call $item->id() (for atom - * compatibility) or $item->guid(). To check if this guid is a permalink call - * $item->guid("ispermalink"). - * - * @param string $method - the method name being called - * @param array $params - parameters required - * @return string the guid or value of ispermalink - */ - protected function getGuid($method, $params) - { - $attribute = (isset($params[0]) and $params[0] == 'ispermalink') ? - true : false; - $tag = $this->model->getElementsByTagName('guid'); - if ($tag->length > 0) { - if ($attribute) { - if ($tag->hasAttribute("ispermalink")) { - return $tag->getAttribute("ispermalink"); - } - } - return $tag->item(0)->nodeValue; - } - return false; - } - - /** - * Access details of file enclosures - * - * The RSS2 spec is ambiguous as to whether an enclosure element must be - * unique in a given entry. For now we will assume it needn't, and allow - * for an offset. - * - * @param string $method - the method being called - * @param array $parameters - we expect the first of these to be our offset - * @return array|false - */ - protected function getEnclosure($method, $parameters) - { - $encs = $this->model->getElementsByTagName('enclosure'); - $offset = isset($parameters[0]) ? $parameters[0] : 0; - if ($encs->length > $offset) { - try { - if (! $encs->item($offset)->hasAttribute('url')) { - return false; - } - $attrs = $encs->item($offset)->attributes; - return array( - 'url' => $attrs->getNamedItem('url')->value, - 'length' => $attrs->getNamedItem('length')->value, - 'type' => $attrs->getNamedItem('type')->value); - } catch (Exception $e) { - return false; - } - } - return false; - } - - /** - * Get the entry source if specified - * - * source is an optional sub-element of item. Like atom:source it tells - * us about where the entry came from (eg. if it's been copied from another - * feed). It is not a rich source of metadata in the same way as atom:source - * and while it would be good to maintain compatibility by returning an - * XML_Feed_Parser_RSS2 element, it makes a lot more sense to return an array. - * - * @return array|false - */ - protected function getSource() - { - $get = $this->model->getElementsByTagName('source'); - if ($get->length) { - $source = $get->item(0); - $array = array( - 'content' => $source->nodeValue); - foreach ($source->attributes as $attribute) { - $array[$attribute->name] = $attribute->value; - } - return $array; - } - return false; - } -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/Type.php b/plugins/OStatus/extlib/XML/Feed/Parser/Type.php deleted file mode 100644 index 75052619b..000000000 --- a/plugins/OStatus/extlib/XML/Feed/Parser/Type.php +++ /dev/null @@ -1,467 +0,0 @@ -<?php -/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ - -/** - * Abstract class providing common methods for XML_Feed_Parser feeds. - * - * PHP versions 5 - * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * - * @category XML - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @copyright 2005 James Stewart <james@jystewart.net> - * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1 - * @version CVS: $Id: Type.php,v 1.25 2008/03/08 18:39:09 jystewart Exp $ - * @link http://pear.php.net/package/XML_Feed_Parser/ - */ - -/** - * This abstract class provides some general methods that are likely to be - * implemented exactly the same way for all feed types. - * - * @package XML_Feed_Parser - * @author James Stewart <james@jystewart.net> - * @version Release: 1.0.3 - */ -abstract class XML_Feed_Parser_Type -{ - /** - * Where we store our DOM object for this feed - * @var DOMDocument - */ - public $model; - - /** - * For iteration we'll want a count of the number of entries - * @var int - */ - public $numberEntries; - - /** - * Where we store our entry objects once instantiated - * @var array - */ - public $entries = array(); - - /** - * Store mappings between entry IDs and their position in the feed - */ - public $idMappings = array(); - - /** - * Proxy to allow use of element names as method names - * - * We are not going to provide methods for every entry type so this - * function will allow for a lot of mapping. We rely pretty heavily - * on this to handle our mappings between other feed types and atom. - * - * @param string $call - the method attempted - * @param array $arguments - arguments to that method - * @return mixed - */ - function __call($call, $arguments = array()) - { - if (! is_array($arguments)) { - $arguments = array(); - } - - if (isset($this->compatMap[$call])) { - $tempMap = $this->compatMap; - $tempcall = array_pop($tempMap[$call]); - if (! empty($tempMap)) { - $arguments = array_merge($arguments, $tempMap[$call]); - } - $call = $tempcall; - } - - /* To be helpful, we allow a case-insensitive search for this method */ - if (! isset($this->map[$call])) { - foreach (array_keys($this->map) as $key) { - if (strtoupper($key) == strtoupper($call)) { - $call = $key; - break; - } - } - } - - if (empty($this->map[$call])) { - return false; - } - - $method = 'get' . $this->map[$call][0]; - if ($method == 'getLink') { - $offset = empty($arguments[0]) ? 0 : $arguments[0]; - $attribute = empty($arguments[1]) ? 'href' : $arguments[1]; - $params = isset($arguments[2]) ? $arguments[2] : array(); - return $this->getLink($offset, $attribute, $params); - } - if (method_exists($this, $method)) { - return $this->$method($call, $arguments); - } - - return false; - } - - /** - * Proxy to allow use of element names as attribute names - * - * For many elements variable-style access will be desirable. This function - * provides for that. - * - * @param string $value - the variable required - * @return mixed - */ - function __get($value) - { - return $this->__call($value, array()); - } - - /** - * Utility function to help us resolve xml:base values - * - * We have other methods which will traverse the DOM and work out the different - * xml:base declarations we need to be aware of. We then need to combine them. - * If a declaration starts with a protocol then we restart the string. If it - * starts with a / then we add on to the domain name. Otherwise we simply tag - * it on to the end. - * - * @param string $base - the base to add the link to - * @param string $link - */ - function combineBases($base, $link) - { - if (preg_match('/^[A-Za-z]+:\/\//', $link)) { - return $link; - } else if (preg_match('/^\//', $link)) { - /* Extract domain and suffix link to that */ - preg_match('/^([A-Za-z]+:\/\/.*)?\/*/', $base, $results); - $firstLayer = $results[0]; - return $firstLayer . "/" . $link; - } else if (preg_match('/^\.\.\//', $base)) { - /* Step up link to find place to be */ - preg_match('/^((\.\.\/)+)(.*)$/', $link, $bases); - $suffix = $bases[3]; - $count = preg_match_all('/\.\.\//', $bases[1], $steps); - $url = explode("/", $base); - for ($i = 0; $i <= $count; $i++) { - array_pop($url); - } - return implode("/", $url) . "/" . $suffix; - } else if (preg_match('/^(?!\/$)/', $base)) { - $base = preg_replace('/(.*\/).*$/', '$1', $base) ; - return $base . $link; - } else { - /* Just stick it on the end */ - return $base . $link; - } - } - - /** - * Determine whether we need to apply our xml:base rules - * - * Gets us the xml:base data and then processes that with regard - * to our current link. - * - * @param string - * @param DOMElement - * @return string - */ - function addBase($link, $element) - { - if (preg_match('/^[A-Za-z]+:\/\//', $link)) { - return $link; - } - - return $this->combineBases($element->baseURI, $link); - } - - /** - * Get an entry by its position in the feed, starting from zero - * - * As well as allowing the items to be iterated over we want to allow - * users to be able to access a specific entry. This is one of two ways of - * doing that, the other being by ID. - * - * @param int $offset - * @return XML_Feed_Parser_RSS1Element - */ - function getEntryByOffset($offset) - { - if (! isset($this->entries[$offset])) { - $entries = $this->model->getElementsByTagName($this->itemElement); - if ($entries->length > $offset) { - $xmlBase = $entries->item($offset)->baseURI; - $this->entries[$offset] = new $this->itemClass( - $entries->item($offset), $this, $xmlBase); - if ($id = $this->entries[$offset]->id) { - $this->idMappings[$id] = $this->entries[$offset]; - } - } else { - throw new XML_Feed_Parser_Exception('No entries found'); - } - } - - return $this->entries[$offset]; - } - - /** - * Return a date in seconds since epoch. - * - * Get a date construct. We use PHP's strtotime to return it as a unix datetime, which - * is the number of seconds since 1970-01-01 00:00:00. - * - * @link http://php.net/strtotime - * @param string $method The name of the date construct we want - * @param array $arguments Included for compatibility with our __call usage - * @return int|false datetime - */ - protected function getDate($method, $arguments) - { - $time = $this->model->getElementsByTagName($method); - if ($time->length == 0 || empty($time->item(0)->nodeValue)) { - return false; - } - return strtotime($time->item(0)->nodeValue); - } - - /** - * Get a text construct. - * - * @param string $method The name of the text construct we want - * @param array $arguments Included for compatibility with our __call usage - * @return string - */ - protected function getText($method, $arguments = array()) - { - $tags = $this->model->getElementsByTagName($method); - if ($tags->length > 0) { - $value = $tags->item(0)->nodeValue; - return $value; - } - return false; - } - - /** - * Apply various rules to retrieve category data. - * - * There is no single way of declaring a category in RSS1/1.1 as there is in RSS2 - * and Atom. Instead the usual approach is to use the dublin core namespace to - * declare categories. For example delicious use both: - * <dc:subject>PEAR</dc:subject> and: <taxo:topics><rdf:Bag> - * <rdf:li resource="http://del.icio.us/tag/PEAR" /></rdf:Bag></taxo:topics> - * to declare a categorisation of 'PEAR'. - * - * We need to be sensitive to this where possible. - * - * @param string $call for compatibility with our overloading - * @param array $arguments - arg 0 is the offset, arg 1 is whether to return as array - * @return string|array|false - */ - protected function getCategory($call, $arguments) - { - $categories = $this->model->getElementsByTagName('subject'); - $offset = empty($arguments[0]) ? 0 : $arguments[0]; - $array = empty($arguments[1]) ? false : true; - if ($categories->length <= $offset) { - return false; - } - if ($array) { - $list = array(); - foreach ($categories as $category) { - array_push($list, $category->nodeValue); - } - return $list; - } - return $categories->item($offset)->nodeValue; - } - - /** - * Count occurrences of an element - * - * This function will tell us how many times the element $type - * appears at this level of the feed. - * - * @param string $type the element we want to get a count of - * @return int - */ - protected function count($type) - { - if ($tags = $this->model->getElementsByTagName($type)) { - return $tags->length; - } - return 0; - } - - /** - * Part of our xml:base processing code - * - * We need a couple of methods to access XHTML content stored in feeds. - * This is because we dereference all xml:base references before returning - * the element. This method handles the attributes. - * - * @param DOMElement $node The DOM node we are iterating over - * @return string - */ - function processXHTMLAttributes($node) { - $return = ''; - foreach ($node->attributes as $attribute) { - if ($attribute->name == 'src' or $attribute->name == 'href') { - $attribute->value = $this->addBase(htmlentities($attribute->value, NULL, 'utf-8'), $attribute); - } - if ($attribute->name == 'base') { - continue; - } - $return .= $attribute->name . '="' . htmlentities($attribute->value, NULL, 'utf-8') .'" '; - } - if (! empty($return)) { - return ' ' . trim($return); - } - return ''; - } - - /** - * Convert HTML entities based on the current character set. - * - * @param String - * @return String - */ - function processEntitiesForNodeValue($node) - { - if (function_exists('iconv')) { - $current_encoding = $node->ownerDocument->encoding; - $value = iconv($current_encoding, 'UTF-8', $node->nodeValue); - } else if ($current_encoding == 'iso-8859-1') { - $value = utf8_encode($node->nodeValue); - } else { - $value = $node->nodeValue; - } - - $decoded = html_entity_decode($value, NULL, 'UTF-8'); - return htmlentities($decoded, NULL, 'UTF-8'); - } - - /** - * Part of our xml:base processing code - * - * We need a couple of methods to access XHTML content stored in feeds. - * This is because we dereference all xml:base references before returning - * the element. This method recurs through the tree descending from the node - * and builds our string. - * - * @param DOMElement $node The DOM node we are processing - * @return string - */ - function traverseNode($node) - { - $content = ''; - - /* Add the opening of this node to the content */ - if ($node instanceof DOMElement) { - $content .= '<' . $node->tagName . - $this->processXHTMLAttributes($node) . '>'; - } - - /* Process children */ - if ($node->hasChildNodes()) { - foreach ($node->childNodes as $child) { - $content .= $this->traverseNode($child); - } - } - - if ($node instanceof DOMText) { - $content .= $this->processEntitiesForNodeValue($node); - } - - /* Add the closing of this node to the content */ - if ($node instanceof DOMElement) { - $content .= '</' . $node->tagName . '>'; - } - - return $content; - } - - /** - * Get content from RSS feeds (atom has its own implementation) - * - * The official way to include full content in an RSS1 entry is to use - * the content module's element 'encoded', and RSS2 feeds often duplicate that. - * Often, however, the 'description' element is used instead. We will offer that - * as a fallback. Atom uses its own approach and overrides this method. - * - * @return string|false - */ - protected function getContent() - { - $options = array('encoded', 'description'); - foreach ($options as $element) { - $test = $this->model->getElementsByTagName($element); - if ($test->length == 0) { - continue; - } - if ($test->item(0)->hasChildNodes()) { - $value = ''; - foreach ($test->item(0)->childNodes as $child) { - if ($child instanceof DOMText) { - $value .= $child->nodeValue; - } else { - $simple = simplexml_import_dom($child); - $value .= $simple->asXML(); - } - } - return $value; - } else if ($test->length > 0) { - return $test->item(0)->nodeValue; - } - } - return false; - } - - /** - * Checks if this element has a particular child element. - * - * @param String - * @param Integer - * @return bool - **/ - function hasKey($name, $offset = 0) - { - $search = $this->model->getElementsByTagName($name); - return $search->length > $offset; - } - - /** - * Return an XML serialization of the feed, should it be required. Most - * users however, will already have a serialization that they used when - * instantiating the object. - * - * @return string XML serialization of element - */ - function __toString() - { - $simple = simplexml_import_dom($this->model); - return $simple->asXML(); - } - - /** - * Get directory holding RNG schemas. Method is based on that - * found in Contact_AddressBook. - * - * @return string PEAR data directory. - * @access public - * @static - */ - static function getSchemaDir() - { - require_once 'PEAR/Config.php'; - $config = new PEAR_Config; - return $config->get('data_dir') . '/XML_Feed_Parser/schemas'; - } -} - -?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/atom10-entryonly.xml b/plugins/OStatus/extlib/XML/Feed/samples/atom10-entryonly.xml deleted file mode 100755 index 02e1c5800..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/atom10-entryonly.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<entry xmlns="http://www.w3.org/2005/Atom"> - <title>Atom draft-07 snapshot</title> - <link rel="alternate" type="text/html" - href="http://example.org/2005/04/02/atom"/> - <link rel='enclosure' type="audio/mpeg" length="1337" - href="http://example.org/audio/ph34r_my_podcast.mp3"/> - <id>tag:example.org,2003:3.2397</id> - <updated>2005-07-10T12:29:29Z</updated> - <published>2003-12-13T08:29:29-04:00</published> - <author> - <name>Mark Pilgrim</name> - <uri>http://example.org/</uri> - <email>f8dy@example.com</email> - </author> - <contributor> - <name>Sam Ruby</name> - </contributor> - <contributor> - <name>Joe Gregorio</name> - </contributor> - <content type="xhtml" xml:lang="en" - xml:base="http://diveintomark.org/"> - <div xmlns="http://www.w3.org/1999/xhtml"> - <p><i>[Update: The Atom draft is finished.]</i></p> - </div> - </content> - </entry>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/atom10-example1.xml b/plugins/OStatus/extlib/XML/Feed/samples/atom10-example1.xml deleted file mode 100755 index d181d2b6f..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/atom10-example1.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<feed xmlns="http://www.w3.org/2005/Atom"> - - <title>Example Feed</title> - <link href="http://example.org/"/> - <updated>2003-12-13T18:30:02Z</updated> - <author> - <name>John Doe</name> - </author> - <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id> - - <entry> - <title>Atom-Powered Robots Run Amok</title> - <link href="http://example.org/2003/12/13/atom03"/> - <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> - <updated>2003-12-13T18:30:02Z</updated> - <summary>Some text.</summary> - </entry> - -</feed>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/atom10-example2.xml b/plugins/OStatus/extlib/XML/Feed/samples/atom10-example2.xml deleted file mode 100755 index 98abf9d54..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/atom10-example2.xml +++ /dev/null @@ -1,45 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - <feed xmlns="http://www.w3.org/2005/Atom"> - <title type="text">dive into mark</title> - <subtitle type="html"> - A <em>lot</em> of effort - went into making this effortless - </subtitle> - <updated>2005-07-31T12:29:29Z</updated> - <id>tag:example.org,2003:3</id> - <link rel="alternate" type="text/html" - hreflang="en" href="http://example.org/"/> - <link rel="self" type="application/atom+xml" - href="http://example.org/feed.atom"/> - <rights>Copyright (c) 2003, Mark Pilgrim</rights> - <generator uri="http://www.example.com/" version="1.0"> - Example Toolkit - </generator> - <entry> - <title>Atom draft-07 snapshot</title> - <link rel="alternate" type="text/html" - href="http://example.org/2005/04/02/atom"/> - <link rel='enclosure' type="audio/mpeg" length="1337" - href="http://example.org/audio/ph34r_my_podcast.mp3"/> - <id>tag:example.org,2003:3.2397</id> - <updated>2005-07-31T12:29:29Z</updated> - <published>2003-12-13T08:29:29-04:00</published> - <author> - <name>Mark Pilgrim</name> - <uri>http://example.org/</uri> - <email>f8dy@example.com</email> - </author> - <contributor> - <name>Sam Ruby</name> - </contributor> - <contributor> - <name>Joe Gregorio</name> - </contributor> - <content type="xhtml" xml:lang="en" - xml:base="http://diveintomark.org/"> - <div xmlns="http://www.w3.org/1999/xhtml"> - <p><i>[Update: The Atom draft is finished.]</i></p> - </div> - </content> - </entry> - </feed>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/delicious.feed b/plugins/OStatus/extlib/XML/Feed/samples/delicious.feed deleted file mode 100755 index 32f9fa493..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/delicious.feed +++ /dev/null @@ -1,177 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> - -<rdf:RDF - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns="http://purl.org/rss/1.0/" - xmlns:cc="http://web.resource.org/cc/" - xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:syn="http://purl.org/rss/1.0/modules/syndication/" - xmlns:admin="http://webns.net/mvcb/" -> -<channel rdf:about="http://del.icio.us/tag/greenbelt"> -<title>del.icio.us/tag/greenbelt</title> -<link>http://del.icio.us/tag/greenbelt</link> -<description>Text</description> -<items> - <rdf:Seq> - <rdf:li rdf:resource="http://www.greenbelt.org.uk/" /> - <rdf:li rdf:resource="http://www.greenbelt.org.uk/" /> - <rdf:li rdf:resource="http://www.natuerlichwien.at/rundumadum/dergruenguertel/" /> - <rdf:li rdf:resource="http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars" /> - <rdf:li rdf:resource="http://www.greenbelt.ca/home.htm" /> - <rdf:li rdf:resource="http://pipwilsonbhp.blogspot.com/" /> - <rdf:li rdf:resource="http://maggidawn.typepad.com/maggidawn/" /> - <rdf:li rdf:resource="http://www.johndavies.org/" /> - <rdf:li rdf:resource="http://jonnybaker.blogs.com/" /> - </rdf:Seq> -</items> -</channel> - -<item rdf:about="http://www.greenbelt.org.uk/"> -<dc:title>Greenbelt - Homepage Section</dc:title> -<link>http://www.greenbelt.org.uk/</link> -<dc:creator>jonnybaker</dc:creator> -<dc:date>2005-05-16T16:30:38Z</dc:date> -<dc:subject>greenbelt</dc:subject> -<taxo:topics> - <rdf:Bag> - <rdf:li resource="http://del.icio.us/tag/greenbelt" /> - </rdf:Bag> -</taxo:topics> -</item> - -<item rdf:about="http://www.greenbelt.org.uk/"> -<title>Greenbelt festival (uk)</title> -<link>http://www.greenbelt.org.uk/</link> -<dc:creator>sssshhhh</dc:creator> -<dc:date>2005-05-14T18:19:40Z</dc:date> -<dc:subject>audiology festival gigs greenbelt</dc:subject> -<taxo:topics> - <rdf:Bag> - <rdf:li resource="http://del.icio.us/tag/gigs" /> - <rdf:li resource="http://del.icio.us/tag/audiology" /> - <rdf:li resource="http://del.icio.us/tag/festival" /> - <rdf:li resource="http://del.icio.us/tag/greenbelt" /> - </rdf:Bag> -</taxo:topics> -</item> - -<item rdf:about="http://www.natuerlichwien.at/rundumadum/dergruenguertel/"> -<title>Natuerlichwien.at - Rundumadum</title> -<link>http://www.natuerlichwien.at/rundumadum/dergruenguertel/</link> -<dc:creator>egmilman47</dc:creator> -<dc:date>2005-05-06T21:33:41Z</dc:date> -<dc:subject>Austria Vienna Wien greenbelt nature walking</dc:subject> -<taxo:topics> - <rdf:Bag> - <rdf:li resource="http://del.icio.us/tag/Vienna" /> - <rdf:li resource="http://del.icio.us/tag/Wien" /> - <rdf:li resource="http://del.icio.us/tag/Austria" /> - <rdf:li resource="http://del.icio.us/tag/walking" /> - <rdf:li resource="http://del.icio.us/tag/nature" /> - <rdf:li resource="http://del.icio.us/tag/greenbelt" /> - </rdf:Bag> -</taxo:topics> -</item> - -<item rdf:about="http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars"> -<title>Tank - GBMediaWiki</title> -<link>http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars</link> -<dc:creator>jystewart</dc:creator> -<dc:date>2005-03-21T22:44:11Z</dc:date> -<dc:subject>greenbelt</dc:subject> -<taxo:topics> - <rdf:Bag> - <rdf:li resource="http://del.icio.us/tag/greenbelt" /> - </rdf:Bag> -</taxo:topics> -</item> - -<item rdf:about="http://www.greenbelt.ca/home.htm"> -<title>Greenbelt homepage</title> -<link>http://www.greenbelt.ca/home.htm</link> -<dc:creator>Gooberoo</dc:creator> -<dc:date>2005-03-01T22:43:17Z</dc:date> -<dc:subject>greenbelt ontario</dc:subject> -<taxo:topics> - <rdf:Bag> - <rdf:li resource="http://del.icio.us/tag/ontario" /> - <rdf:li resource="http://del.icio.us/tag/greenbelt" /> - </rdf:Bag> -</taxo:topics> -</item> - -<item rdf:about="http://pipwilsonbhp.blogspot.com/"> -<title>Pip Wilson bhp ...... blog</title> -<link>http://pipwilsonbhp.blogspot.com/</link> -<dc:creator>sssshhhh</dc:creator> -<dc:date>2004-12-27T11:20:51Z</dc:date> -<dc:subject>Greenbelt friend ideas links thinking weblog</dc:subject> -<taxo:topics> - <rdf:Bag> - <rdf:li resource="http://del.icio.us/tag/Greenbelt" /> - <rdf:li resource="http://del.icio.us/tag/thinking" /> - <rdf:li resource="http://del.icio.us/tag/ideas" /> - <rdf:li resource="http://del.icio.us/tag/links" /> - <rdf:li resource="http://del.icio.us/tag/friend" /> - <rdf:li resource="http://del.icio.us/tag/weblog" /> - </rdf:Bag> -</taxo:topics> -</item> - -<item rdf:about="http://maggidawn.typepad.com/maggidawn/"> -<title>maggi dawn</title> -<link>http://maggidawn.typepad.com/maggidawn/</link> -<dc:creator>sssshhhh</dc:creator> -<dc:date>2004-12-27T11:20:11Z</dc:date> -<dc:subject>Greenbelt ideas links thinking weblog</dc:subject> -<taxo:topics> - <rdf:Bag> - <rdf:li resource="http://del.icio.us/tag/Greenbelt" /> - <rdf:li resource="http://del.icio.us/tag/thinking" /> - <rdf:li resource="http://del.icio.us/tag/ideas" /> - <rdf:li resource="http://del.icio.us/tag/links" /> - <rdf:li resource="http://del.icio.us/tag/weblog" /> - </rdf:Bag> -</taxo:topics> -</item> - -<item rdf:about="http://www.johndavies.org/"> -<title>John Davies</title> -<link>http://www.johndavies.org/</link> -<dc:creator>sssshhhh</dc:creator> -<dc:date>2004-12-27T11:18:37Z</dc:date> -<dc:subject>Greenbelt ideas links thinking weblog</dc:subject> -<taxo:topics> - <rdf:Bag> - <rdf:li resource="http://del.icio.us/tag/Greenbelt" /> - <rdf:li resource="http://del.icio.us/tag/thinking" /> - <rdf:li resource="http://del.icio.us/tag/ideas" /> - <rdf:li resource="http://del.icio.us/tag/links" /> - <rdf:li resource="http://del.icio.us/tag/weblog" /> - </rdf:Bag> -</taxo:topics> -</item> - -<item rdf:about="http://jonnybaker.blogs.com/"> -<title>jonnybaker</title> -<link>http://jonnybaker.blogs.com/</link> -<dc:creator>sssshhhh</dc:creator> -<dc:date>2004-12-27T11:18:17Z</dc:date> -<dc:subject>Greenbelt event ideas links resources thinking weblog youth</dc:subject> -<taxo:topics> - <rdf:Bag> - <rdf:li resource="http://del.icio.us/tag/Greenbelt" /> - <rdf:li resource="http://del.icio.us/tag/thinking" /> - <rdf:li resource="http://del.icio.us/tag/ideas" /> - <rdf:li resource="http://del.icio.us/tag/links" /> - <rdf:li resource="http://del.icio.us/tag/weblog" /> - <rdf:li resource="http://del.icio.us/tag/youth" /> - <rdf:li resource="http://del.icio.us/tag/event" /> - <rdf:li resource="http://del.icio.us/tag/resources" /> - </rdf:Bag> -</taxo:topics> -</item> - -</rdf:RDF> diff --git a/plugins/OStatus/extlib/XML/Feed/samples/flickr.feed b/plugins/OStatus/extlib/XML/Feed/samples/flickr.feed deleted file mode 100755 index 57e83af57..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/flickr.feed +++ /dev/null @@ -1,184 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?>
-<feed version="0.3" xmlns="http://purl.org/atom/ns#"
- xmlns:dc="http://purl.org/dc/elements/1.1/">
-
- <title>jamesstewart - Everyone's Tagged Photos</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/tags/jamesstewart/"/>
- <link rel="icon" type="image/jpeg" href="http://www.flickr.com/images/buddyicon.jpg"/>
- <info type="text/html" mode="escaped">A feed of jamesstewart - Everyone's Tagged Photos</info>
- <modified>2005-08-01T18:50:26Z</modified>
- <generator url="http://www.flickr.com/">Flickr</generator>
-
- <entry>
- <title>Oma and James</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/30484029@N00/30367516/"/>
- <link rel='enclosure' type="application/xml" href="http://james.anthropiccollective.org" />
- <id>tag:flickr.com,2004:/photo/30367516</id>
- <issued>2005-08-01T18:50:26Z</issued>
- <modified>2005-08-01T18:50:26Z</modified>
- <content type="text/html" mode="escaped"><p><a href="http://www.flickr.com/people/30484029@N00/">kstewart</a> posted a photo:</p>
-
-<p><a href="http://www.flickr.com/photos/30484029@N00/30367516/" title="Oma and James"><img src="http://photos23.flickr.com/30367516_1f685a16e8_m.jpg" width="240" height="180" alt="Oma and James" style="border: 1px solid #000000;" /></a></p>
-
-<p>I have a beautiful Oma and a gorgeous husband.</p></content>
- <author>
- <name>kstewart</name>
- <url>http://www.flickr.com/people/30484029@N00/</url>
- </author>
- <dc:subject>jamesstewart oma stoelfamily</dc:subject>
- </entry>
- <entry>
- <title></title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21376174/"/>
- <id>tag:flickr.com,2004:/photo/21376174</id>
- <issued>2005-06-25T02:00:35Z</issued>
- <modified>2005-06-25T02:00:35Z</modified>
- <content type="text/html" mode="escaped"><p><a href="http://www.flickr.com/people/buddscreek/">Lan Rover</a> posted a photo:</p>
-
-<p><a href="http://www.flickr.com/photos/buddscreek/21376174/" title=""><img src="http://photos17.flickr.com/21376174_4314fd8d5c_m.jpg" width="240" height="160" alt="" style="border: 1px solid #000000;" /></a></p>
-
-<p>AMA Motocross Championship 2005, Budds Creek, Maryland</p></content>
- <author>
- <name>Lan Rover</name>
- <url>http://www.flickr.com/people/buddscreek/</url>
- </author>
- <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational rickycarmichael 259 jamesstewart 4</dc:subject>
- </entry>
- <entry>
- <title></title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21375650/"/>
- <id>tag:flickr.com,2004:/photo/21375650</id>
- <issued>2005-06-25T01:56:24Z</issued>
- <modified>2005-06-25T01:56:24Z</modified>
- <content type="text/html" mode="escaped"><p><a href="http://www.flickr.com/people/buddscreek/">Lan Rover</a> posted a photo:</p>
-
-<p><a href="http://www.flickr.com/photos/buddscreek/21375650/" title=""><img src="http://photos16.flickr.com/21375650_5c60e0dab1_m.jpg" width="240" height="160" alt="" style="border: 1px solid #000000;" /></a></p>
-
-</content>
- <author>
- <name>Lan Rover</name>
- <url>http://www.flickr.com/people/buddscreek/</url>
- </author>
- <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart</dc:subject>
- </entry>
- <entry>
- <title></title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21375345/"/>
- <id>tag:flickr.com,2004:/photo/21375345</id>
- <issued>2005-06-25T01:54:11Z</issued>
- <modified>2005-06-25T01:54:11Z</modified>
- <content type="text/html" mode="escaped"><p><a href="http://www.flickr.com/people/buddscreek/">Lan Rover</a> posted a photo:</p>
-
-<p><a href="http://www.flickr.com/photos/buddscreek/21375345/" title=""><img src="http://photos15.flickr.com/21375345_4205fdd22b_m.jpg" width="160" height="240" alt="" style="border: 1px solid #000000;" /></a></p>
-
-</content>
- <author>
- <name>Lan Rover</name>
- <url>http://www.flickr.com/people/buddscreek/</url>
- </author>
- <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart</dc:subject>
- </entry>
- <entry>
- <title>Lunch with Kari & James, café in the crypt of St Martin in the fields</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/16516618/"/>
- <id>tag:flickr.com,2004:/photo/16516618</id>
- <issued>2005-05-30T21:56:39Z</issued>
- <modified>2005-05-30T21:56:39Z</modified>
- <content type="text/html" mode="escaped"><p><a href="http://www.flickr.com/people/fidothe/">fidothe</a> posted a photo:</p>
-
-<p><a href="http://www.flickr.com/photos/fidothe/16516618/" title="Lunch with Kari &amp; James, café in the crypt of St Martin in the fields"><img src="http://photos14.flickr.com/16516618_afaa4a395e_m.jpg" width="240" height="180" alt="Lunch with Kari &amp; James, café in the crypt of St Martin in the fields" style="border: 1px solid #000000;" /></a></p>
-
-</content>
- <author>
- <name>fidothe</name>
- <url>http://www.flickr.com/people/fidothe/</url>
- </author>
- <dc:subject>nokia7610 london stmartininthefields clarepatterson jamesstewart parvinstewart jimstewart susanstewart</dc:subject>
- </entry>
- <entry>
- <title>Stewart keeping it low over the obstacle.</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/pqbon/10224728/"/>
- <id>tag:flickr.com,2004:/photo/10224728</id>
- <issued>2005-04-21T07:30:29Z</issued>
- <modified>2005-04-21T07:30:29Z</modified>
- <content type="text/html" mode="escaped"><p><a href="http://www.flickr.com/people/pqbon/">pqbon</a> posted a photo:</p>
-
-<p><a href="http://www.flickr.com/photos/pqbon/10224728/" title="Stewart keeping it low over the obstacle."><img src="http://photos7.flickr.com/10224728_b756341957_m.jpg" width="240" height="180" alt="Stewart keeping it low over the obstacle." style="border: 1px solid #000000;" /></a></p>
-
-</content>
- <author>
- <name>pqbon</name>
- <url>http://www.flickr.com/people/pqbon/</url>
- </author>
- <dc:subject>ama hangtown motocross jamesstewart bubba</dc:subject>
- </entry>
- <entry>
- <title>king james stewart</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/jjlook/7152910/"/>
- <id>tag:flickr.com,2004:/photo/7152910</id>
- <issued>2005-03-22T21:53:37Z</issued>
- <modified>2005-03-22T21:53:37Z</modified>
- <content type="text/html" mode="escaped"><p><a href="http://www.flickr.com/people/jjlook/">jj look</a> posted a photo:</p>
-
-<p><a href="http://www.flickr.com/photos/jjlook/7152910/" title="king james stewart"><img src="http://photos7.flickr.com/7152910_a02ab5a750_m.jpg" width="180" height="240" alt="king james stewart" style="border: 1px solid #000000;" /></a></p>
-
-<p>11th</p></content>
- <author>
- <name>jj look</name>
- <url>http://www.flickr.com/people/jjlook/</url>
- </author>
- <dc:subject>dilomar05 eastside austin texas 78702 kingjames stewart jamesstewart borrowed</dc:subject>
- </entry>
- <entry>
- <title>It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586562/"/>
- <id>tag:flickr.com,2004:/photo/1586562</id>
- <issued>2004-11-20T09:34:28Z</issued>
- <modified>2004-11-20T09:34:28Z</modified>
- <content type="text/html" mode="escaped"><p><a href="http://www.flickr.com/people/fidothe/">fidothe</a> posted a photo:</p>
-
-<p><a href="http://www.flickr.com/photos/fidothe/1586562/" title="It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)"><img src="http://photos2.flickr.com/1586562_0bc5313a3e_m.jpg" width="240" height="180" alt="It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)" style="border: 1px solid #000000;" /></a></p>
-
-</content>
- <author>
- <name>fidothe</name>
- <url>http://www.flickr.com/people/fidothe/</url>
- </author>
- <dc:subject>holiday grandrapids jamesstewart</dc:subject>
- </entry>
- <entry>
- <title>It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586539/"/>
- <id>tag:flickr.com,2004:/photo/1586539</id>
- <issued>2004-11-20T09:28:16Z</issued>
- <modified>2004-11-20T09:28:16Z</modified>
- <content type="text/html" mode="escaped"><p><a href="http://www.flickr.com/people/fidothe/">fidothe</a> posted a photo:</p>
-
-<p><a href="http://www.flickr.com/photos/fidothe/1586539/" title="It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)"><img src="http://photos2.flickr.com/1586539_c51e5f2e7a_m.jpg" width="240" height="180" alt="It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)" style="border: 1px solid #000000;" /></a></p>
-
-</content>
- <author>
- <name>fidothe</name>
- <url>http://www.flickr.com/people/fidothe/</url>
- </author>
- <dc:subject>holiday grandrapids jamesstewart</dc:subject>
- </entry>
- <entry>
- <title>It's a Grind, James and Jim can't decide)</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586514/"/>
- <id>tag:flickr.com,2004:/photo/1586514</id>
- <issued>2004-11-20T09:25:05Z</issued>
- <modified>2004-11-20T09:25:05Z</modified>
- <content type="text/html" mode="escaped"><p><a href="http://www.flickr.com/people/fidothe/">fidothe</a> posted a photo:</p>
-
-<p><a href="http://www.flickr.com/photos/fidothe/1586514/" title="It's a Grind, James and Jim can't decide)"><img src="http://photos2.flickr.com/1586514_733c2dfa3e_m.jpg" width="240" height="180" alt="It's a Grind, James and Jim can't decide)" style="border: 1px solid #000000;" /></a></p>
-
-</content>
- <author>
- <name>fidothe</name>
- <url>http://www.flickr.com/people/fidothe/</url>
- </author>
- <dc:subject>holiday grandrapids jamesstewart johnkentish</dc:subject>
- </entry>
-
-</feed>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/grwifi-atom.xml b/plugins/OStatus/extlib/XML/Feed/samples/grwifi-atom.xml deleted file mode 100755 index c351d3c16..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/grwifi-atom.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="iso-8859-1"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en">
<title>Updates to Grand Rapids WiFi hotspot details</title>
<link rel="alternate" type="text/html" href="http://grwifi.net/"/>
<link rel="self" type="application/atom+xml" href="http://grwifi.net/atom/locations"/>
<updated>2005-09-01T15:43:01-05:00</updated>
<subtitle>WiFi Hotspots in Grand Rapids, MI</subtitle>
<id>http://grwifi.net/atom/locations</id>
<rights>Creative Commons Attribution-NonCommercial-ShareAlike 2.0 http://creativecommons.org/licenses/by-nc-sa/2.0/ </rights>
<entry>
<title>Hotspot Details Updated: Sweetwaters</title>
<link rel="alternate" type="text/html" href="http://grwifi.net/location/sweetwaters"/>
<id>http://grwifi.net/location/sweetwaters</id>
<updated>2005-09-01T15:43:01-05:00</updated>
<summary type="html">
The details of the WiFi hotspot at: Sweetwaters have been updated. Find out more at: -http://grwifi.net/location/sweetwaters
</summary>
<author>
<name>James</name>
<uri>http://jystewart.net</uri>
<email>james@jystewart.net</email> </author>
<dc:subject>wifi hotspot</dc:subject>
</entry>
<entry>
<title>Hotspot Details Updated: Common Ground Coffee Shop</title>
<link rel="alternate" type="text/html" href="http://grwifi.net/location/common-ground"/>
<id>http://grwifi.net/location/common-ground</id>
<updated>2005-09-01T15:42:39-05:00</updated>
<summary type="html">
The details of the WiFi hotspot at: Common Ground Coffee Shop have been updated. Find out more at: -http://grwifi.net/location/common-ground
</summary>
<author>
<name>James</name>
<uri>http://jystewart.net</uri>
<email>james@jystewart.net</email> </author>
<dc:subject>wifi hotspot</dc:subject>
</entry>
<entry>
<title>Hotspot Details Updated: Grand Rapids Public Library, Main Branch</title>
<link rel="alternate" type="text/html" href="http://grwifi.net/location/grpl-main-branch"/>
<id>http://grwifi.net/location/grpl-main-branch</id>
<updated>2005-09-01T15:42:20-05:00</updated>
<summary type="html">
The details of the WiFi hotspot at: Grand Rapids Public Library, Main Branch have been updated. Find out more at: -http://grwifi.net/location/grpl-main-branch
</summary>
<author>
<name>James</name>
<uri>http://jystewart.net</uri>
<email>james@jystewart.net</email> </author>
<dc:subject>wifi hotspot</dc:subject>
</entry>
<entry>
<title>Hotspot Details Updated: Four Friends Coffee House</title>
<link rel="alternate" type="text/html" href="http://grwifi.net/location/four-friends"/>
<id>http://grwifi.net/location/four-friends</id>
<updated>2005-09-01T15:41:35-05:00</updated>
<summary type="html">
The details of the WiFi hotspot at: Four Friends Coffee House have been updated. Find out more at: -http://grwifi.net/location/four-friends
</summary>
<author>
<name>James</name>
<uri>http://jystewart.net</uri>
<email>james@jystewart.net</email> </author>
<dc:subject>wifi hotspot</dc:subject>
</entry>
<entry>
<title>Hotspot Details Updated: Barnes and Noble, Rivertown Crossings</title>
<link rel="alternate" type="text/html" href="http://grwifi.net/location/barnes-noble-rivertown"/>
<id>http://grwifi.net/location/barnes-noble-rivertown</id>
<updated>2005-09-01T15:40:41-05:00</updated>
<summary type="html">
The details of the WiFi hotspot at: Barnes and Noble, Rivertown Crossings have been updated. Find out more at: -http://grwifi.net/location/barnes-noble-rivertown
</summary>
<author>
<name>James</name>
<uri>http://jystewart.net</uri>
<email>james@jystewart.net</email> </author>
<dc:subject>wifi hotspot</dc:subject>
</entry>
<entry>
<title>Hotspot Details Updated: The Boss Sports Bar & Grille</title>
<link rel="alternate" type="text/html" href="http://grwifi.net/location/boss-sports-bar"/>
<id>http://grwifi.net/location/boss-sports-bar</id>
<updated>2005-09-01T15:40:19-05:00</updated>
<summary type="html">
The details of the WiFi hotspot at: The Boss Sports Bar & Grille have been updated. Find out more at: -http://grwifi.net/location/boss-sports-bar
</summary>
<author>
<name>James</name>
<uri>http://jystewart.net</uri>
<email>james@jystewart.net</email> </author>
<dc:subject>wifi hotspot</dc:subject>
</entry>
</feed>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/hoder.xml b/plugins/OStatus/extlib/XML/Feed/samples/hoder.xml deleted file mode 100755 index 099463570..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/hoder.xml +++ /dev/null @@ -1,102 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<rss version="2.0" - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" - xmlns:admin="http://webns.net/mvcb/" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> - -<channel> -<title>Editor: Myself (Persian)</title> -<link>http://editormyself.info</link> -<description>This is a Persian (Farsi) weblog, written by Hossein Derakhshan (aka, Hoder), an Iranian Multimedia designer and a journalist who lives in Toronto since Dec 2000. He also keeps an English weblog with the same name.</description> -<dc:language>en-us</dc:language> -<dc:creator>hoder@hotmail.com</dc:creator> -<dc:date>2005-10-12T19:45:32-05:00</dc:date> -<admin:generatorAgent rdf:resource="http://www.movabletype.org/?v=3.15" /> -<sy:updatePeriod>hourly</sy:updatePeriod> -<sy:updateFrequency>1</sy:updateFrequency> -<sy:updateBase>2000-01-01T12:00+00:00</sy:updateBase> - - -<item> -<title>لينکدونی | جلسهی امریکن انترپرایز برای تقسیم قومی ایران</title> -<link>http://www.aei.org/events/type.upcoming,eventID.1166,filter.all/event_detail.asp</link> -<description>چطور بعضیها فکر میکنند دست راستیهای آمریکا از خامنهای ملیگراترند</description> -<guid isPermaLink="false">14645@http://i.hoder.com/</guid> -<dc:subject>iran</dc:subject> -<dc:date>2005-10-12T19:45:32-05:00</dc:date> -</item> - -<item> -<title>لينکدونی | به صبحانه آگهی بدهید</title> -<link>http://www.adbrite.com/mb/commerce/purchase_form.php?opid=24346&afsid=1</link> -<description>خیلی ارزان و راحت است</description> -<guid isPermaLink="false">14644@http://i.hoder.com/</guid> -<dc:subject>media/journalism</dc:subject> -<dc:date>2005-10-12T17:23:15-05:00</dc:date> -</item> - -<item> -<title>لينکدونی | نیروی انتظامی چگونه تابوهای همجنسگرایانه را میشکند؛ فرنگوپولیس</title> -<link>http://farangeopolis.blogspot.com/2005/10/blog-post_08.html</link> -<description>از پس و پیش و حاشیهی این ماجرا میتوان یک مستند بینظیر ساخت</description> -<guid isPermaLink="false">14643@http://i.hoder.com/</guid> -<dc:subject>soc_popculture</dc:subject> -<dc:date>2005-10-12T17:06:40-05:00</dc:date> -</item> - -<item> -<title>لينکدونی | بازتاب توقیف شد</title> -<link>http://www.baztab.com/news/30201.php</link> -<description>اگر گفتید یک وبسایت را چطور توقیف میکنند؟ لابد ماوسشان را قایم میکنند.</description> -<guid isPermaLink="false">14642@http://i.hoder.com/</guid> -<dc:subject>media/journalism</dc:subject> -<dc:date>2005-10-12T14:41:57-05:00</dc:date> -</item> - -<item> -<title>لينکدونی | رشد وب در سال 2005 از همیشه بیشتر بوده است" بی.بی.سی</title> -<link>http://news.bbc.co.uk/2/hi/technology/4325918.stm</link> -<description></description> -<guid isPermaLink="false">14640@http://i.hoder.com/</guid> -<dc:subject>tech</dc:subject> -<dc:date>2005-10-12T13:04:46-05:00</dc:date> -</item> - - - -<item> -<title>==قرعه کشی گرین کارد به زودی شروع میشود==</title> -<link>http://nice.newsxphotos.biz/05/09/2007_dv_lottery_registration_to_begin_oct_5_14589.php</link> -<description></description> -<guid isPermaLink="false">14613@http://vagrantly.com</guid> -<dc:subject>ads03</dc:subject> -<dc:date>2005-09-27T04:49:22-05:00</dc:date> -</item> - - - - - - -<item> -<title>پروژهی هاروارد، قدم دوم</title> -<link>http://editormyself.info/archives/2005/10/051012_014641.shtml</link> -<description><![CDATA[<p>اگر یادتان باشد <a href="/archives/2005/09/050906_014504.shtml">چند وقت پیش نوشتم</a> که دانشگاه هاروارد پروژهای دارد با نام آواهای جهانی که در آن به وبلاگهای غیر انگلیسیزبان میپردازد. خواشتم که اگر کسی علاقه دارد ایمیل بزند. تعداد زیادی جواب دادند و ابراز علاقه کردند. حالا وقت قدم دوم است.</p> - -<p>قدم دوم این است که برای اینکه مسوولین پروژه بتوانند تصمیم بگیرند که با چه کسی کار کنند، میخواهند نمونهی کارهای علاقمندان مشارکت در این پرزوه را ببینند.</p> - -<p>برای همین از همهی علاقماندان، حتی کسانی که قبلا اعلام آمادگی نکرده بودند، میخواهم که یک موضوع رایج این روزهای وبلاگستان فارسی را انتخاب کنند و در هفتصد کلمه، به انگلیسی، بنویسند که وبلاگدارهای دربارهاش چه میگویند. لینک به پنج، شش وبلاگ و بازنویسی آنچه آنها از جنبههای گوناگون دربارهی آن موضوع نوشتهاند با نقل قول مستقیم از آنها (البته ترجمه شده از فارسی) کافی است. دو سه جمله هم اول کار توضیح دهید که چرا این موضوع مهم است.</p> - -<p>متن نمونه را به آدرس ایمیل من hoder@hoder.com و نیز برای افراد زیر تا روز دوشنبه بفرستید:<br /> -ربکا : rmackinnon@cyber.law.harvard.edu<br /> -هیثم: haitham.sabbah@gmail.com</p>]]></description> -<guid isPermaLink="false">14641@http://editormyself.info</guid> -<dc:subject>weblog</dc:subject> -<dc:date>2005-10-12T14:04:23-05:00</dc:date> -</item> - - - -</channel> -</rss>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/illformed_atom10.xml b/plugins/OStatus/extlib/XML/Feed/samples/illformed_atom10.xml deleted file mode 100755 index 612186897..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/illformed_atom10.xml +++ /dev/null @@ -1,13 +0,0 @@ -<!--
-Description: entry author name
-Expect: bozo and entries[0]['author_detail']['name'] == u'Example author'
--->
-<feed xmlns="http://www.w3.org/2005/Atom">
-<entry>
- <author>
- <name>Example author</name>
- <email>me@example.com</email>
- <uri>http://example.com/</uri>
- </author>
-</entry>
-</feed
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss091-complete.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss091-complete.xml deleted file mode 100755 index b0a1fee2d..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/rss091-complete.xml +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0"?> -<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd"> -<rss version="0.91"> -<channel> -<copyright>Copyright 1997-1999 UserLand Software, Inc.</copyright> -<pubDate>Thu, 08 Jul 1999 07:00:00 GMT</pubDate> -<lastBuildDate>Thu, 08 Jul 1999 16:20:26 GMT</lastBuildDate> -<docs>http://my.userland.com/stories/storyReader$11</docs> -<description>News and commentary from the cross-platform scripting community.</description> -<link>http://www.scripting.com/</link> -<title>Scripting News</title> -<image> -<link>http://www.scripting.com/</link> -<title>Scripting News</title> -<url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url> -<height>40</height> -<width>78</width> -<description>What is this used for?</description> -</image> -<managingEditor>dave@userland.com (Dave Winer)</managingEditor> -<webMaster>dave@userland.com (Dave Winer)</webMaster> -<language>en-us</language> -<skipHours> -<hour>6</hour> -<hour>7</hour> -<hour>8</hour> -<hour>9</hour> -<hour>10</hour> -<hour>11</hour> -</skipHours> -<skipDays> -<day>Sunday</day> -</skipDays> -<rating>(PICS-1.1 "http://www.rsac.org/ratingsv01.html" l gen true comment "RSACi North America Server" for "http://www.rsac.org" on "1996.04.16T08:15-0500" r (n 0 s 0 v 0 l 0))</rating> -<item> -<title>stuff</title> -<link>http://bar</link> -<description>This is an article about some stuff</description> -</item> -<textinput> -<title>Search Now!</title> -<description>Enter your search <terms></description> -<name>find</name> -<link>http://my.site.com/search.cgi</link> -</textinput> -</channel> -</rss>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss091-international.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss091-international.xml deleted file mode 100755 index cfe91691f..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/rss091-international.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="EuC-JP"?> -<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd"> -<rss version="0.91"> -<channel> -<title>膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ</title> -<link>http://www.mozilla.org</link> -<description>膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ</description> -<language>ja</language> <!-- tagged as Japanese content --> -<item> -<title>NYÒ™Á¢¸»ÌêÛì15285.25´ƒ´‘Á£´Û´—´ÀÁ¹´ê´Ì´éÒ™Ûì¡êçÒÕ‰ÌêÁ£</title> -<link>http://www.mozilla.org/status/</link> -<description>This is an item description...</description> -</item> -<item> -<title>‚§±Çç¡ËßÛÂÒéøÓ¸Á£Ë²®Ÿè†Ûèå±ÇÌ’¡Íæ—éøë‡Á£</title> -<link>http://www.mozilla.org/status/</link> -<description>This is an item description...</description> -</item> -<item> -<title>ËÜË”ïÌëÈšÁ¢È†Ë§æàÀ豎ˉۂÁ¢Ë‚åܼšÛ˜íËüËÁ£</title> -<link>http://www.mozilla.org/status/</link> -<description>This is an item description...</description> -</item> -<item> -<title>2000‚øíŠåÁ¢«‘¦éÛë¹ÛçéÛ§ÛÂè†ÒæÓ¸Á£Ì¾«…æ—ÕÝéøƒ¸Á£</title> -<link>http://www.mozilla.org/status/</link> -<description>This is an item description...</description> -</item> -</channel> -</rss>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss091-simple.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss091-simple.xml deleted file mode 100755 index f0964a227..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/rss091-simple.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0"?> -<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd"> -<rss version="0.91"> -<channel> -<language>en</language> -<description>News and commentary from the cross-platform scripting community.</description> -<link>http://www.scripting.com/</link> -<title>Scripting News</title> -<image> -<link>http://www.scripting.com/</link> -<title>Scripting News</title> -<url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url> -</image> -</channel> -</rss>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss092-sample.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss092-sample.xml deleted file mode 100755 index 5d75c352b..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/rss092-sample.xml +++ /dev/null @@ -1,103 +0,0 @@ -<?xml version="1.0"?> -<!-- RSS generation done by 'Radio UserLand' on Fri, 13 Apr 2001 19:23:02 GMT --> -<rss version="0.92"> - <channel> - <title>Dave Winer: Grateful Dead</title> - <link>http://www.scripting.com/blog/categories/gratefulDead.html</link> - <description>A high-fidelity Grateful Dead song every day. This is where we're experimenting with enclosures on RSS news items that download when you're not using your computer. If it works (it will) it will be the end of the Click-And-Wait multimedia experience on the Internet. </description> - <lastBuildDate>Fri, 13 Apr 2001 19:23:02 GMT</lastBuildDate> - <docs>http://backend.userland.com/rss092</docs> - <managingEditor>dave@userland.com (Dave Winer)</managingEditor> - <webMaster>dave@userland.com (Dave Winer)</webMaster> - <cloud domain="data.ourfavoritesongs.com" port="80" path="/RPC2" registerProcedure="ourFavoriteSongs.rssPleaseNotify" protocol="xml-rpc"/> - <item> - <description>It's been a few days since I added a song to the Grateful Dead channel. Now that there are all these new Radio users, many of whom are tuned into this channel (it's #16 on the hotlist of upstreaming Radio users, there's no way of knowing how many non-upstreaming users are subscribing, have to do something about this..). Anyway, tonight's song is a live version of Weather Report Suite from Dick's Picks Volume 7. It's wistful music. Of course a beautiful song, oft-quoted here on Scripting News. <i>A little change, the wind and rain.</i> -</description> - <enclosure url="http://www.scripting.com/mp3s/weatherReportDicksPicsVol7.mp3" length="6182912" type="audio/mpeg"/> - </item> - <item> - <description>Kevin Drennan started a <a href="http://deadend.editthispage.com/">Grateful Dead Weblog</a>. Hey it's cool, he even has a <a href="http://deadend.editthispage.com/directory/61">directory</a>. <i>A Frontier 7 feature.</i></description> - <source url="http://scriptingnews.userland.com/xml/scriptingNews2.xml">Scripting News</source> - </item> - <item> - <description><a href="http://arts.ucsc.edu/GDead/AGDL/other1.html">The Other One</a>, live instrumental, One From The Vault. Very rhythmic very spacy, you can listen to it many times, and enjoy something new every time.</description> - <enclosure url="http://www.scripting.com/mp3s/theOtherOne.mp3" length="6666097" type="audio/mpeg"/> - </item> - <item> - <description>This is a test of a change I just made. Still diggin..</description> - </item> - <item> - <description>The HTML rendering almost <a href="http://validator.w3.org/check/referer">validates</a>. Close. Hey I wonder if anyone has ever published a style guide for ALT attributes on images? What are you supposed to say in the ALT attribute? I sure don't know. If you're blind send me an email if u cn rd ths. </description> - </item> - <item> - <description><a href="http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Franklin's_Tower.txt">Franklin's Tower</a>, a live version from One From The Vault.</description> - <enclosure url="http://www.scripting.com/mp3s/franklinsTower.mp3" length="6701402" type="audio/mpeg"/> - </item> - <item> - <description>Moshe Weitzman says Shakedown Street is what I'm lookin for for tonight. I'm listening right now. It's one of my favorites. "Don't tell me this town ain't got no heart." Too bright. I like the jazziness of Weather Report Suite. Dreamy and soft. How about The Other One? "Spanish lady come to me.."</description> - <source url="http://scriptingnews.userland.com/xml/scriptingNews2.xml">Scripting News</source> - </item> - <item> - <description><a href="http://www.scripting.com/mp3s/youWinAgain.mp3">The news is out</a>, all over town..<p> -You've been seen, out runnin round. <p> -The lyrics are <a href="http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/You_Win_Again.txt">here</a>, short and sweet. <p> -<i>You win again!</i> -</description> - <enclosure url="http://www.scripting.com/mp3s/youWinAgain.mp3" length="3874816" type="audio/mpeg"/> - </item> - <item> - <description><a href="http://www.getlyrics.com/lyrics/grateful-dead/wake-of-the-flood/07.htm">Weather Report Suite</a>: "Winter rain, now tell me why, summers fade, and roses die? The answer came. The wind and rain. Golden hills, now veiled in grey, summer leaves have blown away. Now what remains? The wind and rain."</description> - <enclosure url="http://www.scripting.com/mp3s/weatherReportSuite.mp3" length="12216320" type="audio/mpeg"/> - </item> - <item> - <description><a href="http://arts.ucsc.edu/gdead/agdl/darkstar.html">Dark Star</a> crashes, pouring its light into ashes.</description> - <enclosure url="http://www.scripting.com/mp3s/darkStar.mp3" length="10889216" type="audio/mpeg"/> - </item> - <item> - <description>DaveNet: <a href="http://davenet.userland.com/2001/01/21/theUsBlues">The U.S. Blues</a>.</description> - </item> - <item> - <description>Still listening to the US Blues. <i>"Wave that flag, wave it wide and high.."</i> Mistake made in the 60s. We gave our country to the assholes. Ah ah. Let's take it back. Hey I'm still a hippie. <i>"You could call this song The United States Blues."</i></description> - </item> - <item> - <description><a href="http://www.sixties.com/html/garcia_stack_0.html"><img src="http://www.scripting.com/images/captainTripsSmall.gif" height="51" width="42" border="0" hspace="10" vspace="10" align="right"></a>In celebration of today's inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the <a href="http://searchlyrics2.homestead.com/gd_usblues.html">lyrics</a>. Click on the audio icon to the left to give it a listen. "Red and white, blue suede shoes, I'm Uncle Sam, how do you do?" It's a different kind of patriotic music, but man I love my country and I love Jerry and the band. <i>I truly do!</i></description> - <enclosure url="http://www.scripting.com/mp3s/usBlues.mp3" length="5272510" type="audio/mpeg"/> - </item> - <item> - <description>Grateful Dead: "Tennessee, Tennessee, ain't no place I'd rather be."</description> - <enclosure url="http://www.scripting.com/mp3s/tennesseeJed.mp3" length="3442648" type="audio/mpeg"/> - </item> - <item> - <description>Ed Cone: "Had a nice Deadhead experience with my wife, who never was one but gets the vibe and knows and likes a lot of the music. Somehow she made it to the age of 40 without ever hearing Wharf Rat. We drove to Jersey and back over Christmas with the live album commonly known as Skull and Roses in the CD player much of the way, and it was cool to see her discover one the band's finest moments. That song is unique and underappreciated. Fun to hear that disc again after a few years off -- you get Jerry as blues-guitar hero on Big Railroad Blues and a nice version of Bertha."</description> - <enclosure url="http://www.scripting.com/mp3s/darkStarWharfRat.mp3" length="27503386" type="audio/mpeg"/> - </item> - <item> - <description><a href="http://arts.ucsc.edu/GDead/AGDL/fotd.html">Tonight's Song</a>: "If I get home before daylight I just might get some sleep tonight." </description> - <enclosure url="http://www.scripting.com/mp3s/friendOfTheDevil.mp3" length="3219742" type="audio/mpeg"/> - </item> - <item> - <description><a href="http://arts.ucsc.edu/GDead/AGDL/uncle.html">Tonight's song</a>: "Come hear Uncle John's Band by the river side. Got some things to talk about here beside the rising tide."</description> - <enclosure url="http://www.scripting.com/mp3s/uncleJohnsBand.mp3" length="4587102" type="audio/mpeg"/> - </item> - <item> - <description><a href="http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Me_and_My_Uncle.txt">Me and My Uncle</a>: "I loved my uncle, God rest his soul, taught me good, Lord, taught me all I know. Taught me so well, I grabbed that gold and I left his dead ass there by the side of the road." -</description> - <enclosure url="http://www.scripting.com/mp3s/meAndMyUncle.mp3" length="2949248" type="audio/mpeg"/> - </item> - <item> - <description>Truckin, like the doo-dah man, once told me gotta play your hand. Sometimes the cards ain't worth a dime, if you don't lay em down.</description> - <enclosure url="http://www.scripting.com/mp3s/truckin.mp3" length="4847908" type="audio/mpeg"/> - </item> - <item> - <description>Two-Way-Web: <a href="http://www.thetwowayweb.com/payloadsForRss">Payloads for RSS</a>. "When I started talking with Adam late last year, he wanted me to think about high quality video on the Internet, and I totally didn't want to hear about it."</description> - </item> - <item> - <description>A touch of gray, kinda suits you anyway..</description> - <enclosure url="http://www.scripting.com/mp3s/touchOfGrey.mp3" length="5588242" type="audio/mpeg"/> - </item> - <item> - <description><a href="http://www.sixties.com/html/garcia_stack_0.html"><img src="http://www.scripting.com/images/captainTripsSmall.gif" height="51" width="42" border="0" hspace="10" vspace="10" align="right"></a>In celebration of today's inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the <a href="http://searchlyrics2.homestead.com/gd_usblues.html">lyrics</a>. Click on the audio icon to the left to give it a listen. "Red and white, blue suede shoes, I'm Uncle Sam, how do you do?" It's a different kind of patriotic music, but man I love my country and I love Jerry and the band. <i>I truly do!</i></description> - <enclosure url="http://www.scripting.com/mp3s/usBlues.mp3" length="5272510" type="audio/mpeg"/> - </item> - </channel> - </rss>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss10-example1.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss10-example1.xml deleted file mode 100755 index 0edecf58e..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/rss10-example1.xml +++ /dev/null @@ -1,62 +0,0 @@ -<?xml version="1.0"?> - -<rdf:RDF - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns="http://purl.org/rss/1.0/" -> - - <channel rdf:about="http://www.xml.com/xml/news.rss"> - <title>XML.com</title> - <link>http://xml.com/pub</link> - <description> - XML.com features a rich mix of information and services - for the XML community. - </description> - - <image rdf:resource="http://xml.com/universal/images/xml_tiny.gif" /> - - <items> - <rdf:Seq> - <rdf:li resource="http://xml.com/pub/2000/08/09/xslt/xslt.html" /> - <rdf:li resource="http://xml.com/pub/2000/08/09/rdfdb/index.html" /> - </rdf:Seq> - </items> - - <textinput rdf:resource="http://search.xml.com" /> - - </channel> - - <image rdf:about="http://xml.com/universal/images/xml_tiny.gif"> - <title>XML.com</title> - <link>http://www.xml.com</link> - <url>http://xml.com/universal/images/xml_tiny.gif</url> - </image> - - <item rdf:about="http://xml.com/pub/2000/08/09/xslt/xslt.html"> - <title>Processing Inclusions with XSLT</title> - <link>http://xml.com/pub/2000/08/09/xslt/xslt.html</link> - <description> - Processing document inclusions with general XML tools can be - problematic. This article proposes a way of preserving inclusion - information through SAX-based processing. - </description> - </item> - - <item rdf:about="http://xml.com/pub/2000/08/09/rdfdb/index.html"> - <title>Putting RDF to Work</title> - <link>http://xml.com/pub/2000/08/09/rdfdb/index.html</link> - <description> - Tool and API support for the Resource Description Framework - is slowly coming of age. Edd Dumbill takes a look at RDFDB, - one of the most exciting new RDF toolkits. - </description> - </item> - - <textinput rdf:about="http://search.xml.com"> - <title>Search XML.com</title> - <description>Search XML.com's XML collection</description> - <name>s</name> - <link>http://search.xml.com</link> - </textinput> - -</rdf:RDF>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss10-example2.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss10-example2.xml deleted file mode 100755 index 26235f78f..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/rss10-example2.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<rdf:RDF - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" - xmlns:co="http://purl.org/rss/1.0/modules/company/" - xmlns:ti="http://purl.org/rss/1.0/modules/textinput/" - xmlns="http://purl.org/rss/1.0/" -> - - <channel rdf:about="http://meerkat.oreillynet.com/?_fl=rss1.0"> - <title>Meerkat</title> - <link>http://meerkat.oreillynet.com</link> - <description>Meerkat: An Open Wire Service</description> - <dc:publisher>The O'Reilly Network</dc:publisher> - <dc:creator>Rael Dornfest (mailto:rael@oreilly.com)</dc:creator> - <dc:rights>Copyright © 2000 O'Reilly & Associates, Inc.</dc:rights> - <dc:date>2000-01-01T12:00+00:00</dc:date> - <sy:updatePeriod>hourly</sy:updatePeriod> - <sy:updateFrequency>2</sy:updateFrequency> - <sy:updateBase>2000-01-01T12:00+00:00</sy:updateBase> - - <image rdf:resource="http://meerkat.oreillynet.com/icons/meerkat-powered.jpg" /> - - <items> - <rdf:Seq> - <rdf:li resource="http://c.moreover.com/click/here.pl?r123" /> - </rdf:Seq> - </items> - - <textinput rdf:resource="http://meerkat.oreillynet.com" /> - - </channel> - - <image rdf:about="http://meerkat.oreillynet.com/icons/meerkat-powered.jpg"> - <title>Meerkat Powered!</title> - <url>http://meerkat.oreillynet.com/icons/meerkat-powered.jpg</url> - <link>http://meerkat.oreillynet.com</link> - </image> - - <item rdf:about="http://c.moreover.com/click/here.pl?r123"> - <title>XML: A Disruptive Technology</title> - <link>http://c.moreover.com/click/here.pl?r123</link> - <dc:description> - XML is placing increasingly heavy loads on the existing technical - infrastructure of the Internet. - </dc:description> - <dc:publisher>The O'Reilly Network</dc:publisher> - <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator> - <dc:rights>Copyright © 2000 O'Reilly & Associates, Inc.</dc:rights> - <dc:subject>XML</dc:subject> - <co:name>XML.com</co:name> - <co:market>NASDAQ</co:market> - <co:symbol>XML</co:symbol> - </item> - - <textinput rdf:about="http://meerkat.oreillynet.com"> - <title>Search Meerkat</title> - <description>Search Meerkat's RSS Database...</description> - <name>s</name> - <link>http://meerkat.oreillynet.com/</link> - <ti:function>search</ti:function> - <ti:inputType>regex</ti:inputType> - </textinput> - -</rdf:RDF>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss2sample.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss2sample.xml deleted file mode 100755 index 53483cc51..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/rss2sample.xml +++ /dev/null @@ -1,42 +0,0 @@ -<?xml version="1.0"?>
-<rss version="2.0" xmlns:content="http://purl.org/rss/1.0.modules/content/">
- <channel>
- <title>Liftoff News</title>
- <link>http://liftoff.msfc.nasa.gov/</link>
- <description>Liftoff to Space Exploration.</description>
- <language>en-us</language>
- <pubDate>Tue, 10 Jun 2003 04:00:00 GMT</pubDate>
- <lastBuildDate>Tue, 10 Jun 2003 09:41:01 GMT</lastBuildDate>
- <docs>http://blogs.law.harvard.edu/tech/rss</docs>
- <generator>Weblog Editor 2.0</generator>
- <managingEditor>editor@example.com</managingEditor>
- <webMaster>webmaster@example.com</webMaster>
- <item>
- <title>Star City</title>
- <link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>
- <description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.</description>
- <pubDate>Tue, 03 Jun 2003 09:39:21 GMT</pubDate>
- <guid>http://liftoff.msfc.nasa.gov/2003/06/03.html#item573</guid>
- </item>
- <item>
- <description>Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm">partial eclipse of the Sun</a> on Saturday, May 31st.</description>
- <pubDate>Fri, 30 May 2003 11:06:42 GMT</pubDate>
- <guid>http://liftoff.msfc.nasa.gov/2003/05/30.html#item572</guid>
- </item>
- <item>
- <title>The Engine That Does More</title>
- <link>http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp</link>
- <description>Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that.</description>
- <pubDate>Tue, 27 May 2003 08:37:32 GMT</pubDate>
- <guid>http://liftoff.msfc.nasa.gov/2003/05/27.html#item571</guid>
- <content:encoded><![CDATA[<p>Test content</p>]]></content:encoded>
- </item>
- <item>
- <title>Astronauts' Dirty Laundry</title>
- <link>http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp</link>
- <description>Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options.</description>
- <pubDate>Tue, 20 May 2003 08:56:02 GMT</pubDate>
- <guid>http://liftoff.msfc.nasa.gov/2003/05/20.html#item570</guid>
- </item>
- </channel>
-</rss>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/sixapart-jp.xml b/plugins/OStatus/extlib/XML/Feed/samples/sixapart-jp.xml deleted file mode 100755 index f8a04bba5..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/sixapart-jp.xml +++ /dev/null @@ -1,226 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<rss version="2.0"> -<channel> -<title>Six Apart - News</title> -<link>http://www.sixapart.jp/</link> -<description></description> -<language>ja</language> -<copyright>Copyright 2005</copyright> -<lastBuildDate>Fri, 07 Oct 2005 19:09:34 +0900</lastBuildDate> -<generator>http://www.movabletype.org/?v=3.2-ja</generator> -<docs>http://blogs.law.harvard.edu/tech/rss</docs> - -<item> -<title>ファイブ・ディーが、Movable Typeでブログプロモーションをスタート</title> -<description><![CDATA[<p><img alt="MIYAZAWAblog_banner.jpg" src="http://www.sixapart.jp/MIYAZAWAblog_banner.jpg" width="200" height="88" align="right" /><br /> -ファイブ・ディーは、Movable Typeで構築したプロモーション ブログ『宮沢和史 中南米ツアーblog Latin America 2005』を開設しました。</p> - -<p>9月21日に開設されたこのブログは、ブラジル、ホンジュラス、ニカラグア、メキシコ、キューバの5か国を巡る「Latin America 2005」ツアーに合わせ、そのツアーの模様を同行マネージャーがレポートしていきます。<br /> -さらに今月2日からは宮沢和史自身が日々録音した声をPodcastingするという点でも、ブログを使ったユニークなプロモーションとなっています。</p> - -<p><a href="http://www.five-d.co.jp/miyazawa/jp/blog/la2005/">「宮沢和史 中南米ツアーblog Latin America 2005」</a></p> - -<p>※シックス・アパートではこうしたブログを使ったプロモーションに最適な製品をご用意しております。<br /> -<ul><li><a href="/movabletype/">Movable Type</a><br /> -<li><a href="/typepad/typepad_promotion.html">TypePad Promotion</a><br /> -</ul></p>]]></description> -<link>http://www.sixapart.jp/news/2005/10/07-1909.html</link> -<guid>http://www.sixapart.jp/news/2005/10/07-1909.html</guid> -<category>news</category> -<pubDate>Fri, 07 Oct 2005 19:09:34 +0900</pubDate> -</item> -<item> -<title>Movable Type 3.2日本語版の提供を開始</title> -<description><![CDATA[<p><img alt="Movable Type Logo" src="/images/mt3-logo-small.gif" width="151" height="37"/></p> -<p>シックス・アパートは、Movable Type 3.2日本語版の提供を開始いたしました。<br /> -ベータテストにご協力いただいた多くの皆様に、スタッフ一同、心から感謝いたします。</p> -<p>製品概要など、詳しくは<a href="http://www.sixapart.jp/press_releases/2005/09/29-1529.html" title="Six Apart - News: シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始">プレスリリース</a>をご参照下さい。</p> -<p>ご購入のご検討は、<a href="http://www.sixapart.jp/movabletype/purchase-mt.html">Movable Typeのご購入</a>からどうぞ。</p>]]></description> -<link>http://www.sixapart.jp/news/2005/09/29-1530.html</link> -<guid>http://www.sixapart.jp/news/2005/09/29-1530.html</guid> -<category>news</category> -<pubDate>Thu, 29 Sep 2005 15:30:00 +0900</pubDate> -</item> -<item> -<title>シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始</title> -<description><![CDATA[<p><プレスリリース資料></p> -<ul> - <li><a href="http://www.sixapart.jp/sixapart20050929.pdf">印刷用(PDF版)</a></li> -</ul> -<p><strong>シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始 ~ スパムの自動判別機能や新ユーザー・インターフェースで、運用管理の機能を強化 ~</strong></p> -<p>2005年9月29日<br /> -シックス・アパート株式会社</p> -<p>ブログ・ソフトウェア大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、「Movable Type(ムーバブル・タイプ) 3.2 日本語版」(URL:<a href="http://www.sixapart.jp/movabletype/">http://www.sixapart.jp/movabletype/</a>)を9月29日より提供開始いたします。</p>]]></description> -<link>http://www.sixapart.jp/press_releases/2005/09/29-1529.html</link> -<guid>http://www.sixapart.jp/press_releases/2005/09/29-1529.html</guid> -<category>Press Releases</category> -<pubDate>Thu, 29 Sep 2005 15:29:00 +0900</pubDate> -</item> -<item> -<title>スタッフを募集しています</title> -<description><![CDATA[<p>シックス・アパートはMovable TypeやTypePadの開発エンジニアなど、スタッフを広く募集しています。具体的な募集職種は次の通りです。</p> - -<ul> -<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0007.html">Movable Type開発エンジニア</a></li> -<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0004.html">TypePad開発エンジニア</a></li> -<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0003.html">カスタマーサポート・ディレクター</a></li> -<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0002.html">マーケティング・広報アシスタント</a></li> -<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0001.html">開発アシスタント</a></li> -</ul> - -<p>拡大を続ける、日本のブログ市場を積極的にリードする人材を、シックス・アパートは募集しています。上記以外の職種につきましても、お気軽にお問い合わせください。詳しい募集要項や応募方法については、<a href="/jobs/">求人情報のページ</a>をご覧ください。<br /> -</p>]]></description> -<link>http://www.sixapart.jp/news/2005/09/27-0906.html</link> -<guid>http://www.sixapart.jp/news/2005/09/27-0906.html</guid> -<category>news</category> -<pubDate>Tue, 27 Sep 2005 09:06:10 +0900</pubDate> -</item> -<item> -<title>サイト接続不具合に関するお詫びと復旧のお知らせ</title> -<description><![CDATA[<p>9月24日(土)の14:45ごろから、同日18:30ごろまで、シックス・アパート社のウェブサイトが不安定になっており、断続的に接続できない不具合が発生しておりました。このため、この期間中にウェブサイトの閲覧や製品のダウンロードができませんでした。</p> - -<p>なお現在は不具合は解消しております。みなさまにご迷惑をおかけしたことをお詫びいたします。</p>]]></description> -<link>http://www.sixapart.jp/news/2005/09/26-1000.html</link> -<guid>http://www.sixapart.jp/news/2005/09/26-1000.html</guid> -<category>news</category> -<pubDate>Mon, 26 Sep 2005 10:00:56 +0900</pubDate> -</item> -<item> -<title>企業ブログ向けパッケージ「TypePad Promotion」を新発売</title> -<description><![CDATA[<p>シックス・アパートは、ウェブログ・サービスTypePadの企業ブログ向けパッケージ「TypePad Promotion」(タイプパッド・プロモーションの発売を10月下旬から開始いたします。</p> - -<p>詳しくは、<a href="http://www.sixapart.jp/press_releases/2005/09/20-1500.html" title="プレスリリース: 「TypePad Promotion」新発売">プレスリリース</a>をご参照下さい。</p>]]></description> -<link>http://www.sixapart.jp/news/2005/09/20-1500.html</link> -<guid>http://www.sixapart.jp/news/2005/09/20-1500.html</guid> -<category>news</category> -<pubDate>Tue, 20 Sep 2005 15:00:01 +0900</pubDate> -</item> -<item> -<title>シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売</title> -<description><![CDATA[<p><プレスリリース資料><br /> -<a href="http://www.sixapart.jp/sixapart20050920.pdf">印刷用(PDF版)</a></p> - -<p><br /> -<strong>シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売<br /> -~PR/IRサイトやキャンペーンサイトなど企業のプロモーションニーズに特化~<br /> -</strong><br /> -2005年9月20日<br /> -シックス・アパート株式会社</p> - -<p>ブログ・サービス大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、法人向けプロモーションブログ・パッケージ「TypePad Promotion(タイプパッド・プロモーション)」(URL:<a href="http://www.sixapart.jp/typepad/typepad_promotion.html">http://www.sixapart.jp/typepad/typepad_promotion.html</a>)を10月下旬より販売開始いたします。</p>]]></description> -<link>http://www.sixapart.jp/press_releases/2005/09/20-1500.html</link> -<guid>http://www.sixapart.jp/press_releases/2005/09/20-1500.html</guid> -<category>Press Releases</category> -<pubDate>Tue, 20 Sep 2005 15:00:00 +0900</pubDate> -</item> -<item> -<title>Six [days] Apart Week</title> -<description><![CDATA[<p>本日、9月16日はSix Apartの創業者ミナ・トロットの誕生日です。<br /> -私たちの会社は、創業者のトロット夫妻(ベンとミナ)の誕生日が、6日離れていることからSix [days] Apart →Six Apartという風に名付けられています。本日から22日までの6日間を社名の由来となる Six [days] Apart Weekとして、私たちのプロダクトをご紹介させていただきます。</p> - -<p>今日は、ブログ・サービスのTypePad(タイプパッド)をご紹介します。<br /> -<img alt="tp-logo.gif" src="http://www.sixapart.jp/tp-logo.gif" width="227" height="52" /></p> - -<p>TypePadは、米国PC MAGAZINE誌の2003年EDITOR'S CHOICE とBEST OF THE YEARに選ばれております。<br /> -<img alt="pcmag-ad.gif" src="http://www.sixapart.jp/pcmag-ad.gif" width="297" height="100" /><br /> -</p>]]></description> -<link>http://www.sixapart.jp/news/2005/09/16-1941.html</link> -<guid>http://www.sixapart.jp/news/2005/09/16-1941.html</guid> -<category>news</category> -<pubDate>Fri, 16 Sep 2005 19:41:47 +0900</pubDate> -</item> -<item> -<title>ハイパーワークスが商用フォントを利用できるMovable Typeホスティングサービスを開始</title> -<description><![CDATA[<p>ソフト開発会社の<a href="http://www.hyperwrx.co.jp/">有限会社ハイパーワークス</a>は、商用フォントなど多彩なフォントをブログ上で利用できるブログ・サービス「<a href="http://glyph-on.jp/">Glyph-On!(グリフォン) Movable Type ホスティング サービス</a>」の提供を開始しました。<br /> -</p>]]></description> -<link>http://www.sixapart.jp/news/2005/09/14-1700.html</link> -<guid>http://www.sixapart.jp/news/2005/09/14-1700.html</guid> -<category>news</category> -<pubDate>Wed, 14 Sep 2005 17:00:00 +0900</pubDate> -</item> -<item> -<title>Movable Type開発エンジニアの募集</title> -<description><![CDATA[<p> -勤務形態: フルタイム<br /> -勤務地: 東京 (赤坂)<br /> -職種: ソフトウェア・エンジニア<br /> -職務内容: Movable Typeの開発業務全般<br /> -募集人数: 若干名 -</p>]]></description> -<link>http://www.sixapart.jp/jobs/2005/09/13-0007.html</link> -<guid>http://www.sixapart.jp/jobs/2005/09/13-0007.html</guid> -<category>Jobs</category> -<pubDate>Tue, 13 Sep 2005 00:07:00 +0900</pubDate> -</item> -<item> -<title>TypePad開発エンジニアの募集</title> -<description><![CDATA[<p> -勤務形態: フルタイム<br /> -勤務地: 東京 (赤坂)<br /> -職種: アプリケーション・エンジニア<br /> -職務内容: TypePadのカスタマイズ、周辺開発<br /> -募集人数: 若干名 -</p>]]></description> -<link>http://www.sixapart.jp/jobs/2005/09/13-0004.html</link> -<guid>http://www.sixapart.jp/jobs/2005/09/13-0004.html</guid> -<category>Jobs</category> -<pubDate>Tue, 13 Sep 2005 00:04:00 +0900</pubDate> -</item> -<item> -<title>カスタマーサポート・ディレクターの募集</title> -<description><![CDATA[<p>勤務形態: フルタイム<br /> -勤務地: 東京(赤坂)<br /> -職種: カスタマーサポート・ディレクター<br /> -職務内容: TypePadやMovable Typeのカスタマーサポート業務の統括<br /> -募集人数: 若干名 -</p> -]]></description> -<link>http://www.sixapart.jp/jobs/2005/09/13-0003.html</link> -<guid>http://www.sixapart.jp/jobs/2005/09/13-0003.html</guid> -<category>Jobs</category> -<pubDate>Tue, 13 Sep 2005 00:03:30 +0900</pubDate> -</item> -<item> -<title>アルバイト(マーケティング・広報アシスタント)の募集</title> -<description><![CDATA[<p>勤務形態: アルバイト<br /> -勤務地: 東京(港区)<br /> -職種:マーケティング・PRのアシスタント業務<br /> -募集人数: 若干名<br /> -時給:1000円~(但し、試用期間終了後に応相談)。交通費支給<br /> -時間:平日10時30分~18時30分まで。週3日以上(応相談)<br /> -</p>]]></description> -<link>http://www.sixapart.jp/jobs/2005/09/13-0002.html</link> -<guid>http://www.sixapart.jp/jobs/2005/09/13-0002.html</guid> -<category>Jobs</category> -<pubDate>Tue, 13 Sep 2005 00:02:00 +0900</pubDate> -</item> -<item> -<title>アルバイト(開発アシスタント)の募集</title> -<description><![CDATA[<p>勤務形態: アルバイト<br /> -勤務地: 東京(港区)<br /> -職種: アプリケーション開発のアシスタント業務<br /> -募集人数: 若干名<br /> -時給:1000円~(但し、試用期間終了後に応相談)。交通費支給<br /> -時間:平日10時30分~18時30分まで。週3日以上(応相談) -</p>]]></description> -<link>http://www.sixapart.jp/jobs/2005/09/13-0001.html</link> -<guid>http://www.sixapart.jp/jobs/2005/09/13-0001.html</guid> -<category>Jobs</category> -<pubDate>Tue, 13 Sep 2005 00:01:00 +0900</pubDate> -</item> -<item> -<title>TypePad Japan がバージョンアップしました。</title> -<description><![CDATA[<p><a href="http://www.sixapart.jp/typepad/">「TypePad Japan(タイプパッドジャパン)」</a>において、本日、「TypePad 1.6 日本語版」へのバージョンアップを行いました。最新版となる「TypePad 1.6 日本語版」では、ブログデザインの機能強化、ポッドキャスティング対応、モブログ対応に加え、今回新たに大幅な容量アップが行われております。皆様、新しくなった<a href="http://www.sixapart.jp/typepad/">TypePad Japan</a>にどうぞご期待ください。</p> - -<p>なお、TypePadの携帯対応強化に関しましては、本日よりTypePad Japanのお客様を対象にオープン・ベータを開始しております。</p> - -<p>2005年9月5日発表のTypePad日本語版 1.6プレスリリースは<a href="http://www.sixapart.jp/press_releases/2005/09/05-1420.html">こちら</a>をご覧下さい。</p>]]></description> -<link>http://www.sixapart.jp/news/2005/09/12-1953.html</link> -<guid>http://www.sixapart.jp/news/2005/09/12-1953.html</guid> -<category>news</category> -<pubDate>Mon, 12 Sep 2005 19:53:07 +0900</pubDate> -</item> - - -</channel> -</rss>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/samples/technorati.feed b/plugins/OStatus/extlib/XML/Feed/samples/technorati.feed deleted file mode 100755 index 6274a32cd..000000000 --- a/plugins/OStatus/extlib/XML/Feed/samples/technorati.feed +++ /dev/null @@ -1,54 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<rss version="2.0" - xmlns:tapi="http://api.technorati.com/dtd/tapi-002.xml"> - <channel> - <title>[Technorati] Tag results for greenbelt</title> - <link>http://www.technorati.com/tag/greenbelt</link> - <description>Posts tagged with "greenbelt" on Technorati.</description> - <pubDate>Mon, 08 Aug 2005 15:15:08 GMT</pubDate> - <category domain="http://www.technorati.com/tag">greenbelt</category> - <tapi:inboundblogs>2</tapi:inboundblogs> - <tapi:inboundlinks>2</tapi:inboundlinks> - <cloud domain="rpc.sys.com" port="80" path="/RPC2" registerProcedure="myCloud.rssPleaseNotify" protocol="xml-rpc" /> - <generator>Technorati v1.0</generator> - <image> - <url>http://static.technorati.com/pix/logos/logo_reverse_sm.gif</url> - <title>Technorati logo</title> - <link>http://www.technorati.com</link> - </image> - <skipHours> - <hour>1</hour> - <hour>7</hour> - <hour>9</hour> - </skipHours> - <webMaster>support@technorati.com (Technorati Support)</webMaster> - <docs>http://blogs.law.harvad.edu/tech/rss</docs> - <ttl>60</ttl> - <item> - <title>Greenbelt</title> - <link>http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html</link> - <description>So if the plan goes according to plan (!)... I'll be speaking at Greenbelt at these times: Slot 1...</description> - <guid isPermaLink="true">http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html</guid> - <pubDate>Mon, 18 Jul 2005 02:11:42 GMT</pubDate> - <category>James</category> - <tapi:linkcreated>2005-07-11 02:08:12</tapi:linkcreated> - <comments>http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fmaggidawn.typepad.com%2Fmaggidawn%2F2005%2F07%2Fgreenbelt.html</comments> - <tapi:inboundblogs>190</tapi:inboundblogs> - <tapi:inboundlinks>237</tapi:inboundlinks> - <source url="http://maggidawn.typepad.com/maggidawn/index.rdf">maggi dawn</source> - </item> - - <item> - <title>Walking along the Greenbelt</title> - <link>http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html</link> - <description>[IMG] Photo of homeless man walking near the greenbelt in Boise, Idaho Tags: photo homeless greenbelt Boise Idaho picture</description> - <guid isPermaLink="true">http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html</guid> - <pubDate>Tue, 28 Jun 2005 01:41:24 GMT</pubDate> - <tapi:linkcreated>2005-06-26 17:24:03</tapi:linkcreated> - <comments>http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fpictureshomeless.blogspot.com%2F2005%2F06%2Fwalking-along-greenbelt.html</comments> - <tapi:inboundblogs>2</tapi:inboundblogs> - <tapi:inboundlinks>2</tapi:inboundlinks> - </item> - - </channel> -</rss> diff --git a/plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc b/plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc deleted file mode 100755 index e662d2626..000000000 --- a/plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc +++ /dev/null @@ -1,338 +0,0 @@ -# -*- rnc -*- -# RELAX NG Compact Syntax Grammar for the -# Atom Format Specification Version 11 - -namespace atom = "http://www.w3.org/2005/Atom" -namespace xhtml = "http://www.w3.org/1999/xhtml" -namespace s = "http://www.ascc.net/xml/schematron" -namespace local = "" - -start = atomFeed | atomEntry - -# Common attributes - -atomCommonAttributes = - attribute xml:base { atomUri }?, - attribute xml:lang { atomLanguageTag }?, - undefinedAttribute* - -# Text Constructs - -atomPlainTextConstruct = - atomCommonAttributes, - attribute type { "text" | "html" }?, - text - -atomXHTMLTextConstruct = - atomCommonAttributes, - attribute type { "xhtml" }, - xhtmlDiv - -atomTextConstruct = atomPlainTextConstruct | atomXHTMLTextConstruct - -# Person Construct - -atomPersonConstruct = - atomCommonAttributes, - (element atom:name { text } - & element atom:uri { atomUri }? - & element atom:email { atomEmailAddress }? - & extensionElement*) - -# Date Construct - -atomDateConstruct = - atomCommonAttributes, - xsd:dateTime - -# atom:feed - -atomFeed = - [ - s:rule [ - context = "atom:feed" - s:assert [ - test = "atom:author or not(atom:entry[not(atom:author)])" - "An atom:feed must have an atom:author unless all " - ~ "of its atom:entry children have an atom:author." - ] - ] - ] - element atom:feed { - atomCommonAttributes, - (atomAuthor* - & atomCategory* - & atomContributor* - & atomGenerator? - & atomIcon? - & atomId - & atomLink* - & atomLogo? - & atomRights? - & atomSubtitle? - & atomTitle - & atomUpdated - & extensionElement*), - atomEntry* - } - -# atom:entry - -atomEntry = - [ - s:rule [ - context = "atom:entry" - s:assert [ - test = "atom:link[@rel='alternate'] " - ~ "or atom:link[not(@rel)] " - ~ "or atom:content" - "An atom:entry must have at least one atom:link element " - ~ "with a rel attribute of 'alternate' " - ~ "or an atom:content." - ] - ] - s:rule [ - context = "atom:entry" - s:assert [ - test = "atom:author or " - ~ "../atom:author or atom:source/atom:author" - "An atom:entry must have an atom:author " - ~ "if its feed does not." - ] - ] - ] - element atom:entry { - atomCommonAttributes, - (atomAuthor* - & atomCategory* - & atomContent? - & atomContributor* - & atomId - & atomLink* - & atomPublished? - & atomRights? - & atomSource? - & atomSummary? - & atomTitle - & atomUpdated - & extensionElement*) - } - -# atom:content - -atomInlineTextContent = - element atom:content { - atomCommonAttributes, - attribute type { "text" | "html" }?, - (text)* - } - -atomInlineXHTMLContent = - element atom:content { - atomCommonAttributes, - attribute type { "xhtml" }, - xhtmlDiv - } - -atomInlineOtherContent = - element atom:content { - atomCommonAttributes, - attribute type { atomMediaType }?, - (text|anyElement)* - } - -atomOutOfLineContent = - element atom:content { - atomCommonAttributes, - attribute type { atomMediaType }?, - attribute src { atomUri }, - empty - } - -atomContent = atomInlineTextContent - | atomInlineXHTMLContent - | atomInlineOtherContent - | atomOutOfLineContent - -# atom:author - -atomAuthor = element atom:author { atomPersonConstruct } - -# atom:category - -atomCategory = - element atom:category { - atomCommonAttributes, - attribute term { text }, - attribute scheme { atomUri }?, - attribute label { text }?, - undefinedContent - } - -# atom:contributor - -atomContributor = element atom:contributor { atomPersonConstruct } - -# atom:generator - -atomGenerator = element atom:generator { - atomCommonAttributes, - attribute uri { atomUri }?, - attribute version { text }?, - text -} - -# atom:icon - -atomIcon = element atom:icon { - atomCommonAttributes, - (atomUri) -} - -# atom:id - -atomId = element atom:id { - atomCommonAttributes, - (atomUri) -} - -# atom:logo - -atomLogo = element atom:logo { - atomCommonAttributes, - (atomUri) -} - -# atom:link - -atomLink = - element atom:link { - atomCommonAttributes, - attribute href { atomUri }, - attribute rel { atomNCName | atomUri }?, - attribute type { atomMediaType }?, - attribute hreflang { atomLanguageTag }?, - attribute title { text }?, - attribute length { text }?, - undefinedContent - } - -# atom:published - -atomPublished = element atom:published { atomDateConstruct } - -# atom:rights - -atomRights = element atom:rights { atomTextConstruct } - -# atom:source - -atomSource = - element atom:source { - atomCommonAttributes, - (atomAuthor* - & atomCategory* - & atomContributor* - & atomGenerator? - & atomIcon? - & atomId? - & atomLink* - & atomLogo? - & atomRights? - & atomSubtitle? - & atomTitle? - & atomUpdated? - & extensionElement*) - } - -# atom:subtitle - -atomSubtitle = element atom:subtitle { atomTextConstruct } - -# atom:summary - -atomSummary = element atom:summary { atomTextConstruct } - -# atom:title - -atomTitle = element atom:title { atomTextConstruct } - -# atom:updated - -atomUpdated = element atom:updated { atomDateConstruct } - -# Low-level simple types - -atomNCName = xsd:string { minLength = "1" pattern = "[^:]*" } - -# Whatever a media type is, it contains at least one slash -atomMediaType = xsd:string { pattern = ".+/.+" } - -# As defined in RFC 3066 -atomLanguageTag = xsd:string { - pattern = "[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*" -} - -# Unconstrained; it's not entirely clear how IRI fit into -# xsd:anyURI so let's not try to constrain it here -atomUri = text - -# Whatever an email address is, it contains at least one @ -atomEmailAddress = xsd:string { pattern = ".+@.+" } - -# Simple Extension - -simpleExtensionElement = - element * - atom:* { - text - } - -# Structured Extension - -structuredExtensionElement = - element * - atom:* { - (attribute * { text }+, - (text|anyElement)*) - | (attribute * { text }*, - (text?, anyElement+, (text|anyElement)*)) - } - -# Other Extensibility - -extensionElement = - simpleExtensionElement | structuredExtensionElement - -undefinedAttribute = - attribute * - (xml:base | xml:lang | local:*) { text } - -undefinedContent = (text|anyForeignElement)* - -anyElement = - element * { - (attribute * { text } - | text - | anyElement)* - } - -anyForeignElement = - element * - atom:* { - (attribute * { text } - | text - | anyElement)* - } - -# XHTML - -anyXHTML = element xhtml:* { - (attribute * { text } - | text - | anyXHTML)* -} - -xhtmlDiv = element xhtml:div { - (attribute * { text } - | text - | anyXHTML)* -} - -# EOF
\ No newline at end of file diff --git a/plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc b/plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc deleted file mode 100755 index 725094788..000000000 --- a/plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc +++ /dev/null @@ -1,113 +0,0 @@ -<?xml version='1.0' encoding='UTF-8'?> -<!-- http://www.xml.com/lpt/a/2002/01/23/relaxng.html --> -<!-- http://www.oasis-open.org/committees/relax-ng/tutorial-20011203.html --> -<!-- http://www.zvon.org/xxl/XMLSchemaTutorial/Output/ser_wildcards_st8.html --> - -<grammar xmlns='http://relaxng.org/ns/structure/1.0' - xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' - xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' - ns='http://purl.org/rss/1.0/' - datatypeLibrary='http://www.w3.org/2001/XMLSchema-datatypes'> - - <start> - <element name='RDF' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> - <ref name='RDFContent'/> - </element> - </start> - - <define name='RDFContent' ns='http://purl.org/rss/1.0/'> - <interleave> - <element name='channel'> - <ref name='channelContent'/> - </element> - <optional> - <element name='image'><ref name='imageContent'/></element> - </optional> - <oneOrMore> - <element name='item'><ref name='itemContent'/></element> - </oneOrMore> - </interleave> - </define> - - <define name='channelContent' combine="interleave"> - <interleave> - <element name='title'><data type='string'/></element> - <element name='link'><data type='anyURI'/></element> - <element name='description'><data type='string'/></element> - <element name='image'> - <attribute name='resource' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> - <data type='anyURI'/> - </attribute> - </element> - <element name='items'> - <ref name='itemsContent'/> - </element> - <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> - <data type='anyURI'/> - </attribute> - </interleave> - </define> - - <define name="itemsContent"> - <element name="Seq" ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> - <oneOrMore> - <element name="li" ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> - <choice> - <attribute name='resource'> <!-- Why doesn't RDF/RSS1.0 ns qualify this attribute? --> - <data type='anyURI'/> - </attribute> - <attribute name='resource' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> - <data type='anyURI'/> - </attribute> - </choice> - </element> - </oneOrMore> - </element> - </define> - - <define name='imageContent'> - <interleave> - <element name='title'><data type='string'/></element> - <element name='link'><data type='anyURI'/></element> - <element name='url'><data type='anyURI'/></element> - <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> - <data type='anyURI'/> - </attribute> - </interleave> - </define> - - <define name='itemContent'> - <interleave> - <element name='title'><data type='string'/></element> - <element name='link'><data type='anyURI'/></element> - <optional><element name='description'><data type='string'/></element></optional> - <ref name="anyThing"/> - <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> - <data type='anyURI'/> - </attribute> - </interleave> - </define> - - - <define name='anyThing'> - <zeroOrMore> - <choice> - <text/> - <element> - <anyName> - <except> - <nsName/> - </except> - </anyName> - <ref name='anyThing'/> - <zeroOrMore> - <attribute> - <anyName/> - </attribute> - </zeroOrMore> - </element> - </choice> - </zeroOrMore> - </define> - -</grammar> diff --git a/plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc b/plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc deleted file mode 100755 index c8633766f..000000000 --- a/plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc +++ /dev/null @@ -1,218 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - RELAX NG Compact Schema for RSS 1.1 - Sean B. Palmer, inamidst.com - Christopher Schmidt, crschmidt.net - License: This schema is in the public domain ---> -<grammar xmlns:rss="http://purl.org/net/rss1.1#" xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ns="http://purl.org/net/rss1.1#" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"> - <start> - <ref name="Channel"/> - </start> - <define name="Channel"> - <a:documentation>http://purl.org/net/rss1.1#Channel</a:documentation> - <element name="Channel"> - <ref name="Channel.content"/> - - </element> - </define> - <define name="Channel.content"> - <optional> - <ref name="AttrXMLLang"/> - </optional> - <optional> - <ref name="AttrXMLBase"/> - </optional> - - <ref name="AttrRDFAbout"/> - <interleave> - <ref name="title"/> - <ref name="link"/> - <ref name="description"/> - <optional> - <ref name="image"/> - </optional> - <zeroOrMore> - - <ref name="Any"/> - </zeroOrMore> - <ref name="items"/> - </interleave> - </define> - <define name="title"> - <a:documentation>http://purl.org/net/rss1.1#title</a:documentation> - <element name="title"> - - <ref name="title.content"/> - </element> - </define> - <define name="title.content"> - <optional> - <ref name="AttrXMLLang"/> - </optional> - <text/> - </define> - - <define name="link"> - <a:documentation>http://purl.org/net/rss1.1#link</a:documentation> - <element name="link"> - <ref name="link.content"/> - </element> - </define> - <define name="link.content"> - <data type="anyURI"/> - - </define> - <define name="description"> - <a:documentation>http://purl.org/net/rss1.1#description</a:documentation> - <element name="description"> - <ref name="description.content"/> - </element> - </define> - <define name="description.content"> - - <optional> - <ref name="AttrXMLLang"/> - </optional> - <text/> - </define> - <define name="image"> - <a:documentation>http://purl.org/net/rss1.1#image</a:documentation> - <element name="image"> - - <ref name="image.content"/> - </element> - </define> - <define name="image.content"> - <optional> - <ref name="AttrXMLLang"/> - </optional> - <ref name="AttrRDFResource"/> - <interleave> - - <ref name="title"/> - <optional> - <ref name="link"/> - </optional> - <ref name="url"/> - <zeroOrMore> - <ref name="Any"/> - </zeroOrMore> - </interleave> - - </define> - <define name="url"> - <a:documentation>http://purl.org/net/rss1.1#url</a:documentation> - <element name="url"> - <ref name="url.content"/> - </element> - </define> - <define name="url.content"> - - <data type="anyURI"/> - </define> - <define name="items"> - <a:documentation>http://purl.org/net/rss1.1#items</a:documentation> - <element name="items"> - <ref name="items.content"/> - </element> - </define> - - <define name="items.content"> - <optional> - <ref name="AttrXMLLang"/> - </optional> - <ref name="AttrRDFCollection"/> - <zeroOrMore> - <ref name="item"/> - </zeroOrMore> - </define> - - <define name="item"> - <a:documentation>http://purl.org/net/rss1.1#item</a:documentation> - <element name="item"> - <ref name="item.content"/> - </element> - </define> - <define name="item.content"> - <optional> - - <ref name="AttrXMLLang"/> - </optional> - <ref name="AttrRDFAbout"/> - <interleave> - <ref name="title"/> - <ref name="link"/> - <optional> - <ref name="description"/> - </optional> - - <optional> - <ref name="image"/> - </optional> - <zeroOrMore> - <ref name="Any"/> - </zeroOrMore> - </interleave> - </define> - <define name="Any"> - - <a:documentation>http://purl.org/net/rss1.1#Any</a:documentation> - <element> - <anyName> - <except> - <nsName/> - </except> - </anyName> - <ref name="Any.content"/> - - </element> - </define> - <define name="Any.content"> - <zeroOrMore> - <attribute> - <anyName> - <except> - <nsName/> - <nsName ns=""/> - - </except> - </anyName> - </attribute> - </zeroOrMore> - <mixed> - <zeroOrMore> - <ref name="Any"/> - </zeroOrMore> - </mixed> - - </define> - <define name="AttrXMLLang"> - <attribute name="xml:lang"> - <data type="language"/> - </attribute> - </define> - <define name="AttrXMLBase"> - <attribute name="xml:base"> - <data type="anyURI"/> - - </attribute> - </define> - <define name="AttrRDFAbout"> - <attribute name="rdf:about"> - <data type="anyURI"/> - </attribute> - </define> - <define name="AttrRDFResource"> - <attribute name="rdf:parseType"> - - <value>Resource</value> - </attribute> - </define> - <define name="AttrRDFCollection"> - <attribute name="rdf:parseType"> - <value>Collection</value> - </attribute> - </define> - -</grammar> diff --git a/plugins/OStatus/extlib/hkit/hcard.profile.php b/plugins/OStatus/extlib/hkit/hcard.profile.php new file mode 100644 index 000000000..6ec0dc890 --- /dev/null +++ b/plugins/OStatus/extlib/hkit/hcard.profile.php @@ -0,0 +1,105 @@ +<?php + // hcard profile for hkit + + $this->root_class = 'vcard'; + + $this->classes = array( + 'fn', array('honorific-prefix', 'given-name', 'additional-name', 'family-name', 'honorific-suffix'), + 'n', array('honorific-prefix', 'given-name', 'additional-name', 'family-name', 'honorific-suffix'), + 'adr', array('post-office-box', 'extended-address', 'street-address', 'postal-code', 'country-name', 'type', 'region', 'locality'), + 'label', 'bday', 'agent', 'nickname', 'photo', 'class', + 'email', array('type', 'value'), + 'category', 'key', 'logo', 'mailer', 'note', + 'org', array('organization-name', 'organization-unit'), + 'tel', array('type', 'value'), + 'geo', array('latitude', 'longitude'), + 'tz', 'uid', 'url', 'rev', 'role', 'sort-string', 'sound', 'title' + ); + + // classes that must only appear once per card + $this->singles = array( + 'fn' + ); + + // classes that are required (not strictly enforced - give at least one!) + $this->required = array( + 'fn' + ); + + $this->att_map = array( + 'fn' => array('IMG|alt'), + 'url' => array('A|href', 'IMG|src', 'AREA|href'), + 'photo' => array('IMG|src'), + 'bday' => array('ABBR|title'), + 'logo' => array('IMG|src'), + 'email' => array('A|href'), + 'geo' => array('ABBR|title') + ); + + + $this->callbacks = array( + 'url' => array($this, 'resolvePath'), + 'photo' => array($this, 'resolvePath'), + 'logo' => array($this, 'resolvePath'), + 'email' => array($this, 'resolveEmail') + ); + + + + function hKit_hcard_post($a) + { + + foreach ($a as &$vcard){ + + hKit_implied_n_optimization($vcard); + hKit_implied_n_from_fn($vcard); + + } + + return $a; + + } + + + function hKit_implied_n_optimization(&$vcard) + { + if (array_key_exists('fn', $vcard) && !is_array($vcard['fn']) && + !array_key_exists('n', $vcard) && (!array_key_exists('org', $vcard) || $vcard['fn'] != $vcard['org'])){ + + if (sizeof(explode(' ', $vcard['fn'])) == 2){ + $patterns = array(); + $patterns[] = array('/^(\S+),\s*(\S{1})$/', 2, 1); // Lastname, Initial + $patterns[] = array('/^(\S+)\s*(\S{1})\.*$/', 2, 1); // Lastname Initial(.) + $patterns[] = array('/^(\S+),\s*(\S+)$/', 2, 1); // Lastname, Firstname + $patterns[] = array('/^(\S+)\s*(\S+)$/', 1, 2); // Firstname Lastname + + foreach ($patterns as $pattern){ + if (preg_match($pattern[0], $vcard['fn'], $matches) === 1){ + $n = array(); + $n['given-name'] = $matches[$pattern[1]]; + $n['family-name'] = $matches[$pattern[2]]; + $vcard['n'] = $n; + + + break; + } + } + } + } + } + + + function hKit_implied_n_from_fn(&$vcard) + { + if (array_key_exists('fn', $vcard) && is_array($vcard['fn']) + && !array_key_exists('n', $vcard) && (!array_key_exists('org', $vcard) || $vcard['fn'] != $vcard['org'])){ + + $vcard['n'] = $vcard['fn']; + } + + if (array_key_exists('fn', $vcard) && is_array($vcard['fn'])){ + $vcard['fn'] = $vcard['fn']['text']; + } + } + +?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/hkit/hkit.class.php b/plugins/OStatus/extlib/hkit/hkit.class.php new file mode 100644 index 000000000..c3a54cff6 --- /dev/null +++ b/plugins/OStatus/extlib/hkit/hkit.class.php @@ -0,0 +1,475 @@ +<?php + + /* + + hKit Library for PHP5 - a generic library for parsing Microformats + Copyright (C) 2006 Drew McLellan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + Author + Drew McLellan - http://allinthehead.com/ + + Contributors: + Scott Reynen - http://www.randomchaos.com/ + + Version 0.5, 22-Jul-2006 + fixed by-ref issue cropping up in PHP 5.0.5 + fixed a bug with a@title + added support for new fn=n optimisation + added support for new a.include include-pattern + Version 0.4, 23-Jun-2006 + prevented nested includes from causing infinite loops + returns false if URL can't be fetched + added pre-flight check for base support level + added deduping of once-only classnames + prevented accumulation of multiple 'value' values + tuned whitespace handling and treatment of DEL elements + Version 0.3, 21-Jun-2006 + added post-processor callback method into profiles + fixed minor problems raised by hcard testsuite + added support for include-pattern + added support for td@headers pattern + added implied-n optimization into default hcard profile + Version 0.2, 20-Jun-2006 + added class callback mechanism + added resolvePath & resolveEmail + added basic BASE support + Version 0.1.1, 19-Jun-2006 (different timezone, no time machine) + added external Tidy option + Version 0.1, 20-Jun-2006 + initial release + + + + + */ + + class hKit + { + + public $tidy_mode = 'proxy'; // 'proxy', 'exec', 'php' or 'none' + public $tidy_proxy = 'http://cgi.w3.org/cgi-bin/tidy?forceXML=on&docAddr='; // required only for tidy_mode=proxy + public $tmp_dir = '/path/to/writable/dir/'; // required only for tidy_mode=exec + + private $root_class = ''; + private $classes = ''; + private $singles = ''; + private $required = ''; + private $att_map = ''; + private $callbacks = ''; + private $processor = ''; + + private $url = ''; + private $base = ''; + private $doc = ''; + + + public function hKit() + { + // pre-flight checks + $pass = true; + $required = array('dom_import_simplexml', 'file_get_contents', 'simplexml_load_string'); + $missing = array(); + + foreach ($required as $f){ + if (!function_exists($f)){ + $pass = false; + $missing[] = $f . '()'; + } + } + + if (!$pass) + die('hKit error: these required functions are not available: <strong>' . implode(', ', $missing) . '</strong>'); + + } + + + public function getByURL($profile='', $url='') + { + + if ($profile=='' || $url == '') return false; + + $this->loadProfile($profile); + + $source = $this->loadURL($url); + + if ($source){ + $tidy_xhtml = $this->tidyThis($source); + + $fragment = false; + + if (strrchr($url, '#')) + $fragment = array_pop(explode('#', $url)); + + $doc = $this->loadDoc($tidy_xhtml, $fragment); + $s = $this->processNodes($doc, $this->classes); + $s = $this->postProcess($profile, $s); + + return $s; + }else{ + return false; + } + } + + public function getByString($profile='', $input_xml='') + { + if ($profile=='' || $input_xml == '') return false; + + $this->loadProfile($profile); + + $doc = $this->loadDoc($input_xml); + $s = $this->processNodes($doc, $this->classes); + $s = $this->postProcess($profile, $s); + + return $s; + + } + + private function processNodes($items, $classes, $allow_includes=true){ + + $out = array(); + + foreach($items as $item){ + $data = array(); + + for ($i=0; $i<sizeof($classes); $i++){ + + if (!is_array($classes[$i])){ + + $xpath = ".//*[contains(concat(' ',normalize-space(@class),' '),' " . $classes[$i] . " ')]"; + $results = $item->xpath($xpath); + + if ($results){ + foreach ($results as $result){ + if (isset($classes[$i+1]) && is_array($classes[$i+1])){ + $nodes = $this->processNodes($results, $classes[$i+1]); + if (sizeof($nodes) > 0){ + $nodes = array_merge(array('text'=>$this->getNodeValue($result, $classes[$i])), $nodes); + $data[$classes[$i]] = $nodes; + }else{ + $data[$classes[$i]] = $this->getNodeValue($result, $classes[$i]); + } + + }else{ + if (isset($data[$classes[$i]])){ + if (is_array($data[$classes[$i]])){ + // is already an array - append + $data[$classes[$i]][] = $this->getNodeValue($result, $classes[$i]); + + }else{ + // make it an array + if ($classes[$i] == 'value'){ // unless it's the 'value' of a type/value pattern + $data[$classes[$i]] .= $this->getNodeValue($result, $classes[$i]); + }else{ + $old_val = $data[$classes[$i]]; + $data[$classes[$i]] = array($old_val, $this->getNodeValue($result, $classes[$i])); + $old_val = false; + } + } + }else{ + // set as normal value + $data[$classes[$i]] = $this->getNodeValue($result, $classes[$i]); + + } + } + + // td@headers pattern + if (strtoupper(dom_import_simplexml($result)->tagName)== "TD" && $result['headers']){ + $include_ids = explode(' ', $result['headers']); + $doc = $this->doc; + foreach ($include_ids as $id){ + $xpath = "//*[@id='$id']/.."; + $includes = $doc->xpath($xpath); + foreach ($includes as $include){ + $tmp = $this->processNodes($include, $this->classes); + if (is_array($tmp)) $data = array_merge($data, $tmp); + } + } + } + } + } + } + $result = false; + } + + // include-pattern + if ($allow_includes){ + $xpath = ".//*[contains(concat(' ',normalize-space(@class),' '),' include ')]"; + $results = $item->xpath($xpath); + + if ($results){ + foreach ($results as $result){ + $tagName = strtoupper(dom_import_simplexml($result)->tagName); + if ((($tagName == "OBJECT" && $result['data']) || ($tagName == "A" && $result['href'])) + && preg_match('/\binclude\b/', $result['class'])){ + $att = ($tagName == "OBJECT" ? 'data' : 'href'); + $id = str_replace('#', '', $result[$att]); + $doc = $this->doc; + $xpath = "//*[@id='$id']"; + $includes = $doc->xpath($xpath); + foreach ($includes as $include){ + $include = simplexml_load_string('<root1><root2>'.$include->asXML().'</root2></root1>'); // don't ask. + $tmp = $this->processNodes($include, $this->classes, false); + if (is_array($tmp)) $data = array_merge($data, $tmp); + } + } + } + } + } + $out[] = $data; + } + + if (sizeof($out) > 1){ + return $out; + }else if (isset($data)){ + return $data; + }else{ + return array(); + } + } + + + private function getNodeValue($node, $className) + { + + $tag_name = strtoupper(dom_import_simplexml($node)->tagName); + $s = false; + + // ignore DEL tags + if ($tag_name == 'DEL') return $s; + + // look up att map values + if (array_key_exists($className, $this->att_map)){ + + foreach ($this->att_map[$className] as $map){ + if (preg_match("/$tag_name\|/", $map)){ + $s = ''.$node[array_pop($foo = explode('|', $map))]; + } + } + } + + // if nothing and OBJ, try data. + if (!$s && $tag_name=='OBJECT' && $node['data']) $s = ''.$node['data']; + + // if nothing and IMG, try alt. + if (!$s && $tag_name=='IMG' && $node['alt']) $s = ''.$node['alt']; + + // if nothing and AREA, try alt. + if (!$s && $tag_name=='AREA' && $node['alt']) $s = ''.$node['alt']; + + //if nothing and not A, try title. + if (!$s && $tag_name!='A' && $node['title']) $s = ''.$node['title']; + + + // if nothing found, go with node text + $s = ($s ? $s : implode(array_filter($node->xpath('child::node()'), array(&$this, "filterBlankValues")), ' ')); + + // callbacks + if (array_key_exists($className, $this->callbacks)){ + $s = preg_replace_callback('/.*/', $this->callbacks[$className], $s, 1); + } + + // trim and remove line breaks + if ($tag_name != 'PRE'){ + $s = trim(preg_replace('/[\r\n\t]+/', '', $s)); + $s = trim(preg_replace('/(\s{2})+/', ' ', $s)); + } + + return $s; + } + + private function filterBlankValues($s){ + return preg_match("/\w+/", $s); + } + + + private function tidyThis($source) + { + switch ( $this->tidy_mode ) + { + case 'exec': + $tmp_file = $this->tmp_dir.md5($source).'.txt'; + file_put_contents($tmp_file, $source); + exec("tidy -utf8 -indent -asxhtml -numeric -bare -quiet $tmp_file", $tidy); + unlink($tmp_file); + return implode("\n", $tidy); + break; + + case 'php': + $tidy = tidy_parse_string($source); + return tidy_clean_repair($tidy); + break; + + default: + return $source; + break; + } + + } + + + private function loadProfile($profile) + { + require_once("$profile.profile.php"); + } + + + private function loadDoc($input_xml, $fragment=false) + { + $xml = simplexml_load_string($input_xml); + + $this->doc = $xml; + + if ($fragment){ + $doc = $xml->xpath("//*[@id='$fragment']"); + $xml = simplexml_load_string($doc[0]->asXML()); + $doc = null; + } + + // base tag + if ($xml->head->base['href']) $this->base = $xml->head->base['href']; + + // xml:base attribute - PITA with SimpleXML + preg_match('/xml:base="(.*)"/', $xml->asXML(), $matches); + if (is_array($matches) && sizeof($matches)>1) $this->base = $matches[1]; + + return $xml->xpath("//*[contains(concat(' ',normalize-space(@class),' '),' $this->root_class ')]"); + + } + + + private function loadURL($url) + { + $this->url = $url; + + if ($this->tidy_mode == 'proxy' && $this->tidy_proxy != ''){ + $url = $this->tidy_proxy . $url; + } + + return @file_get_contents($url); + + } + + + private function postProcess($profile, $s) + { + $required = $this->required; + + if (is_array($s) && array_key_exists($required[0], $s)){ + $s = array($s); + } + + $s = $this->dedupeSingles($s); + + if (function_exists('hKit_'.$profile.'_post')){ + $s = call_user_func('hKit_'.$profile.'_post', $s); + } + + $s = $this->removeTextVals($s); + + return $s; + } + + + private function resolvePath($filepath) + { // ugly code ahoy: needs a serious tidy up + + $filepath = $filepath[0]; + + $base = $this->base; + $url = $this->url; + + if ($base != '' && strpos($base, '://') !== false) + $url = $base; + + $r = parse_url($url); + $domain = $r['scheme'] . '://' . $r['host']; + + if (!isset($r['path'])) $r['path'] = '/'; + $path = explode('/', $r['path']); + $file = explode('/', $filepath); + $new = array(''); + + if (strpos($filepath, '://') !== false || strpos($filepath, 'data:') !== false){ + return $filepath; + } + + if ($file[0] == ''){ + // absolute path + return ''.$domain . implode('/', $file); + }else{ + // relative path + if ($path[sizeof($path)-1] == '') array_pop($path); + if (strpos($path[sizeof($path)-1], '.') !== false) array_pop($path); + + foreach ($file as $segment){ + if ($segment == '..'){ + array_pop($path); + }else{ + $new[] = $segment; + } + } + return ''.$domain . implode('/', $path) . implode('/', $new); + } + } + + private function resolveEmail($v) + { + $parts = parse_url($v[0]); + return ($parts['path']); + } + + + private function dedupeSingles($s) + { + $singles = $this->singles; + + foreach ($s as &$item){ + foreach ($singles as $classname){ + if (array_key_exists($classname, $item) && is_array($item[$classname])){ + if (isset($item[$classname][0])) $item[$classname] = $item[$classname][0]; + } + } + } + + return $s; + } + + private function removeTextVals($s) + { + foreach ($s as $key => &$val){ + if ($key){ + $k = $key; + }else{ + $k = ''; + } + + if (is_array($val)){ + $val = $this->removeTextVals($val); + }else{ + if ($k == 'text'){ + $val = ''; + } + } + } + + return array_filter($s); + } + + } + + +?>
\ No newline at end of file diff --git a/plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch b/plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch deleted file mode 100644 index c53bd9737..000000000 --- a/plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch +++ /dev/null @@ -1,14 +0,0 @@ -diff --git a/htdocs/lib/pear/XML/Feed/Parser/RSS2.php b/htdocs/lib/pear/XML/Feed/Parser/RSS2.php -index c5d79d1..308a4ab 100644 ---- a/htdocs/lib/pear/XML/Feed/Parser/RSS2.php -+++ b/htdocs/lib/pear/XML/Feed/Parser/RSS2.php -@@ -321,7 +321,8 @@ class XML_Feed_Parser_RSS2 extends XML_Feed_Parser_Type - */ - function getLink($offset, $attribute = 'href', $params = array()) - { -- $links = $this->model->getElementsByTagName('link'); -+ $xPath = new DOMXPath($this->model); -+ $links = $xPath->query('//link'); - - if ($links->length <= $offset) { - return false; diff --git a/plugins/OStatus/js/ostatus.js b/plugins/OStatus/js/ostatus.js index 671795558..bd29b5c0c 100644 --- a/plugins/OStatus/js/ostatus.js +++ b/plugins/OStatus/js/ostatus.js @@ -1,6 +1,38 @@ +/* + * StatusNet - a 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/>. + * + * @category OStatus UI interaction + * @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/ + * @note Everything in here should eventually migrate over to /js/util.js's SN. + */ + +SN.Init.OStatusCookie = function() { + if (SN.U.StatusNetInstance.Get() === null) { + SN.U.StatusNetInstance.Set({RemoteProfile: null}); + } +}; + SN.U.DialogBox = { Subscribe: function(a) { - var f = a.parent().find('#form_ostatus_connect'); + var f = a.parent().find('.form_settings'); if (f.length > 0) { f.show(); } @@ -8,7 +40,7 @@ SN.U.DialogBox = { $.ajax({ type: 'GET', dataType: 'xml', - url: a[0].href+'&ajax=1', + url: a[0].href + ((a[0].href.match(/[\\?]/) === null)?'?':'&') + 'ajax=1', beforeSend: function(formData) { a.addClass('processing'); }, @@ -19,7 +51,7 @@ SN.U.DialogBox = { if (typeof($('form', data)[0]) != 'undefined') { a.after(document._importNode($('form', data)[0], true)); - var form = a.parent().find('#form_ostatus_connect'); + var form = a.parent().find('.form_settings'); form .addClass('dialogbox') @@ -39,7 +71,17 @@ SN.U.DialogBox = { return false; }); - form.find('#acct').focus(); + form.find('#profile').focus(); + + if (form.attr('id') == 'form_ostatus_connect') { + SN.Init.OStatusCookie(); + form.find('#profile').val(SN.U.StatusNetInstance.Get().RemoteProfile); + + form.find("[type=submit]").bind('click', function() { + SN.U.StatusNetInstance.Set({RemoteProfile: form.find('#profile').val()}); + return true; + }); + } } a.removeClass('processing'); @@ -50,11 +92,11 @@ SN.U.DialogBox = { }; SN.Init.Subscribe = function() { - $('.entity_subscribe a').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; }); + $('.entity_subscribe .entity_remote_subscribe').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; }); }; $(document).ready(function() { - if ($('.entity_subscribe .entity_remote_subscribe').length > 0) { - SN.Init.Subscribe(); - } + SN.Init.Subscribe(); + + $('.form_remote_authorize').bind('submit', function() { $(this).addClass(SN.C.S.Processing); return true; }); }); diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php deleted file mode 100644 index f137946ab..000000000 --- a/plugins/OStatus/lib/activity.php +++ /dev/null @@ -1,448 +0,0 @@ -<?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/OStatus/lib/discovery.php b/plugins/OStatus/lib/discovery.php new file mode 100644 index 000000000..f8449b309 --- /dev/null +++ b/plugins/OStatus/lib/discovery.php @@ -0,0 +1,310 @@ +<?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/ + */ + +/** + * This class implements LRDD-based service discovery based on the "Hammer Draft" + * (including webfinger) + * + * @see http://groups.google.com/group/webfinger/browse_thread/thread/9f3d93a479e91bbf + */ +class Discovery +{ + + const LRDD_REL = 'lrdd'; + const PROFILEPAGE = 'http://webfinger.net/rel/profile-page'; + const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from'; + const HCARD = 'http://microformats.org/profile/hcard'; + + public $methods = array(); + + public function __construct() + { + $this->registerMethod('Discovery_LRDD_Host_Meta'); + $this->registerMethod('Discovery_LRDD_Link_Header'); + $this->registerMethod('Discovery_LRDD_Link_HTML'); + } + + + public function registerMethod($class) + { + $this->methods[] = $class; + } + + /** + * Given a "user id" make sure it's normalized to either a webfinger + * acct: uri or a profile HTTP URL. + */ + public static function normalize($user_id) + { + if (substr($user_id, 0, 5) == 'http:' || + substr($user_id, 0, 6) == 'https:' || + substr($user_id, 0, 5) == 'acct:') { + return $user_id; + } + + if (strpos($user_id, '@') !== FALSE) { + return 'acct:' . $user_id; + } + + return 'http://' . $user_id; + } + + public static function isWebfinger($user_id) + { + $uri = Discovery::normalize($user_id); + + return (substr($uri, 0, 5) == 'acct:'); + } + + /** + * This implements the actual lookup procedure + */ + public function lookup($id) + { + // Normalize the incoming $id to make sure we have a uri + $uri = $this->normalize($id); + + foreach ($this->methods as $class) { + $links = call_user_func(array($class, 'discover'), $uri); + if ($link = Discovery::getService($links, Discovery::LRDD_REL)) { + // Load the LRDD XRD + if (!empty($link['template'])) { + $xrd_uri = Discovery::applyTemplate($link['template'], $uri); + } else { + $xrd_uri = $link['href']; + } + + $xrd = $this->fetchXrd($xrd_uri); + if ($xrd) { + return $xrd; + } + } + } + + throw new Exception('Unable to find services for '. $id); + } + + public static function getService($links, $service) { + if (!is_array($links)) { + return false; + } + + foreach ($links as $link) { + if ($link['rel'] == $service) { + return $link; + } + } + } + + + public static function applyTemplate($template, $id) + { + $template = str_replace('{uri}', urlencode($id), $template); + + return $template; + } + + + public static function fetchXrd($url) + { + try { + $client = new HTTPClient(); + $response = $client->get($url); + } catch (HTTP_Request2_Exception $e) { + return false; + } + + if ($response->getStatus() != 200) { + return false; + } + + return XRD::parse($response->getBody()); + } +} + +interface Discovery_LRDD +{ + public function discover($uri); +} + +class Discovery_LRDD_Host_Meta implements Discovery_LRDD +{ + public function discover($uri) + { + if (!Discovery::isWebfinger($uri)) { + return false; + } + + // We have a webfinger acct: - start with host-meta + list($name, $domain) = explode('@', $uri); + $url = 'http://'. $domain .'/.well-known/host-meta'; + + $xrd = Discovery::fetchXrd($url); + + if ($xrd) { + if ($xrd->host != $domain) { + return false; + } + + return $xrd->links; + } + } +} + +class Discovery_LRDD_Link_Header implements Discovery_LRDD +{ + public function discover($uri) + { + try { + $client = new HTTPClient(); + $response = $client->get($uri); + } catch (HTTP_Request2_Exception $e) { + return false; + } + + if ($response->getStatus() != 200) { + return false; + } + + $link_header = $response->getHeader('Link'); + if (!$link_header) { + // return false; + } + + return Discovery_LRDD_Link_Header::parseHeader($link_header); + } + + protected static function parseHeader($header) + { + preg_match('/^<[^>]+>/', $header, $uri_reference); + //if (empty($uri_reference)) return; + + $links = array(); + + $link_uri = trim($uri_reference[0], '<>'); + $link_rel = array(); + $link_type = null; + + // remove uri-reference from header + $header = substr($header, strlen($uri_reference[0])); + + // parse link-params + $params = explode(';', $header); + + foreach ($params as $param) { + if (empty($param)) continue; + list($param_name, $param_value) = explode('=', $param, 2); + $param_name = trim($param_name); + $param_value = preg_replace('(^"|"$)', '', trim($param_value)); + + // for now we only care about 'rel' and 'type' link params + // TODO do something with the other links-params + switch ($param_name) { + case 'rel': + $link_rel = trim($param_value); + break; + + case 'type': + $link_type = trim($param_value); + } + } + + $links[] = array( + 'href' => $link_uri, + 'rel' => $link_rel, + 'type' => $link_type); + + return $links; + } +} + +class Discovery_LRDD_Link_HTML implements Discovery_LRDD +{ + public function discover($uri) + { + try { + $client = new HTTPClient(); + $response = $client->get($uri); + } catch (HTTP_Request2_Exception $e) { + return false; + } + + if ($response->getStatus() != 200) { + return false; + } + + return Discovery_LRDD_Link_HTML::parse($response->getBody()); + } + + + public function parse($html) + { + $links = array(); + + preg_match('/<head(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches); + $head_html = $head_matches[2]; + + preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches); + + foreach ($link_matches[0] as $link_html) { + $link_url = null; + $link_rel = null; + $link_type = null; + + preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches); + if ( isset($rel_matches[3]) ) { + $link_rel = $rel_matches[3]; + } else if ( isset($rel_matches[1]) ) { + $link_rel = $rel_matches[1]; + } + + preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches); + if ( isset($href_matches[3]) ) { + $link_uri = $href_matches[3]; + } else if ( isset($href_matches[1]) ) { + $link_uri = $href_matches[1]; + } + + preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches); + if ( isset($type_matches[3]) ) { + $link_type = $type_matches[3]; + } else if ( isset($type_matches[1]) ) { + $link_type = $type_matches[1]; + } + + $links[] = array( + 'href' => $link_url, + 'rel' => $link_rel, + 'type' => $link_type, + ); + } + + return $links; + } +} diff --git a/plugins/OStatus/lib/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php index 39985fc90..7afb71bdc 100644 --- a/plugins/OStatus/lib/feeddiscovery.php +++ b/plugins/OStatus/lib/feeddiscovery.php @@ -48,6 +48,14 @@ class FeedSubNoFeedException extends FeedSubException { } +class FeedSubBadXmlException extends FeedSubException +{ +} + +class FeedSubNoHubException extends FeedSubException +{ +} + /** * Given a web page or feed URL, discover the final location of the feed * and return its current contents. @@ -57,21 +65,25 @@ class FeedSubNoFeedException extends FeedSubException * if ($feed->discoverFromURL($url)) { * print $feed->uri; * print $feed->type; - * processFeed($feed->body); + * processFeed($feed->feed); // DOMDocument * } */ class FeedDiscovery { public $uri; public $type; - public $body; + public $feed; + /** Post-initialize query helper... */ + public function getLink($rel, $type=null) + { + // @fixme check for non-Atom links in RSS2 feeds as well + return self::getAtomLink($rel, $type); + } - public function feedMunger() + public function getAtomLink($rel, $type=null) { - require_once 'XML/Feed/Parser.php'; - $feed = new XML_Feed_Parser($this->body, false, false, true); // @fixme - return new FeedMunger($feed, $this->uri); + return ActivityUtils::getLink($this->feed->documentElement, $rel, $type); } /** @@ -90,6 +102,7 @@ class FeedDiscovery $client = new HTTPClient(); $response = $client->get($url); } catch (HTTP_Request2_Exception $e) { + common_log(LOG_ERR, __METHOD__ . " Failure for $url - " . $e->getMessage()); throw new FeedSubBadURLException($e); } @@ -107,7 +120,12 @@ class FeedDiscovery return $this->initFromResponse($response); } - + + function discoverFromFeedURL($url) + { + return $this->discoverFromURL($url, false); + } + function initFromResponse($response) { if (!$response->isOk()) { @@ -122,16 +140,26 @@ class FeedDiscovery $type = $response->getHeader('Content-Type'); if (preg_match('!^(text/xml|application/xml|application/(rss|atom)\+xml)!i', $type)) { - $this->uri = $sourceurl; - $this->type = $type; - $this->body = $body; - return true; + return $this->init($sourceurl, $type, $body); } else { common_log(LOG_WARNING, "Unrecognized feed type $type for $sourceurl"); throw new FeedSubUnrecognizedTypeException($type); } } + function init($sourceurl, $type, $body) + { + $feed = new DOMDocument(); + if ($feed->loadXML($body)) { + $this->uri = $sourceurl; + $this->type = $type; + $this->feed = $feed; + return $this->uri; + } else { + throw new FeedSubBadXmlException($url); + } + } + /** * @param string $url source URL, used to resolve relative links * @param string $body HTML body text diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php deleted file mode 100644 index e8c46de90..000000000 --- a/plugins/OStatus/lib/feedmunger.php +++ /dev/null @@ -1,350 +0,0 @@ -<?php -/* - * 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); } - -class FeedSubPreviewNotice extends Notice -{ - protected $fetched = true; - - function __construct($profile) - { - $this->profile = $profile; - $this->profile_id = 0; - } - - function getProfile() - { - return $this->profile; - } - - function find() - { - return true; - } - - function fetch() - { - $got = $this->fetched; - $this->fetched = false; - return $got; - } -} - -class FeedSubPreviewProfile extends Profile -{ - function getAvatar($width, $height=null) - { - return new FeedSubPreviewAvatar($width, $height, $this->avatar); - } -} - -class FeedSubPreviewAvatar extends Avatar -{ - function __construct($width, $height, $remote) - { - $this->remoteImage = $remote; - } - - function displayUrl() { - return $this->remoteImage; - } -} - -class FeedMunger -{ - /** - * @param XML_Feed_Parser $feed - */ - function __construct($feed, $url=null) - { - $this->feed = $feed; - $this->url = $url; - } - - function ostatusProfile() - { - $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()) - { - // XML_Feed_Parser gets confused by multiple <link> elements. - $dom = $item->model; - - // Note that RSS feeds would embed an <atom:link> so this should work for both. - /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds - // <link rel='hub' href='http://pubsubhubbub.appspot.com/'/> - $links = $dom->getElementsByTagNameNS('http://www.w3.org/2005/Atom', 'link'); - for ($i = 0; $i < $links->length; $i++) { - $node = $links->item($i); - if ($node->hasAttributes()) { - $href = $node->attributes->getNamedItem('href'); - if ($href) { - $matches = 0; - foreach ($attribs as $name => $val) { - $attrib = $node->attributes->getNamedItem($name); - if ($attrib && $attrib->value == $val) { - $matches++; - } - } - if ($matches == count($attribs)) { - return $href->value; - } - } - } - } - return false; - } - - function getRssLink($item) - { - // XML_Feed_Parser gets confused by multiple <link> elements. - $dom = $item->model; - - // Note that RSS feeds would embed an <atom:link> so this should work for both. - /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds - // <link rel='hub' href='http://pubsubhubbub.appspot.com/'/> - $links = $dom->getElementsByTagName('link'); - for ($i = 0; $i < $links->length; $i++) { - $node = $links->item($i); - if (!$node->hasAttributes()) { - return $node->textContent; - } - } - return false; - } - - function getAltLink($item) - { - // Check for an atom link... - $link = $this->getAtomLink($item, array('rel' => 'alternate', 'type' => 'text/html')); - if (!$link) { - $link = $this->getRssLink($item); - } - return $link; - } - - function getHubLink() - { - 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) { - $profile = new FeedSubPreviewProfile(); - } else { - $profile = new Profile(); - } - - // @todo validate/normalize nick? - $profile->nickname = $this->feed->title; - $profile->fullname = $this->feed->title; - $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? - - return $profile; - } - - function notice($index=1, $preview=false) - { - $entry = $this->feed->getEntryByOffset($index); - 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); // @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. - - $title = $entry->title; - $link = $this->getAltLink($entry); - 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 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/hubverifyqueuehandler.php b/plugins/OStatus/lib/hubconfqueuehandler.php index 125d13a77..c8e0b72fe 100644 --- a/plugins/OStatus/lib/hubverifyqueuehandler.php +++ b/plugins/OStatus/lib/hubconfqueuehandler.php @@ -22,24 +22,25 @@ * @package Hub * @author Brion Vibber <brion@status.net> */ -class HubVerifyQueueHandler extends QueueHandler +class HubConfQueueHandler extends QueueHandler { function transport() { - return 'hubverify'; + return 'hubconf'; } function handle($data) { $sub = $data['sub']; $mode = $data['mode']; + $token = $data['token']; assert($sub instanceof HubSub); assert($mode === 'subscribe' || $mode === 'unsubscribe'); common_log(LOG_INFO, __METHOD__ . ": $mode $sub->callback $sub->topic"); try { - $sub->verify($mode); + $sub->verify($mode, $token); } catch (Exception $e) { common_log(LOG_ERR, "Failed PuSH $mode verify to $sub->callback for $sub->topic: " . $e->getMessage()); diff --git a/plugins/OStatus/lib/huboutqueuehandler.php b/plugins/OStatus/lib/huboutqueuehandler.php index 0791c7e5d..3ad94646e 100644 --- a/plugins/OStatus/lib/huboutqueuehandler.php +++ b/plugins/OStatus/lib/huboutqueuehandler.php @@ -33,6 +33,7 @@ class HubOutQueueHandler extends QueueHandler { $sub = $data['sub']; $atom = $data['atom']; + $retries = $data['retries']; assert($sub instanceof HubSub); assert(is_string($atom)); @@ -40,13 +41,20 @@ class HubOutQueueHandler extends QueueHandler 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; + $retries--; + $msg = "Failed PuSH to $sub->callback for $sub->topic: " . + $e->getMessage(); + if ($retries > 0) { + common_log(LOG_ERR, "$msg; scheduling for $retries more tries"); + + // @fixme when we have infrastructure to schedule a retry + // after a delay, use it. + $sub->distribute($atom, $retries); + } else { + common_log(LOG_ERR, "$msg; discarding"); + } } return true; } } - diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php new file mode 100644 index 000000000..230d81ba1 --- /dev/null +++ b/plugins/OStatus/lib/magicenvelope.php @@ -0,0 +1,201 @@ +<?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 MagicEnvelope +{ + const ENCODING = 'base64url'; + + const NS = 'http://salmon-protocol.org/ns/magic-env'; + + private function normalizeUser($user_id) + { + if (substr($user_id, 0, 5) == 'http:' || + substr($user_id, 0, 6) == 'https:' || + substr($user_id, 0, 5) == 'acct:') { + return $user_id; + } + + if (strpos($user_id, '@') !== FALSE) { + return 'acct:' . $user_id; + } + + return 'http://' . $user_id; + } + + public function getKeyPair($signer_uri) + { + $disco = new Discovery(); + + try { + $xrd = $disco->lookup($signer_uri); + } catch (Exception $e) { + return false; + } + if ($xrd->links) { + if ($link = Discovery::getService($xrd->links, Magicsig::PUBLICKEYREL)) { + list($type, $keypair) = explode(';', $link['href']); + return $keypair; + } + } + throw new Exception('Unable to locate signer public key'); + } + + + public function signMessage($text, $mimetype, $keypair) + { + $signature_alg = Magicsig::fromString($keypair); + $armored_text = base64_encode($text); + + return array( + 'data' => $armored_text, + 'encoding' => MagicEnvelope::ENCODING, + 'data_type' => $mimetype, + 'sig' => $signature_alg->sign($armored_text), + 'alg' => $signature_alg->getName() + ); + + + } + + public function toXML($env) { + $dom = new DOMDocument(); + + $envelope = $dom->createElementNS(MagicEnvelope::NS, 'me:env'); + $envelope->setAttribute('xmlns:me', MagicEnvelope::NS); + $data = $dom->createElementNS(MagicEnvelope::NS, 'me:data', $env['data']); + $data->setAttribute('type', $env['data_type']); + $envelope->appendChild($data); + $enc = $dom->createElementNS(MagicEnvelope::NS, 'me:encoding', $env['encoding']); + $envelope->appendChild($enc); + $alg = $dom->createElementNS(MagicEnvelope::NS, 'me:alg', $env['alg']); + $envelope->appendChild($alg); + $sig = $dom->createElementNS(MagicEnvelope::NS, 'me:sig', $env['sig']); + $envelope->appendChild($sig); + + $dom->appendChild($envelope); + + + return $dom->saveXML(); + } + + + public function unfold($env) + { + $dom = new DOMDocument(); + $dom->loadXML(base64_decode($env['data'])); + + if ($dom->documentElement->tagName != 'entry') { + return false; + } + + $prov = $dom->createElementNS(MagicEnvelope::NS, 'me:provenance'); + $prov->setAttribute('xmlns:me', MagicEnvelope::NS); + $data = $dom->createElementNS(MagicEnvelope::NS, 'me:data', $env['data']); + $data->setAttribute('type', $env['data_type']); + $prov->appendChild($data); + $enc = $dom->createElementNS(MagicEnvelope::NS, 'me:encoding', $env['encoding']); + $prov->appendChild($enc); + $alg = $dom->createElementNS(MagicEnvelope::NS, 'me:alg', $env['alg']); + $prov->appendChild($alg); + $sig = $dom->createElementNS(MagicEnvelope::NS, 'me:sig', $env['sig']); + $prov->appendChild($sig); + + $dom->documentElement->appendChild($prov); + + return $dom->saveXML(); + } + + public function getAuthor($text) { + $doc = new DOMDocument(); + if (!$doc->loadXML($text)) { + return FALSE; + } + + if ($doc->documentElement->tagName == 'entry') { + $authors = $doc->documentElement->getElementsByTagName('author'); + foreach ($authors as $author) { + $uris = $author->getElementsByTagName('uri'); + foreach ($uris as $uri) { + return $this->normalizeUser($uri->nodeValue); + } + } + } + } + + public function checkAuthor($text, $signer_uri) + { + return ($this->getAuthor($text) == $signer_uri); + } + + public function verify($env) + { + if ($env['alg'] != 'RSA-SHA256') { + return false; + } + + if ($env['encoding'] != MagicEnvelope::ENCODING) { + return false; + } + + $text = base64_decode($env['data']); + $signer_uri = $this->getAuthor($text); + + $verifier = Magicsig::fromString($this->getKeyPair($signer_uri)); + + return $verifier->verify($env['data'], $env['sig']); + } + + public function parse($text) + { + $dom = DOMDocument::loadXML($text); + return $this->fromDom($dom); + } + + public function fromDom($dom) + { + if ($dom->documentElement->tagName == 'entry') { + $env_element = $dom->getElementsByTagNameNS(MagicEnvelope::NS, 'provenance')->item(0); + } else if ($dom->documentElement->tagName == 'me:env') { + $env_element = $dom->documentElement; + } else { + return false; + } + + $data_element = $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'data')->item(0); + + return array( + 'data' => trim($data_element->nodeValue), + 'data_type' => $data_element->getAttribute('type'), + 'encoding' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'encoding')->item(0)->nodeValue, + 'alg' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'alg')->item(0)->nodeValue, + 'sig' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'sig')->item(0)->nodeValue, + ); + } + +} diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/ostatusqueuehandler.php index 245a57f72..6ca31c485 100644 --- a/plugins/OStatus/lib/hubdistribqueuehandler.php +++ b/plugins/OStatus/lib/ostatusqueuehandler.php @@ -18,46 +18,77 @@ */ /** - * Send a PuSH subscription verification from our internal hub. - * Queue up final distribution for - * @package Hub + * Prepare PuSH and Salmon distributions for an outgoing message. + * + * @package OStatusPlugin * @author Brion Vibber <brion@status.net> */ -class HubDistribQueueHandler extends QueueHandler +class OStatusQueueHandler extends QueueHandler { function transport() { - return 'hubdistrib'; + return 'ostatus'; } function handle($notice) { assert($notice instanceof Notice); - $this->pushUser($notice); + $this->notice = $notice; + $this->user = User::staticGet($notice->profile_id); + + $this->pushUser(); + foreach ($notice->getGroups() as $group) { - $this->pushGroup($notice, $group->group_id); + $oprofile = Ostatus_profile::staticGet('group_id', $group->id); + if ($oprofile) { + $this->pingReply($oprofile); + } else { + $this->pushGroup($group->id); + } + } + + foreach ($notice->getReplies() as $profile_id) { + $oprofile = Ostatus_profile::staticGet('profile_id', $profile_id); + if ($oprofile) { + $this->pingReply($oprofile); + } } + return true; } - - function pushUser($notice) + + function pushUser() { - // 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); + if ($this->user) { + // For local posts, ping the PuSH hub to update their feed. + // http://identi.ca/api/statuses/user_timeline/1.atom + $feed = common_local_url('ApiTimelineUser', + array('id' => $this->user->id, + 'format' => 'atom')); + $this->pushFeed($feed, array($this, 'userFeedForNotice')); + } } - function pushGroup($notice, $group_id) + function pushGroup($group_id) { + // For a local group, ping the PuSH hub to update its feed. + // Updates may come from either a local or a remote user. $feed = common_local_url('ApiTimelineGroup', array('id' => $group_id, 'format' => 'atom')); - $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id, $notice); + $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id); + } + + function pingReply($oprofile) + { + if ($this->user) { + // For local posts, send a Salmon ping to the mentioned + // remote user or group. + // @fixme as an optimization we can skip this if the + // remote profile is subscribed to the author. + $oprofile->notifyDeferred($this->notice, $this->user); + } } /** @@ -122,31 +153,26 @@ class HubDistribQueueHandler extends QueueHandler 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'); + $sub->distribute($atom); } } /** * Build a single-item version of the sending user's Atom feed. - * @param Notice $notice * @return string */ - function userFeedForNotice($notice) + function userFeedForNotice() { // @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, + $api->prepare(array('id' => $this->notice->profile_id, 'format' => 'atom', - 'max_id' => $notice->id, - 'since_id' => $notice->id - 1)); + 'max_id' => $this->notice->id, + 'since_id' => $this->notice->id - 1)); $api->showTimeline(); $feed = ob_get_clean(); @@ -158,7 +184,7 @@ class HubDistribQueueHandler extends QueueHandler return $feed; } - function groupFeedForNotice($group_id, $notice) + function groupFeedForNotice($group_id) { // @fixme this feels VERY hacky... // should probably be a cleaner way to do it @@ -167,8 +193,8 @@ class HubDistribQueueHandler extends QueueHandler $api = new ApiTimelineGroupAction(); $args = array('id' => $group_id, 'format' => 'atom', - 'max_id' => $notice->id, - 'since_id' => $notice->id - 1); + 'max_id' => $this->notice->id, + 'since_id' => $this->notice->id - 1); $api->prepare($args); $api->handle($args); $feed = ob_get_clean(); diff --git a/plugins/OStatus/lib/pushinqueuehandler.php b/plugins/OStatus/lib/pushinqueuehandler.php new file mode 100644 index 000000000..1fd29ae30 --- /dev/null +++ b/plugins/OStatus/lib/pushinqueuehandler.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/>. + */ + +/** + * Process a feed distribution POST from a PuSH hub. + * @package FeedSub + * @author Brion Vibber <brion@status.net> + */ + +class PushInQueueHandler extends QueueHandler +{ + function transport() + { + return 'pushin'; + } + + function handle($data) + { + assert(is_array($data)); + + $feedsub_id = $data['feedsub_id']; + $post = $data['post']; + $hmac = $data['hmac']; + + $feedsub = FeedSub::staticGet('id', $feedsub_id); + if ($feedsub) { + try { + $feedsub->receive($post, $hmac); + } catch(Exception $e) { + common_log(LOG_ERR, "Exception during PuSH input processing for $feedsub->uri: " . $e->getMessage()); + } + } else { + common_log(LOG_ERR, "Discarding POST to unknown feed subscription id $feedsub_id"); + } + return true; + } +} diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index 8c77222a6..3d3341bc6 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -28,37 +28,83 @@ */ class Salmon { - public function post($endpoint_uri, $xml) + + const NS_REPLIES = "http://salmon-protocol.org/ns/salmon-replies"; + + const NS_MENTIONS = "http://salmon-protocol.org/ns/salmon-mention"; + + /** + * Sign and post the given Atom entry as a Salmon message. + * + * @fixme pass through the actor for signing? + * + * @param string $endpoint_uri + * @param string $xml + * @return boolean success + */ + public function post($endpoint_uri, $xml, $actor) { if (empty($endpoint_uri)) { - return FALSE; + return false; + } + + try { + $xml = $this->createMagicEnv($xml, $actor); + } catch (Exception $e) { + common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage()); + return false; } - $headers = array('Content-type: application/atom+xml'); + $headers = array('Content-Type: application/magic-envelope+xml'); try { $client = new HTTPClient(); $client->setBody($xml); $response = $client->post($endpoint_uri, $headers); } catch (HTTP_Request2_Exception $e) { + common_log(LOG_ERR, "Salmon post to $endpoint_uri failed: " . $e->getMessage()); return false; } if ($response->getStatus() != 200) { + common_log(LOG_ERR, "Salmon at $endpoint_uri returned status " . + $response->getStatus() . ': ' . $response->getBody()); return false; } - + return true; } - public function createMagicEnv($text, $userid) + public function createMagicEnv($text, $actor) { + $magic_env = new MagicEnvelope(); + $user = User::staticGet('id', $actor->id); + if ($user->id) { + // Use local key + $magickey = Magicsig::staticGet('user_id', $user->id); + if (!$magickey) { + // No keypair yet, let's generate one. + $magickey = new Magicsig(); + $magickey->generate($user->id); + } + } else { + throw new Exception("Salmon invalid actor for signing"); + } + try { + $env = $magic_env->signMessage($text, 'application/atom+xml', $magickey->toString()); + } catch (Exception $e) { + return $text; + } + return $magic_env->toXML($env); } - public function verifyMagicEnv($env) + public function verifyMagicEnv($text) { + $magic_env = new MagicEnvelope(); + + $env = $magic_env->parse($text); - + return $magic_env->verify($env); } } diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php new file mode 100644 index 000000000..fa9dc3b1d --- /dev/null +++ b/plugins/OStatus/lib/salmonaction.php @@ -0,0 +1,196 @@ +<?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 $xml = null; + var $activity = null; + + function prepare($args) + { + StatusNet::setApi(true); // Send smaller error pages + + parent::prepare($args); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError(_m('This method requires a POST.')); + } + + if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/magic-envelope+xml') { + $this->clientError(_m('Salmon requires application/magic-envelope+xml')); + } + + $xml = file_get_contents('php://input'); + + + // Check the signature + $salmon = new Salmon; + if (!$salmon->verifyMagicEnv($xml)) { + common_log(LOG_DEBUG, "Salmon signature verification failed."); + $this->clientError(_m('Salmon signature verification failed.')); + } else { + $magic_env = new MagicEnvelope(); + $env = $magic_env->parse($xml); + $xml = $magic_env->unfold($env); + } + + + $dom = DOMDocument::loadXML($xml); + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + common_log(LOG_DEBUG, "Got invalid Salmon post: $xml"); + $this->clientError(_m('Salmon post must be an Atom entry.')); + } + + $this->act = new Activity($dom->documentElement); + return true; + } + + /** + * Check the posted activity type and break out to appropriate processing. + */ + + function handle($args) + { + StatusNet::setApi(true); // Send smaller error pages + + common_log(LOG_DEBUG, "Got a " . $this->act->verb); + if (Event::handle('StartHandleSalmon', array($this->activity))) { + switch ($this->act->verb) + { + case ActivityVerb::POST: + $this->handlePost(); + break; + case ActivityVerb::SHARE: + $this->handleShare(); + break; + case ActivityVerb::FAVORITE: + $this->handleFavorite(); + break; + case ActivityVerb::UNFAVORITE: + $this->handleUnfavorite(); + break; + case ActivityVerb::FOLLOW: + case ActivityVerb::FRIEND: + $this->handleFollow(); + break; + case ActivityVerb::UNFOLLOW: + $this->handleUnfollow(); + break; + case ActivityVerb::JOIN: + $this->handleJoin(); + break; + case ActivityVerb::LEAVE: + $this->handleLeave(); + break; + case ActivityVerb::UPDATE_PROFILE: + $this->handleUpdateProfile(); + break; + default: + throw new ClientException(_m("Unrecognized activity type.")); + } + Event::handle('EndHandleSalmon', array($this->activity)); + } + } + + function handlePost() + { + throw new ClientException(_m("This target doesn't understand posts.")); + } + + function handleFollow() + { + throw new ClientException(_m("This target doesn't understand follows.")); + } + + function handleUnfollow() + { + throw new ClientException(_m("This target doesn't understand unfollows.")); + } + + function handleFavorite() + { + throw new ClientException(_m("This target doesn't understand favorites.")); + } + + function handleUnfavorite() + { + throw new ClientException(_m("This target doesn't understand unfavorites.")); + } + + function handleShare() + { + throw new ClientException(_m("This target doesn't understand share events.")); + } + + function handleJoin() + { + throw new ClientException(_m("This target doesn't understand joins.")); + } + + function handleLeave() + { + throw new ClientException(_m("This target doesn't understand leave events.")); + } + + /** + * Remote user sent us an update to their profile. + * If we already know them, accept the updates. + */ + function handleUpdateProfile() + { + $oprofile = Ostatus_profile::getActorProfile($this->act); + if ($oprofile) { + common_log(LOG_INFO, "Got a profile-update ping from $oprofile->uri"); + $oprofile->updateFromActivityObject($this->act->actor); + } else { + common_log(LOG_INFO, "Ignoring profile-update ping from unknown " . $this->act->actor->id); + } + } + + /** + * @return Ostatus_profile + */ + function ensureProfile() + { + $actor = $this->act->actor; + if (empty($actor->id)) { + common_log(LOG_ERR, "broken actor: " . var_export($actor, true)); + common_log(LOG_ERR, "activity with no actor: " . var_export($this->act, true)); + throw new Exception("Received a salmon slap from unidentified actor."); + } + + return Ostatus_profile::ensureActivityObjectProfile($actor); + } + + function saveNotice() + { + $oprofile = $this->ensureProfile(); + return $oprofile->processPost($this->act, 'salmon'); + } +} diff --git a/plugins/OStatus/lib/salmonqueuehandler.php b/plugins/OStatus/lib/salmonqueuehandler.php new file mode 100644 index 000000000..7eeb5f8e9 --- /dev/null +++ b/plugins/OStatus/lib/salmonqueuehandler.php @@ -0,0 +1,46 @@ +<?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 Salmon notification in the background. + * @package OStatusPlugin + * @author Brion Vibber <brion@status.net> + */ +class SalmonQueueHandler extends QueueHandler +{ + function transport() + { + return 'salmon'; + } + + function handle($data) + { + assert(is_array($data)); + assert(is_string($data['salmonuri'])); + assert(is_string($data['entry'])); + + $actor = Profile::staticGet($data['actor']); + + $salmon = new Salmon(); + $salmon->post($data['salmonuri'], $data['entry'], $actor); + + // @fixme detect failure and attempt to resend + return true; + } +} diff --git a/plugins/OStatus/lib/webfinger.php b/plugins/OStatus/lib/webfinger.php deleted file mode 100644 index 417d54904..000000000 --- a/plugins/OStatus/lib/webfinger.php +++ /dev/null @@ -1,143 +0,0 @@ -<?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 index 16d27f8eb..85df26c54 100644 --- a/plugins/OStatus/lib/xrd.php +++ b/plugins/OStatus/lib/xrd.php @@ -53,17 +53,22 @@ class XRD $xrd = new XRD(); $dom = new DOMDocument(); - $dom->loadXML($xml); + if (!$dom->loadXML($xml)) { + throw new Exception("Invalid XML"); + } $xrd_element = $dom->getElementsByTagName('XRD')->item(0); // Check for host-meta host - $host = $xrd_element->getElementsByTagName('Host')->item(0)->nodeValue; + $host = $xrd_element->getElementsByTagName('Host')->item(0); if ($host) { - $xrd->host = $host; + $xrd->host = $host->nodeValue; } // Loop through other elements foreach ($xrd_element->childNodes as $node) { + if (!($node instanceof DOMElement)) { + continue; + } switch ($node->tagName) { case 'Expires': $xrd->expires = $node->nodeValue; @@ -156,20 +161,20 @@ class XRD function saveLink($doc, $link) { $link_element = $doc->createElement('Link'); - if ($link['rel']) { + if (!empty($link['rel'])) { $link_element->setAttribute('rel', $link['rel']); } - if ($link['type']) { + if (!empty($link['type'])) { $link_element->setAttribute('type', $link['type']); } - if ($link['href']) { + if (!empty($link['href'])) { $link_element->setAttribute('href', $link['href']); } - if ($link['template']) { + if (!empty($link['template'])) { $link_element->setAttribute('template', $link['template']); } - if (is_array($link['title'])) { + if (!empty($link['title']) && is_array($link['title'])) { foreach($link['title'] as $title) { $title = $doc->createElement('Title', $title); $link_element->appendChild($title); diff --git a/plugins/OStatus/locale/OStatus.po b/plugins/OStatus/locale/OStatus.po index dedc018e3..7e33a0eed 100644 --- a/plugins/OStatus/locale/OStatus.po +++ b/plugins/OStatus/locale/OStatus.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-12-07 20:38-0800\n" +"POT-Creation-Date: 2010-03-01 14:58-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -16,89 +16,297 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: tests/gettext-speedtest.php:57 FeedSubPlugin.php:76 -msgid "Feeds" +#: actions/groupsalmon.php:51 +msgid "Can't accept remote posts for a remote group." +msgstr "" + +#: actions/groupsalmon.php:123 +msgid "Can't read profile to set up group membership." msgstr "" -#: FeedSubPlugin.php:77 -msgid "Feed subscription options" +#: actions/groupsalmon.php:126 actions/groupsalmon.php:169 +msgid "Groups can't join groups." msgstr "" -#: feedmunger.php:215 +#: actions/groupsalmon.php:153 #, php-format -msgid "New post: \"%1$s\" %2$s" +msgid "Could not join remote user %1$s to group %2$s." msgstr "" -#: actions/feedsubsettings.php:41 -msgid "Feed subscriptions" +#: actions/groupsalmon.php:166 +msgid "Can't read profile to cancel group membership." msgstr "" -#: actions/feedsubsettings.php:52 -msgid "" -"You can subscribe to feeds from other sites; updates will appear in your " -"personal timeline." +#: actions/groupsalmon.php:182 +#, php-format +msgid "Could not remove remote user %1$s from group %2$s." +msgstr "" + +#: actions/ostatusinit.php:40 +msgid "You can use the local subscription!" +msgstr "" + +#: actions/ostatusinit.php:61 +msgid "There was a problem with your session token. Try again, please." +msgstr "" + +#: actions/ostatusinit.php:79 actions/ostatussub.php:439 +msgid "Subscribe to user" +msgstr "" + +#: actions/ostatusinit.php:97 +#, php-format +msgid "Subscribe to %s" msgstr "" -#: actions/feedsubsettings.php:96 +#: actions/ostatusinit.php:102 +msgid "User nickname" +msgstr "" + +#: actions/ostatusinit.php:103 +msgid "Nickname of the user you want to follow" +msgstr "" + +#: actions/ostatusinit.php:106 +msgid "Profile Account" +msgstr "" + +#: actions/ostatusinit.php:107 +msgid "Your account id (i.e. user@identi.ca)" +msgstr "" + +#: actions/ostatusinit.php:110 actions/ostatussub.php:115 +#: OStatusPlugin.php:205 msgid "Subscribe" msgstr "" -#: actions/feedsubsettings.php:98 +#: actions/ostatusinit.php:128 +msgid "Must provide a remote profile." +msgstr "" + +#: actions/ostatusinit.php:138 +msgid "Couldn't look up OStatus account profile." +msgstr "" + +#: actions/ostatusinit.php:153 +msgid "Couldn't confirm remote profile address." +msgstr "" + +#: actions/ostatusinit.php:171 +msgid "OStatus Connect" +msgstr "" + +#: actions/ostatussub.php:68 +msgid "Address or profile URL" +msgstr "" + +#: actions/ostatussub.php:70 +msgid "Enter the profile URL of a PubSubHubbub-enabled feed" +msgstr "" + +#: actions/ostatussub.php:74 msgid "Continue" msgstr "" -#: actions/feedsubsettings.php:151 -msgid "Empty feed URL!" +#: actions/ostatussub.php:112 OStatusPlugin.php:503 +msgid "Join" +msgstr "" + +#: actions/ostatussub.php:113 +msgid "Join this group" +msgstr "" + +#: actions/ostatussub.php:116 +msgid "Subscribe to this user" +msgstr "" + +#: actions/ostatussub.php:137 +msgid "You are already subscribed to this user." +msgstr "" + +#: actions/ostatussub.php:165 +msgid "You are already a member of this group." msgstr "" -#: actions/feedsubsettings.php:161 +#: actions/ostatussub.php:286 +msgid "Empty remote profile URL!" +msgstr "" + +#: actions/ostatussub.php:297 +msgid "Invalid address format." +msgstr "" + +#: actions/ostatussub.php:302 msgid "Invalid URL or could not reach server." msgstr "" -#: actions/feedsubsettings.php:164 +#: actions/ostatussub.php:304 msgid "Cannot read feed; server returned error." msgstr "" -#: actions/feedsubsettings.php:167 +#: actions/ostatussub.php:306 msgid "Cannot read feed; server returned an empty page." msgstr "" -#: actions/feedsubsettings.php:170 +#: actions/ostatussub.php:308 msgid "Bad HTML, could not find feed link." msgstr "" -#: actions/feedsubsettings.php:173 +#: actions/ostatussub.php:310 msgid "Could not find a feed linked from this URL." msgstr "" -#: actions/feedsubsettings.php:176 +#: actions/ostatussub.php:312 msgid "Not a recognized feed type." msgstr "" -#: actions/feedsubsettings.php:180 -msgid "Bad feed URL." +#: actions/ostatussub.php:315 +#, php-format +msgid "Bad feed URL: %s %s" +msgstr "" + +#. TRANS: OStatus remote group subscription dialog error. +#: actions/ostatussub.php:336 +msgid "Already a member!" msgstr "" -#: actions/feedsubsettings.php:188 -msgid "Feed is not PuSH-enabled; cannot subscribe." +#. TRANS: OStatus remote group subscription dialog error. +#: actions/ostatussub.php:346 +msgid "Remote group join failed!" msgstr "" -#: actions/feedsubsettings.php:208 -msgid "Feed subscription failed! Bad response from hub." +#. TRANS: OStatus remote group subscription dialog error. +#: actions/ostatussub.php:350 +msgid "Remote group join aborted!" msgstr "" -#: actions/feedsubsettings.php:218 +#. TRANS: OStatus remote subscription dialog error. +#: actions/ostatussub.php:356 msgid "Already subscribed!" msgstr "" -#: actions/feedsubsettings.php:220 -msgid "Feed subscribed!" +#. TRANS: OStatus remote subscription dialog error. +#: actions/ostatussub.php:361 +msgid "Remote subscription failed!" msgstr "" -#: actions/feedsubsettings.php:222 -msgid "Feed subscription failed!" +#. TRANS: Page title for OStatus remote subscription form +#: actions/ostatussub.php:459 +msgid "Authorize subscription" +msgstr "" + +#: actions/ostatussub.php:470 +msgid "" +"You can subscribe to users from other supported sites. Paste their address " +"or profile URI below:" +msgstr "" + +#: classes/Ostatus_profile.php:789 +#, php-format +msgid "Tried to update avatar for unsaved remote profile %s" msgstr "" -#: actions/feedsubsettings.php:231 -msgid "Previewing feed:" +#: classes/Ostatus_profile.php:797 +#, php-format +msgid "Unable to fetch avatar from %s" +msgstr "" + +#: lib/salmonaction.php:41 +msgid "This method requires a POST." +msgstr "" + +#: lib/salmonaction.php:45 +msgid "Salmon requires application/magic-envelope+xml" +msgstr "" + +#: lib/salmonaction.php:55 +msgid "Salmon signature verification failed." +msgstr "" + +#: lib/salmonaction.php:67 +msgid "Salmon post must be an Atom entry." +msgstr "" + +#: lib/salmonaction.php:115 +msgid "Unrecognized activity type." +msgstr "" + +#: lib/salmonaction.php:123 +msgid "This target doesn't understand posts." +msgstr "" + +#: lib/salmonaction.php:128 +msgid "This target doesn't understand follows." +msgstr "" + +#: lib/salmonaction.php:133 +msgid "This target doesn't understand unfollows." +msgstr "" + +#: lib/salmonaction.php:138 +msgid "This target doesn't understand favorites." +msgstr "" + +#: lib/salmonaction.php:143 +msgid "This target doesn't understand unfavorites." +msgstr "" + +#: lib/salmonaction.php:148 +msgid "This target doesn't understand share events." +msgstr "" + +#: lib/salmonaction.php:153 +msgid "This target doesn't understand joins." +msgstr "" + +#: lib/salmonaction.php:158 +msgid "This target doesn't understand leave events." +msgstr "" + +#: OStatusPlugin.php:319 +#, php-format +msgid "Sent from %s via OStatus" +msgstr "" + +#: OStatusPlugin.php:371 +msgid "Could not set up remote subscription." +msgstr "" + +#: OStatusPlugin.php:487 +msgid "Could not set up remote group membership." +msgstr "" + +#: OStatusPlugin.php:504 +#, php-format +msgid "%s has joined group %s." +msgstr "" + +#: OStatusPlugin.php:512 +msgid "Failed joining remote group." +msgstr "" + +#: OStatusPlugin.php:553 +msgid "Leave" +msgstr "" + +#: OStatusPlugin.php:554 +#, php-format +msgid "%s has left group %s." +msgstr "" + +#: OStatusPlugin.php:685 +msgid "Subscribe to remote user" +msgstr "" + +#: OStatusPlugin.php:726 +msgid "Profile update" +msgstr "" + +#: OStatusPlugin.php:727 +#, php-format +msgid "%s has updated their profile page." +msgstr "" + +#: tests/gettext-speedtest.php:57 +msgid "Feeds" msgstr "" diff --git a/plugins/OStatus/scripts/updateostatus.php b/plugins/OStatus/scripts/updateostatus.php new file mode 100644 index 000000000..d553a7d62 --- /dev/null +++ b/plugins/OStatus/scripts/updateostatus.php @@ -0,0 +1,127 @@ +#!/usr/bin/env php +<?php +/* + * StatusNet - a distributed open-source microblogging tool + * Copyright (C) 2008, 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/>. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); + +$shortoptions = 'i:n:a'; +$longoptions = array('id=', 'nickname=', 'all'); + +$helptext = <<<END_OF_UPDATEOSTATUS_HELP +updateostatus.php [options] +update the OMB subscriptions of a user to use OStatus if possible + + -i --id ID of user to update + -n --nickname nickname of the user to update + -a --all update all + +END_OF_UPDATEOSTATUS_HELP; + +require_once INSTALLDIR.'/scripts/commandline.inc'; + +try { + $user = null; + + if (have_option('i', 'id')) { + $id = get_option_value('i', 'id'); + $user = User::staticGet('id', $id); + if (empty($user)) { + throw new Exception("Can't find user with id '$id'."); + } + updateProfileURL($user); + } else if (have_option('n', 'nickname')) { + $nickname = get_option_value('n', 'nickname'); + $user = User::staticGet('nickname', $nickname); + if (empty($user)) { + throw new Exception("Can't find user with nickname '$nickname'"); + } + updateProfileURL($user); + } else if (have_option('a', 'all')) { + $user = new User(); + if ($user->find()) { + while ($user->fetch()) { + updateOStatus($user); + } + } + } else { + show_help(); + exit(1); + } +} catch (Exception $e) { + print $e->getMessage()."\n"; + exit(1); +} + +function updateOStatus($user) +{ + if (!have_option('q', 'quiet')) { + echo "{$user->nickname}..."; + } + + $up = $user->getProfile(); + + $sp = $user->getSubscriptions(); + + $rps = array(); + + while ($sp->fetch()) { + $remote = Remote_profile::staticGet('id', $sp->id); + + if (!empty($remote)) { + $rps[] = clone($sp); + } + } + + if (!have_option('q', 'quiet')) { + echo count($rps) . "\n"; + } + + foreach ($rps as $rp) { + try { + if (!have_option('q', 'quiet')) { + echo "Checking {$rp->nickname}..."; + } + + $op = Ostatus_profile::ensureProfile($rp->profileurl); + + if (empty($op)) { + echo "can't convert.\n"; + continue; + } else { + if (!have_option('q', 'quiet')) { + echo "Converting..."; + } + Subscription::cancel($up, $rp); + Subscription::start($up, $op->localProfile()); + if (!have_option('q', 'quiet')) { + echo "done.\n"; + } + } + + } catch (Exception $e) { + if (!have_option('q', 'quiet')) { + echo "fail.\n"; + } + continue; + common_log(LOG_WARNING, "Couldn't convert OMB subscription (" . $up->nickname . ", " . $rp->nickname . + ") to OStatus: " . $e->getMessage()); + continue; + } + } +} diff --git a/plugins/OStatus/tests/ActivityParseTests.php b/plugins/OStatus/tests/ActivityParseTests.php deleted file mode 100644 index fa8bcdda2..000000000 --- a/plugins/OStatus/tests/ActivityParseTests.php +++ /dev/null @@ -1,147 +0,0 @@ -<?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/OStatus/tests/FeedMungerTest.php b/plugins/OStatus/tests/FeedMungerTest.php deleted file mode 100644 index 0ce24c9fb..000000000 --- a/plugins/OStatus/tests/FeedMungerTest.php +++ /dev/null @@ -1,147 +0,0 @@ -<?php - -if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { - print "This script must be run from the command line\n"; - exit(); -} - -define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); -define('STATUSNET', true); -define('LACONICA', true); - -require_once INSTALLDIR . '/lib/common.php'; -require_once INSTALLDIR . '/plugins/FeedSub/feedsub.php'; - -require_once 'XML/Feed/Parser.php'; - -class FeedMungerTest extends PHPUnit_Framework_TestCase -{ - /** - * @dataProvider profileProvider - * - */ - public function testProfiles($xml, $expected) - { - $feed = new XML_Feed_Parser($xml, false, false, true); - - $munger = new FeedMunger($feed); - $profile = $munger->profile(); - - foreach ($expected as $field => $val) { - $this->assertEquals($expected[$field], $profile->$field, "profile->$field"); - } - } - - static public function profileProvider() - { - return array( - array(self::samplefeed(), - array('nickname' => 'leŭksman', // @todo does this need to be asciified? - 'fullname' => 'leŭksman', - 'bio' => 'reticula, electronica, & oddities', - 'homepage' => 'http://leuksman.com/log'))); - } - - /** - * @dataProvider noticeProvider - * - */ - public function testNotices($xml, $entryIndex, $expected) - { - $feed = new XML_Feed_Parser($xml, false, false, true); - $entry = $feed->getEntryByOffset($entryIndex); - - $munger = new FeedMunger($feed); - $notice = $munger->noticeFromEntry($entry); - - $this->assertTrue(mb_strlen($notice) <= Notice::maxContent()); - $this->assertEquals($expected, $notice); - } - - static public function noticeProvider() - { - return array( - array('<rss version="2.0"><channel><item><title>A fairly short title</title><link>http://example.com/short/link</link></item></channel></rss>', 0, - 'New post: "A fairly short title" http://example.com/short/link'), - // Requires URL shortening ... - array('<rss version="2.0"><channel><item><title>A fairly short title</title><link>http://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh</link></item></channel></rss>', 0, - 'New post: "A fairly short title" http://ur1.ca/g2o1'), - array('<rss version="2.0"><channel><item><title>A fairly long title in this case, which will have to get cut down at some point alongside its very long link. Really who even makes titles this long? It\'s just ridiculous imo...</title><link>http://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh</link></item></channel></rss>', 0, - 'New post: "A fairly long title in this case, which will have to get cut down at some point alongside its very long li…" http://ur1.ca/g2o1'), - // Some real sample feeds - array(self::samplefeed(), 0, - 'New post: "Compiling PHP on Snow Leopard" http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/'), - array(self::samplefeedBlogspot(), 0, - 'New post: "I love posting" http://briontest.blogspot.com/2009/11/i-love-posting.html'), - array(self::samplefeedBlogspot(), 1, - 'New post: "Hey dude" http://briontest.blogspot.com/2009/11/hey-dude.html'), - ); - } - - static protected function samplefeed() - { - $xml = '<' . '?xml version="1.0" encoding="UTF-8"?' . ">\n"; - $samplefeed = $xml . <<<END -<rss version="2.0" - xmlns:content="http://purl.org/rss/1.0/modules/content/" - xmlns:wfw="http://wellformedweb.org/CommentAPI/" - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:atom="http://www.w3.org/2005/Atom" - xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" - xmlns:slash="http://purl.org/rss/1.0/modules/slash/" - > - -<channel> - <title>leŭksman</title> - <atom:link href="http://leuksman.com/log/feed/" rel="self" type="application/rss+xml" /> - <link>http://leuksman.com/log</link> - <description>reticula, electronica, & oddities</description> - - <lastBuildDate>Thu, 12 Nov 2009 17:44:42 +0000</lastBuildDate> - <generator>http://wordpress.org/?v=2.8.6</generator> - <language>en</language> - <sy:updatePeriod>hourly</sy:updatePeriod> - <sy:updateFrequency>1</sy:updateFrequency> - <item> - - <title>Compiling PHP on Snow Leopard</title> - <link>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/</link> - <comments>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/#comments</comments> - <pubDate>Thu, 12 Nov 2009 17:44:42 +0000</pubDate> - <dc:creator>brion</dc:creator> - <category><![CDATA[apple]]></category> - - <category><![CDATA[devel]]></category> - - <guid isPermaLink="false">http://leuksman.com/log/?p=649</guid> - <description><![CDATA[If you’ve been having trouble compiling your own PHP installations on Mac OS X 10.6, here’s the secret to making it not suck! After running the configure script, edit the generated Makefile and make these fixes: - -Find the EXTRA_LIBS definition and add -lresolv to the end -Find the EXE_EXT definition and remove .dSYM - -Standard make and make install [...]]]></description> - <content:encoded><![CDATA[<p>If you’ve been having trouble compiling your own PHP installations on Mac OS X 10.6, here’s the secret to making it not suck! After running the configure script, edit the generated Makefile and make these fixes:</p> -<ul> -<li>Find the <strong>EXTRA_LIBS</strong> definition and add <strong>-lresolv</strong> to the end</li> -<li>Find the <strong>EXE_EXT</strong> definition and remove <strong>.dSYM</strong></li> -</ul> -<p>Standard make and make install should work from here…</p> -<p>For reference, here’s the whole configure line I currently use; MySQL is installed from the downloadable installer; other deps from MacPorts:</p> -<p>‘./configure’ ‘–prefix=/opt/php52′ ‘–with-mysql=/usr/local/mysql’ ‘–with-zlib’ ‘–with-bz2′ ‘–enable-mbstring’ ‘–enable-exif’ ‘–enable-fastcgi’ ‘–with-xmlrpc’ ‘–with-xsl’ ‘–with-readline=/opt/local’ –without-iconv –with-gd –with-png-dir=/opt/local –with-jpeg-dir=/opt/local –with-curl –with-gettext=/opt/local –with-mysqli=/usr/local/mysql/bin/mysql_config –with-tidy=/opt/local –enable-pcntl –with-openssl</p> -]]></content:encoded> - <wfw:commentRss>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/feed/</wfw:commentRss> - <slash:comments>0</slash:comments> - </item> -</channel> -</rss> -END; - return $samplefeed; - } - - static protected function samplefeedBlogspot() - { - return <<<END -<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss'><id>tag:blogger.com,1999:blog-7780083508531697167</id><updated>2009-11-19T12:56:11.233-08:00</updated><title type='text'>Brion's Cool Test Blog</title><subtitle type='html'></subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>2</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-7780083508531697167.post-8456671879000290677</id><published>2009-11-19T12:55:00.000-08:00</published><updated>2009-11-19T12:56:11.241-08:00</updated><title type='text'>I love posting</title><content type='html'>It's pretty awesome, if you like that sort of thing.<div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7780083508531697167-8456671879000290677?l=briontest.blogspot.com' alt='' /></div></content><link rel='replies' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/8456671879000290677/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://briontest.blogspot.com/2009/11/i-love-posting.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8456671879000290677'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8456671879000290677'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/2009/11/i-love-posting.html' title='I love posting'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='05912464053145602436'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7780083508531697167.post-8202296917897346633</id><published>2009-11-18T13:52:00.001-08:00</published><updated>2009-11-18T13:52:48.444-08:00</updated><title type='text'>Hey dude</title><content type='html'>testingggggggggg<div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7780083508531697167-8202296917897346633?l=briontest.blogspot.com' alt='' /></div></content><link rel='replies' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/8202296917897346633/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://briontest.blogspot.com/2009/11/hey-dude.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8202296917897346633'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8202296917897346633'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/2009/11/hey-dude.html' title='Hey dude'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='05912464053145602436'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry></feed> -END; - } -} diff --git a/plugins/OStatus/theme/base/css/ostatus.css b/plugins/OStatus/theme/base/css/ostatus.css index 9bc90a731..13e30ef5d 100644 --- a/plugins/OStatus/theme/base/css/ostatus.css +++ b/plugins/OStatus/theme/base/css/ostatus.css @@ -7,24 +7,42 @@ * @link http://status.net/ */ -#form_ostatus_connect.dialogbox { +#form_ostatus_connect.dialogbox, +#form_ostatus_sub.dialogbox { width:70%; background-image:none; } -#form_ostatus_connect.dialogbox .form_data label { +#form_ostatus_sub.dialogbox { +width:65%; +} +#form_ostatus_connect.dialogbox .form_data label, +#form_ostatus_sub.dialogbox .form_data label { width:34%; } -#form_ostatus_connect.dialogbox .form_data input { +#form_ostatus_connect.dialogbox .form_data input, +#form_ostatus_sub.dialogbox .form_data input { width:57%; } -#form_ostatus_connect.dialogbox .form_data .form_guide { +#form_ostatus_connect.dialogbox .form_data .form_guide, +#form_ostatus_sub.dialogbox .form_data .form_guide { margin-left:36%; } -#form_ostatus_connect.dialogbox #ostatus_nickname { +#form_ostatus_connect.dialogbox #ostatus_nickname, +#form_ostatus_sub.dialogbox #ostatus_nickname { display:none; } -#form_ostatus_connect.dialogbox .submit_dialogbox { +#form_ostatus_connect.dialogbox .submit_dialogbox, +#form_ostatus_sub.dialogbox .submit_dialogbox { min-width:96px; } + +#entity_remote_subscribe { +padding:0; +float:right; +} + +#all #entity_remote_subscribe { +margin-top:-52px; +} diff --git a/plugins/OpenID/OpenIDPlugin.php b/plugins/OpenID/OpenIDPlugin.php index 248afe3fa..6b35ec3e1 100644 --- a/plugins/OpenID/OpenIDPlugin.php +++ b/plugins/OpenID/OpenIDPlugin.php @@ -235,9 +235,14 @@ class OpenIDPlugin extends Plugin switch ($name) { case 'register': - $instr = '(Have an [OpenID](http://openid.net/)? ' . - 'Try our [OpenID registration]'. - '(%%action.openidlogin%%)!)'; + if (common_logged_in()) { + $instr = '(Have an [OpenID](http://openid.net/)? ' . + '[Add an OpenID to your account](%%action.openidsettings%%)!'; + } else { + $instr = '(Have an [OpenID](http://openid.net/)? ' . + 'Try our [OpenID registration]'. + '(%%action.openidlogin%%)!)'; + } break; case 'login': $instr = '(Have an [OpenID](http://openid.net/)? ' . diff --git a/plugins/OpenID/doc-src/openid b/plugins/OpenID/doc-src/openid index c741e3674..f2dc610a5 100644 --- a/plugins/OpenID/doc-src/openid +++ b/plugins/OpenID/doc-src/openid @@ -3,7 +3,7 @@ If you already have an account on %%site.name%%, you can [login](%%action.login%%) with your username and password as usual. To use OpenID in the future, you can [add an OpenID to your account](%%action.openidsettings%%) after you have logged in normally. -There are many [Public OpenID providers](http://wiki.openid.net/Public_OpenID_providers), and you may already have an OpenID-enabled account on another service. +There are many [Public OpenID providers](http://wiki.openid.net/OpenID-Providers), and you may already have an OpenID-enabled account on another service. * On wikis: If you have an account on an OpenID-enabled wiki, like [Wikitravel](http://wikitravel.org/), [wikiHow](http://www.wikihow.com/), [Vinismo](http://vinismo.com/), [AboutUs](http://aboutus.org/) or [Keiki](http://kei.ki/), you can log in to %%site.name%% by entering the **full URL** of your user page on that other wiki in the box above. For example, *http://kei.ki/en/User:Evan*. * [Yahoo!](http://openid.yahoo.com/) : If you have an account with Yahoo!, you can log in to this site by entering your Yahoo!-provided OpenID in the box above. Yahoo! OpenID URLs have the form *https://me.yahoo.com/yourusername*. diff --git a/plugins/OpenID/locale/OpenID.po b/plugins/OpenID/locale/OpenID.po index 34738bc75..7ed879835 100644 --- a/plugins/OpenID/locale/OpenID.po +++ b/plugins/OpenID/locale/OpenID.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-12-07 20:38-0800\n" +"POT-Creation-Date: 2010-03-01 14:58-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -16,73 +16,152 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: openidlogin.php:30 finishopenidlogin.php:34 +#: finishaddopenid.php:67 +msgid "Not logged in." +msgstr "" + +#: finishaddopenid.php:88 finishopenidlogin.php:149 +msgid "OpenID authentication cancelled." +msgstr "" + +#: finishaddopenid.php:92 finishopenidlogin.php:153 +#, php-format +msgid "OpenID authentication failed: %s" +msgstr "" + +#: finishaddopenid.php:112 +msgid "You already have this OpenID!" +msgstr "" + +#: finishaddopenid.php:114 +msgid "Someone else already has this OpenID." +msgstr "" + +#: finishaddopenid.php:126 +msgid "Error connecting user." +msgstr "" + +#: finishaddopenid.php:131 +msgid "Error updating profile" +msgstr "" + +#: finishaddopenid.php:170 openidlogin.php:95 +msgid "OpenID Login" +msgstr "" + +#: finishopenidlogin.php:34 openidlogin.php:30 msgid "Already logged in." msgstr "" -#: openidlogin.php:37 openidsettings.php:194 finishopenidlogin.php:38 +#: finishopenidlogin.php:38 openidlogin.php:37 openidsettings.php:194 msgid "There was a problem with your session token. Try again, please." msgstr "" -#: openidlogin.php:66 +#: finishopenidlogin.php:43 +msgid "You can't register if you don't agree to the license." +msgstr "" + +#: finishopenidlogin.php:52 openidsettings.php:208 +msgid "Something weird happened." +msgstr "" + +#: finishopenidlogin.php:66 #, php-format msgid "" -"For security reasons, please re-login with your [OpenID](%%doc.openid%%) " -"before changing your settings." +"This is the first time you've logged into %s so we must connect your OpenID " +"to a local account. You can either create a new account, or connect with " +"your existing account, if you have one." msgstr "" -#: openidlogin.php:70 -#, php-format -msgid "Login with an [OpenID](%%doc.openid%%) account." +#: finishopenidlogin.php:72 +msgid "OpenID Account Setup" msgstr "" -#: openidlogin.php:95 finishaddopenid.php:170 -msgid "OpenID Login" +#: finishopenidlogin.php:97 +msgid "Create new account" msgstr "" -#: openidlogin.php:112 -msgid "OpenID login" +#: finishopenidlogin.php:99 +msgid "Create a new user with this nickname." msgstr "" -#: openidlogin.php:117 openidsettings.php:107 -msgid "OpenID URL" +#: finishopenidlogin.php:102 +msgid "New nickname" msgstr "" -#: openidlogin.php:119 -msgid "Your OpenID URL" +#: finishopenidlogin.php:104 +msgid "1-64 lowercase letters or numbers, no punctuation or spaces" msgstr "" -#: openidlogin.php:122 -msgid "Remember me" +#: finishopenidlogin.php:114 +msgid "My text and files are available under " msgstr "" -#: openidlogin.php:123 -msgid "Automatically login in the future; not for shared computers!" +#: finishopenidlogin.php:117 +msgid "" +" except this private data: password, email address, IM address, phone number." msgstr "" -#: openidlogin.php:127 -msgid "Login" +#: finishopenidlogin.php:121 +msgid "Create" msgstr "" -#: openidserver.php:106 -#, php-format -msgid "You are not authorized to use the identity %s" +#: finishopenidlogin.php:126 +msgid "Connect existing account" msgstr "" -#: openidserver.php:126 -msgid "Just an OpenID provider. Nothing to see here, move along..." +#: finishopenidlogin.php:128 +msgid "" +"If you already have an account, login with your username and password to " +"connect it to your OpenID." msgstr "" -#: OpenIDPlugin.php:123 OpenIDPlugin.php:135 -msgid "OpenID" +#: finishopenidlogin.php:131 +msgid "Existing nickname" msgstr "" -#: OpenIDPlugin.php:124 -msgid "Login or register with OpenID" +#: finishopenidlogin.php:134 +msgid "Password" msgstr "" -#: OpenIDPlugin.php:136 -msgid "Add or remove OpenIDs" +#: finishopenidlogin.php:137 +msgid "Connect" +msgstr "" + +#: finishopenidlogin.php:215 finishopenidlogin.php:224 +msgid "Registration not allowed." +msgstr "" + +#: finishopenidlogin.php:231 +msgid "Not a valid invitation code." +msgstr "" + +#: finishopenidlogin.php:241 +msgid "Nickname must have only lowercase letters and numbers and no spaces." +msgstr "" + +#: finishopenidlogin.php:246 +msgid "Nickname not allowed." +msgstr "" + +#: finishopenidlogin.php:251 +msgid "Nickname already in use. Try another one." +msgstr "" + +#: finishopenidlogin.php:258 finishopenidlogin.php:338 +msgid "Stored OpenID not found." +msgstr "" + +#: finishopenidlogin.php:267 +msgid "Creating new account for OpenID that already has a user." +msgstr "" + +#: finishopenidlogin.php:327 +msgid "Invalid username or password." +msgstr "" + +#: finishopenidlogin.php:345 +msgid "Error connecting user to OpenID." msgstr "" #: openid.php:141 @@ -126,57 +205,65 @@ msgstr "" msgid "OpenID Auto-Submit" msgstr "" -#: openidtrust.php:51 -msgid "OpenID Identity Verification" -msgstr "" - -#: openidtrust.php:69 +#: openidlogin.php:66 +#, php-format msgid "" -"This page should only be reached during OpenID processing, not directly." +"For security reasons, please re-login with your [OpenID](%%doc.openid%%) " +"before changing your settings." msgstr "" -#: openidtrust.php:118 +#: openidlogin.php:70 #, php-format -msgid "" -"%s has asked to verify your identity. Click Continue to verify your " -"identity and login without creating a new password." +msgid "Login with an [OpenID](%%doc.openid%%) account." msgstr "" -#: openidtrust.php:136 -msgid "Continue" +#: openidlogin.php:112 +msgid "OpenID login" msgstr "" -#: openidtrust.php:137 -msgid "Cancel" +#: openidlogin.php:117 openidsettings.php:107 +msgid "OpenID URL" msgstr "" -#: finishaddopenid.php:67 -msgid "Not logged in." +#: openidlogin.php:119 +msgid "Your OpenID URL" msgstr "" -#: finishaddopenid.php:88 finishopenidlogin.php:149 -msgid "OpenID authentication cancelled." +#: openidlogin.php:122 +msgid "Remember me" msgstr "" -#: finishaddopenid.php:92 finishopenidlogin.php:153 -#, php-format -msgid "OpenID authentication failed: %s" +#: openidlogin.php:123 +msgid "Automatically login in the future; not for shared computers!" msgstr "" -#: finishaddopenid.php:112 -msgid "You already have this OpenID!" +#: openidlogin.php:127 +msgid "Login" msgstr "" -#: finishaddopenid.php:114 -msgid "Someone else already has this OpenID." +#: OpenIDPlugin.php:123 OpenIDPlugin.php:135 +msgid "OpenID" msgstr "" -#: finishaddopenid.php:126 -msgid "Error connecting user." +#: OpenIDPlugin.php:124 +msgid "Login or register with OpenID" msgstr "" -#: finishaddopenid.php:131 -msgid "Error updating profile" +#: OpenIDPlugin.php:136 +msgid "Add or remove OpenIDs" +msgstr "" + +#: OpenIDPlugin.php:324 +msgid "Use <a href=\"http://openid.net/\">OpenID</a> to login to the site." +msgstr "" + +#: openidserver.php:106 +#, php-format +msgid "You are not authorized to use the identity %s." +msgstr "" + +#: openidserver.php:126 +msgid "Just an OpenID provider. Nothing to see here, move along..." msgstr "" #: openidsettings.php:59 @@ -224,10 +311,6 @@ msgstr "" msgid "Remove" msgstr "" -#: openidsettings.php:208 finishopenidlogin.php:52 -msgid "Something weird happened." -msgstr "" - #: openidsettings.php:228 msgid "No such OpenID." msgstr "" @@ -240,105 +323,26 @@ msgstr "" msgid "OpenID removed." msgstr "" -#: finishopenidlogin.php:43 -msgid "You can't register if you don't agree to the license." -msgstr "" - -#: finishopenidlogin.php:66 -#, php-format -msgid "" -"This is the first time you've logged into %s so we must connect your OpenID " -"to a local account. You can either create a new account, or connect with " -"your existing account, if you have one." -msgstr "" - -#: finishopenidlogin.php:72 -msgid "OpenID Account Setup" -msgstr "" - -#: finishopenidlogin.php:97 -msgid "Create new account" -msgstr "" - -#: finishopenidlogin.php:99 -msgid "Create a new user with this nickname." -msgstr "" - -#: finishopenidlogin.php:102 -msgid "New nickname" -msgstr "" - -#: finishopenidlogin.php:104 -msgid "1-64 lowercase letters or numbers, no punctuation or spaces" -msgstr "" - -#: finishopenidlogin.php:114 -msgid "My text and files are available under " +#: openidtrust.php:51 +msgid "OpenID Identity Verification" msgstr "" -#: finishopenidlogin.php:117 +#: openidtrust.php:69 msgid "" -" except this private data: password, email address, IM address, phone number." -msgstr "" - -#: finishopenidlogin.php:121 -msgid "Create" -msgstr "" - -#: finishopenidlogin.php:126 -msgid "Connect existing account" +"This page should only be reached during OpenID processing, not directly." msgstr "" -#: finishopenidlogin.php:128 +#: openidtrust.php:118 +#, php-format msgid "" -"If you already have an account, login with your username and password to " -"connect it to your OpenID." -msgstr "" - -#: finishopenidlogin.php:131 -msgid "Existing nickname" -msgstr "" - -#: finishopenidlogin.php:134 -msgid "Password" -msgstr "" - -#: finishopenidlogin.php:137 -msgid "Connect" -msgstr "" - -#: finishopenidlogin.php:215 finishopenidlogin.php:224 -msgid "Registration not allowed." -msgstr "" - -#: finishopenidlogin.php:231 -msgid "Not a valid invitation code." -msgstr "" - -#: finishopenidlogin.php:241 -msgid "Nickname must have only lowercase letters and numbers and no spaces." -msgstr "" - -#: finishopenidlogin.php:246 -msgid "Nickname not allowed." -msgstr "" - -#: finishopenidlogin.php:251 -msgid "Nickname already in use. Try another one." -msgstr "" - -#: finishopenidlogin.php:258 finishopenidlogin.php:338 -msgid "Stored OpenID not found." -msgstr "" - -#: finishopenidlogin.php:267 -msgid "Creating new account for OpenID that already has a user." +"%s has asked to verify your identity. Click Continue to verify your " +"identity and login without creating a new password." msgstr "" -#: finishopenidlogin.php:327 -msgid "Invalid username or password." +#: openidtrust.php:136 +msgid "Continue" msgstr "" -#: finishopenidlogin.php:345 -msgid "Error connecting user to OpenID." +#: openidtrust.php:137 +msgid "Cancel" msgstr "" diff --git a/plugins/PoweredByStatusNet/locale/PoweredByStatusNet.po b/plugins/PoweredByStatusNet/locale/PoweredByStatusNet.po index bd39124ef..8f8434a85 100644 --- a/plugins/PoweredByStatusNet/locale/PoweredByStatusNet.po +++ b/plugins/PoweredByStatusNet/locale/PoweredByStatusNet.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-01-22 15:03-0800\n" +"POT-Creation-Date: 2010-03-01 14:58-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -16,16 +16,16 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: PoweredByStatusNetPlugin.php:49 +#: PoweredByStatusNetPlugin.php:50 #, php-format msgid "powered by %s" msgstr "" -#: PoweredByStatusNetPlugin.php:51 +#: PoweredByStatusNetPlugin.php:52 msgid "StatusNet" msgstr "" -#: PoweredByStatusNetPlugin.php:64 +#: PoweredByStatusNetPlugin.php:65 msgid "" "Outputs powered by <a href=\"http://status.net/\">StatusNet</a> after site " "name." diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php index e8c44a743..b559d80c6 100644 --- a/plugins/Realtime/RealtimePlugin.php +++ b/plugins/Realtime/RealtimePlugin.php @@ -244,8 +244,6 @@ class RealtimePlugin extends Plugin // of refactoring from within a plugin, so I'm just abusing // the ApiAction method. Don't do this unless you're me! - require_once(INSTALLDIR.'/lib/api.php'); - $act = new ApiAction('/dev/null'); $arr = $act->twitterStatusArray($notice, true); diff --git a/plugins/Realtime/icon_external.gif b/plugins/Realtime/icon_external.gif Binary files differdeleted file mode 100644 index c4118d53b..000000000 --- a/plugins/Realtime/icon_external.gif +++ /dev/null diff --git a/plugins/Realtime/icon_pause.gif b/plugins/Realtime/icon_pause.gif Binary files differdeleted file mode 100644 index ced0b6440..000000000 --- a/plugins/Realtime/icon_pause.gif +++ /dev/null diff --git a/plugins/Realtime/icon_play.gif b/plugins/Realtime/icon_play.gif Binary files differdeleted file mode 100644 index 794ec85b6..000000000 --- a/plugins/Realtime/icon_play.gif +++ /dev/null diff --git a/plugins/Realtime/realtimeupdate.css b/plugins/Realtime/realtimeupdate.css index 31e7c2ae6..f43c97de5 100644 --- a/plugins/Realtime/realtimeupdate.css +++ b/plugins/Realtime/realtimeupdate.css @@ -64,18 +64,9 @@ float: left; } #realtime_play { -background: url(icon_play.gif) no-repeat 47% 47%; margin-left: 4px; } -#realtime_pause { -background: url(icon_pause.gif) no-repeat 47% 47%; -} - -#realtime_popup { -background: url(icon_external.gif) no-repeat 0 30%; -} - #queued_counter { float:left; line-height:1.2; diff --git a/plugins/RegisterThrottle/RegisterThrottlePlugin.php b/plugins/RegisterThrottle/RegisterThrottlePlugin.php new file mode 100644 index 000000000..05709b780 --- /dev/null +++ b/plugins/RegisterThrottle/RegisterThrottlePlugin.php @@ -0,0 +1,249 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * Throttle registration by IP address + * + * 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 Spam + * @package StatusNet + * @author Evan Prodromou <evan@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); +} + +/** + * Throttle registration by IP address + * + * We a) record IP address of registrants and b) throttle registrations. + * + * @category Spam + * @package StatusNet + * @author Evan Prodromou <evan@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 RegisterThrottlePlugin extends Plugin +{ + /** + * Array of time spans in seconds to limits. + * + * Default is 3 registrations per hour, 5 per day, 10 per week. + */ + + public $regLimits = array(604800 => 10, // per week + 86400 => 5, // per day + 3600 => 3); // per hour + + /** + * Database schema setup + * + * We store user registrations in a table registration_ip. + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onCheckSchema() + { + $schema = Schema::get(); + + // For storing user-submitted flags on profiles + + $schema->ensureTable('registration_ip', + array(new ColumnDef('user_id', 'integer', null, + false, 'PRI'), + new ColumnDef('ipaddress', 'varchar', 15, false, 'MUL'), + new ColumnDef('created', 'timestamp', null, false, 'MUL'))); + + return true; + } + + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onAutoload($cls) + { + $dir = dirname(__FILE__); + + switch ($cls) + { + case 'Registration_ip': + include_once $dir . '/'.$cls.'.php'; + return false; + default: + return true; + } + } + + /** + * Called when someone tries to register. + * + * We check the IP here to determine if it goes over any of our + * configured limits. + * + * @param Action $action Action that is being executed + * + * @return boolean hook value + * + */ + + function onStartRegistrationTry($action) + { + $ipaddress = $this->_getIpAddress(); + + if (empty($ipaddress)) { + throw new ServerException(_m('Cannot find IP address.')); + } + + foreach ($this->regLimits as $seconds => $limit) { + + $this->debug("Checking $seconds ($limit)"); + + $reg = $this->_getNthReg($ipaddress, $limit); + + if (!empty($reg)) { + $this->debug("Got a {$limit}th registration."); + $regtime = strtotime($reg->created); + $now = time(); + $this->debug("Comparing {$regtime} to {$now}"); + if ($now - $regtime < $seconds) { + throw new Exception(_("Too many registrations. Take a break and try again later.")); + } + } + } + + return true; + } + + /** + * Called after someone registers. + * + * We record the successful registration and IP address. + * + * @param Action $action Action that is being executed + * + * @return boolean hook value + * + */ + + function onEndRegistrationTry($action) + { + $ipaddress = $this->_getIpAddress(); + + if (empty($ipaddress)) { + throw new ServerException(_m('Cannot find IP address.')); + } + + $user = common_current_user(); + + if (empty($user)) { + throw new ServerException(_m('Cannot find user after successful registration.')); + } + + $reg = new Registration_ip(); + + $reg->user_id = $user->id; + $reg->ipaddress = $ipaddress; + + $result = $reg->insert(); + + if (!$result) { + common_log_db_error($reg, 'INSERT', __FILE__); + // @todo throw an exception? + } + + return true; + } + + /** + * Check the version of the plugin. + * + * @param array &$versions Version array. + * + * @return boolean hook value + */ + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'RegisterThrottle', + 'version' => STATUSNET_VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:RegisterThrottle', + 'description' => + _m('Throttles excessive registration from a single IP.')); + return true; + } + + /** + * Gets the current IP address. + * + * @return string IP address or null if not found. + */ + + private function _getIpAddress() + { + $keys = array('HTTP_X_FORWARDED_FOR', + 'CLIENT-IP', + 'REMOTE_ADDR'); + + foreach ($keys as $k) { + if (!empty($_SERVER[$k])) { + return $_SERVER[$k]; + } + } + + return null; + } + + /** + * Gets the Nth registration with the given IP address. + * + * @param string $ipaddress Address to key on + * @param integer $n Nth address + * + * @return Registration_ip nth registration or null if not found. + */ + + private function _getNthReg($ipaddress, $n) + { + $reg = new Registration_ip(); + + $reg->ipaddress = $ipaddress; + + $reg->orderBy('created DESC'); + $reg->limit($n - 1, 1); + + if ($reg->find(true)) { + return $reg; + } else { + return null; + } + } +} diff --git a/plugins/RegisterThrottle/Registration_ip.php b/plugins/RegisterThrottle/Registration_ip.php new file mode 100644 index 000000000..7e61d089e --- /dev/null +++ b/plugins/RegisterThrottle/Registration_ip.php @@ -0,0 +1,124 @@ +<?php +/** + * Data class for storing IP addresses of new registrants. + * + * PHP version 5 + * + * @category Data + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * 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/>. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for storing IP addresses of new registrants. + * + * @category Spam + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ + +class Registration_ip extends Memcached_DataObject +{ + public $__table = 'registration_ip'; // table name + public $user_id; // int(4) primary_key not_null + public $ipaddress; // varchar(15) + public $created; // timestamp + + /** + * Get an instance by key + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return User_greeting_count object found, or null for no hits + * + */ + + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Registration_ip', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * @return array array of column definitions + */ + + function table() + { + return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'ipaddress' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'created' => DB_DATAOBJECT_MYSQLTIMESTAMP + DB_DATAOBJECT_NOTNULL); + } + + /** + * 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('user_id' => 'K'); + } + + /** + * 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(); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + + function sequenceKey() + { + return array(false, false, false); + } +} diff --git a/plugins/ReverseUsernameAuthentication/README b/plugins/ReverseUsernameAuthentication/README index e9160ed9b..57b53219e 100644 --- a/plugins/ReverseUsernameAuthentication/README +++ b/plugins/ReverseUsernameAuthentication/README @@ -8,7 +8,10 @@ add "addPlugin('reverseUsernameAuthentication', array('setting'=>'value', 'setti Settings ======== -provider_name*: a unique name for this authentication provider. +provider_name*: This is a identifier designated to the connection. + It's how StatusNet will refer to the authentication source. + For the most part, any name can be used, so long as each authentication source has a different identifier. + In most cases there will be only one authentication source used. password_changeable*: must be set to false. This plugin does not support changing passwords. authoritative (false): Set to true if this plugin's responses are authoritative (meaning if this fails, do check any other plugins or the internal password database). autoregistration (false): Set to true if users should be automatically created when they attempt to login. diff --git a/plugins/Sample/hello.php b/plugins/Sample/hello.php index 0cfd8a1c3..dfbd0ad4f 100644 --- a/plugins/Sample/hello.php +++ b/plugins/Sample/hello.php @@ -119,13 +119,15 @@ class HelloAction extends Action } /** - * show content in the content area + * Show content in the content area * * The default StatusNet page has a lot of decorations: menus, * logos, tabs, all that jazz. This method is used to show * content in the content area of the page; it's the main * thing you want to overload. * + * This method also demonstrates use of a plural localized string. + * * @return void */ @@ -138,7 +140,9 @@ class HelloAction extends Action $this->element('p', array('class' => 'greeting'), sprintf(_m('Hello, %s'), $this->user->nickname)); $this->element('p', array('class' => 'greeting_count'), - sprintf(_m('I have greeted you %d time(s).'), + sprintf(_m('I have greeted you %d time.', + 'I have greeted you %d times.', + $this->gc->greeting_count), $this->gc->greeting_count)); } } diff --git a/plugins/Sample/locale/Sample.po b/plugins/Sample/locale/Sample.po new file mode 100644 index 000000000..a52c4ec01 --- /dev/null +++ b/plugins/Sample/locale/Sample.po @@ -0,0 +1,56 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-03-01 14:58-0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: hello.php:115 SamplePlugin.php:266 +msgid "Hello" +msgstr "" + +#: hello.php:117 hello.php:141 +#, php-format +msgid "Hello, %s" +msgstr "" + +#: hello.php:138 +msgid "Hello, stranger!" +msgstr "" + +#: hello.php:143 +#, php-format +msgid "I have greeted you %d time." +msgid_plural "I have greeted you %d times." +msgstr[0] "" +msgstr[1] "" + +#: SamplePlugin.php:266 +msgid "A warm greeting" +msgstr "" + +#: SamplePlugin.php:277 +msgid "A sample plugin to show basics of development for new hackers." +msgstr "" + +#: User_greeting_count.php:163 +#, php-format +msgid "Could not save new greeting count for %d" +msgstr "" + +#: User_greeting_count.php:176 +#, php-format +msgid "Could not increment greeting count for %d" +msgstr "" diff --git a/plugins/SphinxSearch/sphinxsearch.php b/plugins/SphinxSearch/sphinxsearch.php index 71f330828..654b9c9d5 100644 --- a/plugins/SphinxSearch/sphinxsearch.php +++ b/plugins/SphinxSearch/sphinxsearch.php @@ -75,7 +75,7 @@ class SphinxSearch extends SearchEngine { if ('chron' === $mode) { $this->sphinx->SetSortMode(SPH_SORT_ATTR_DESC, 'created_ts'); - return $this->target->orderBy('created desc'); + return $this->target->orderBy('id desc'); } } diff --git a/plugins/TabFocus/TabFocusPlugin.php b/plugins/TabFocus/TabFocusPlugin.php new file mode 100644 index 000000000..bf89c478c --- /dev/null +++ b/plugins/TabFocus/TabFocusPlugin.php @@ -0,0 +1,57 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Plugin to enable Twitter-like "tab-space" pattern for a user to submit a notice + * + * 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 Plugin + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @author Paul Irish <paul.irish@isobar.net> + * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +class TabFocusPlugin extends Plugin +{ + function __construct() + { + parent::__construct(); + } + + function onEndShowScripts($action) + { + $action->script('plugins/TabFocus/tabfocus.js'); + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'TabFocus', + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews and Paul Irish', + 'homepage' => 'http://status.net/wiki/Plugin:TabFocus', + 'rawdescription' => + _m('TabFocus changes the notice form behavior so that, while in the text area, pressing the tab key focuses the "Send" button, matching the behavor of Twitter.')); + return true; + } +} diff --git a/plugins/TabFocus/tabfocus.js b/plugins/TabFocus/tabfocus.js new file mode 100644 index 000000000..e2c1c6521 --- /dev/null +++ b/plugins/TabFocus/tabfocus.js @@ -0,0 +1,7 @@ +jQuery(function($){ + $('#notice_data-text').bind('keydown',function(e){ + if (e.which==9) { + setTimeout(function(){ $('#notice_action-submit').focus(); },15); + } + }); +}); diff --git a/plugins/TwitterBridge/README b/plugins/TwitterBridge/README index d3bcda598..72278b32e 100644 --- a/plugins/TwitterBridge/README +++ b/plugins/TwitterBridge/README @@ -1,47 +1,89 @@ +Twitter Bridge Plugin +===================== + This Twitter "bridge" plugin allows you to integrate your StatusNet instance with Twitter. Installing it will allow your users to: - - automatically post notices to thier Twitter accounts + - automatically post notices to their Twitter accounts - automatically subscribe to other Twitter users who are also using your StatusNet install, if possible (requires running a daemon) - import their Twitter friends' tweets (requires running a daemon) + - allow users to authenticate using Twitter ('Sign in with Twitter') Installation ------------ -To enable the plugin, add the following to your config.php: - - addPlugin("TwitterBridge"); - -OAuth is used to to access protected resources on Twitter (as opposed to -HTTP Basic Auth)*. To use Twitter bridging you will need to register -your instance of StatusNet as an application on Twitter -(http://twitter.com/apps), and update the following variables in your -config.php with the consumer key and secret Twitter generates for you: - - $config['twitter']['consumer_key'] = 'YOURKEY'; - $config['twitter']['consumer_secret'] = 'YOURSECRET'; +OAuth 1.0a (http://oauth.net) is used to to access protected resources +on Twitter (as opposed to HTTP Basic Auth)*. To use Twitter bridging +you will need to register your instance of StatusNet as an application +on Twitter (http://twitter.com/apps). During the application +registration process your application will be assigned a "consumer" key +and secret, which the plugin will use to make OAuth requests to Twitter. +You can either pass the consumer key and secret in when you enable the +plugin, or set it using the Twitter administration panel. When registering your application with Twitter set the type to "Browser" and your Callback URL to: http://example.org/mublog/twitter/authorization -The default access type should be, "Read & Write". +(Change "example.org" to your site domain and "mublog" to your site +path.) + +The default access type should be "Read & Write". + +To enable the plugin, add the following to your config.php: + + addPlugin( + 'TwitterBridge', + array( + 'consumer_key' => 'YOUR_CONSUMER_KEY', + 'consumer_secret' => 'YOUR_CONSUMER_SECRET' + ) + ); * Note: The plugin will still push notices to Twitter for users who - have previously setup the Twitter bridge using their Twitter name and - password under an older versions of StatusNet, but all new Twitter + have previously set up the Twitter bridge using their Twitter name and + password under an older version of StatusNet, but all new Twitter bridge connections will use OAuth. -Deamons +Administration panel +-------------------- + +As of StatusNet 0.9.0 there is a new administration panel that allows +you to configure Twitter bridge settings within StatusNet itself, +instead of having to specify them manually in your config.php. To enable +the administration panel, you will need to add it to the list of active +administration panels. You can do this via your config.php. E.g.: + + $config['admin']['panels'][] = 'twitter'; + +And to access it, you'll need to use a user with the "administrator" +role (see: scripts/userrole.php). + +Sign in with Twitter +-------------------- + +With 0.9.0, StatusNet optionally allows users to register and +authenticate using their Twitter credentials via the "Sign in with +Twitter" pattern described here: + + http://apiwiki.twitter.com/Sign-in-with-Twitter + +The option is _on_ by default when you install the plugin, but it can +disabled via the Twitter bridge administration panel, or by adding the +following line to your config.php: + + $config['twitter']['signin'] = false; + +Daemons ------- -For friend syncing and importing notices running two additional daemon -scripts is necessary (synctwitterfriends.php and -twitterstatusfetcher.php). +For friend syncing and importing Twitter tweets, running two +additional daemon scripts is necessary: synctwitterfriends.php and +twitterstatusfetcher.php. -In the daemons subidrectory of the plugin are three scripts: +In the daemons subdirectory of the plugin are three scripts: * Twitter Friends Syncing (daemons/synctwitterfriends.php) @@ -51,13 +93,13 @@ subscribe to "friends" (people they "follow") on Twitter who also have accounts on your StatusNet system, and who have previously set up a link for automatically posting notices to Twitter. -The plugin will try to start this daemon when you run -scripts/startdaemons.sh. +The plugin will start this daemon when you run scripts/startdaemons.sh. * Importing statuses from Twitter (daemons/twitterstatusfetcher.php) -To allow your users to import their friends' Twitter statuses, you will -need to enable the bidirectional Twitter bridge in your config.php: +You can allow uses to enable importing of your friends' Twitter +timelines either in the Twitter bridge administration panel or in your +config.php using the following configuration line: $config['twitterimport']['enabled'] = true; @@ -66,8 +108,9 @@ other daemons when you run scripts/startdaemons.sh. Additionally, you will want to set the integration source variable, which will keep notices posted to Twitter via StatusNet from looping -back. The integration source should be set to the name of your -application, exactly as you specified it on the settings page for your +back. You can do this in the Twitter bridge administration panel, or +via config.php. The integration source should be set to the name of your +application _exactly_ as you specified it on the settings page for your StatusNet application on Twitter, e.g.: $config['integration']['source'] = 'YourApp'; @@ -79,7 +122,9 @@ set up Twitter bridging. It's not strictly necessary to run this queue handler, and sites that haven't enabled queuing are still able to push notices to Twitter, but -for larger sites and sites that wish to improve performance, this -script allows notices to be sent "offline" via a separate process. +for larger sites and sites that wish to improve performance the script +allows notices to be sent "offline" via a separate process. -The plugin will start this script when you run scripts/startdaemons.sh. +StatusNet will automatically use the TwitterQueueHandler if you have +enabled the queuing subsystem. See the "Queues and daemons" section of +the main README file for more information about how to do that. diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php index c7f57ffc7..6ce69d5e2 100644 --- a/plugins/TwitterBridge/TwitterBridgePlugin.php +++ b/plugins/TwitterBridge/TwitterBridgePlugin.php @@ -23,7 +23,7 @@ * @author Julien C <chaumond@gmail.com> * @copyright 2009-2010 Control Yourself, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ + * @link http://status.net/ */ if (!defined('STATUSNET')) { @@ -32,8 +32,6 @@ if (!defined('STATUSNET')) { require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php'; -define('TWITTERBRIDGEPLUGIN_VERSION', '0.9'); - /** * Plugin for sending and importing Twitter statuses * @@ -44,19 +42,41 @@ define('TWITTERBRIDGEPLUGIN_VERSION', '0.9'); * @author Zach Copley <zach@status.net> * @author Julien C <chaumond@gmail.com> * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ + * @link http://status.net/ * @link http://twitter.com/ */ class TwitterBridgePlugin extends Plugin { + + const VERSION = STATUSNET_VERSION; + /** * Initializer for the plugin. */ - function __construct() + function initialize() { - parent::__construct(); + // Allow the key and secret to be passed in + // Control panel will override + + if (isset($this->consumer_key)) { + $key = common_config('twitter', 'consumer_key'); + if (empty($key)) { + Config::save('twitter', 'consumer_key', $this->consumer_key); + } + } + + if (isset($this->consumer_secret)) { + $secret = common_config('twitter', 'consumer_secret'); + if (empty($secret)) { + Config::save( + 'twitter', + 'consumer_secret', + $this->consumer_secret + ); + } + } } /** @@ -71,10 +91,17 @@ class TwitterBridgePlugin extends Plugin function onRouterInitialized($m) { - $m->connect('twitter/authorization', - array('action' => 'twitterauthorization')); + $m->connect( + 'twitter/authorization', + array('action' => 'twitterauthorization') + ); $m->connect('settings/twitter', array('action' => 'twittersettings')); - $m->connect('main/twitterlogin', array('action' => 'twitterlogin')); + + if (common_config('twitter', 'signin')) { + $m->connect('main/twitterlogin', array('action' => 'twitterlogin')); + } + + $m->connect('admin/twitter', array('action' => 'twitteradminpanel')); return true; } @@ -88,13 +115,16 @@ class TwitterBridgePlugin extends Plugin */ function onEndLoginGroupNav(&$action) { - $action_name = $action->trimmed('action'); - $action->menuItem(common_local_url('twitterlogin'), - _('Twitter'), - _('Login or register using Twitter'), - 'twitterlogin' === $action_name); + if (common_config('twitter', 'signin')) { + $action->menuItem( + common_local_url('twitterlogin'), + _m('Twitter'), + _m('Login or register using Twitter'), + 'twitterlogin' === $action_name + ); + } return true; } @@ -110,10 +140,12 @@ class TwitterBridgePlugin extends Plugin { $action_name = $action->trimmed('action'); - $action->menuItem(common_local_url('twittersettings'), - _m('Twitter'), - _m('Twitter integration options'), - $action_name === 'twittersettings'); + $action->menuItem( + common_local_url('twittersettings'), + _m('Twitter'), + _m('Twitter integration options'), + $action_name === 'twittersettings' + ); return true; } @@ -132,6 +164,7 @@ class TwitterBridgePlugin extends Plugin case 'TwittersettingsAction': case 'TwitterauthorizationAction': case 'TwitterloginAction': + case 'TwitteradminpanelAction': include_once INSTALLDIR . '/plugins/TwitterBridge/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; @@ -173,12 +206,18 @@ class TwitterBridgePlugin extends Plugin */ function onGetValidDaemons($daemons) { - array_push($daemons, INSTALLDIR . - '/plugins/TwitterBridge/daemons/synctwitterfriends.php'); + array_push( + $daemons, + INSTALLDIR + . '/plugins/TwitterBridge/daemons/synctwitterfriends.php' + ); if (common_config('twitterimport', 'enabled')) { - array_push($daemons, INSTALLDIR - . '/plugins/TwitterBridge/daemons/twitterstatusfetcher.php'); + array_push( + $daemons, + INSTALLDIR + . '/plugins/TwitterBridge/daemons/twitterstatusfetcher.php' + ); } return true; @@ -197,17 +236,55 @@ class TwitterBridgePlugin extends Plugin return true; } + /** + * Add a Twitter tab to the admin panel + * + * @param Widget $nav Admin panel nav + * + * @return boolean hook value + */ + + function onEndAdminPanelNav($nav) + { + if (AdminPanelAction::canAdmin('twitter')) { + + $action_name = $nav->action->trimmed('action'); + + $nav->out->menuItem( + common_local_url('twitteradminpanel'), + _m('Twitter'), + _m('Twitter bridge configuration'), + $action_name == 'twitteradminpanel', + 'nav_twitter_admin_panel' + ); + } + + return true; + } + + /** + * Plugin version data + * + * @param array &$versions array of version blocks + * + * @return boolean hook value + */ + function onPluginVersion(&$versions) { - $versions[] = array('name' => 'TwitterBridge', - 'version' => TWITTERBRIDGEPLUGIN_VERSION, - 'author' => 'Zach Copley', - 'homepage' => 'http://status.net/wiki/Plugin:TwitterBridge', - 'rawdescription' => - _m('The Twitter "bridge" plugin allows you to integrate ' . - 'your StatusNet instance with ' . - '<a href="http://twitter.com/">Twitter</a>.')); + $versions[] = array( + 'name' => 'TwitterBridge', + 'version' => self::VERSION, + 'author' => 'Zach Copley, Julien C', + 'homepage' => 'http://status.net/wiki/Plugin:TwitterBridge', + 'rawdescription' => _m( + 'The Twitter "bridge" plugin allows you to integrate ' . + 'your StatusNet instance with ' . + '<a href="http://twitter.com/">Twitter</a>.' + ) + ); return true; } } + diff --git a/plugins/TwitterBridge/locale/TwitterBridge.po b/plugins/TwitterBridge/locale/TwitterBridge.po index 14c30f1c9..eff125579 100644 --- a/plugins/TwitterBridge/locale/TwitterBridge.po +++ b/plugins/TwitterBridge/locale/TwitterBridge.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-12-07 20:38-0800\n" +"POT-Creation-Date: 2010-03-01 14:58-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -16,23 +16,48 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: twitterauthorization.php:81 -msgid "Not logged in." +#: twitter.php:320 +msgid "Your Twitter bridge has been disabled." msgstr "" -#: twitterauthorization.php:131 twitterauthorization.php:150 -#: twitterauthorization.php:170 twitterauthorization.php:217 +#: twitter.php:324 +#, php-format +msgid "" +"Hi, %1$s. We're sorry to inform you that your link to Twitter has been " +"disabled. We no longer seem to have permission to update your Twitter " +"status. (Did you revoke %3$s's access?)\n" +"\n" +"You can re-enable your Twitter bridge by visiting your Twitter settings " +"page:\n" +"\n" +"\t%2$s\n" +"\n" +"Regards,\n" +"%3$s\n" +msgstr "" + +#: twitterauthorization.php:181 twitterauthorization.php:229 msgid "Couldn't link your Twitter account." msgstr "" -#: TwitterBridgePlugin.php:89 +#: twitterauthorization.php:201 +msgid "Couldn't link your Twitter account: oauth_token mismatch." +msgstr "" + +#: TwitterBridgePlugin.php:114 msgid "Twitter" msgstr "" -#: TwitterBridgePlugin.php:90 +#: TwitterBridgePlugin.php:115 msgid "Twitter integration options" msgstr "" +#: TwitterBridgePlugin.php:207 +msgid "" +"The Twitter \"bridge\" plugin allows you to integrate your StatusNet " +"instance with <a href=\"http://twitter.com/\">Twitter</a>." +msgstr "" + #: twittersettings.php:59 msgid "Twitter settings" msgstr "" @@ -51,78 +76,81 @@ msgstr "" msgid "Connected Twitter account" msgstr "" -#: twittersettings.php:125 -msgid "Remove" +#: twittersettings.php:128 +msgid "Disconnect my account from Twitter" +msgstr "" + +#: twittersettings.php:133 +msgid "Disconnecting your Twitter could make it impossible to log in! Please " +msgstr "" + +#: twittersettings.php:137 +msgid "set a password" msgstr "" -#: twittersettings.php:131 +#: twittersettings.php:139 +msgid " first." +msgstr "" + +#: twittersettings.php:143 +#, php-format +msgid "" +"Keep your %1$s account but disconnect from Twitter. You can use your %1$s " +"password to log in." +msgstr "" + +#: twittersettings.php:151 +msgid "Disconnect" +msgstr "" + +#: twittersettings.php:158 msgid "Preferences" msgstr "" -#: twittersettings.php:135 +#: twittersettings.php:162 msgid "Automatically send my notices to Twitter." msgstr "" -#: twittersettings.php:142 +#: twittersettings.php:169 msgid "Send local \"@\" replies to Twitter." msgstr "" -#: twittersettings.php:149 +#: twittersettings.php:176 msgid "Subscribe to my Twitter friends here." msgstr "" -#: twittersettings.php:158 +#: twittersettings.php:185 msgid "Import my Friends Timeline." msgstr "" -#: twittersettings.php:174 +#: twittersettings.php:201 msgid "Save" msgstr "" -#: twittersettings.php:176 +#: twittersettings.php:203 msgid "Add" msgstr "" -#: twittersettings.php:201 +#: twittersettings.php:228 msgid "There was a problem with your session token. Try again, please." msgstr "" -#: twittersettings.php:211 +#: twittersettings.php:238 msgid "Unexpected form submission." msgstr "" -#: twittersettings.php:230 +#: twittersettings.php:257 msgid "Couldn't remove Twitter user." msgstr "" -#: twittersettings.php:234 -msgid "Twitter account removed." +#: twittersettings.php:261 +msgid "Twitter account disconnected." msgstr "" -#: twittersettings.php:255 twittersettings.php:265 +#: twittersettings.php:282 twittersettings.php:292 msgid "Couldn't save Twitter preferences." msgstr "" -#: twittersettings.php:269 +#: twittersettings.php:296 msgid "Twitter preferences saved." msgstr "" - -#: twitter.php:333 -msgid "Your Twitter bridge has been disabled." -msgstr "" - -#: twitter.php:337 -#, php-format -msgid "" -"Hi, %1$s. We're sorry to inform you that your link to Twitter has been " -"disabled. We no longer seem to have permission to update your Twitter " -"status. (Did you revoke %3$s's access?)\n" -"\n" -"You can re-enable your Twitter bridge by visiting your Twitter settings " -"page:\n" -"\n" -"\t%2$s\n" -"\n" -"Regards,\n" -"%3$s\n" -msgstr "" diff --git a/plugins/TwitterBridge/twitteradminpanel.php b/plugins/TwitterBridge/twitteradminpanel.php new file mode 100644 index 000000000..b22e6d99f --- /dev/null +++ b/plugins/TwitterBridge/twitteradminpanel.php @@ -0,0 +1,280 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Twitter bridge administration panel + * + * 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 Settings + * @package StatusNet + * @author Zach Copley <zach@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/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Administer global Twitter bridge settings + * + * @category Admin + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class TwitteradminpanelAction extends AdminPanelAction +{ + /** + * Returns the page title + * + * @return string page title + */ + + function title() + { + return _m('Twitter'); + } + + /** + * Instructions for using this form. + * + * @return string instructions + */ + + function getInstructions() + { + return _m('Twitter bridge settings'); + } + + /** + * Show the Twitter admin panel form + * + * @return void + */ + + function showForm() + { + $form = new TwitterAdminPanelForm($this); + $form->show(); + return; + } + + /** + * Save settings from the form + * + * @return void + */ + + function saveSettings() + { + static $settings = array( + 'twitter' => array('consumer_key', 'consumer_secret'), + 'integration' => array('source') + ); + + static $booleans = array( + 'twitter' => array('signin'), + 'twitterimport' => array('enabled') + ); + + $values = array(); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] + = $this->trimmed($setting); + } + } + + foreach ($booleans as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] + = ($this->boolean($setting)) ? 1 : 0; + } + } + + // This throws an exception on validation errors + + $this->validate($values); + + // assert(all values are valid); + + $config = new Config(); + + $config->query('BEGIN'); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + foreach ($booleans as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + $config->query('COMMIT'); + + return; + } + + function validate(&$values) + { + // Validate consumer key and secret (can't be too long) + + if (mb_strlen($values['twitter']['consumer_key']) > 255) { + $this->clientError( + _m("Invalid consumer key. Max length is 255 characters.") + ); + } + + if (mb_strlen($values['twitter']['consumer_secret']) > 255) { + $this->clientError( + _m("Invalid consumer secret. Max length is 255 characters.") + ); + } + } +} + +class TwitterAdminPanelForm extends AdminForm +{ + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'twitteradminpanel'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('twitteradminpanel'); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $this->out->elementStart( + 'fieldset', + array('id' => 'settings_twitter-application') + ); + $this->out->element('legend', null, _m('Twitter application settings')); + $this->out->elementStart('ul', 'form_data'); + + $this->li(); + $this->input( + 'consumer_key', + _m('Consumer key'), + _m('Consumer key assigned by Twitter'), + 'twitter' + ); + $this->unli(); + + $this->li(); + $this->input( + 'consumer_secret', + _m('Consumer secret'), + _m('Consumer secret assigned by Twitter'), + 'twitter' + ); + $this->unli(); + + $this->li(); + $this->input( + 'source', + _m('Integration source'), + _m('Name of your Twitter application'), + 'integration' + ); + $this->unli(); + + $this->out->elementEnd('ul'); + $this->out->elementEnd('fieldset'); + + $this->out->elementStart( + 'fieldset', + array('id' => 'settings_twitter-options') + ); + $this->out->element('legend', null, _m('Options')); + + $this->out->elementStart('ul', 'form_data'); + + $this->li(); + + $this->out->checkbox( + 'signin', _m('Enable "Sign-in with Twitter"'), + (bool) $this->value('signin', 'twitter'), + _m('Allow users to login with their Twitter credentials') + ); + $this->unli(); + + $this->li(); + $this->out->checkbox( + 'enabled', _m('Enable Twitter import'), + (bool) $this->value('enabled', 'twitterimport'), + _m('Allow users to import their Twitter friends\' timelines') + ); + $this->unli(); + + $this->out->elementEnd('ul'); + + $this->out->elementEnd('fieldset'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', _('Save'), 'submit', null, _('Save Twitter settings')); + } +} diff --git a/plugins/TwitterBridge/twitterauthorization.php b/plugins/TwitterBridge/twitterauthorization.php index cabf69d7a..c93f6666b 100644 --- a/plugins/TwitterBridge/twitterauthorization.php +++ b/plugins/TwitterBridge/twitterauthorization.php @@ -47,7 +47,7 @@ require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php'; * @author Zach Copley <zach@status.net> * @author Julien C <chaumond@gmail.com> * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ + * @link http://status.net/ * */ class TwitterauthorizationAction extends Action |