From 5e0cc07b0e687cf0d28d57ae80e5024b7e711fbd Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 5 Feb 2010 01:13:23 +0000 Subject: Fix issue with OAuth request parameters being parsed/stored twice when calling /api/account/verify_credentials.:format --- actions/apiaccountverifycredentials.php | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/actions/apiaccountverifycredentials.php b/actions/apiaccountverifycredentials.php index 1095d5162..ea61a3205 100644 --- a/actions/apiaccountverifycredentials.php +++ b/actions/apiaccountverifycredentials.php @@ -66,18 +66,21 @@ class ApiAccountVerifyCredentialsAction extends ApiAuthAction { parent::handle($args); - switch ($this->format) { - case 'xml': - case 'json': - $args['id'] = $this->auth_user->id; - $action_obj = new ApiUserShowAction(); - if ($action_obj->prepare($args)) { - $action_obj->handle($args); - } - break; - default: - header('Content-Type: text/html; charset=utf-8'); - print 'Authorized'; + if (!in_array($this->format, array('xml', 'json'))) { + $this->clientError(_('API method not found.'), $code = 404); + return; + } + + $twitter_user = $this->twitterUserArray($this->auth_user->getProfile(), true); + + if ($this->format == 'xml') { + $this->initDocument('xml'); + $this->showTwitterXmlUser($twitter_user); + $this->endDocument('xml'); + } elseif ($this->format == 'json') { + $this->initDocument('json'); + $this->showJsonObjects($twitter_user); + $this->endDocument('json'); } } @@ -86,14 +89,14 @@ class ApiAccountVerifyCredentialsAction extends ApiAuthAction * Is this action read only? * * @param array $args other arguments - * + * * @return boolean true * **/ - + function isReadOnly($args) { return true; } - + } -- cgit v1.2.3-54-g00ecf From 82f11190734c203ed6b2fd4a07cb9460f25b2183 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 5 Feb 2010 01:24:21 +0000 Subject: OAuth app name should not be null --- classes/statusnet.ini | 2 +- db/statusnet.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 4ace4407b..2c09033f6 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -353,7 +353,7 @@ notice_id = K id = 129 owner = 129 consumer_key = 130 -name = 2 +name = 130 description = 2 icon = 130 source_url = 2 diff --git a/db/statusnet.sql b/db/statusnet.sql index 8946f4d7e..343464801 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -214,7 +214,7 @@ create table oauth_application ( id integer auto_increment primary key comment 'unique identifier', owner integer not null comment 'owner of the application' references profile (id), consumer_key varchar(255) not null comment 'application consumer key' references consumer (consumer_key), - name varchar(255) unique key comment 'name of the application', + name varchar(255) not null unique key comment 'name of the application', description varchar(255) comment 'description of the application', icon varchar(255) not null comment 'application icon', source_url varchar(255) comment 'application homepage - used for source link', -- cgit v1.2.3-54-g00ecf From 10dfcde0b2099a169ccd3af0ecfbf2de9da551d6 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 5 Feb 2010 01:38:29 +0000 Subject: Actually store the timestamp on each nonce --- lib/oauthstore.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/oauthstore.php b/lib/oauthstore.php index b30fb49d5..eabe37f9f 100644 --- a/lib/oauthstore.php +++ b/lib/oauthstore.php @@ -65,7 +65,7 @@ class StatusNetOAuthDataStore extends OAuthDataStore { $n = new Nonce(); $n->consumer_key = $consumer->key; - $n->ts = $timestamp; + $n->ts = common_sql_date($timestamp); $n->nonce = $nonce; if ($n->find(true)) { return true; @@ -362,7 +362,6 @@ class StatusNetOAuthDataStore extends OAuthDataStore array('is_local' => Notice::REMOTE_OMB, 'uri' => $omb_notice->getIdentifierURI())); - } /** -- cgit v1.2.3-54-g00ecf From 52397f14741463cd518512e2f024b3ea7e18e136 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 1 Feb 2010 20:31:56 +0100 Subject: Sentence case for app statistics --- actions/showapplication.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/showapplication.php b/actions/showapplication.php index a6ff425c7..090e11882 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -201,7 +201,7 @@ class ShowApplicationAction extends OwnerDesignAction $userCnt = $appUsers->count(); $this->raw(sprintf( - _('created by %1$s - %2$s access by default - %3$d users'), + _('Created by %1$s - %2$s access by default - %3$d users'), $profile->getBestName(), $defaultAccess, $userCnt -- cgit v1.2.3-54-g00ecf From 8a0a89196043bc12e1fafea6d4638db5e61a181a Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 1 Feb 2010 20:32:18 +0100 Subject: Prevents app statistic text from wrapping around avatar --- theme/base/css/display.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 0d6395d05..2240e42af 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -632,7 +632,8 @@ margin-bottom:18px; .entity_profile .entity_url, .entity_profile .entity_note, .entity_profile .entity_tags, -.entity_profile .entity_aliases { +.entity_profile .entity_aliases, +.entity_profile .entity_statistics { margin-left:113px; margin-bottom:4px; } -- cgit v1.2.3-54-g00ecf From dc183f23cf3bd8e0fbd604ad2af4b12f77837bf2 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 1 Feb 2010 20:58:29 +0000 Subject: OAuth app names should be unique. --- actions/editapplication.php | 24 ++++++++++++++++++++++++ actions/newapplication.php | 20 ++++++++++++++++++++ classes/statusnet.ini | 3 ++- db/statusnet.sql | 2 +- 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/actions/editapplication.php b/actions/editapplication.php index 9cc3e3cea..029b622e8 100644 --- a/actions/editapplication.php +++ b/actions/editapplication.php @@ -179,6 +179,9 @@ class EditApplicationAction extends OwnerDesignAction } elseif (mb_strlen($name) > 255) { $this->showForm(_('Name is too long (max 255 chars).')); return; + } else if ($this->nameExists($name)) { + $this->showForm(_('Name already in use. Try another one.')); + return; } elseif (empty($description)) { $this->showForm(_('Description is required.')); return; @@ -260,5 +263,26 @@ class EditApplicationAction extends OwnerDesignAction common_redirect(common_local_url('oauthappssettings'), 303); } + /** + * Does the app name already exist? + * + * Checks the DB to see someone has already registered and app + * with the same name. + * + * @param string $name app name to check + * + * @return boolean true if the name already exists + */ + + function nameExists($name) + { + $newapp = Oauth_application::staticGet('name', $name); + if (!$newapp) { + return false; + } else { + return $newapp->id != $this->app->id; + } + } + } diff --git a/actions/newapplication.php b/actions/newapplication.php index c499fe7c7..ba1cca5c9 100644 --- a/actions/newapplication.php +++ b/actions/newapplication.php @@ -158,6 +158,9 @@ class NewApplicationAction extends OwnerDesignAction if (empty($name)) { $this->showForm(_('Name is required.')); return; + } else if ($this->nameExists($name)) { + $this->showForm(_('Name already in use. Try another one.')); + return; } elseif (mb_strlen($name) > 255) { $this->showForm(_('Name is too long (max 255 chars).')); return; @@ -273,5 +276,22 @@ class NewApplicationAction extends OwnerDesignAction } + /** + * Does the app name already exist? + * + * Checks the DB to see someone has already registered and app + * with the same name. + * + * @param string $name app name to check + * + * @return boolean true if the name already exists + */ + + function nameExists($name) + { + $app = Oauth_application::staticGet('name', $name); + return ($app !== false); + } + } diff --git a/classes/statusnet.ini b/classes/statusnet.ini index e28424ce2..a535159e8 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -353,7 +353,7 @@ notice_id = K id = 129 owner = 129 consumer_key = 130 -name = 130 +name = 2 description = 2 icon = 130 source_url = 2 @@ -367,6 +367,7 @@ modified = 384 [oauth_application__keys] id = N +name = U [oauth_application_user] profile_id = 129 diff --git a/db/statusnet.sql b/db/statusnet.sql index 17de4fd0d..71a6e724c 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -214,7 +214,7 @@ create table oauth_application ( id integer auto_increment primary key comment 'unique identifier', owner integer not null comment 'owner of the application' references profile (id), consumer_key varchar(255) not null comment 'application consumer key' references consumer (consumer_key), - name varchar(255) not null comment 'name of the application', + name varchar(255) unique key comment 'name of the application', description varchar(255) comment 'description of the application', icon varchar(255) not null comment 'application icon', source_url varchar(255) comment 'application homepage - used for source link', -- cgit v1.2.3-54-g00ecf From e495ac356c10a6abc0e10c81892830b5e198ef60 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 2 Feb 2010 06:26:03 +0000 Subject: Allow developers to delete OAuth applications --- actions/deleteapplication.php | 176 ++++++++++++++++++++++++++++++++++++++++++ actions/showapplication.php | 19 ++++- classes/Consumer.php | 30 +++++++ classes/Oauth_application.php | 17 ++++ lib/router.php | 4 + 5 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 actions/deleteapplication.php diff --git a/actions/deleteapplication.php b/actions/deleteapplication.php new file mode 100644 index 000000000..17526e111 --- /dev/null +++ b/actions/deleteapplication.php @@ -0,0 +1,176 @@ +. + * + * @category Action + * @package StatusNet + * @author Zach Copley + * @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') && !defined('LACONICA')) { + exit(1); +} + +/** + * Delete an OAuth appliction + * + * @category Action + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ + +class DeleteapplicationAction extends Action +{ + var $app = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + + function prepare($args) + { + if (!parent::prepare($args)) { + return false; + } + + if (!common_logged_in()) { + $this->clientError(_('You must be logged in to delete an application.')); + return false; + } + + $id = (int)$this->arg('id'); + $this->app = Oauth_application::staticGet('id', $id); + + if (empty($this->app)) { + $this->clientError(_('Application not found.')); + return false; + } + + $cur = common_current_user(); + + if ($cur->id != $this->app->owner) { + $this->clientError(_('You are not the owner of this application.'), 401); + return false; + } + + return true; + } + + /** + * Handle request + * + * Shows a page with list of favorite notices + * + * @param array $args $_REQUEST args; handled in prepare() + * + * @return void + */ + + function handle($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + + // CSRF protection + $token = $this->trimmed('token'); + if (!$token || $token != common_session_token()) { + $this->clientError(_('There was a problem with your session token.')); + return; + } + + if ($this->arg('no')) { + common_redirect(common_local_url('showapplication', + array('id' => $this->app->id)), 303); + } elseif ($this->arg('yes')) { + $this->handlePost(); + common_redirect(common_local_url('oauthappssettings'), 303); + } else { + $this->showPage(); + } + } + } + + function showContent() { + $this->areYouSureForm(); + } + + function title() { + return _('Delete application'); + } + + function showNoticeForm() { + // nop + } + + /** + * Confirm with user. + * + * Shows a confirmation form. + * + * @return void + */ + function areYouSureForm() + { + $id = $this->app->id; + $this->elementStart('form', array('id' => 'deleteapplication-' . $id, + 'method' => 'post', + 'class' => 'form_settings form_entity_block', + 'action' => common_local_url('deleteapplication', + array('id' => $this->app->id)))); + $this->elementStart('fieldset'); + $this->hidden('token', common_session_token()); + $this->element('legend', _('Delete application')); + $this->element('p', null, + _('Are you sure you want to delete this application? '. + 'This will clear all data about the application from the '. + 'database, including all existing user connections.')); + $this->submit('form_action-no', + _('No'), + 'submit form_action-primary', + 'no', + _("Do not delete this application")); + $this->submit('form_action-yes', + _('Yes'), + 'submit form_action-secondary', + 'yes', _('Delete this application')); + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + } + + /** + * Actually delete the app + * + * @return void + */ + + function handlePost() + { + $this->app->delete(); + } +} + diff --git a/actions/showapplication.php b/actions/showapplication.php index 090e11882..020d62480 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -222,18 +222,33 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementStart('li', 'entity_reset_keysecret'); $this->elementStart('form', array( - 'id' => 'forma_reset_key', + 'id' => 'form_reset_key', 'class' => 'form_reset_key', 'method' => 'POST', 'action' => common_local_url('showapplication', array('id' => $this->application->id)))); - $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); $this->submit('reset', _('Reset key & secret')); $this->elementEnd('fieldset'); $this->elementEnd('form'); $this->elementEnd('li'); + + $this->elementStart('li', 'entity_delete'); + $this->elementStart('form', array( + 'id' => 'form_delete_application', + 'class' => 'form_delete_application', + 'method' => 'POST', + 'action' => common_local_url('deleteapplication', + array('id' => $this->application->id)))); + + $this->elementStart('fieldset'); + $this->hidden('token', common_session_token()); + $this->submit('delete', _('Delete')); + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + $this->elementEnd('li'); + $this->elementEnd('ul'); $this->elementEnd('div'); diff --git a/classes/Consumer.php b/classes/Consumer.php index ad64a8491..ce399f278 100644 --- a/classes/Consumer.php +++ b/classes/Consumer.php @@ -36,4 +36,34 @@ class Consumer extends Memcached_DataObject return $cons; } + /** + * Delete a Consumer and related tokens and nonces + * + * XXX: Should this happen in an OAuthDataStore instead? + * + */ + function delete() + { + // XXX: Is there any reason NOT to do this kind of cleanup? + + $this->_deleteTokens(); + $this->_deleteNonces(); + + parent::delete(); + } + + function _deleteTokens() + { + $token = new Token(); + $token->consumer_key = $this->consumer_key; + $token->delete(); + } + + function _deleteNonces() + { + $nonce = new Nonce(); + $nonce->consumer_key = $this->consumer_key; + $nonce->delete(); + } + } diff --git a/classes/Oauth_application.php b/classes/Oauth_application.php index a6b539087..748b64220 100644 --- a/classes/Oauth_application.php +++ b/classes/Oauth_application.php @@ -137,4 +137,21 @@ class Oauth_application extends Memcached_DataObject } } + function delete() + { + $this->_deleteAppUsers(); + + $consumer = $this->getConsumer(); + $consumer->delete(); + + parent::delete(); + } + + function _deleteAppUsers() + { + $oauser = new Oauth_application_user(); + $oauser->application_id = $this->id; + $oauser->delete(); + } + } diff --git a/lib/router.php b/lib/router.php index 4b5b8d0bb..5981ef5d7 100644 --- a/lib/router.php +++ b/lib/router.php @@ -152,6 +152,10 @@ class Router array('action' => 'editapplication'), array('id' => '[0-9]+') ); + $m->connect('settings/oauthapps/delete/:id', + array('action' => 'deleteapplication'), + array('id' => '[0-9]+') + ); // search -- cgit v1.2.3-54-g00ecf From b31c79cee1565ca9bca5bcaffcbec04ddb312041 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 2 Feb 2010 07:35:54 +0000 Subject: Better token revocation --- actions/apioauthauthorize.php | 22 ++++++---------------- actions/oauthconnectionssettings.php | 24 +++++++++++++++--------- db/statusnet.sql | 2 +- lib/apioauthstore.php | 27 +++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 26 deletions(-) diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index 15c3a9dad..05d925d26 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -99,24 +99,17 @@ class ApiOauthAuthorizeAction extends ApiOauthAction } else { - // XXX: make better error messages - if (empty($this->oauth_token)) { - - common_debug("No request token found."); - - $this->clientError(_('Bad request.')); + $this->clientError(_('No oauth_token parameter provided.')); return; } if (empty($this->app)) { - common_debug('No app for that token.'); - $this->clientError(_('Bad request.')); + $this->clientError(_('Invalid token.')); return; } $name = $this->app->name; - common_debug("Requesting auth for app: " . $name); $this->showForm(); } @@ -124,8 +117,6 @@ class ApiOauthAuthorizeAction extends ApiOauthAction function handlePost() { - common_debug("handlePost()"); - // check session token for CSRF protection. $token = $this->trimmed('token'); @@ -210,13 +201,9 @@ class ApiOauthAuthorizeAction extends ApiOauthAction if (!empty($this->callback)) { - // XXX: Need better way to build this redirect url. - $target_url = $this->getCallback($this->callback, array('oauth_token' => $this->oauth_token)); - common_debug("Doing callback to $target_url"); - common_redirect($target_url, 303); } else { common_debug("callback was empty!"); @@ -236,9 +223,12 @@ class ApiOauthAuthorizeAction extends ApiOauthAction } else if ($this->arg('deny')) { + $datastore = new ApiStatusNetOAuthDataStore(); + $datastore->revoke_token($this->oauth_token, 0); + $this->elementStart('p'); - $this->raw(sprintf(_("The request token %s has been denied."), + $this->raw(sprintf(_("The request token %s has been denied and revoked."), $this->oauth_token)); $this->elementEnd('p'); diff --git a/actions/oauthconnectionssettings.php b/actions/oauthconnectionssettings.php index c2e8d441b..b1467f0d0 100644 --- a/actions/oauthconnectionssettings.php +++ b/actions/oauthconnectionssettings.php @@ -33,6 +33,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { require_once INSTALLDIR . '/lib/connectsettingsaction.php'; require_once INSTALLDIR . '/lib/applicationlist.php'; +require_once INSTALLDIR . '/lib/apioauthstore.php'; /** * Show connected OAuth applications @@ -71,11 +72,6 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction return _('Connected applications'); } - function isReadOnly($args) - { - return true; - } - /** * Instructions for use * @@ -153,6 +149,13 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction } } + /** + * Revoke access to an authorized OAuth application + * + * @param int $appId the ID of the application + * + */ + function revokeAccess($appId) { $cur = common_current_user(); @@ -164,6 +167,8 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction return false; } + // XXX: Transaction here? + $appUser = Oauth_application_user::getByKeys($cur, $app); if (empty($appUser)) { @@ -171,12 +176,13 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction return false; } - $orig = clone($appUser); - $appUser->access_type = 0; // No access - $result = $appUser->update(); + $datastore = new ApiStatusNetOAuthDataStore(); + $datastore->revoke_token($appUser->token, 1); + + $result = $appUser->delete(); if (!$result) { - common_log_db_error($orig, 'UPDATE', __FILE__); + common_log_db_error($orig, 'DELETE', __FILE__); $this->clientError(_('Unable to revoke access for app: ' . $app->id)); return false; } diff --git a/db/statusnet.sql b/db/statusnet.sql index 71a6e724c..8946f4d7e 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -230,7 +230,7 @@ create table oauth_application ( create table oauth_application_user ( profile_id integer not null comment 'user of the application' references profile (id), application_id integer not null comment 'id of the application' references oauth_application (id), - access_type tinyint default 0 comment 'access type, bit 1 = read, bit 2 = write, bit 3 = revoked', + access_type tinyint default 0 comment 'access type, bit 1 = read, bit 2 = write', token varchar(255) comment 'request or access token', created datetime not null comment 'date this record was created', modified timestamp comment 'date this record was modified', diff --git a/lib/apioauthstore.php b/lib/apioauthstore.php index 32110d057..1bb11cbca 100644 --- a/lib/apioauthstore.php +++ b/lib/apioauthstore.php @@ -159,5 +159,32 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore } } + /** + * Revoke specified access token + * + * Revokes the token specified by $token_key. + * Throws exceptions in case of error. + * + * @param string $token_key the token to be revoked + * @param int $type type of token (0 = req, 1 = access) + * + * @access public + * + * @return void + */ + + public function revoke_token($token_key, $type = 0) { + $rt = new Token(); + $rt->tok = $token_key; + $rt->type = $type; + $rt->state = 0; + if (!$rt->find(true)) { + throw new Exception('Tried to revoke unknown token'); + } + if (!$rt->delete()) { + throw new Exception('Failed to delete revoked token'); + } + } + } -- cgit v1.2.3-54-g00ecf From e9ecd8062a5d8223b7c0914255a24288c317d2a1 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 2 Feb 2010 07:59:28 +0000 Subject: Suppress notice input box on OAuth authorization page --- actions/apioauthauthorize.php | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index 05d925d26..2caa8d20b 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -67,8 +67,6 @@ class ApiOauthAuthorizeAction extends ApiOauthAction { parent::prepare($args); - common_debug("apioauthauthorize"); - $this->nickname = $this->trimmed('nickname'); $this->password = $this->arg('password'); $this->oauth_token = $this->arg('oauth_token'); @@ -193,8 +191,6 @@ class ApiOauthAuthorizeAction extends ApiOauthAction // A callback specified in the app setup overrides whatever // is passed in with the request. - common_debug("Req token is authorized - doing callback"); - if (!empty($this->app->callback_url)) { $this->callback = $this->app->callback_url; } @@ -295,12 +291,15 @@ class ApiOauthAuthorizeAction extends ApiOauthAction $msg = _('The application %1$s by ' . '%2$s would like the ability ' . - 'to %3$s your account data.'); + 'to %3$s your %4$s account data. ' . + 'You should only give access to your %4$s account ' . + 'to third parties you trust.'); $this->raw(sprintf($msg, $this->app->name, $this->app->organization, - $access)); + $access, + common_config('site', 'name'))); $this->elementEnd('p'); $this->elementEnd('li'); $this->elementEnd('ul'); @@ -362,6 +361,31 @@ class ApiOauthAuthorizeAction extends ApiOauthAction function showLocalNav() { + // NOP + } + + /** + * Show site notice. + * + * @return nothing + */ + + function showSiteNotice() + { + // NOP + } + + /** + * Show notice form. + * + * Show the form for posting a new notice + * + * @return nothing + */ + + function showNoticeForm() + { + // NOP } } -- cgit v1.2.3-54-g00ecf From 54171248847e0c535697c6b1e8ff0e89f42f0087 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 2 Feb 2010 08:47:14 +0000 Subject: Linkify notice source when posting from registered OAuth apps --- lib/api.php | 19 ++++++++++++++++++- lib/noticelist.php | 20 ++++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/api.php b/lib/api.php index 10a2fae28..f81975216 100644 --- a/lib/api.php +++ b/lib/api.php @@ -1249,10 +1249,27 @@ class ApiAction extends Action case 'api': break; default: + + $name = null; + $url = null; + $ns = Notice_source::staticGet($source); + if ($ns) { - $source_name = '' . $ns->name . ''; + $name = $ns->name; + $url = $ns->url; + } else { + $app = Oauth_application::staticGet('name', $source); + if ($app) { + $name = $app->name; + $url = $app->source_url; + } + } + + if (!empty($name) && !empty($url)) { + $source_name = '' . $name . ''; } + break; } return $source_name; diff --git a/lib/noticelist.php b/lib/noticelist.php index 85c169716..a4a0f2651 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -486,12 +486,28 @@ class NoticeListItem extends Widget $this->out->element('span', 'device', $source_name); break; default: + + $name = null; + $url = null; + $ns = Notice_source::staticGet($this->notice->source); + if ($ns) { + $name = $ns->name; + $url = $ns->url; + } else { + $app = Oauth_application::staticGet('name', $this->notice->source); + if ($app) { + $name = $app->name; + $url = $app->source_url; + } + } + + if (!empty($name) && !empty($url)) { $this->out->elementStart('span', 'device'); - $this->out->element('a', array('href' => $ns->url, + $this->out->element('a', array('href' => $url, 'rel' => 'external'), - $ns->name); + $name); $this->out->elementEnd('span'); } else { $this->out->element('span', 'device', $source_name); -- cgit v1.2.3-54-g00ecf From 4041a59282c5ebb751e3763b5489be2bfef7f74a Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 2 Feb 2010 23:16:44 +0000 Subject: Always check for an OAuth request. This allows OAuth clients to set an auth user, similar to how they can set one via http basic auth, even if one is not required. I think I finally got this right. --- lib/apiauth.php | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/apiauth.php b/lib/apiauth.php index 99500404f..25e2196cf 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -55,6 +55,7 @@ class ApiAuthAction extends ApiAction { var $auth_user_nickname = null; var $auth_user_password = null; + var $oauth_source = null; /** * Take arguments for running, looks for an OAuth request, @@ -73,20 +74,18 @@ class ApiAuthAction extends ApiAction // NOTE: $this->auth_user has to get set in prepare(), not handle(), // because subclasses do stuff with it in their prepares. - if ($this->requiresAuth()) { + $oauthReq = $this->getOAuthRequest(); - $oauthReq = $this->getOAuthRequest(); - - if (!$oauthReq) { + if (!$oauthReq) { + if ($this->requiresAuth()) { $this->checkBasicAuthUser(true); } else { - $this->checkOAuthRequest($oauthReq); + // Check to see if a basic auth user is there even + // if one's not required + $this->checkBasicAuthUser(false); } } else { - - // Check to see if a basic auth user is there even - // if one's not required - $this->checkBasicAuthUser(false); + $this->checkOAuthRequest($oauthReq); } // Reject API calls with the wrong access level @@ -108,7 +107,6 @@ class ApiAuthAction extends ApiAction * This is to avoid doign any unnecessary DB lookups. * * @return mixed the OAuthRequest or false - * */ function getOAuthRequest() @@ -137,7 +135,6 @@ class ApiAuthAction extends ApiAction * @param OAuthRequest $request the OAuth Request * * @return nothing - * */ function checkOAuthRequest($request) -- cgit v1.2.3-54-g00ecf From 7931875bbbfb127c0fa2f49331c137f0c6f1824a Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 3 Feb 2010 01:43:59 +0000 Subject: Confirm dialog for reset OAuth consumer key and secret button --- actions/editapplication.php | 2 +- actions/newapplication.php | 2 +- actions/showapplication.php | 54 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/actions/editapplication.php b/actions/editapplication.php index 029b622e8..ca5dba1e4 100644 --- a/actions/editapplication.php +++ b/actions/editapplication.php @@ -266,7 +266,7 @@ class EditApplicationAction extends OwnerDesignAction /** * Does the app name already exist? * - * Checks the DB to see someone has already registered and app + * Checks the DB to see someone has already registered an app * with the same name. * * @param string $name app name to check diff --git a/actions/newapplication.php b/actions/newapplication.php index ba1cca5c9..c0c520797 100644 --- a/actions/newapplication.php +++ b/actions/newapplication.php @@ -279,7 +279,7 @@ class NewApplicationAction extends OwnerDesignAction /** * Does the app name already exist? * - * Checks the DB to see someone has already registered and app + * Checks the DB to see someone has already registered an app * with the same name. * * @param string $name app name to check diff --git a/actions/showapplication.php b/actions/showapplication.php index 020d62480..fa4484481 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -149,7 +149,6 @@ class ShowApplicationAction extends OwnerDesignAction function showContent() { - $cur = common_current_user(); $consumer = $this->application->getConsumer(); @@ -229,7 +228,13 @@ class ShowApplicationAction extends OwnerDesignAction array('id' => $this->application->id)))); $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); - $this->submit('reset', _('Reset key & secret')); + + $this->element('input', array('type' => 'submit', + 'id' => 'reset', + 'name' => 'reset', + 'class' => 'submit', + 'value' => _('Reset key & secret'), + 'onClick' => 'return confirmReset()')); $this->elementEnd('fieldset'); $this->elementEnd('form'); $this->elementEnd('li'); @@ -291,14 +296,53 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementEnd('p'); } + /** + * Add a confirm script for Consumer key/secret reset + * + * @return void + */ + + function showScripts() + { + parent::showScripts(); + + $msg = _('Are you sure you want to reset your consumer key and secret?'); + + $js = 'function confirmReset() { '; + $js .= ' var agree = confirm("' . $msg . '"); '; + $js .= ' return agree;'; + $js .= '}'; + + $this->inlineScript($js); + } + + /** + * Reset an application's Consumer key and secret + * + * XXX: Should this be moved to its own page with a confirm? + * + */ + function resetKey() { $this->application->query('BEGIN'); + $oauser = new Oauth_application_user(); + $oauser->application_id = $this->application->id; + $result = $oauser->delete(); + + if ($result === false) { + common_log_db_error($oauser, 'DELETE', __FILE__); + $this->success = false; + $this->msg = ('Unable to reset consumer key and secret.'); + $this->showPage(); + return; + } + $consumer = $this->application->getConsumer(); $result = $consumer->delete(); - if (!$result) { + if ($result === false) { common_log_db_error($consumer, 'DELETE', __FILE__); $this->success = false; $this->msg = ('Unable to reset consumer key and secret.'); @@ -310,7 +354,7 @@ class ShowApplicationAction extends OwnerDesignAction $result = $consumer->insert(); - if (!$result) { + if (empty($result)) { common_log_db_error($consumer, 'INSERT', __FILE__); $this->application->query('ROLLBACK'); $this->success = false; @@ -323,7 +367,7 @@ class ShowApplicationAction extends OwnerDesignAction $this->application->consumer_key = $consumer->consumer_key; $result = $this->application->update($orig); - if (!$result) { + if ($result === false) { common_log_db_error($application, 'UPDATE', __FILE__); $this->application->query('ROLLBACK'); $this->success = false; -- cgit v1.2.3-54-g00ecf From 586d8e8524236c2682287f6a3b45fb572b3e3181 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 3 Feb 2010 18:13:21 +0100 Subject: Added right margin for notice text. Helps Conversation notices look better. --- theme/base/css/display.css | 1 + 1 file changed, 1 insertion(+) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 2240e42af..ed8853e57 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -1024,6 +1024,7 @@ float:none; } #content .notice .entry-title { margin-left:59px; +margin-right:7px; } .vcard .url { -- cgit v1.2.3-54-g00ecf From af9f23c2d9db2966284e5146026ec05d4bb37367 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 4 Feb 2010 01:53:08 +0000 Subject: - Fix cache handling in TwitterStatusFetcher - Other stability fixes --- .../TwitterBridge/daemons/twitterstatusfetcher.php | 53 ++++++++++++++++++---- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php index 36732ce46..bff657eb6 100755 --- a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php +++ b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php @@ -2,7 +2,7 @@ is_local = Notice::GATEWAY; if (Event::handle('StartNoticeSave', array(&$notice))) { - $id = $notice->insert(); + $notice->insert(); Event::handle('EndNoticeSave', array($notice)); } @@ -270,17 +270,41 @@ class TwitterStatusFetcher extends ParallelizingDaemon Inbox::insertNotice($flink->user_id, $notice->id); - $notice->blowCaches(); + $notice->blowOnInsert(); return $notice; } + /** + * Look up a Profile by profileurl field. Profile::staticGet() was + * not working consistently. + * + * @param string $url the profile url + * + * @return mixed the first profile with that url, or null + */ + + function getProfileByUrl($nickname, $profileurl) + { + $profile = new Profile(); + $profile->nickname = $nickname; + $profile->profileurl = $profileurl; + $profile->limit(1); + + if ($profile->find()) { + $profile->fetch(); + return $profile; + } + + return null; + } + function ensureProfile($user) { // check to see if there's already a profile for this user $profileurl = 'http://twitter.com/' . $user->screen_name; - $profile = Profile::staticGet('profileurl', $profileurl); + $profile = $this->getProfileByUrl($user->screen_name, $profileurl); if (!empty($profile)) { common_debug($this->name() . @@ -292,6 +316,7 @@ class TwitterStatusFetcher extends ParallelizingDaemon return $profile->id; } else { + common_debug($this->name() . ' - Adding profile and remote profile ' . "for Twitter user: $profileurl."); @@ -306,7 +331,11 @@ class TwitterStatusFetcher extends ParallelizingDaemon $profile->profileurl = $profileurl; $profile->created = common_sql_now(); - $id = $profile->insert(); + try { + $id = $profile->insert(); + } catch(Exception $e) { + common_log(LOG_WARNING, $this->name . ' Couldn\'t insert profile - ' . $e->getMessage()); + } if (empty($id)) { common_log_db_error($profile, 'INSERT', __FILE__); @@ -326,7 +355,11 @@ class TwitterStatusFetcher extends ParallelizingDaemon $remote_pro->uri = $profileurl; $remote_pro->created = common_sql_now(); - $rid = $remote_pro->insert(); + try { + $rid = $remote_pro->insert(); + } catch (Exception $e) { + common_log(LOG_WARNING, $this->name() . ' Couldn\'t save remote profile - ' . $e->getMessage()); + } if (empty($rid)) { common_log_db_error($profile, 'INSERT', __FILE__); @@ -446,7 +479,7 @@ class TwitterStatusFetcher extends ParallelizingDaemon if ($this->fetchAvatar($url, $filename)) { $this->newAvatar($id, $size, $mediatype, $filename); } else { - common_log(LOG_WARNING, $this->id() . + common_log(LOG_WARNING, $id() . " - Problem fetching Avatar: $url"); } } @@ -507,7 +540,11 @@ class TwitterStatusFetcher extends ParallelizingDaemon $avatar->created = common_sql_now(); - $id = $avatar->insert(); + try { + $id = $avatar->insert(); + } catch (Exception $e) { + common_log(LOG_WARNING, $this->name() . ' Couldn\'t insert avatar - ' . $e->getMessage()); + } if (empty($id)) { common_log_db_error($avatar, 'INSERT', __FILE__); -- cgit v1.2.3-54-g00ecf From 4379027432b4d35b60649624466a4c0e2abb5271 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 5 Feb 2010 01:13:23 +0000 Subject: Fix issue with OAuth request parameters being parsed/stored twice when calling /api/account/verify_credentials.:format --- actions/apiaccountverifycredentials.php | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/actions/apiaccountverifycredentials.php b/actions/apiaccountverifycredentials.php index 1095d5162..ea61a3205 100644 --- a/actions/apiaccountverifycredentials.php +++ b/actions/apiaccountverifycredentials.php @@ -66,18 +66,21 @@ class ApiAccountVerifyCredentialsAction extends ApiAuthAction { parent::handle($args); - switch ($this->format) { - case 'xml': - case 'json': - $args['id'] = $this->auth_user->id; - $action_obj = new ApiUserShowAction(); - if ($action_obj->prepare($args)) { - $action_obj->handle($args); - } - break; - default: - header('Content-Type: text/html; charset=utf-8'); - print 'Authorized'; + if (!in_array($this->format, array('xml', 'json'))) { + $this->clientError(_('API method not found.'), $code = 404); + return; + } + + $twitter_user = $this->twitterUserArray($this->auth_user->getProfile(), true); + + if ($this->format == 'xml') { + $this->initDocument('xml'); + $this->showTwitterXmlUser($twitter_user); + $this->endDocument('xml'); + } elseif ($this->format == 'json') { + $this->initDocument('json'); + $this->showJsonObjects($twitter_user); + $this->endDocument('json'); } } @@ -86,14 +89,14 @@ class ApiAccountVerifyCredentialsAction extends ApiAuthAction * Is this action read only? * * @param array $args other arguments - * + * * @return boolean true * **/ - + function isReadOnly($args) { return true; } - + } -- cgit v1.2.3-54-g00ecf From 208eec6511b13635b5feb8f100078f401cb0ce20 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 5 Feb 2010 01:24:21 +0000 Subject: OAuth app name should not be null --- classes/statusnet.ini | 2 +- db/statusnet.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/statusnet.ini b/classes/statusnet.ini index a535159e8..5f8da7cf5 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -353,7 +353,7 @@ notice_id = K id = 129 owner = 129 consumer_key = 130 -name = 2 +name = 130 description = 2 icon = 130 source_url = 2 diff --git a/db/statusnet.sql b/db/statusnet.sql index 8946f4d7e..343464801 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -214,7 +214,7 @@ create table oauth_application ( id integer auto_increment primary key comment 'unique identifier', owner integer not null comment 'owner of the application' references profile (id), consumer_key varchar(255) not null comment 'application consumer key' references consumer (consumer_key), - name varchar(255) unique key comment 'name of the application', + name varchar(255) not null unique key comment 'name of the application', description varchar(255) comment 'description of the application', icon varchar(255) not null comment 'application icon', source_url varchar(255) comment 'application homepage - used for source link', -- cgit v1.2.3-54-g00ecf From 857494c9c61d872b7decf69de226bba6cd250d99 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 5 Feb 2010 01:38:29 +0000 Subject: Actually store the timestamp on each nonce --- lib/oauthstore.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/oauthstore.php b/lib/oauthstore.php index b30fb49d5..eabe37f9f 100644 --- a/lib/oauthstore.php +++ b/lib/oauthstore.php @@ -65,7 +65,7 @@ class StatusNetOAuthDataStore extends OAuthDataStore { $n = new Nonce(); $n->consumer_key = $consumer->key; - $n->ts = $timestamp; + $n->ts = common_sql_date($timestamp); $n->nonce = $nonce; if ($n->find(true)) { return true; @@ -362,7 +362,6 @@ class StatusNetOAuthDataStore extends OAuthDataStore array('is_local' => Notice::REMOTE_OMB, 'uri' => $omb_notice->getIdentifierURI())); - } /** -- cgit v1.2.3-54-g00ecf From 875e1a70ce231b6b07765210328656abb353ad5b Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 5 Feb 2010 09:47:56 -0800 Subject: Don't spew warnings on usage of MEMCACHE_COMPRESSED constant when memcache PHP extension is not present. Switched to a locally-defined Cache::COMPRESSED, translating that to MEMCACHE_COMPRESSED in the plugin. --- classes/Memcached_DataObject.php | 2 +- lib/cache.php | 4 +++- plugins/MemcachePlugin.php | 18 ++++++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index ab65c30ce..dfd06b57e 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -363,7 +363,7 @@ class Memcached_DataObject extends DB_DataObject $cached[] = clone($inst); } $inst->free(); - $c->set($ckey, $cached, MEMCACHE_COMPRESSED, $expiry); + $c->set($ckey, $cached, Cache::COMPRESSED, $expiry); return new ArrayWrapper($cached); } diff --git a/lib/cache.php b/lib/cache.php index 635c96ad4..df6fc3649 100644 --- a/lib/cache.php +++ b/lib/cache.php @@ -47,6 +47,8 @@ class Cache var $_items = array(); static $_inst = null; + const COMPRESSED = 1; + /** * Singleton constructor * @@ -133,7 +135,7 @@ class Cache * * @param string $key The key to use for lookups * @param string $value The value to store - * @param integer $flag Flags to use, mostly ignored + * @param integer $flag Flags to use, may include Cache::COMPRESSED * @param integer $expiry Expiry value, mostly ignored * * @return boolean success flag diff --git a/plugins/MemcachePlugin.php b/plugins/MemcachePlugin.php index 2bc4b892b..c5e74fb41 100644 --- a/plugins/MemcachePlugin.php +++ b/plugins/MemcachePlugin.php @@ -102,7 +102,7 @@ class MemcachePlugin extends Plugin * * @param string &$key in; Key to use for lookups * @param mixed &$value in; Value to associate - * @param integer &$flag in; Flag (passed through to Memcache) + * @param integer &$flag in; Flag empty or Cache::COMPRESSED * @param integer &$expiry in; Expiry (passed through to Memcache) * @param boolean &$success out; Whether the set was successful * @@ -115,7 +115,7 @@ class MemcachePlugin extends Plugin if ($expiry === null) { $expiry = $this->defaultExpiry; } - $success = $this->_conn->set($key, $value, $flag, $expiry); + $success = $this->_conn->set($key, $value, $this->flag(intval($flag)), $expiry); Event::handle('EndCacheSet', array($key, $value, $flag, $expiry)); return false; @@ -197,6 +197,20 @@ class MemcachePlugin extends Plugin } } + /** + * Translate general flags to Memcached-specific flags + * @param int $flag + * @return int + */ + protected function flag($flag) + { + $out = 0; + if ($flag & Cache::COMPRESSED == Cache::COMPRESSED) { + $out |= MEMCACHE_COMPRESSED; + } + return $out; + } + function onPluginVersion(&$versions) { $versions[] = array('name' => 'Memcache', -- cgit v1.2.3-54-g00ecf From 558934d1ddc1c1163c6a142bfe1c4232496b25d6 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 5 Feb 2010 21:39:29 -0800 Subject: Store Twitter screen_name, not name, for foreign_user.nickname when saving Twitter user. --- plugins/TwitterBridge/twitterauthorization.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/TwitterBridge/twitterauthorization.php b/plugins/TwitterBridge/twitterauthorization.php index b2657ff61..dbef438a4 100644 --- a/plugins/TwitterBridge/twitterauthorization.php +++ b/plugins/TwitterBridge/twitterauthorization.php @@ -219,7 +219,7 @@ class TwitterauthorizationAction extends Action $user = common_current_user(); $this->saveForeignLink($user->id, $twitter_user->id, $atok); - save_twitter_user($twitter_user->id, $twitter_user->name); + save_twitter_user($twitter_user->id, $twitter_user->screen_name); } else { -- cgit v1.2.3-54-g00ecf From 70abea3ac4034cbbfc68cdc8288fc7e2d1cea17c Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Sat, 6 Feb 2010 06:46:00 +0000 Subject: Delete old Twitter user record when user changes screen name instead of updating. Simpler. --- plugins/TwitterBridge/twitter.php | 54 ++++++--------------------------------- 1 file changed, 8 insertions(+), 46 deletions(-) diff --git a/plugins/TwitterBridge/twitter.php b/plugins/TwitterBridge/twitter.php index 33dfb788b..de30d9ebf 100644 --- a/plugins/TwitterBridge/twitter.php +++ b/plugins/TwitterBridge/twitter.php @@ -26,38 +26,6 @@ define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1 require_once INSTALLDIR . '/plugins/TwitterBridge/twitterbasicauthclient.php'; require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php'; -function updateTwitter_user($twitter_id, $screen_name) -{ - $uri = 'http://twitter.com/' . $screen_name; - $fuser = new Foreign_user(); - - $fuser->query('BEGIN'); - - // Dropping down to SQL because regular DB_DataObject udpate stuff doesn't seem - // to work so good with tables that have multiple column primary keys - - // Any time we update the uri for a forein user we have to make sure there - // are no dupe entries first -- unique constraint on the uri column - - $qry = 'UPDATE foreign_user set uri = \'\' WHERE uri = '; - $qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE; - - $fuser->query($qry); - - // Update the user - - $qry = 'UPDATE foreign_user SET nickname = '; - $qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' '; - $qry .= 'WHERE id = ' . $twitter_id . ' AND service = ' . TWITTER_SERVICE; - - $fuser->query('COMMIT'); - - $fuser->free(); - unset($fuser); - - return true; -} - function add_twitter_user($twitter_id, $screen_name) { @@ -105,7 +73,6 @@ function add_twitter_user($twitter_id, $screen_name) // Creates or Updates a Twitter user function save_twitter_user($twitter_id, $screen_name) { - // Check to see whether the Twitter user is already in the system, // and update its screen name and uri if so. @@ -115,25 +82,20 @@ function save_twitter_user($twitter_id, $screen_name) $result = true; - // Only update if Twitter screen name has changed + // Delete old record if Twitter user changed screen name if ($fuser->nickname != $screen_name) { - $result = updateTwitter_user($twitter_id, $screen_name); - - common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' . - "$fuser->id to $screen_name, was $fuser->nickname"); + $oldname = $fuser->nickname; + $fuser->delete(); + common_log(LOG_INFO, sprintf('Twitter bridge - Updated nickname (and URI) ' . + 'for Twitter user %1$d - %2$s, was %3$s.', + $fuser->id, + $screen_name, + $oldname)); } - return $result; - - } else { return add_twitter_user($twitter_id, $screen_name); } - - $fuser->free(); - unset($fuser); - - return true; } function is_twitter_bound($notice, $flink) { -- cgit v1.2.3-54-g00ecf From 5fdcd88176010a72b6a157170784a8aad7bf4131 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 6 Feb 2010 11:36:59 +0100 Subject: Moderator can make users admins of a group --- actions/groupmembers.php | 4 +++- actions/makeadmin.php | 3 ++- classes/Profile.php | 1 + lib/right.php | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/actions/groupmembers.php b/actions/groupmembers.php index 0f47c268d..f16e972a4 100644 --- a/actions/groupmembers.php +++ b/actions/groupmembers.php @@ -192,7 +192,9 @@ class GroupMemberListItem extends ProfileListItem { $user = common_current_user(); - if (!empty($user) && $user->id != $this->profile->id && $user->isAdmin($this->group) && + if (!empty($user) && + $user->id != $this->profile->id && + ($user->isAdmin($this->group) || $user->hasRight(Right::MAKEGROUPADMIN)) && !$this->profile->isAdmin($this->group)) { $this->out->elementStart('li', 'entity_make_admin'); $maf = new MakeAdminForm($this->out, $this->profile, $this->group, diff --git a/actions/makeadmin.php b/actions/makeadmin.php index 9ad7d6e7c..f19348648 100644 --- a/actions/makeadmin.php +++ b/actions/makeadmin.php @@ -87,7 +87,8 @@ class MakeadminAction extends Action return false; } $user = common_current_user(); - if (!$user->isAdmin($this->group)) { + if (!$user->isAdmin($this->group) && + !$user->hasRight(Right::MAKEGROUPADMIN)) { $this->clientError(_('Only an admin can make another user an admin.'), 401); return false; } diff --git a/classes/Profile.php b/classes/Profile.php index 1076fb2cb..feabc2508 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -716,6 +716,7 @@ class Profile extends Memcached_DataObject switch ($right) { case Right::DELETEOTHERSNOTICE: + case Right::MAKEGROUPADMIN: case Right::SANDBOXUSER: case Right::SILENCEUSER: case Right::DELETEUSER: diff --git a/lib/right.php b/lib/right.php index 5e66eae0e..4e9c5a918 100644 --- a/lib/right.php +++ b/lib/right.php @@ -57,5 +57,6 @@ class Right const EMAILONREPLY = 'emailonreply'; const EMAILONSUBSCRIBE = 'emailonsubscribe'; const EMAILONFAVE = 'emailonfave'; + const MAKEGROUPADMIN = 'makegroupadmin'; } -- cgit v1.2.3-54-g00ecf From dc09453a77f33c4dfdff306321ce93cf5fbd2d57 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Feb 2010 11:06:03 -0800 Subject: First steps on converting FeedSub into the pub/sub basis for OStatus communications: * renamed FeedSub plugin to OStatus * now setting avatar on subscriptions * general fixes for subscription * integrated PuSH hub to handle only user timelines on canonical ID url; sends updates directly * set $config['feedsub']['nohub'] = true to test w/ foreign feeds that don't have hubs (won't actually receive updates though) * a few bits of code documentation * HMAC support for verified distributions (safest if sub setup is on HTTPS) And a couple core changes: * minimizing HTML output for exceptions in API requests to aid in debugging * fix for rel=self link in apitimelineuser when id given This does not not yet include any of the individual subscription management (Salmon notifications for sub/unsub, etc) nor a nice UI for user subscriptions. Needs some further cleanup to treat posts as status updates instead of link references. --- actions/apitimelineuser.php | 5 +- lib/api.php | 1 + lib/error.php | 10 +- lib/httpclient.php | 5 +- lib/mysqlschema.php | 1 + lib/statusnet.php | 13 +- plugins/FeedSub/FeedSubPlugin.php | 115 ----- plugins/FeedSub/README | 24 -- plugins/FeedSub/actions/feedsubcallback.php | 100 ----- plugins/FeedSub/actions/feedsubsettings.php | 255 ----------- plugins/FeedSub/extlib/README | 9 - plugins/FeedSub/extlib/XML/Feed/Parser.php | 351 ---------------- plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php | 365 ---------------- .../FeedSub/extlib/XML/Feed/Parser/AtomElement.php | 261 ------------ .../FeedSub/extlib/XML/Feed/Parser/Exception.php | 42 -- plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php | 214 ---------- .../extlib/XML/Feed/Parser/RSS09Element.php | 62 --- plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php | 277 ------------ plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php | 276 ------------ .../extlib/XML/Feed/Parser/RSS11Element.php | 151 ------- .../FeedSub/extlib/XML/Feed/Parser/RSS1Element.php | 116 ----- plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php | 335 --------------- .../FeedSub/extlib/XML/Feed/Parser/RSS2Element.php | 171 -------- plugins/FeedSub/extlib/XML/Feed/Parser/Type.php | 467 --------------------- .../extlib/XML/Feed/samples/atom10-entryonly.xml | 28 -- .../extlib/XML/Feed/samples/atom10-example1.xml | 20 - .../extlib/XML/Feed/samples/atom10-example2.xml | 45 -- .../FeedSub/extlib/XML/Feed/samples/delicious.feed | 177 -------- .../FeedSub/extlib/XML/Feed/samples/flickr.feed | 184 -------- .../extlib/XML/Feed/samples/grwifi-atom.xml | 7 - plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml | 102 ----- .../extlib/XML/Feed/samples/illformed_atom10.xml | 13 - .../extlib/XML/Feed/samples/rss091-complete.xml | 47 --- .../XML/Feed/samples/rss091-international.xml | 30 -- .../extlib/XML/Feed/samples/rss091-simple.xml | 15 - .../extlib/XML/Feed/samples/rss092-sample.xml | 103 ----- .../extlib/XML/Feed/samples/rss10-example1.xml | 62 --- .../extlib/XML/Feed/samples/rss10-example2.xml | 67 --- .../FeedSub/extlib/XML/Feed/samples/rss2sample.xml | 42 -- .../extlib/XML/Feed/samples/sixapart-jp.xml | 226 ---------- .../extlib/XML/Feed/samples/technorati.feed | 54 --- plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc | 338 --------------- plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc | 113 ----- plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc | 218 ---------- .../FeedSub/extlib/xml-feed-parser-bug-16416.patch | 14 - plugins/FeedSub/feeddiscovery.php | 209 --------- plugins/FeedSub/feedinfo.php | 268 ------------ plugins/FeedSub/feedinfo.sql | 14 - plugins/FeedSub/feedmunger.php | 238 ----------- plugins/FeedSub/images/24px-Feed-icon.svg.png | Bin 1204 -> 0 bytes plugins/FeedSub/images/48px-Feed-icon.svg.png | Bin 2434 -> 0 bytes plugins/FeedSub/images/96px-Feed-icon.svg.png | Bin 5440 -> 0 bytes plugins/FeedSub/images/README | 5 - plugins/FeedSub/locale/FeedSub.po | 104 ----- plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po | 106 ----- plugins/FeedSub/tests/FeedDiscoveryTest.php | 111 ----- plugins/FeedSub/tests/FeedMungerTest.php | 147 ------- plugins/FeedSub/tests/gettext-speedtest.php | 78 ---- plugins/OStatus/OStatusPlugin.php | 159 +++++++ plugins/OStatus/README | 24 ++ plugins/OStatus/actions/feedsubcallback.php | 105 +++++ plugins/OStatus/actions/feedsubsettings.php | 258 ++++++++++++ plugins/OStatus/actions/hub.php | 176 ++++++++ plugins/OStatus/classes/Feedinfo.php | 345 +++++++++++++++ plugins/OStatus/classes/HubSub.php | 272 ++++++++++++ plugins/OStatus/extlib/README | 9 + plugins/OStatus/extlib/XML/Feed/Parser.php | 351 ++++++++++++++++ plugins/OStatus/extlib/XML/Feed/Parser/Atom.php | 365 ++++++++++++++++ .../OStatus/extlib/XML/Feed/Parser/AtomElement.php | 261 ++++++++++++ .../OStatus/extlib/XML/Feed/Parser/Exception.php | 42 ++ plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php | 214 ++++++++++ .../extlib/XML/Feed/Parser/RSS09Element.php | 62 +++ plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php | 277 ++++++++++++ plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php | 276 ++++++++++++ .../extlib/XML/Feed/Parser/RSS11Element.php | 151 +++++++ .../OStatus/extlib/XML/Feed/Parser/RSS1Element.php | 116 +++++ plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php | 335 +++++++++++++++ .../OStatus/extlib/XML/Feed/Parser/RSS2Element.php | 171 ++++++++ plugins/OStatus/extlib/XML/Feed/Parser/Type.php | 467 +++++++++++++++++++++ .../extlib/XML/Feed/samples/atom10-entryonly.xml | 28 ++ .../extlib/XML/Feed/samples/atom10-example1.xml | 20 + .../extlib/XML/Feed/samples/atom10-example2.xml | 45 ++ .../OStatus/extlib/XML/Feed/samples/delicious.feed | 177 ++++++++ .../OStatus/extlib/XML/Feed/samples/flickr.feed | 184 ++++++++ .../extlib/XML/Feed/samples/grwifi-atom.xml | 7 + plugins/OStatus/extlib/XML/Feed/samples/hoder.xml | 102 +++++ .../extlib/XML/Feed/samples/illformed_atom10.xml | 13 + .../extlib/XML/Feed/samples/rss091-complete.xml | 47 +++ .../XML/Feed/samples/rss091-international.xml | 30 ++ .../extlib/XML/Feed/samples/rss091-simple.xml | 15 + .../extlib/XML/Feed/samples/rss092-sample.xml | 103 +++++ .../extlib/XML/Feed/samples/rss10-example1.xml | 62 +++ .../extlib/XML/Feed/samples/rss10-example2.xml | 67 +++ .../OStatus/extlib/XML/Feed/samples/rss2sample.xml | 42 ++ .../extlib/XML/Feed/samples/sixapart-jp.xml | 226 ++++++++++ .../extlib/XML/Feed/samples/technorati.feed | 54 +++ plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc | 338 +++++++++++++++ plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc | 113 +++++ plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc | 218 ++++++++++ .../OStatus/extlib/xml-feed-parser-bug-16416.patch | 14 + plugins/OStatus/images/24px-Feed-icon.svg.png | Bin 0 -> 1204 bytes plugins/OStatus/images/48px-Feed-icon.svg.png | Bin 0 -> 2434 bytes plugins/OStatus/images/96px-Feed-icon.svg.png | Bin 0 -> 5440 bytes plugins/OStatus/images/README | 5 + plugins/OStatus/lib/feeddiscovery.php | 221 ++++++++++ plugins/OStatus/lib/feedmunger.php | 270 ++++++++++++ plugins/OStatus/lib/hubdistribqueuehandler.php | 87 ++++ plugins/OStatus/lib/huboutqueuehandler.php | 52 +++ plugins/OStatus/lib/hubverifyqueuehandler.php | 53 +++ plugins/OStatus/locale/OStatus.po | 104 +++++ plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po | 106 +++++ plugins/OStatus/tests/FeedDiscoveryTest.php | 111 +++++ plugins/OStatus/tests/FeedMungerTest.php | 147 +++++++ plugins/OStatus/tests/gettext-speedtest.php | 78 ++++ 114 files changed, 7604 insertions(+), 6782 deletions(-) delete mode 100644 plugins/FeedSub/FeedSubPlugin.php delete mode 100644 plugins/FeedSub/README delete mode 100644 plugins/FeedSub/actions/feedsubcallback.php delete mode 100644 plugins/FeedSub/actions/feedsubsettings.php delete mode 100644 plugins/FeedSub/extlib/README delete mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser.php delete mode 100644 plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php delete mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php delete mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php delete mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php delete mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php delete mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php delete mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php delete mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php delete mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php delete mode 100644 plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php delete mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php delete mode 100644 plugins/FeedSub/extlib/XML/Feed/Parser/Type.php delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml delete mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed delete mode 100755 plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc delete mode 100755 plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc delete mode 100755 plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc delete mode 100644 plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch delete mode 100644 plugins/FeedSub/feeddiscovery.php delete mode 100644 plugins/FeedSub/feedinfo.php delete mode 100644 plugins/FeedSub/feedinfo.sql delete mode 100644 plugins/FeedSub/feedmunger.php delete mode 100644 plugins/FeedSub/images/24px-Feed-icon.svg.png delete mode 100644 plugins/FeedSub/images/48px-Feed-icon.svg.png delete mode 100644 plugins/FeedSub/images/96px-Feed-icon.svg.png delete mode 100644 plugins/FeedSub/images/README delete mode 100644 plugins/FeedSub/locale/FeedSub.po delete mode 100644 plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po delete mode 100644 plugins/FeedSub/tests/FeedDiscoveryTest.php delete mode 100644 plugins/FeedSub/tests/FeedMungerTest.php delete mode 100644 plugins/FeedSub/tests/gettext-speedtest.php create mode 100644 plugins/OStatus/OStatusPlugin.php create mode 100644 plugins/OStatus/README create mode 100644 plugins/OStatus/actions/feedsubcallback.php create mode 100644 plugins/OStatus/actions/feedsubsettings.php create mode 100644 plugins/OStatus/actions/hub.php create mode 100644 plugins/OStatus/classes/Feedinfo.php create mode 100644 plugins/OStatus/classes/HubSub.php create mode 100644 plugins/OStatus/extlib/README create mode 100755 plugins/OStatus/extlib/XML/Feed/Parser.php create mode 100644 plugins/OStatus/extlib/XML/Feed/Parser/Atom.php create mode 100755 plugins/OStatus/extlib/XML/Feed/Parser/AtomElement.php create mode 100755 plugins/OStatus/extlib/XML/Feed/Parser/Exception.php create mode 100755 plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php create mode 100755 plugins/OStatus/extlib/XML/Feed/Parser/RSS09Element.php create mode 100755 plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php create mode 100755 plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php create mode 100755 plugins/OStatus/extlib/XML/Feed/Parser/RSS11Element.php create mode 100755 plugins/OStatus/extlib/XML/Feed/Parser/RSS1Element.php create mode 100644 plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php create mode 100755 plugins/OStatus/extlib/XML/Feed/Parser/RSS2Element.php create mode 100644 plugins/OStatus/extlib/XML/Feed/Parser/Type.php create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/atom10-entryonly.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/atom10-example1.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/atom10-example2.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/delicious.feed create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/flickr.feed create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/grwifi-atom.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/hoder.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/illformed_atom10.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/rss091-complete.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/rss091-international.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/rss091-simple.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/rss092-sample.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/rss10-example1.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/rss10-example2.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/rss2sample.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/sixapart-jp.xml create mode 100755 plugins/OStatus/extlib/XML/Feed/samples/technorati.feed create mode 100755 plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc create mode 100755 plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc create mode 100755 plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc create mode 100644 plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch create mode 100644 plugins/OStatus/images/24px-Feed-icon.svg.png create mode 100644 plugins/OStatus/images/48px-Feed-icon.svg.png create mode 100644 plugins/OStatus/images/96px-Feed-icon.svg.png create mode 100644 plugins/OStatus/images/README create mode 100644 plugins/OStatus/lib/feeddiscovery.php create mode 100644 plugins/OStatus/lib/feedmunger.php create mode 100644 plugins/OStatus/lib/hubdistribqueuehandler.php create mode 100644 plugins/OStatus/lib/huboutqueuehandler.php create mode 100644 plugins/OStatus/lib/hubverifyqueuehandler.php create mode 100644 plugins/OStatus/locale/OStatus.po create mode 100644 plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po create mode 100644 plugins/OStatus/tests/FeedDiscoveryTest.php create mode 100644 plugins/OStatus/tests/FeedMungerTest.php create mode 100644 plugins/OStatus/tests/gettext-speedtest.php diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 830b16941..ed9104905 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -145,10 +145,11 @@ class ApiTimelineUserAction extends ApiBareAuthAction ); break; case 'atom': - if (isset($apidata['api_arg'])) { + $id = $this->arg('id'); + if ($id) { $selfuri = common_root_url() . 'api/statuses/user_timeline/' . - $apidata['api_arg'] . '.atom'; + rawurlencode($id) . '.atom'; } else { $selfuri = common_root_url() . 'api/statuses/user_timeline.atom'; diff --git a/lib/api.php b/lib/api.php index f81975216..fd07bbbbe 100644 --- a/lib/api.php +++ b/lib/api.php @@ -77,6 +77,7 @@ class ApiAction extends Action function prepare($args) { + StatusNet::setApi(true); // reduce exception reports to aid in debugging parent::prepare($args); $this->format = $this->arg('format'); diff --git a/lib/error.php b/lib/error.php index 87a4d913b..a6a29119f 100644 --- a/lib/error.php +++ b/lib/error.php @@ -56,6 +56,7 @@ class ErrorAction extends Action $this->code = $code; $this->message = $message; + $this->minimal = StatusNet::isApi(); // XXX: hack alert: usually we aren't going to // call this page directly, but because it's @@ -102,7 +103,14 @@ class ErrorAction extends Action function showPage() { - parent::showPage(); + if ($this->minimal) { + // Even more minimal -- we're in a machine API + // and don't want to flood the output. + $this->extraHeaders(); + $this->showContent(); + } else { + parent::showPage(); + } // We don't want to have any more output after this exit(); diff --git a/lib/httpclient.php b/lib/httpclient.php index 3f8262076..4c3af8d7d 100644 --- a/lib/httpclient.php +++ b/lib/httpclient.php @@ -81,12 +81,13 @@ class HTTPResponse extends HTTP_Request2_Response } /** - * Check if the response is OK, generally a 200 status code. + * Check if the response is OK, generally a 200 or other 2xx status code. * @return bool */ function isOk() { - return ($this->getStatus() == 200); + $status = $this->getStatus(); + return ($status >= 200 && $status < 300); } } diff --git a/lib/mysqlschema.php b/lib/mysqlschema.php index 1f7c3d092..485096ac4 100644 --- a/lib/mysqlschema.php +++ b/lib/mysqlschema.php @@ -213,6 +213,7 @@ class MysqlSchema extends Schema $sql .= "); "; + common_log(LOG_INFO, $sql); $res = $this->conn->query($sql); if (PEAR::isError($res)) { diff --git a/lib/statusnet.php b/lib/statusnet.php index 29e903026..4f82fdaa6 100644 --- a/lib/statusnet.php +++ b/lib/statusnet.php @@ -30,6 +30,7 @@ global $config, $_server, $_path; class StatusNet { protected static $have_config; + protected static $is_api; /** * Configure and instantiate a plugin into the current configuration. @@ -63,7 +64,7 @@ class StatusNet } } if (!class_exists($pluginclass)) { - throw new ServerException(500, "Plugin $name not found."); + throw new ServerException("Plugin $name not found.", 500); } } @@ -147,6 +148,16 @@ class StatusNet return self::$have_config; } + public function isApi() + { + return self::$is_api; + } + + public function setApi($mode) + { + self::$is_api = $mode; + } + /** * Build default configuration array * @return array diff --git a/plugins/FeedSub/FeedSubPlugin.php b/plugins/FeedSub/FeedSubPlugin.php deleted file mode 100644 index e49e2a648..000000000 --- a/plugins/FeedSub/FeedSubPlugin.php +++ /dev/null @@ -1,115 +0,0 @@ - -Author URI: http://status.net/ -*/ - -/* - * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2009, StatusNet, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** - * @package FeedSubPlugin - * @maintainer Brion Vibber - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - -define('FEEDSUB_SERVICE', 100); // fixme -- avoid hardcoding these? - -// We bundle the XML_Parse_Feed library... -set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib'); - -class FeedSubException extends Exception -{ -} - -class FeedSubPlugin extends Plugin -{ - /** - * Hook for RouterInitialized event. - * - * @param Net_URL_Mapper $m path-to-action mapper - * @return boolean hook return - */ - function onRouterInitialized($m) - { - $m->connect('feedsub/callback/:feed', - array('action' => 'feedsubcallback'), - array('feed' => '[0-9]+')); - $m->connect('settings/feedsub', - array('action' => 'feedsubsettings')); - return true; - } - - /** - * Add the feed settings page to the Connect Settings menu - * - * @param Action &$action The calling page - * - * @return boolean hook return - */ - function onEndConnectSettingsNav(&$action) - { - $action_name = $action->trimmed('action'); - - $action->menuItem(common_local_url('feedsubsettings'), - _m('Feeds'), - _m('Feed subscription options'), - $action_name === 'feedsubsettings'); - - return true; - } - - /** - * Automatically load the actions and libraries used by the plugin - * - * @param Class $cls the class - * - * @return boolean hook return - * - */ - function onAutoload($cls) - { - $base = dirname(__FILE__); - $lower = strtolower($cls); - $files = array("$base/$lower.php"); - if (substr($lower, -6) == 'action') { - $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php"; - } - foreach ($files as $file) { - if (file_exists($file)) { - include_once $file; - return false; - } - } - return true; - } - - function onCheckSchema() { - // warning: the autoincrement doesn't seem to set. - // alter table feedinfo change column id id int(11) not null auto_increment; - $schema = Schema::get(); - $schema->ensureTable('feedinfo', Feedinfo::schemaDef()); - return true; - } -} diff --git a/plugins/FeedSub/README b/plugins/FeedSub/README deleted file mode 100644 index cbf3adbb9..000000000 --- a/plugins/FeedSub/README +++ /dev/null @@ -1,24 +0,0 @@ -Plugin to support importing updates from external RSS and Atom feeds into your timeline. - -Uses PubSubHubbub for push feed updates; currently non-PuSH feeds cannot be subscribed. - -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 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? -* 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/FeedSub/actions/feedsubcallback.php b/plugins/FeedSub/actions/feedsubcallback.php deleted file mode 100644 index 0c4280c1f..000000000 --- a/plugins/FeedSub/actions/feedsubcallback.php +++ /dev/null @@ -1,100 +0,0 @@ -. - */ - -/** - * @package FeedSubPlugin - * @maintainer Brion Vibber - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - - -class FeedSubCallbackAction extends Action -{ - function handle() - { - parent::handle(); - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $this->handlePost(); - } else { - $this->handleGet(); - } - } - - /** - * Handler for POST content updates from the hub - */ - function handlePost() - { - $feedid = $this->arg('feed'); - common_log(LOG_INFO, "POST for feed id $feedid"); - if (!$feedid) { - throw new ServerException('Empty or invalid feed id', 400); - } - - $feedinfo = Feedinfo::staticGet('id', $feedid); - if (!$feedinfo) { - throw new ServerException('Unknown feed id ' . $feedid, 400); - } - - $post = file_get_contents('php://input'); - $feedinfo->postUpdates($post); - } - - /** - * Handler for GET verification requests from the hub - */ - function handleGet() - { - $mode = $this->arg('hub_mode'); - $topic = $this->arg('hub_topic'); - $challenge = $this->arg('hub_challenge'); - $lease_seconds = $this->arg('hub_lease_seconds'); - $verify_token = $this->arg('hub_verify_token'); - - if ($mode != 'subscribe' && $mode != 'unsubscribe') { - common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\""); - throw new ServerException("Bogus hub callback: bad mode", 404); - } - - $feedinfo = Feedinfo::staticGet('feeduri', $topic); - if (!$feedinfo) { - common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); - throw new ServerException("Bogus hub callback: unknown feed", 404); - } - - # Can't currently set the token in our sub api - #if ($feedinfo->verify_token !== $verify_token) { - # common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); - # throw new ServerError("Bogus hub callback: bad token", 404); - #} - - // OK! - common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); - $feedinfo->sub_start = common_sql_date(time()); - if ($lease_seconds > 0) { - $feedinfo->sub_end = common_sql_date(time() + $lease_seconds); - } else { - $feedinfo->sub_end = null; - } - $feedinfo->update(); - - print $challenge; - } -} diff --git a/plugins/FeedSub/actions/feedsubsettings.php b/plugins/FeedSub/actions/feedsubsettings.php deleted file mode 100644 index 0fba20a39..000000000 --- a/plugins/FeedSub/actions/feedsubsettings.php +++ /dev/null @@ -1,255 +0,0 @@ -. - */ - -/** - * @package FeedSubPlugin - * @maintainer Brion Vibber - */ - -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->feedinfo = $this->munger->feedInfo(); - - if ($this->feedinfo->huburi == '') { - $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); - return false; - } - - return true; - } - - function saveFeed() - { - if ($this->validateFeed()) { - $this->preview = true; - $this->feedinfo = Feedinfo::ensureProfile($this->munger); - - // If not already in use, subscribe to updates via the hub - if ($this->feedinfo->sub_start) { - common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); - } else { - $ok = $this->feedinfo->subscribe(); - common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); - if (!$ok) { - $this->showForm(_m('Feed subscription failed! Bad response from hub.')); - return; - } - } - - // And subscribe the current user to the local profile - $user = common_current_user(); - $profile = $this->feedinfo->getProfile(); - - if ($user->isSubscribed($profile)) { - $this->showForm(_m('Already subscribed!')); - } elseif ($user->subscribeTo($profile)) { - $this->showForm(_m('Feed subscribed!')); - } else { - $this->showForm(_m('Feed subscription failed!')); - } - } - } - - function validateAndPreview() - { - if ($this->validateFeed()) { - $this->preview = true; - $this->showForm(_m('Previewing feed:')); - } - } - - function previewFeed() - { - $feedinfo = $this->munger->feedinfo(); - $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/FeedSub/extlib/README b/plugins/FeedSub/extlib/README deleted file mode 100644 index 799b40c47..000000000 --- a/plugins/FeedSub/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 - 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/FeedSub/extlib/XML/Feed/Parser.php b/plugins/FeedSub/extlib/XML/Feed/Parser.php deleted file mode 100755 index ffe8220a5..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser.php +++ /dev/null @@ -1,351 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/Atom.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php deleted file mode 100644 index c7e218a1e..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php +++ /dev/null @@ -1,365 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/AtomElement.php b/plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php deleted file mode 100755 index 063ecb617..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php +++ /dev/null @@ -1,261 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/Exception.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php deleted file mode 100755 index 1e76e3f85..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/RSS09.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php deleted file mode 100755 index 07f38f911..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php +++ /dev/null @@ -1,214 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php deleted file mode 100755 index d41f36e8d..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php +++ /dev/null @@ -1,62 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/RSS1.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php deleted file mode 100755 index 60c9938ba..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php +++ /dev/null @@ -1,277 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/RSS11.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php deleted file mode 100755 index 3cd1ef15d..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php +++ /dev/null @@ -1,276 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php deleted file mode 100755 index 75918beda..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php +++ /dev/null @@ -1,151 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php deleted file mode 100755 index 8e36d5a9b..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php +++ /dev/null @@ -1,116 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/RSS2.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php deleted file mode 100644 index 0936bd2f5..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php +++ /dev/null @@ -1,335 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php deleted file mode 100755 index 6edf910dc..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php +++ /dev/null @@ -1,171 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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/FeedSub/extlib/XML/Feed/Parser/Type.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Type.php deleted file mode 100644 index 75052619b..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/Parser/Type.php +++ /dev/null @@ -1,467 +0,0 @@ - - * @copyright 2005 James Stewart - * @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 - * @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: - * PEAR and: - * - * 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 .= '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/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml deleted file mode 100755 index 02e1c5800..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - Atom draft-07 snapshot - - - tag:example.org,2003:3.2397 - 2005-07-10T12:29:29Z - 2003-12-13T08:29:29-04:00 - - Mark Pilgrim - http://example.org/ - f8dy@example.com - - - Sam Ruby - - - Joe Gregorio - - -
-

[Update: The Atom draft is finished.]

-
-
-
\ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml deleted file mode 100755 index d181d2b6f..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - Example Feed - - 2003-12-13T18:30:02Z - - John Doe - - urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6 - - - Atom-Powered Robots Run Amok - - urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a - 2003-12-13T18:30:02Z - Some text. - - - \ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml deleted file mode 100755 index 98abf9d54..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - dive into mark - - A <em>lot</em> of effort - went into making this effortless - - 2005-07-31T12:29:29Z - tag:example.org,2003:3 - - - Copyright (c) 2003, Mark Pilgrim - - Example Toolkit - - - Atom draft-07 snapshot - - - tag:example.org,2003:3.2397 - 2005-07-31T12:29:29Z - 2003-12-13T08:29:29-04:00 - - Mark Pilgrim - http://example.org/ - f8dy@example.com - - - Sam Ruby - - - Joe Gregorio - - -
-

[Update: The Atom draft is finished.]

-
-
-
-
\ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed b/plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed deleted file mode 100755 index 32f9fa493..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed +++ /dev/null @@ -1,177 +0,0 @@ - - - - -del.icio.us/tag/greenbelt -http://del.icio.us/tag/greenbelt -Text - - - - - - - - - - - - - - - - -Greenbelt - Homepage Section -http://www.greenbelt.org.uk/ -jonnybaker -2005-05-16T16:30:38Z -greenbelt - - - - - - - - -Greenbelt festival (uk) -http://www.greenbelt.org.uk/ -sssshhhh -2005-05-14T18:19:40Z -audiology festival gigs greenbelt - - - - - - - - - - - -Natuerlichwien.at - Rundumadum -http://www.natuerlichwien.at/rundumadum/dergruenguertel/ -egmilman47 -2005-05-06T21:33:41Z -Austria Vienna Wien greenbelt nature walking - - - - - - - - - - - - - -Tank - GBMediaWiki -http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars -jystewart -2005-03-21T22:44:11Z -greenbelt - - - - - - - - -Greenbelt homepage -http://www.greenbelt.ca/home.htm -Gooberoo -2005-03-01T22:43:17Z -greenbelt ontario - - - - - - - - - -Pip Wilson bhp ...... blog -http://pipwilsonbhp.blogspot.com/ -sssshhhh -2004-12-27T11:20:51Z -Greenbelt friend ideas links thinking weblog - - - - - - - - - - - - - -maggi dawn -http://maggidawn.typepad.com/maggidawn/ -sssshhhh -2004-12-27T11:20:11Z -Greenbelt ideas links thinking weblog - - - - - - - - - - - - -John Davies -http://www.johndavies.org/ -sssshhhh -2004-12-27T11:18:37Z -Greenbelt ideas links thinking weblog - - - - - - - - - - - - -jonnybaker -http://jonnybaker.blogs.com/ -sssshhhh -2004-12-27T11:18:17Z -Greenbelt event ideas links resources thinking weblog youth - - - - - - - - - - - - - - - diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed b/plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed deleted file mode 100755 index 57e83af57..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed +++ /dev/null @@ -1,184 +0,0 @@ - - - - jamesstewart - Everyone's Tagged Photos - - - A feed of jamesstewart - Everyone's Tagged Photos - 2005-08-01T18:50:26Z - Flickr - - - Oma and James - - - tag:flickr.com,2004:/photo/30367516 - 2005-08-01T18:50:26Z - 2005-08-01T18:50:26Z - <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> - - kstewart - http://www.flickr.com/people/30484029@N00/ - - jamesstewart oma stoelfamily - - - - - tag:flickr.com,2004:/photo/21376174 - 2005-06-25T02:00:35Z - 2005-06-25T02:00:35Z - <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> - - Lan Rover - http://www.flickr.com/people/buddscreek/ - - amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational rickycarmichael 259 jamesstewart 4 - - - - - tag:flickr.com,2004:/photo/21375650 - 2005-06-25T01:56:24Z - 2005-06-25T01:56:24Z - <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> - - - - Lan Rover - http://www.flickr.com/people/buddscreek/ - - amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart - - - - - tag:flickr.com,2004:/photo/21375345 - 2005-06-25T01:54:11Z - 2005-06-25T01:54:11Z - <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> - - - - Lan Rover - http://www.flickr.com/people/buddscreek/ - - amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart - - - Lunch with Kari & James, café in the crypt of St Martin in the fields - - tag:flickr.com,2004:/photo/16516618 - 2005-05-30T21:56:39Z - 2005-05-30T21:56:39Z - <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> - - - - fidothe - http://www.flickr.com/people/fidothe/ - - nokia7610 london stmartininthefields clarepatterson jamesstewart parvinstewart jimstewart susanstewart - - - Stewart keeping it low over the obstacle. - - tag:flickr.com,2004:/photo/10224728 - 2005-04-21T07:30:29Z - 2005-04-21T07:30:29Z - <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> - - - - pqbon - http://www.flickr.com/people/pqbon/ - - ama hangtown motocross jamesstewart bubba - - - king james stewart - - tag:flickr.com,2004:/photo/7152910 - 2005-03-22T21:53:37Z - 2005-03-22T21:53:37Z - <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> - - jj look - http://www.flickr.com/people/jjlook/ - - dilomar05 eastside austin texas 78702 kingjames stewart jamesstewart borrowed - - - It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson) - - tag:flickr.com,2004:/photo/1586562 - 2004-11-20T09:34:28Z - 2004-11-20T09:34:28Z - <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> - - - - fidothe - http://www.flickr.com/people/fidothe/ - - holiday grandrapids jamesstewart - - - It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson) - - tag:flickr.com,2004:/photo/1586539 - 2004-11-20T09:28:16Z - 2004-11-20T09:28:16Z - <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> - - - - fidothe - http://www.flickr.com/people/fidothe/ - - holiday grandrapids jamesstewart - - - It's a Grind, James and Jim can't decide) - - tag:flickr.com,2004:/photo/1586514 - 2004-11-20T09:25:05Z - 2004-11-20T09:25:05Z - <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> - - - - fidothe - http://www.flickr.com/people/fidothe/ - - holiday grandrapids jamesstewart johnkentish - - - \ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml b/plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml deleted file mode 100755 index c351d3c16..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml +++ /dev/null @@ -1,7 +0,0 @@ - Updates to Grand Rapids WiFi hotspot details 2005-09-01T15:43:01-05:00 WiFi Hotspots in Grand Rapids, MI http://grwifi.net/atom/locations Creative Commons Attribution-NonCommercial-ShareAlike 2.0 http://creativecommons.org/licenses/by-nc-sa/2.0/ Hotspot Details Updated: Sweetwaters http://grwifi.net/location/sweetwaters 2005-09-01T15:43:01-05:00 The details of the WiFi hotspot at: Sweetwaters have been updated. Find out more at: -http://grwifi.net/location/sweetwaters James http://jystewart.net james@jystewart.net wifi hotspot Hotspot Details Updated: Common Ground Coffee Shop http://grwifi.net/location/common-ground 2005-09-01T15:42:39-05:00 The details of the WiFi hotspot at: Common Ground Coffee Shop have been updated. Find out more at: -http://grwifi.net/location/common-ground James http://jystewart.net james@jystewart.net wifi hotspot Hotspot Details Updated: Grand Rapids Public Library, Main Branch http://grwifi.net/location/grpl-main-branch 2005-09-01T15:42:20-05:00 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 James http://jystewart.net james@jystewart.net wifi hotspot Hotspot Details Updated: Four Friends Coffee House http://grwifi.net/location/four-friends 2005-09-01T15:41:35-05:00 The details of the WiFi hotspot at: Four Friends Coffee House have been updated. Find out more at: -http://grwifi.net/location/four-friends James http://jystewart.net james@jystewart.net wifi hotspot Hotspot Details Updated: Barnes and Noble, Rivertown Crossings http://grwifi.net/location/barnes-noble-rivertown 2005-09-01T15:40:41-05:00 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 James http://jystewart.net james@jystewart.net wifi hotspot Hotspot Details Updated: The Boss Sports Bar & Grille http://grwifi.net/location/boss-sports-bar 2005-09-01T15:40:19-05:00 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 James http://jystewart.net james@jystewart.net wifi hotspot \ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml b/plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml deleted file mode 100755 index 099463570..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - -Editor: Myself (Persian) -http://editormyself.info -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. -en-us -hoder@hotmail.com -2005-10-12T19:45:32-05:00 - -hourly -1 -2000-01-01T12:00+00:00 - - - -لينکدونی‌ | جلسه‌ی امریکن انترپرایز برای تقسیم قومی ایران -http://www.aei.org/events/type.upcoming,eventID.1166,filter.all/event_detail.asp -چطور بعضی‌ها فکر می‌کنند دست راستی‌های آمریکا از خامنه‌ای ملی‌گراترند -14645@http://i.hoder.com/ -iran -2005-10-12T19:45:32-05:00 - - - -لينکدونی‌ | به صبحانه آگهی بدهید -http://www.adbrite.com/mb/commerce/purchase_form.php?opid=24346&afsid=1 -خیلی ارزان و راحت است -14644@http://i.hoder.com/ -media/journalism -2005-10-12T17:23:15-05:00 - - - -لينکدونی‌ | نیروی انتظامی چگونه تابوهای هم‌جنس‌گرایانه را می‌شکند؛ فرنگوپولیس -http://farangeopolis.blogspot.com/2005/10/blog-post_08.html -از پس و پیش و حاشیه‌ی این ماجرا می‌توان یک مستند بی‌نظیر ساخت -14643@http://i.hoder.com/ -soc_popculture -2005-10-12T17:06:40-05:00 - - - -لينکدونی‌ | بازتاب توقیف شد -http://www.baztab.com/news/30201.php -اگر گفتید یک وب‌سایت را چطور توقیف می‌کنند؟ لابد ماوس‌شان را قایم می‌کنند. -14642@http://i.hoder.com/ -media/journalism -2005-10-12T14:41:57-05:00 - - - -لينکدونی‌ | رشد وب در سال 2005 از همیشه بیشتر بوده است" بی.بی.سی -http://news.bbc.co.uk/2/hi/technology/4325918.stm - -14640@http://i.hoder.com/ -tech -2005-10-12T13:04:46-05:00 - - - - - -==قرعه کشی گرین کارد به زودی شروع می‌شود== -http://nice.newsxphotos.biz/05/09/2007_dv_lottery_registration_to_begin_oct_5_14589.php - -14613@http://vagrantly.com -ads03 -2005-09-27T04:49:22-05:00 - - - - - - - - -پروژه‌ی هاروارد، قدم دوم -http://editormyself.info/archives/2005/10/051012_014641.shtml -اگر یادتان باشد چند وقت پیش نوشتم که دانشگاه هاروارد پروژه‌ای دارد با نام آواهای جهانی که در آن به وبلاگ‌های غیر انگلیسی‌زبان می‌پردازد. خواشتم که اگر کسی علاقه دارد ایمیل بزند. تعداد زیادی جواب دادند و ابراز علاقه کردند. حالا وقت قدم دوم است.

- -

قدم دوم این است که برای اینکه مسوولین پروژه بتوانند تصمیم بگیرند که با چه کسی کار کنند، می‌خواهند نمونه‌ی کارهای علاقمندان مشارکت در این پرزو‌ه را ببینند.

- -

برای همین از همه‌ی علاقماندان، حتی کسانی که قبلا اعلام آمادگی نکرده بودند، می‌‌خواهم که یک موضوع رایج این روزهای وبلاگستان فارسی را انتخاب کنند و در هفتصد کلمه، به انگلیسی، بنویسند که وبلاگ‌دارهای درباره‌اش چه می‌گویند. لینک به پنج، شش وبلاگ و بازنویسی آنچه آنها از جنبه‌های گوناگون درباره‌ی آن موضوع نوشته‌اند با نقل قول مستقیم از آنها (البته ترجمه شده از فارسی) کافی است. دو سه جمله هم اول کار توضیح دهید که چرا این موضوع مهم است.

- -

متن نمونه را به آدرس ایمیل من hoder@hoder.com و نیز برای افراد زیر تا روز دوشنبه بفرستید:
-ربکا : rmackinnon@cyber.law.harvard.edu
-هیثم: haitham.sabbah@gmail.com

]]>
-14641@http://editormyself.info -weblog -2005-10-12T14:04:23-05:00 -
- - - -
-
\ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml b/plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml deleted file mode 100755 index 612186897..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Example author - me@example.com - http://example.com/ - - - - - - -Copyright 1997-1999 UserLand Software, Inc. -Thu, 08 Jul 1999 07:00:00 GMT -Thu, 08 Jul 1999 16:20:26 GMT -http://my.userland.com/stories/storyReader$11 -News and commentary from the cross-platform scripting community. -http://www.scripting.com/ -Scripting News - -http://www.scripting.com/ -Scripting News -http://www.scripting.com/gifs/tinyScriptingNews.gif -40 -78 -What is this used for? - -dave@userland.com (Dave Winer) -dave@userland.com (Dave Winer) -en-us - -6 -7 -8 -9 -10 -11 - - -Sunday - -(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)) - -stuff -http://bar -This is an article about some stuff - - -Search Now! -Enter your search <terms> -find -http://my.site.com/search.cgi - - - \ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml deleted file mode 100755 index cfe91691f..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - -膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ -http://www.mozilla.org -膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ -ja - -NYÒ™Á¢¸»ÌêÛì15285.25´ƒ´‘Á£´Û´—´ÀÁ¹´ê´Ì´éÒ™Ûì¡êçÒÕ‰ÌêÁ£ -http://www.mozilla.org/status/ -This is an item description... - - -‚§±Çç¡ËßÛÂҏéøÓ¸Á£Ë²®Ÿè†Ûè危ÇÌ’¡Íæ—éøë‡Á£ -http://www.mozilla.org/status/ -This is an item description... - - -ËÜË”ïÌëÈšÁ¢È†Ë§æàÀ豎ˉۂÁ¢Ë‚åܼšÛ˜íËüËÁ£ -http://www.mozilla.org/status/ -This is an item description... - - -2000‚øíŠåÁ¢«‘¦éÛ빏ېçéÛ§ÛÂè†ÒæÓ¸Á£Ì¾«…æ—ÕÝéøƒ¸Á£ -http://www.mozilla.org/status/ -This is an item description... - - - \ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml deleted file mode 100755 index f0964a227..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - -en -News and commentary from the cross-platform scripting community. -http://www.scripting.com/ -Scripting News - -http://www.scripting.com/ -Scripting News -http://www.scripting.com/gifs/tinyScriptingNews.gif - - - \ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml deleted file mode 100755 index 5d75c352b..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - Dave Winer: Grateful Dead - http://www.scripting.com/blog/categories/gratefulDead.html - 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. - Fri, 13 Apr 2001 19:23:02 GMT - http://backend.userland.com/rss092 - dave@userland.com (Dave Winer) - dave@userland.com (Dave Winer) - - - 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> - - - - - 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> - Scripting News - - - <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. - - - - This is a test of a change I just made. Still diggin.. - - - 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. - - - <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. - - - - 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.." - Scripting News - - - <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> - - - - - <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." - - - - <a href="http://arts.ucsc.edu/gdead/agdl/darkstar.html">Dark Star</a> crashes, pouring its light into ashes. - - - - DaveNet: <a href="http://davenet.userland.com/2001/01/21/theUsBlues">The U.S. Blues</a>. - - - 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> - - - <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> - - - - Grateful Dead: "Tennessee, Tennessee, ain't no place I'd rather be." - - - - 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." - - - - <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." - - - - <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." - - - - <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." - - - - - 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. - - - - 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." - - - A touch of gray, kinda suits you anyway.. - - - - <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> - - - - \ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml deleted file mode 100755 index 0edecf58e..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - XML.com - http://xml.com/pub - - XML.com features a rich mix of information and services - for the XML community. - - - - - - - - - - - - - - - - - XML.com - http://www.xml.com - http://xml.com/universal/images/xml_tiny.gif - - - - Processing Inclusions with XSLT - http://xml.com/pub/2000/08/09/xslt/xslt.html - - Processing document inclusions with general XML tools can be - problematic. This article proposes a way of preserving inclusion - information through SAX-based processing. - - - - - Putting RDF to Work - http://xml.com/pub/2000/08/09/rdfdb/index.html - - 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. - - - - - Search XML.com - Search XML.com's XML collection - s - http://search.xml.com - - - \ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml deleted file mode 100755 index 26235f78f..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - Meerkat - http://meerkat.oreillynet.com - Meerkat: An Open Wire Service - The O'Reilly Network - Rael Dornfest (mailto:rael@oreilly.com) - Copyright © 2000 O'Reilly & Associates, Inc. - 2000-01-01T12:00+00:00 - hourly - 2 - 2000-01-01T12:00+00:00 - - - - - - - - - - - - - - - Meerkat Powered! - http://meerkat.oreillynet.com/icons/meerkat-powered.jpg - http://meerkat.oreillynet.com - - - - XML: A Disruptive Technology - http://c.moreover.com/click/here.pl?r123 - - XML is placing increasingly heavy loads on the existing technical - infrastructure of the Internet. - - The O'Reilly Network - Simon St.Laurent (mailto:simonstl@simonstl.com) - Copyright © 2000 O'Reilly & Associates, Inc. - XML - XML.com - NASDAQ - XML - - - - Search Meerkat - Search Meerkat's RSS Database... - s - http://meerkat.oreillynet.com/ - search - regex - - - \ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml deleted file mode 100755 index 53483cc51..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - Liftoff News - http://liftoff.msfc.nasa.gov/ - Liftoff to Space Exploration. - en-us - Tue, 10 Jun 2003 04:00:00 GMT - Tue, 10 Jun 2003 09:41:01 GMT - http://blogs.law.harvard.edu/tech/rss - Weblog Editor 2.0 - editor@example.com - webmaster@example.com - - Star City - http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp - 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>. - Tue, 03 Jun 2003 09:39:21 GMT - http://liftoff.msfc.nasa.gov/2003/06/03.html#item573 - - - 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. - Fri, 30 May 2003 11:06:42 GMT - http://liftoff.msfc.nasa.gov/2003/05/30.html#item572 - - - The Engine That Does More - http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp - 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. - Tue, 27 May 2003 08:37:32 GMT - http://liftoff.msfc.nasa.gov/2003/05/27.html#item571 - Test content

]]>
-
- - Astronauts' Dirty Laundry - http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp - Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options. - Tue, 20 May 2003 08:56:02 GMT - http://liftoff.msfc.nasa.gov/2003/05/20.html#item570 - -
-
\ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml b/plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml deleted file mode 100755 index f8a04bba5..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml +++ /dev/null @@ -1,226 +0,0 @@ - - - -Six Apart - News -http://www.sixapart.jp/ - -ja -Copyright 2005 -Fri, 07 Oct 2005 19:09:34 +0900 -http://www.movabletype.org/?v=3.2-ja -http://blogs.law.harvard.edu/tech/rss - - -ファイブ・ディーが、Movable Typeでブログプロモーションをスタート -MIYAZAWAblog_banner.jpg
-ファイブ・ディーは、Movable Typeで構築したプロモーション ブログ『宮沢和史 中南米ツアーblog Latin America 2005』を開設しました。

- -

9月21日に開設されたこのブログは、ブラジル、ホンジュラス、ニカラグア、メキシコ、キューバの5か国を巡る「Latin America 2005」ツアーに合わせ、そのツアーの模様を同行マネージャーがレポートしていきます。
-さらに今月2日からは宮沢和史自身が日々録音した声をPodcastingするという点でも、ブログを使ったユニークなプロモーションとなっています。

- -

「宮沢和史 中南米ツアーblog Latin America 2005」

- -

※シックス・アパートではこうしたブログを使ったプロモーションに最適な製品をご用意しております。
-

]]>
-http://www.sixapart.jp/news/2005/10/07-1909.html -http://www.sixapart.jp/news/2005/10/07-1909.html -news -Fri, 07 Oct 2005 19:09:34 +0900 -
- -Movable Type 3.2日本語版の提供を開始 -Movable Type Logo

-

シックス・アパートは、Movable Type 3.2日本語版の提供を開始いたしました。
-ベータテストにご協力いただいた多くの皆様に、スタッフ一同、心から感謝いたします。

-

製品概要など、詳しくはプレスリリースをご参照下さい。

-

ご購入のご検討は、Movable Typeのご購入からどうぞ。

]]>
-http://www.sixapart.jp/news/2005/09/29-1530.html -http://www.sixapart.jp/news/2005/09/29-1530.html -news -Thu, 29 Sep 2005 15:30:00 +0900 -
- -シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始 -<プレスリリース資料>

- -

シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始 ~ スパムの自動判別機能や新ユーザー・インターフェースで、運用管理の機能を強化 ~

-

2005年9月29日
-シックス・アパート株式会社

-

ブログ・ソフトウェア大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、「Movable Type(ムーバブル・タイプ) 3.2 日本語版」(URL:http://www.sixapart.jp/movabletype/)を9月29日より提供開始いたします。

]]>
-http://www.sixapart.jp/press_releases/2005/09/29-1529.html -http://www.sixapart.jp/press_releases/2005/09/29-1529.html -Press Releases -Thu, 29 Sep 2005 15:29:00 +0900 -
- -スタッフを募集しています -シックス・アパートはMovable TypeやTypePadの開発エンジニアなど、スタッフを広く募集しています。具体的な募集職種は次の通りです。

- - - -

拡大を続ける、日本のブログ市場を積極的にリードする人材を、シックス・アパートは募集しています。上記以外の職種につきましても、お気軽にお問い合わせください。詳しい募集要項や応募方法については、求人情報のページをご覧ください。
-

]]>
-http://www.sixapart.jp/news/2005/09/27-0906.html -http://www.sixapart.jp/news/2005/09/27-0906.html -news -Tue, 27 Sep 2005 09:06:10 +0900 -
- -サイト接続不具合に関するお詫びと復旧のお知らせ -9月24日(土)の14:45ごろから、同日18:30ごろまで、シックス・アパート社のウェブサイトが不安定になっており、断続的に接続できない不具合が発生しておりました。このため、この期間中にウェブサイトの閲覧や製品のダウンロードができませんでした。

- -

なお現在は不具合は解消しております。みなさまにご迷惑をおかけしたことをお詫びいたします。

]]>
-http://www.sixapart.jp/news/2005/09/26-1000.html -http://www.sixapart.jp/news/2005/09/26-1000.html -news -Mon, 26 Sep 2005 10:00:56 +0900 -
- -企業ブログ向けパッケージ「TypePad Promotion」を新発売 -シックス・アパートは、ウェブログ・サービスTypePadの企業ブログ向けパッケージ「TypePad Promotion」(タイプパッド・プロモーションの発売を10月下旬から開始いたします。

- -

詳しくは、プレスリリースをご参照下さい。

]]>
-http://www.sixapart.jp/news/2005/09/20-1500.html -http://www.sixapart.jp/news/2005/09/20-1500.html -news -Tue, 20 Sep 2005 15:00:01 +0900 -
- -シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売 -<プレスリリース資料>
-印刷用(PDF版)

- -


-シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売
-~PR/IRサイトやキャンペーンサイトなど企業のプロモーションニーズに特化~
-

-2005年9月20日
-シックス・アパート株式会社

- -

ブログ・サービス大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、法人向けプロモーションブログ・パッケージ「TypePad Promotion(タイプパッド・プロモーション)」(URL:http://www.sixapart.jp/typepad/typepad_promotion.html)を10月下旬より販売開始いたします。

]]>
-http://www.sixapart.jp/press_releases/2005/09/20-1500.html -http://www.sixapart.jp/press_releases/2005/09/20-1500.html -Press Releases -Tue, 20 Sep 2005 15:00:00 +0900 -
- -Six [days] Apart Week -本日、9月16日はSix Apartの創業者ミナ・トロットの誕生日です。
-私たちの会社は、創業者のトロット夫妻(ベンとミナ)の誕生日が、6日離れていることからSix [days] Apart →Six Apartという風に名付けられています。本日から22日までの6日間を社名の由来となる Six [days] Apart Weekとして、私たちのプロダクトをご紹介させていただきます。

- -

今日は、ブログ・サービスのTypePad(タイプパッド)をご紹介します。
-tp-logo.gif

- -

TypePadは、米国PC MAGAZINE誌の2003年EDITOR'S CHOICE とBEST OF THE YEARに選ばれております。
-pcmag-ad.gif
-

]]>
-http://www.sixapart.jp/news/2005/09/16-1941.html -http://www.sixapart.jp/news/2005/09/16-1941.html -news -Fri, 16 Sep 2005 19:41:47 +0900 -
- -ハイパーワークスが商用フォントを利用できるMovable Typeホスティングサービスを開始 -ソフト開発会社の有限会社ハイパーワークスは、商用フォントなど多彩なフォントをブログ上で利用できるブログ・サービス「Glyph-On!(グリフォン) Movable Type ホスティング サービス」の提供を開始しました。
-

]]>
-http://www.sixapart.jp/news/2005/09/14-1700.html -http://www.sixapart.jp/news/2005/09/14-1700.html -news -Wed, 14 Sep 2005 17:00:00 +0900 -
- -Movable Type開発エンジニアの募集 - -勤務形態: フルタイム
-勤務地: 東京 (赤坂)
-職種: ソフトウェア・エンジニア
-職務内容: Movable Typeの開発業務全般
-募集人数: 若干名 -

]]>
-http://www.sixapart.jp/jobs/2005/09/13-0007.html -http://www.sixapart.jp/jobs/2005/09/13-0007.html -Jobs -Tue, 13 Sep 2005 00:07:00 +0900 -
- -TypePad開発エンジニアの募集 - -勤務形態: フルタイム
-勤務地: 東京 (赤坂)
-職種: アプリケーション・エンジニア
-職務内容: TypePadのカスタマイズ、周辺開発
-募集人数: 若干名 -

]]>
-http://www.sixapart.jp/jobs/2005/09/13-0004.html -http://www.sixapart.jp/jobs/2005/09/13-0004.html -Jobs -Tue, 13 Sep 2005 00:04:00 +0900 -
- -カスタマーサポート・ディレクターの募集 -勤務形態: フルタイム
-勤務地: 東京(赤坂)
-職種: カスタマーサポート・ディレクター
-職務内容: TypePadやMovable Typeのカスタマーサポート業務の統括
-募集人数: 若干名 -

-]]>
-http://www.sixapart.jp/jobs/2005/09/13-0003.html -http://www.sixapart.jp/jobs/2005/09/13-0003.html -Jobs -Tue, 13 Sep 2005 00:03:30 +0900 -
- -アルバイト(マーケティング・広報アシスタント)の募集 -勤務形態: アルバイト
-勤務地: 東京(港区)
-職種:マーケティング・PRのアシスタント業務
-募集人数: 若干名
-時給:1000円~(但し、試用期間終了後に応相談)。交通費支給
-時間:平日10時30分~18時30分まで。週3日以上(応相談)
-

]]>
-http://www.sixapart.jp/jobs/2005/09/13-0002.html -http://www.sixapart.jp/jobs/2005/09/13-0002.html -Jobs -Tue, 13 Sep 2005 00:02:00 +0900 -
- -アルバイト(開発アシスタント)の募集 -勤務形態: アルバイト
-勤務地: 東京(港区)
-職種: アプリケーション開発のアシスタント業務
-募集人数: 若干名
-時給:1000円~(但し、試用期間終了後に応相談)。交通費支給
-時間:平日10時30分~18時30分まで。週3日以上(応相談) -

]]>
-http://www.sixapart.jp/jobs/2005/09/13-0001.html -http://www.sixapart.jp/jobs/2005/09/13-0001.html -Jobs -Tue, 13 Sep 2005 00:01:00 +0900 -
- -TypePad Japan がバージョンアップしました。 -「TypePad Japan(タイプパッドジャパン)」において、本日、「TypePad 1.6 日本語版」へのバージョンアップを行いました。最新版となる「TypePad 1.6 日本語版」では、ブログデザインの機能強化、ポッドキャスティング対応、モブログ対応に加え、今回新たに大幅な容量アップが行われております。皆様、新しくなったTypePad Japanにどうぞご期待ください。

- -

なお、TypePadの携帯対応強化に関しましては、本日よりTypePad Japanのお客様を対象にオープン・ベータを開始しております。

- -

2005年9月5日発表のTypePad日本語版 1.6プレスリリースはこちらをご覧下さい。

]]>
-http://www.sixapart.jp/news/2005/09/12-1953.html -http://www.sixapart.jp/news/2005/09/12-1953.html -news -Mon, 12 Sep 2005 19:53:07 +0900 -
- - -
-
\ No newline at end of file diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed b/plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed deleted file mode 100755 index 6274a32cd..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed +++ /dev/null @@ -1,54 +0,0 @@ - - - - [Technorati] Tag results for greenbelt - http://www.technorati.com/tag/greenbelt - Posts tagged with "greenbelt" on Technorati. - Mon, 08 Aug 2005 15:15:08 GMT - greenbelt - 2 - 2 - - Technorati v1.0 - - http://static.technorati.com/pix/logos/logo_reverse_sm.gif - Technorati logo - http://www.technorati.com - - - 1 - 7 - 9 - - support@technorati.com (Technorati Support) - http://blogs.law.harvad.edu/tech/rss - 60 - - Greenbelt - http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html - So if the plan goes according to plan (!)... I'll be speaking at Greenbelt at these times: Slot 1... - http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html - Mon, 18 Jul 2005 02:11:42 GMT - James - 2005-07-11 02:08:12 - http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fmaggidawn.typepad.com%2Fmaggidawn%2F2005%2F07%2Fgreenbelt.html - 190 - 237 - maggi dawn - - - - Walking along the Greenbelt - http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html - [IMG] Photo of homeless man walking near the greenbelt in Boise, Idaho Tags: photo homeless greenbelt Boise Idaho picture - http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html - Tue, 28 Jun 2005 01:41:24 GMT - 2005-06-26 17:24:03 - http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fpictureshomeless.blogspot.com%2F2005%2F06%2Fwalking-along-greenbelt.html - 2 - 2 - - - - diff --git a/plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc b/plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc deleted file mode 100755 index e662d2626..000000000 --- a/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/schemas/rss10.rnc b/plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc deleted file mode 100755 index 725094788..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc b/plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc deleted file mode 100755 index c8633766f..000000000 --- a/plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - - - http://purl.org/net/rss1.1#Channel - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - http://purl.org/net/rss1.1#title - - - - - - - - - - - - - - http://purl.org/net/rss1.1#link - - - - - - - - - - http://purl.org/net/rss1.1#description - - - - - - - - - - - - - http://purl.org/net/rss1.1#image - - - - - - - - - - - - - - - - - - - - - - - - - http://purl.org/net/rss1.1#url - - - - - - - - - - http://purl.org/net/rss1.1#items - - - - - - - - - - - - - - - - - http://purl.org/net/rss1.1#item - - - - - - - - - - - - - - - - - - - - - - - - - - - - http://purl.org/net/rss1.1#Any - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Resource - - - - - Collection - - - - diff --git a/plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch b/plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch deleted file mode 100644 index c53bd9737..000000000 --- a/plugins/FeedSub/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/FeedSub/feeddiscovery.php b/plugins/FeedSub/feeddiscovery.php deleted file mode 100644 index 35edaca33..000000000 --- a/plugins/FeedSub/feeddiscovery.php +++ /dev/null @@ -1,209 +0,0 @@ -. - */ - -/** - * @package FeedSubPlugin - * @maintainer Brion Vibber - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - -class FeedSubBadURLException extends FeedSubException -{ -} - -class FeedSubBadResponseException extends FeedSubException -{ -} - -class FeedSubEmptyException extends FeedSubException -{ -} - -class FeedSubBadHTMLException extends FeedSubException -{ -} - -class FeedSubUnrecognizedTypeException extends FeedSubException -{ -} - -class FeedSubNoFeedException extends FeedSubException -{ -} - -class FeedDiscovery -{ - public $uri; - public $type; - public $body; - - - public function feedMunger() - { - require_once 'XML/Feed/Parser.php'; - $feed = new XML_Feed_Parser($this->body, false, false, true); // @fixme - return new FeedMunger($feed, $this->uri); - } - - /** - * @param string $url - * @param bool $htmlOk - * @return string with validated URL - * @throws FeedSubBadURLException - * @throws FeedSubBadHtmlException - * @throws FeedSubNoFeedException - * @throws FeedSubEmptyException - * @throws FeedSubUnrecognizedTypeException - */ - function discoverFromURL($url, $htmlOk=true) - { - try { - $client = new HTTPClient(); - $response = $client->get($url); - } catch (HTTP_Request2_Exception $e) { - throw new FeedSubBadURLException($e); - } - - if ($htmlOk) { - $type = $response->getHeader('Content-Type'); - $isHtml = preg_match('!^(text/html|application/xhtml\+xml)!i', $type); - if ($isHtml) { - $target = $this->discoverFromHTML($response->getUrl(), $response->getBody()); - if (!$target) { - throw new FeedSubNoFeedException($url); - } - return $this->discoverFromURL($target, false); - } - } - - return $this->initFromResponse($response); - } - - function initFromResponse($response) - { - if (!$response->isOk()) { - throw new FeedSubBadResponseException($response->getCode()); - } - - $sourceurl = $response->getUrl(); - $body = $response->getBody(); - if (!$body) { - throw new FeedSubEmptyException($sourceurl); - } - - $type = $response->getHeader('Content-Type'); - if (preg_match('!^(text/xml|application/xml|application/(rss|atom)\+xml)!i', $type)) { - $this->uri = $sourceurl; - $this->type = $type; - $this->body = $body; - return true; - } else { - common_log(LOG_WARNING, "Unrecognized feed type $type for $sourceurl"); - throw new FeedSubUnrecognizedTypeException($type); - } - } - - /** - * @param string $url source URL, used to resolve relative links - * @param string $body HTML body text - * @return mixed string with URL or false if no target found - */ - function discoverFromHTML($url, $body) - { - // DOMDocument::loadHTML may throw warnings on unrecognized elements. - $old = error_reporting(error_reporting() & ~E_WARNING); - $dom = new DOMDocument(); - $ok = $dom->loadHTML($body); - error_reporting($old); - - if (!$ok) { - throw new FeedSubBadHtmlException(); - } - - // Autodiscovery links may be relative to the page's URL or - $base = false; - $nodes = $dom->getElementsByTagName('base'); - for ($i = 0; $i < $nodes->length; $i++) { - $node = $nodes->item($i); - if ($node->hasAttributes()) { - $href = $node->attributes->getNamedItem('href'); - if ($href) { - $base = trim($href->value); - } - } - } - if ($base) { - $base = $this->resolveURI($base, $url); - } else { - $base = $url; - } - - // Ok... now on to the links! - // @fixme merge with the munger link checks - $nodes = $dom->getElementsByTagName('link'); - for ($i = 0; $i < $nodes->length; $i++) { - $node = $nodes->item($i); - if ($node->hasAttributes()) { - $rel = $node->attributes->getNamedItem('rel'); - $type = $node->attributes->getNamedItem('type'); - $href = $node->attributes->getNamedItem('href'); - if ($rel && $type && $href) { - $rel = trim($rel->value); - $type = trim($type->value); - $href = trim($href->value); - - $feedTypes = array( - 'application/rss+xml', - 'application/atom+xml', - ); - if (trim($rel) == 'alternate' && in_array($type, $feedTypes)) { - return $this->resolveURI($href, $base); - } - } - } - } - - return false; - } - - /** - * Resolve a possibly relative URL against some absolute base URL - * @param string $rel relative or absolute URL - * @param string $base absolute URL - * @return string absolute URL, or original URL if could not be resolved. - */ - function resolveURI($rel, $base) - { - require_once "Net/URL2.php"; - try { - $relUrl = new Net_URL2($rel); - if ($relUrl->isAbsolute()) { - return $rel; - } - $baseUrl = new Net_URL2($base); - $absUrl = $baseUrl->resolve($relUrl); - return $absUrl->getURL(); - } catch (Exception $e) { - common_log(LOG_WARNING, 'Unable to resolve relative link "' . - $rel . '" against base "' . $base . '": ' . $e->getMessage()); - return $rel; - } - } -} diff --git a/plugins/FeedSub/feedinfo.php b/plugins/FeedSub/feedinfo.php deleted file mode 100644 index b166bd6e1..000000000 --- a/plugins/FeedSub/feedinfo.php +++ /dev/null @@ -1,268 +0,0 @@ -subscribe() - generate random verification token - save to verify_token - sends a sub request to the hub... - - feedsub/callback - hub sends confirmation back to us via GET - We verify the request, then echo back the challenge. - On our end, we save the time we subscribed and the lease expiration - - feedsub/callback - hub sends us updates via POST - ? - -*/ - -class FeedDBException extends FeedSubException -{ - public $obj; - - function __construct($obj) - { - parent::__construct('Database insert failure'); - $this->obj = $obj; - } -} - -class Feedinfo extends Memcached_DataObject -{ - public $__table = 'feedinfo'; - - public $id; - public $profile_id; - - public $feeduri; - public $homeuri; - public $huburi; - - // PuSH subscription data - public $verify_token; - public $sub_start; - public $sub_end; - - public $created; - public $lastupdate; - - - public /*static*/ function staticGet($k, $v=null) - { - return parent::staticGet(__CLASS__, $k, $v); - } - - /** - * return table definition for DB_DataObject - * - * DB_DataObject needs to know something about the table to manipulate - * instances. This method provides all the DB_DataObject needs to know. - * - * @return array array of column definitions - */ - - function table() - { - return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - 'huburi' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - 'verify_token' => DB_DATAOBJECT_STR, - 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, - 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, - 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, - 'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); - } - - static function schemaDef() - { - return array(new ColumnDef('id', 'integer', - /*size*/ null, - /*nullable*/ false, - /*key*/ 'PRI', - /*default*/ '0', - /*extra*/ null, - /*auto_increment*/ true), - new ColumnDef('profile_id', 'integer', - null, false), - new ColumnDef('feeduri', 'varchar', - 255, false, 'UNI'), - new ColumnDef('homeuri', 'varchar', - 255, false), - new ColumnDef('huburi', 'varchar', - 255, false), - new ColumnDef('verify_token', 'varchar', - 32, true), - new ColumnDef('sub_start', 'datetime', - null, true), - new ColumnDef('sub_end', 'datetime', - null, true), - new ColumnDef('created', 'datetime', - null, false), - new ColumnDef('lastupdate', 'datetime', - null, false)); - } - - /** - * return key definitions for DB_DataObject - * - * DB_DataObject needs to know about keys that the table has; this function - * defines them. - * - * @return array key definitions - */ - - function keys() - { - return array('id' => 'P'); //? - } - - /** - * return key definitions for Memcached_DataObject - * - * Our caching system uses the same key definitions, but uses a different - * method to get them. - * - * @return array key definitions - */ - - function keyTypes() - { - return $this->keys(); - } - - /** - * Fetch the StatusNet-side profile for this feed - * @return Profile - */ - public function getProfile() - { - return Profile::staticGet('id', $this->profile_id); - } - - /** - * @param FeedMunger $munger - * @return Feedinfo - */ - public static function ensureProfile($munger) - { - $feedinfo = $munger->feedinfo(); - - $current = self::staticGet('feeduri', $feedinfo->feeduri); - if ($current) { - // @fixme we should probably update info as necessary - return $current; - } - - $feedinfo->query('BEGIN'); - - try { - $profile = $munger->profile(); - $result = $profile->insert(); - if (empty($result)) { - throw new FeedDBException($profile); - } - - $feedinfo->profile_id = $profile->id; - $result = $feedinfo->insert(); - if (empty($result)) { - throw new FeedDBException($feedinfo); - } - - $feedinfo->query('COMMIT'); - } catch (FeedDBException $e) { - common_log_db_error($e->obj, 'INSERT', __FILE__); - $feedinfo->query('ROLLBACK'); - return false; - } - return $feedinfo; - } - - /** - * Send a subscription request to the hub for this feed. - * The hub will later send us a confirmation POST to /feedsub/callback. - * - * @return bool true on success, false on failure - */ - public function subscribe() - { - // @fixme use the verification token - #$token = md5(mt_rand() . ':' . $this->feeduri); - #$this->verify_token = $token; - #$this->update(); // @fixme - - try { - $callback = common_local_url('feedsubcallback', array('feed' => $this->id)); - $headers = array('Content-Type: application/x-www-form-urlencoded'); - $post = array('hub.mode' => 'subscribe', - 'hub.callback' => $callback, - 'hub.verify' => 'async', - //'hub.verify_token' => $token, - //'hub.lease_seconds' => 0, - 'hub.topic' => $this->feeduri); - $client = new HTTPClient(); - $response = $client->post($this->huburi, $headers, $post); - if ($response->getStatus() >= 200 && $response->getStatus() < 300) { - common_log(LOG_INFO, __METHOD__ . ': sub req ok'); - return true; - } else { - common_log(LOG_INFO, __METHOD__ . ': sub req failed'); - return false; - } - } catch (Exception $e) { - // wtf! - common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri"); - return false; - } - } - - /** - * Read and post notices for updates from the feed. - * Currently assumes that all items in the feed are new, - * coming from a PuSH hub. - * - * @param string $xml source of Atom or RSS feed - */ - public function postUpdates($xml) - { - common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $xml"); - require_once "XML/Feed/Parser.php"; - $feed = new XML_Feed_Parser($xml, false, false, true); - $munger = new FeedMunger($feed); - - $hits = 0; - foreach ($feed as $index => $entry) { - // @fixme this might sort in wrong order if we get multiple updates - - $notice = $munger->notice($index); - $notice->profile_id = $this->profile_id; - - // Double-check for oldies - // @fixme this could explode horribly for multiple feeds on a blog. sigh - $dupe = new Notice(); - $dupe->uri = $notice->uri; - $dupe->find(); - if ($dupe->fetch()) { - common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}"); - continue; - } - - if (Event::handle('StartNoticeSave', array(&$notice))) { - $id = $notice->insert(); - Event::handle('EndNoticeSave', array($notice)); - } - $notice->addToInboxes(); - - common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\""); - $hits++; - } - if ($hits == 0) { - common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml"); - } - } -} diff --git a/plugins/FeedSub/feedinfo.sql b/plugins/FeedSub/feedinfo.sql deleted file mode 100644 index e9b53d26e..000000000 --- a/plugins/FeedSub/feedinfo.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE `feedinfo` ( - `id` int(11) NOT NULL auto_increment, - `profile_id` int(11) NOT NULL, - `feeduri` varchar(255) NOT NULL, - `homeuri` varchar(255) NOT NULL, - `huburi` varchar(255) NOT NULL, - `verify_token` varchar(32) default NULL, - `sub_start` datetime default NULL, - `sub_end` datetime default NULL, - `created` datetime NOT NULL, - `lastupdate` datetime NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `feedinfo_feeduri_idx` (`feeduri`) -) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; diff --git a/plugins/FeedSub/feedmunger.php b/plugins/FeedSub/feedmunger.php deleted file mode 100644 index f3618b8eb..000000000 --- a/plugins/FeedSub/feedmunger.php +++ /dev/null @@ -1,238 +0,0 @@ -. - */ - -/** - * @package FeedSubPlugin - * @maintainer Brion Vibber - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - -class FeedSubPreviewNotice extends Notice -{ - protected $fetched = true; - - function __construct($profile) - { - //parent::__construct(); // uhhh? - $this->profile = $profile; - } - - 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); - } -} - -class FeedSubPreviewAvatar extends Avatar -{ - function displayUrl() { - return common_path('plugins/FeedSub/images/48px-Feed-icon.svg.png'); - } -} - -class FeedMunger -{ - /** - * @param XML_Feed_Parser $feed - */ - function __construct($feed, $url=null) - { - $this->feed = $feed; - $this->url = $url; - } - - function feedinfo() - { - $feedinfo = new Feedinfo(); - $feedinfo->feeduri = $this->url; - $feedinfo->homeuri = $this->feed->link; - $feedinfo->huburi = $this->getHubLink(); - return $feedinfo; - } - - function getAtomLink($item, $attribs=array()) - { - // XML_Feed_Parser gets confused by multiple elements. - $dom = $item->model; - - // Note that RSS feeds would embed an so this should work for both. - /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds - // - $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 elements. - $dom = $item->model; - - // Note that RSS feeds would embed an so this should work for both. - /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds - // - $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 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); - - // @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(); - } - - $link = $this->getAltLink($entry); - $notice->uri = $link; - $notice->url = $link; - $notice->content = $this->noticeFromEntry($entry); - $notice->rendered = common_render_content($notice->content, $notice); - $notice->created = common_sql_date($entry->updated); // @fixme - $notice->is_local = Notice::GATEWAY; - $notice->source = 'feed'; - - return $notice; - } - - /** - * @param XML_Feed_Type $entry - * @return string notice text, within post size limit - */ - function noticeFromEntry($entry) - { - $title = $entry->title; - $link = $entry->link; - - // @todo We can get entries like this: - // $cats = $entry->getCategory('category', array(0, true)); - // but it feels like an awful hack. If it's accessible cleanly, - // try adding #hashtags from the categories/tags on a post. - - // @todo Should we force a language here? - $format = _m('New post: "%1$s" %2$s'); - $title = $entry->title; - $link = $this->getAltLink($entry); - $out = sprintf($format, $title, $link); - - // Trim link if needed... - $max = Notice::maxContent(); - if (mb_strlen($out) > $max) { - $link = common_shorten_url($link); - $out = sprintf($format, $title, $link); - } - - // Trim title if needed... - if (mb_strlen($out) > $max) { - $ellipsis = "\xe2\x80\xa6"; // U+2026 HORIZONTAL ELLIPSIS - $used = mb_strlen($out) - mb_strlen($title); - $available = $max - $used - mb_strlen($ellipsis); - $title = mb_substr($title, 0, $available) . $ellipsis; - $out = sprintf($format, $title, $link); - } - - return $out; - } -} diff --git a/plugins/FeedSub/images/24px-Feed-icon.svg.png b/plugins/FeedSub/images/24px-Feed-icon.svg.png deleted file mode 100644 index 317225814..000000000 Binary files a/plugins/FeedSub/images/24px-Feed-icon.svg.png and /dev/null differ diff --git a/plugins/FeedSub/images/48px-Feed-icon.svg.png b/plugins/FeedSub/images/48px-Feed-icon.svg.png deleted file mode 100644 index bd1da4f91..000000000 Binary files a/plugins/FeedSub/images/48px-Feed-icon.svg.png and /dev/null differ diff --git a/plugins/FeedSub/images/96px-Feed-icon.svg.png b/plugins/FeedSub/images/96px-Feed-icon.svg.png deleted file mode 100644 index bf16571ec..000000000 Binary files a/plugins/FeedSub/images/96px-Feed-icon.svg.png and /dev/null differ diff --git a/plugins/FeedSub/images/README b/plugins/FeedSub/images/README deleted file mode 100644 index d9379c23e..000000000 --- a/plugins/FeedSub/images/README +++ /dev/null @@ -1,5 +0,0 @@ -Feed icon rendered from http://commons.wikimedia.org/wiki/File:Feed-icon.svg - -Originally distributed by the Mozilla Foundation under a MPL/GPL/LGPL tri-license: - -http://www.mozilla.org/MPL/boilerplate-1.1/mpl-tri-license-html diff --git a/plugins/FeedSub/locale/FeedSub.po b/plugins/FeedSub/locale/FeedSub.po deleted file mode 100644 index dedc018e3..000000000 --- a/plugins/FeedSub/locale/FeedSub.po +++ /dev/null @@ -1,104 +0,0 @@ -# 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 , YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-12-07 20:38-0800\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" -"Content-Transfer-Encoding: 8bit\n" - -#: tests/gettext-speedtest.php:57 FeedSubPlugin.php:76 -msgid "Feeds" -msgstr "" - -#: FeedSubPlugin.php:77 -msgid "Feed subscription options" -msgstr "" - -#: feedmunger.php:215 -#, php-format -msgid "New post: \"%1$s\" %2$s" -msgstr "" - -#: actions/feedsubsettings.php:41 -msgid "Feed subscriptions" -msgstr "" - -#: actions/feedsubsettings.php:52 -msgid "" -"You can subscribe to feeds from other sites; updates will appear in your " -"personal timeline." -msgstr "" - -#: actions/feedsubsettings.php:96 -msgid "Subscribe" -msgstr "" - -#: actions/feedsubsettings.php:98 -msgid "Continue" -msgstr "" - -#: actions/feedsubsettings.php:151 -msgid "Empty feed URL!" -msgstr "" - -#: actions/feedsubsettings.php:161 -msgid "Invalid URL or could not reach server." -msgstr "" - -#: actions/feedsubsettings.php:164 -msgid "Cannot read feed; server returned error." -msgstr "" - -#: actions/feedsubsettings.php:167 -msgid "Cannot read feed; server returned an empty page." -msgstr "" - -#: actions/feedsubsettings.php:170 -msgid "Bad HTML, could not find feed link." -msgstr "" - -#: actions/feedsubsettings.php:173 -msgid "Could not find a feed linked from this URL." -msgstr "" - -#: actions/feedsubsettings.php:176 -msgid "Not a recognized feed type." -msgstr "" - -#: actions/feedsubsettings.php:180 -msgid "Bad feed URL." -msgstr "" - -#: actions/feedsubsettings.php:188 -msgid "Feed is not PuSH-enabled; cannot subscribe." -msgstr "" - -#: actions/feedsubsettings.php:208 -msgid "Feed subscription failed! Bad response from hub." -msgstr "" - -#: actions/feedsubsettings.php:218 -msgid "Already subscribed!" -msgstr "" - -#: actions/feedsubsettings.php:220 -msgid "Feed subscribed!" -msgstr "" - -#: actions/feedsubsettings.php:222 -msgid "Feed subscription failed!" -msgstr "" - -#: actions/feedsubsettings.php:231 -msgid "Previewing feed:" -msgstr "" diff --git a/plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po b/plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po deleted file mode 100644 index f17dfa50a..000000000 --- a/plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po +++ /dev/null @@ -1,106 +0,0 @@ -# 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 , YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-12-07 14:14-0800\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: FeedSubPlugin.php:77 -msgid "Feeds" -msgstr "Flux" - -#: FeedSubPlugin.php:78 -msgid "Feed subscription options" -msgstr "Préférences pour abonnement flux" - -#: feedmunger.php:215 -#, php-format -msgid "New post: \"%1$s\" %2$s" -msgstr "Nouveau: \"%1$s\" %2$s" - -#: actions/feedsubsettings.php:41 -msgid "Feed subscriptions" -msgstr "Abonnements aux fluxes" - -#: actions/feedsubsettings.php:52 -msgid "" -"You can subscribe to feeds from other sites; updates will appear in your " -"personal timeline." -msgstr "" -"Abonner aux fluxes RSS ou Atom des autres sites web; les temps se trouverair" -"en votre flux personnel." - -#: actions/feedsubsettings.php:96 -msgid "Subscribe" -msgstr "Abonner" - -#: actions/feedsubsettings.php:98 -msgid "Continue" -msgstr "Prochaine" - -#: actions/feedsubsettings.php:151 -msgid "Empty feed URL!" -msgstr "" - -#: actions/feedsubsettings.php:161 -msgid "Invalid URL or could not reach server." -msgstr "" - -#: actions/feedsubsettings.php:164 -msgid "Cannot read feed; server returned error." -msgstr "" - -#: actions/feedsubsettings.php:167 -msgid "Cannot read feed; server returned an empty page." -msgstr "" - -#: actions/feedsubsettings.php:170 -msgid "Bad HTML, could not find feed link." -msgstr "" - -#: actions/feedsubsettings.php:173 -msgid "Could not find a feed linked from this URL." -msgstr "" - -#: actions/feedsubsettings.php:176 -msgid "Not a recognized feed type." -msgstr "" - -#: actions/feedsubsettings.php:180 -msgid "Bad feed URL." -msgstr "" - -#: actions/feedsubsettings.php:188 -msgid "Feed is not PuSH-enabled; cannot subscribe." -msgstr "" - -#: actions/feedsubsettings.php:208 -msgid "Feed subscription failed! Bad response from hub." -msgstr "" - -#: actions/feedsubsettings.php:218 -msgid "Already subscribed!" -msgstr "" - -#: actions/feedsubsettings.php:220 -msgid "Feed subscribed!" -msgstr "" - -#: actions/feedsubsettings.php:222 -msgid "Feed subscription failed!" -msgstr "" - -#: actions/feedsubsettings.php:231 -msgid "Previewing feed:" -msgstr "" diff --git a/plugins/FeedSub/tests/FeedDiscoveryTest.php b/plugins/FeedSub/tests/FeedDiscoveryTest.php deleted file mode 100644 index 1c5249701..000000000 --- a/plugins/FeedSub/tests/FeedDiscoveryTest.php +++ /dev/null @@ -1,111 +0,0 @@ -discoverFromHTML($url, $html); - $this->assertEquals($expected, $url); - } - - static public function provider() - { - $sampleHeader = << - - - - - -leŭksman - - - - - - - - - - - - - - - - - - - - - -END; - return array( - array('http://example.com/', - '', - 'http://example.com/feed/rss'), - array('http://example.com/atom', - '', - 'http://example.com/feed/atom'), - array('http://example.com/empty', - '', - false), - array('http://example.com/tagsoup', - '
',
-                           'http://example.com/feed/rss'),
-                     array('http://example.com/relative/link2',
-                           '',
-                           'http://example.com/feed/rss'),
-                     array('http://example.com/relative/link3',
-                           '',
-                           'http://example.com/feed/rss'),
-                     array('http://example.com/base/link1',
-                           '',
-                           'http://target.example.com/feed/rss'),
-                     array('http://example.com/base/link2',
-                           '',
-                           'http://target.example.com/feed/rss'),
-                     array('http://example.com/base/link3',
-                           '',
-                           'http://target.example.com/feed/rss'),
-                     // Trick question! There's a  but no href on it
-                     array('http://example.com/relative/fauxbase',
-                           '',
-                           'http://example.com/feed/rss'),
-                     // Actual WordPress blog header example
-                     array('http://leuksman.com/log/',
-                           $sampleHeader,
-                           'http://leuksman.com/log/feed/'));
-    }
-}
diff --git a/plugins/FeedSub/tests/FeedMungerTest.php b/plugins/FeedSub/tests/FeedMungerTest.php
deleted file mode 100644
index 0ce24c9fb..000000000
--- a/plugins/FeedSub/tests/FeedMungerTest.php
+++ /dev/null
@@ -1,147 +0,0 @@
-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('A fairly short titlehttp://example.com/short/link', 0,
-                           'New post: "A fairly short title" http://example.com/short/link'),
-                     // Requires URL shortening ...
-                     array('A fairly short titlehttp://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh', 0,
-                           'New post: "A fairly short title" http://ur1.ca/g2o1'),
-                     array('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...http://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh', 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 . <<
-
-
-	leŭksman
-	
-	http://leuksman.com/log
-	reticula, electronica, & oddities
-
-	Thu, 12 Nov 2009 17:44:42 +0000
-	http://wordpress.org/?v=2.8.6
-	en
-	hourly
-	1
-			
-
-		Compiling PHP on Snow Leopard
-		http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/
-		http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/#comments
-		Thu, 12 Nov 2009 17:44:42 +0000
-		brion
-				
-
-		
-
-		http://leuksman.com/log/?p=649
-		
-			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 should work from here…

-

For reference, here’s the whole configure line I currently use; MySQL is installed from the downloadable installer; other deps from MacPorts:

-

‘./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

-]]>
- http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/feed/ - 0 -
-
- -END; - return $samplefeed; - } - - static protected function samplefeedBlogspot() - { - return <<tag:blogger.com,1999:blog-77800835085316971672009-11-19T12:56:11.233-08:00Brion's Cool Test Blogbrionhttp://www.blogger.com/profile/12932299467049762017noreply@blogger.comBlogger2125tag:blogger.com,1999:blog-7780083508531697167.post-84566718790002906772009-11-19T12:55:00.000-08:002009-11-19T12:56:11.241-08:00I love postingIt'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>brionhttp://www.blogger.com/profile/12932299467049762017noreply@blogger.com0tag:blogger.com,1999:blog-7780083508531697167.post-82022969178973466332009-11-18T13:52:00.001-08:002009-11-18T13:52:48.444-08:00Hey dudetestingggggggggg<div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7780083508531697167-8202296917897346633?l=briontest.blogspot.com' alt='' /></div>brionhttp://www.blogger.com/profile/12932299467049762017noreply@blogger.com0 -END; - } -} diff --git a/plugins/FeedSub/tests/gettext-speedtest.php b/plugins/FeedSub/tests/gettext-speedtest.php deleted file mode 100644 index 8bbdf5e89..000000000 --- a/plugins/FeedSub/tests/gettext-speedtest.php +++ /dev/null @@ -1,78 +0,0 @@ - $bits) { - list($time, $result) = $bits; - $ms = $time * 1000.0; - printf("%10s %2.4fms %s\n", $func, $ms, $result); -} - - -function fake($str) { - return $str; -} - diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php new file mode 100644 index 000000000..941912112 --- /dev/null +++ b/plugins/OStatus/OStatusPlugin.php @@ -0,0 +1,159 @@ + +Author URI: http://status.net/ +*/ + +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @package FeedSubPlugin + * @maintainer Brion Vibber + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +define('FEEDSUB_SERVICE', 100); // fixme -- avoid hardcoding these? + +// We bundle the XML_Parse_Feed library... +set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib'); + +class FeedSubException extends Exception +{ +} + +class OStatusPlugin extends Plugin +{ + /** + * Hook for RouterInitialized event. + * + * @param Net_URL_Mapper $m path-to-action mapper + * @return boolean hook return + */ + function onRouterInitialized($m) + { + $m->connect('push/hub', array('action' => 'hub')); + + $m->connect('feedsub/callback/:feed', + array('action' => 'feedsubcallback'), + array('feed' => '[0-9]+')); + $m->connect('settings/feedsub', + array('action' => 'feedsubsettings')); + return true; + } + + /** + * Set up queue handlers for outgoing hub pushes + * @param QueueManager $qm + * @return boolean hook return + */ + function onEndInitializeQueueManager(QueueManager $qm) + { + $qm->connect('hubverify', 'HubVerifyQueueHandler'); + $qm->connect('hubdistrib', 'HubDistribQueueHandler'); + $qm->connect('hubout', 'HubOutQueueHandler'); + return true; + } + + /** + * Put saved notices into the queue for pubsub distribution. + */ + function onStartEnqueueNotice($notice, &$transports) + { + $transports[] = 'hubdistrib'; + return true; + } + + /** + * Set up a PuSH hub link to our internal link for canonical timeline + * Atom feeds for users. + */ + function onStartApiAtom(Action $action) + { + if ($action instanceof ApiTimelineUserAction) { + $id = $action->arg('id'); + if (strval(intval($id)) === strval($id)) { + // Canonical form of id in URL? + // Updates will be handled for our internal PuSH hub. + $action->element('link', array('rel' => 'hub', + 'href' => common_local_url('hub'))); + } + } + return true; + } + + /** + * Add the feed settings page to the Connect Settings menu + * + * @param Action &$action The calling page + * + * @return boolean hook return + */ + function onEndConnectSettingsNav(&$action) + { + $action_name = $action->trimmed('action'); + + $action->menuItem(common_local_url('feedsubsettings'), + _m('Feeds'), + _m('Feed subscription options'), + $action_name === 'feedsubsettings'); + + return true; + } + + /** + * Automatically load the actions and libraries used by the plugin + * + * @param Class $cls the class + * + * @return boolean hook return + * + */ + function onAutoload($cls) + { + $base = dirname(__FILE__); + $lower = strtolower($cls); + $files = array("$base/classes/$cls.php", + "$base/lib/$lower.php"); + if (substr($lower, -6) == 'action') { + $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php"; + } + foreach ($files as $file) { + if (file_exists($file)) { + include_once $file; + return false; + } + } + return true; + } + + function onCheckSchema() { + // warning: the autoincrement doesn't seem to set. + // alter table feedinfo change column id id int(11) not null auto_increment; + $schema = Schema::get(); + $schema->ensureTable('feedinfo', Feedinfo::schemaDef()); + $schema->ensureTable('hubsub', HubSub::schemaDef()); + return true; + } +} diff --git a/plugins/OStatus/README b/plugins/OStatus/README new file mode 100644 index 000000000..cbf3adbb9 --- /dev/null +++ b/plugins/OStatus/README @@ -0,0 +1,24 @@ +Plugin to support importing updates from external RSS and Atom feeds into your timeline. + +Uses PubSubHubbub for push feed updates; currently non-PuSH feeds cannot be subscribed. + +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 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? +* 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/feedsubcallback.php b/plugins/OStatus/actions/feedsubcallback.php new file mode 100644 index 000000000..c57ea5b10 --- /dev/null +++ b/plugins/OStatus/actions/feedsubcallback.php @@ -0,0 +1,105 @@ +. + */ + +/** + * @package FeedSubPlugin + * @maintainer Brion Vibber + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + + +class FeedSubCallbackAction extends Action +{ + function handle() + { + parent::handle(); + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->handlePost(); + } else { + $this->handleGet(); + } + } + + /** + * Handler for POST content updates from the hub + */ + function handlePost() + { + $feedid = $this->arg('feed'); + common_log(LOG_INFO, "POST for feed id $feedid"); + if (!$feedid) { + throw new ServerException('Empty or invalid feed id', 400); + } + + $feedinfo = Feedinfo::staticGet('id', $feedid); + if (!$feedinfo) { + throw new ServerException('Unknown feed id ' . $feedid, 400); + } + + $hmac = ''; + if (isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) { + $hmac = $_SERVER['HTTP_X_HUB_SIGNATURE']; + } + + $post = file_get_contents('php://input'); + $feedinfo->postUpdates($post, $hmac); + } + + /** + * Handler for GET verification requests from the hub + */ + function handleGet() + { + $mode = $this->arg('hub_mode'); + $topic = $this->arg('hub_topic'); + $challenge = $this->arg('hub_challenge'); + $lease_seconds = $this->arg('hub_lease_seconds'); + $verify_token = $this->arg('hub_verify_token'); + + if ($mode != 'subscribe' && $mode != 'unsubscribe') { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\""); + throw new ServerException("Bogus hub callback: bad mode", 404); + } + + $feedinfo = Feedinfo::staticGet('feeduri', $topic); + if (!$feedinfo) { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); + throw new ServerException("Bogus hub callback: unknown feed", 404); + } + + # Can't currently set the token in our sub api + #if ($feedinfo->verify_token !== $verify_token) { + # common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); + # throw new ServerError("Bogus hub callback: bad token", 404); + #} + + // OK! + common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); + $feedinfo->sub_start = common_sql_date(time()); + if ($lease_seconds > 0) { + $feedinfo->sub_end = common_sql_date(time() + $lease_seconds); + } else { + $feedinfo->sub_end = null; + } + $feedinfo->update(); + + print $challenge; + } +} diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php new file mode 100644 index 000000000..4d5b7b60f --- /dev/null +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -0,0 +1,258 @@ +. + */ + +/** + * @package FeedSubPlugin + * @maintainer Brion Vibber + */ + +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->feedinfo = $this->munger->feedInfo(); + + if ($this->feedinfo->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->feedinfo = Feedinfo::ensureProfile($this->munger); + + // If not already in use, subscribe to updates via the hub + if ($this->feedinfo->sub_start) { + common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); + } else { + $ok = $this->feedinfo->subscribe(); + common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); + if (!$ok) { + $this->showForm(_m('Feed subscription failed! Bad response from hub.')); + return; + } + } + + // And subscribe the current user to the local profile + $user = common_current_user(); + $profile = $this->feedinfo->getProfile(); + if (!$profile) { + throw new ServerException("Feed profile was not saved properly."); + } + + if ($user->isSubscribed($profile)) { + $this->showForm(_m('Already subscribed!')); + } elseif ($user->subscribeTo($profile)) { + $this->showForm(_m('Feed subscribed!')); + } else { + $this->showForm(_m('Feed subscription failed!')); + } + } + } + + function validateAndPreview() + { + if ($this->validateFeed()) { + $this->preview = true; + $this->showForm(_m('Previewing feed:')); + } + } + + function previewFeed() + { + $feedinfo = $this->munger->feedinfo(); + $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/hub.php b/plugins/OStatus/actions/hub.php new file mode 100644 index 000000000..5caf4b48e --- /dev/null +++ b/plugins/OStatus/actions/hub.php @@ -0,0 +1,176 @@ +. + */ + +/** + * Integrated PuSH hub; lets us only ping them what need it. + * @package Hub + * @maintainer Brion Vibber + */ + +/** + + +Things to consider... +* should we purge incomplete subscriptions that never get a verification pingback? +* when can we send subscription renewal checks? + - at next send time probably ok +* when can we handle trimming of subscriptions? + - at next send time probably ok +* should we keep a fail count? + +*/ + + +class HubAction extends Action +{ + function arg($arg, $def=null) + { + // PHP converts '.'s in incoming var names to '_'s. + // It also merges multiple values, which'll break hub.verify and hub.topic for publishing + // @fixme handle multiple args + $arg = str_replace('.', '_', $arg); + return parent::arg($arg, $def); + } + + function prepare($args) + { + StatusNet::setApi(true); // reduce exception reports to aid in debugging + return parent::prepare($args); + } + + function handle() + { + $mode = $this->trimmed('hub.mode'); + switch ($mode) { + case "subscribe": + $this->subscribe(); + break; + case "unsubscribe": + $this->unsubscribe(); + break; + case "publish": + throw new ServerException("Publishing outside feeds not supported.", 400); + default: + throw new ServerException("Unrecognized mode '$mode'.", 400); + } + } + + /** + * Process a PuSH feed subscription request. + * + * HTTP return codes: + * 202 Accepted - request saved and awaiting verification + * 204 No Content - already subscribed + * 403 Forbidden - rejecting this (not specifically spec'd) + */ + function subscribe() + { + $feed = $this->argUrl('hub.topic'); + $callback = $this->argUrl('hub.callback'); + + common_log(LOG_DEBUG, __METHOD__ . ": checking sub'd to $feed $callback"); + if ($this->getSub($feed, $callback)) { + // Already subscribed; return 204 per spec. + header('HTTP/1.1 204 No Content'); + common_log(LOG_DEBUG, __METHOD__ . ': already subscribed'); + return; + } + + common_log(LOG_DEBUG, __METHOD__ . ': setting up'); + $sub = new HubSub(); + $sub->topic = $feed; + $sub->callback = $callback; + $sub->secret = $this->arg('hub.secret', null); + $sub->setLease(intval($this->arg('hub.lease_seconds'))); + + // @fixme check for feeds we don't manage + // @fixme check the verification mode, might want a return immediately? + + common_log(LOG_DEBUG, __METHOD__ . ': inserting'); + $ok = $sub->insert(); + + if (!$ok) { + throw new ServerException("Failed to save subscription record", 500); + } + + // @fixme check errors ;) + + $data = array('sub' => $sub, 'mode' => 'subscribe'); + $qm = QueueManager::get(); + $qm->enqueue($data, 'hubverify'); + + header('HTTP/1.1 202 Accepted'); + common_log(LOG_DEBUG, __METHOD__ . ': done'); + } + + /** + * Process a PuSH feed unsubscription request. + * + * HTTP return codes: + * 202 Accepted - request saved and awaiting verification + * 204 No Content - already subscribed + * 400 Bad Request - invalid params or rejected feed + */ + function unsubscribe() + { + $feed = $this->argUrl('hub.topic'); + $callback = $this->argUrl('hub.callback'); + $sub = $this->getSub($feed, $callback); + + if ($sub) { + if ($sub->verify('unsubscribe')) { + $sub->delete(); + common_log(LOG_INFO, "PuSH unsubscribed $feed for $callback"); + } else { + throw new ServerException("Failed PuSH unsubscription: verification failed! $feed for $callback"); + } + } else { + throw new ServerException("Failed PuSH unsubscription: not subscribed! $feed for $callback"); + } + } + + /** + * Grab and validate a URL from POST parameters. + * @throws ServerException for malformed or non-http/https URLs + */ + protected function argUrl($arg) + { + $url = $this->arg($arg); + $params = array('domain_check' => false, // otherwise breaks my local tests :P + 'allowed_schemes' => array('http', 'https')); + if (Validate::uri($url, $params)) { + return $url; + } else { + throw new ServerException("Invalid URL passed for $arg: '$url'", 400); + } + } + + /** + * Get HubSub subscription record for a given feed & subscriber. + * + * @param string $feed + * @param string $callback + * @return mixed HubSub or false + */ + protected function getSub($feed, $callback) + { + return HubSub::staticGet($feed, $callback); + } +} + diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php new file mode 100644 index 000000000..f29d08cb0 --- /dev/null +++ b/plugins/OStatus/classes/Feedinfo.php @@ -0,0 +1,345 @@ +. + */ + +/** + * @package FeedSubPlugin + * @maintainer Brion Vibber + */ + +/* +PuSH subscription flow: + + $feedinfo->subscribe() + generate random verification token + save to verify_token + sends a sub request to the hub... + + feedsub/callback + hub sends confirmation back to us via GET + We verify the request, then echo back the challenge. + On our end, we save the time we subscribed and the lease expiration + + feedsub/callback + hub sends us updates via POST + +*/ + +class FeedDBException extends FeedSubException +{ + public $obj; + + function __construct($obj) + { + parent::__construct('Database insert failure'); + $this->obj = $obj; + } +} + +class Feedinfo extends Memcached_DataObject +{ + public $__table = 'feedinfo'; + + public $id; + public $profile_id; + + public $feeduri; + public $homeuri; + public $huburi; + + // PuSH subscription data + public $secret; + public $verify_token; + public $sub_start; + public $sub_end; + + public $created; + public $lastupdate; + + + public /*static*/ function staticGet($k, $v=null) + { + return parent::staticGet(__CLASS__, $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'huburi' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'secret' => DB_DATAOBJECT_STR, + 'verify_token' => DB_DATAOBJECT_STR, + 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + static function schemaDef() + { + return array(new ColumnDef('id', 'integer', + /*size*/ null, + /*nullable*/ false, + /*key*/ 'PRI', + /*default*/ '0', + /*extra*/ null, + /*auto_increment*/ true), + new ColumnDef('profile_id', 'integer', + null, false), + new ColumnDef('feeduri', 'varchar', + 255, false, 'UNI'), + new ColumnDef('homeuri', 'varchar', + 255, false), + new ColumnDef('huburi', 'varchar', + 255, false), + new ColumnDef('verify_token', 'varchar', + 32, true), + new ColumnDef('secret', 'varchar', + 64, true), + new ColumnDef('sub_start', 'datetime', + null, true), + new ColumnDef('sub_end', 'datetime', + null, true), + new ColumnDef('created', 'datetime', + null, false), + new ColumnDef('lastupdate', 'datetime', + null, false)); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keys() + { + return array_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'); // @fixme we'll need a profile_id key at least + } + + function sequenceKey() + { + return array('id', true, false); + } + + /** + * Fetch the StatusNet-side profile for this feed + * @return Profile + */ + public function getProfile() + { + return Profile::staticGet('id', $this->profile_id); + } + + /** + * @param FeedMunger $munger + * @return Feedinfo + */ + public static function ensureProfile($munger) + { + $feedinfo = $munger->feedinfo(); + + $current = self::staticGet('feeduri', $feedinfo->feeduri); + if ($current) { + // @fixme we should probably update info as necessary + return $current; + } + + $feedinfo->query('BEGIN'); + + // Awful hack! Awful hack! + $feedinfo->verify = common_good_rand(16); + $feedinfo->secret = common_good_rand(32); + + try { + $profile = $munger->profile(); + $result = $profile->insert(); + if (empty($result)) { + throw new FeedDBException($profile); + } + + $avatar = $munger->getAvatar(); + if ($avatar) { + // @fixme this should be better encapsulated + // ripped from oauthstore.php (for old OMB client) + $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); + copy($avatar, $temp_filename); + $imagefile = new ImageFile($profile->id, $temp_filename); + $filename = Avatar::filename($profile->id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + $profile->setOriginal($filename); + } + + $feedinfo->profile_id = $profile->id; + $result = $feedinfo->insert(); + if (empty($result)) { + throw new FeedDBException($feedinfo); + } + + $feedinfo->query('COMMIT'); + } catch (FeedDBException $e) { + common_log_db_error($e->obj, 'INSERT', __FILE__); + $feedinfo->query('ROLLBACK'); + return false; + } + return $feedinfo; + } + + /** + * Send a subscription request to the hub for this feed. + * The hub will later send us a confirmation POST to /feedsub/callback. + * + * @return bool true on success, false on failure + */ + public function subscribe() + { + if (common_config('feedsub', 'nohub')) { + // Fake it! We're just testing remote feeds w/o hubs. + return true; + } + // @fixme use the verification token + #$token = md5(mt_rand() . ':' . $this->feeduri); + #$this->verify_token = $token; + #$this->update(); // @fixme + try { + $callback = common_local_url('feedsubcallback', array('feed' => $this->id)); + $headers = array('Content-Type: application/x-www-form-urlencoded'); + $post = array('hub.mode' => 'subscribe', + '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"); + return false; + } + } + + /** + * Read and post notices for updates from the feed. + * Currently assumes that all items in the feed are new, + * coming from a PuSH hub. + * + * @param string $xml source of Atom or RSS feed + * @param string $hmac X-Hub-Signature header, if present + */ + public function postUpdates($xml, $hmac) + { + common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml"); + + if ($this->secret) { + if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) { + $their_hmac = strtolower($matches[1]); + $our_hmac = sha1($xml . $this->secret); + if ($their_hmac !== $our_hmac) { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac"); + return; + } + } else { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'"); + return; + } + } else if ($hmac) { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'"); + return; + } + + require_once "XML/Feed/Parser.php"; + $feed = new XML_Feed_Parser($xml, false, false, true); + $munger = new FeedMunger($feed); + + $hits = 0; + foreach ($feed as $index => $entry) { + // @fixme this might sort in wrong order if we get multiple updates + + $notice = $munger->notice($index); + $notice->profile_id = $this->profile_id; + + // Double-check for oldies + // @fixme this could explode horribly for multiple feeds on a blog. sigh + $dupe = new Notice(); + $dupe->uri = $notice->uri; + if ($dupe->find(true)) { + common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}"); + continue; + } + + if (Event::handle('StartNoticeSave', array(&$notice))) { + $id = $notice->insert(); + Event::handle('EndNoticeSave', array($notice)); + } + $notice->addToInboxes(); + + common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\""); + $hits++; + } + if ($hits == 0) { + common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml"); + } + } +} diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php new file mode 100644 index 000000000..1769f6c94 --- /dev/null +++ b/plugins/OStatus/classes/HubSub.php @@ -0,0 +1,272 @@ +. + */ + +/** + * PuSH feed subscription record + * @package Hub + * @author Brion Vibber + */ +class HubSub extends Memcached_DataObject +{ + public $__table = 'hubsub'; + + public $hashkey; // sha1(topic . '|' . $callback); (topic, callback) key is too long for myisam in utf8 + public $topic; + public $callback; + public $secret; + public $verify_token; + public $challenge; + public $lease; + public $sub_start; + public $sub_end; + public $created; + + public /*static*/ function staticGet($topic, $callback) + { + return parent::staticGet(__CLASS__, 'hashkey', self::hashkey($topic, $callback)); + } + + protected static function hashkey($topic, $callback) + { + return sha1($topic . '|' . $callback); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('hashkey' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'topic' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'callback' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'secret' => DB_DATAOBJECT_STR, + 'verify_token' => DB_DATAOBJECT_STR, + 'challenge' => DB_DATAOBJECT_STR, + 'lease' => DB_DATAOBJECT_INT, + 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + static function schemaDef() + { + return array(new ColumnDef('hashkey', 'char', + /*size*/40, + /*nullable*/false, + /*key*/'PRI'), + new ColumnDef('topic', 'varchar', + /*size*/255, + /*nullable*/false, + /*key*/'KEY'), + new ColumnDef('callback', 'varchar', + 255, false), + new ColumnDef('secret', 'text', + null, true), + new ColumnDef('verify_token', 'text', + null, true), + new ColumnDef('challenge', 'varchar', + 32, true), + new ColumnDef('lease', 'int', + null, true), + new ColumnDef('sub_start', 'datetime', + null, true), + new ColumnDef('sub_end', 'datetime', + null, true), + new ColumnDef('created', 'datetime', + null, false)); + } + + function keys() + { + return array_keys($this->keyTypes()); + } + + function sequenceKeys() + { + return array(false, false, false); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keyTypes() + { + return array('hashkey' => 'K'); + } + + /** + * Validates a requested lease length, sets length plus + * subscription start & end dates. + * + * Does not save to database -- use before insert() or update(). + * + * @param int $length in seconds + */ + function setLease($length) + { + assert(is_int($length)); + + $min = 86400; + $max = 86400 * 30; + + if ($length == 0) { + // We want to garbage collect dead subscriptions! + $length = $max; + } elseif( $length < $min) { + $length = $min; + } else if ($length > $max) { + $length = $max; + } + + $this->lease = $length; + $this->start_sub = common_sql_now(); + $this->end_sub = common_sql_date(time() + $length); + } + + /** + * Send a verification ping to subscriber + * @param string $mode 'subscribe' or 'unsubscribe' + */ + function verify($mode) + { + assert($mode == 'subscribe' || $mode == 'unsubscribe'); + + // Is this needed? data object fun... + $clone = clone($this); + $clone->challenge = common_good_rand(16); + $clone->update($this); + $this->challenge = $clone->challenge; + unset($clone); + + $params = array('hub.mode' => $mode, + 'hub.topic' => $this->topic, + 'hub.challenge' => $this->challenge); + if ($mode == 'subscribe') { + $params['hub.lease_seconds'] = $this->lease; + } + if ($this->verify_token) { + $params['hub.verify_token'] = $this->verify_token; + } + $url = $this->callback . '?' . http_build_query($params, '', '&'); // @fixme ugly urls + + try { + $request = new HTTPClient(); + $response = $request->get($url); + $status = $response->getStatus(); + + if ($status >= 200 && $status < 300) { + $fail = false; + } else { + // @fixme how can we schedule a second attempt? + // Or should we? + $fail = "Returned HTTP $status"; + } + } catch (Exception $e) { + $fail = $e->getMessage(); + } + if ($fail) { + // @fixme how can we schedule a second attempt? + // or save a fail count? + // Or should we? + common_log(LOG_ERR, "Failed to verify $mode for $this->topic at $this->callback: $fail"); + return false; + } else { + if ($mode == 'subscribe') { + // Establish or renew the subscription! + // This seems unnecessary... dataobject fun! + $clone = clone($this); + $clone->challenge = null; + $clone->setLease($this->lease); + $clone->update($this); + unset($clone); + + $this->challenge = null; + $this->setLease($this->lease); + common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic for $this->lease seconds"); + } else if ($mode == 'unsubscribe') { + common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic"); + $this->delete(); + } + return true; + } + } + + /** + * Insert wrapper; transparently set the hash key from topic and callback columns. + * @return boolean success + */ + function insert() + { + $this->hashkey = self::hashkey($this->topic, $this->callback); + return parent::insert(); + } + + /** + * Send a 'fat ping' to the subscriber's callback endpoint + * containing the given Atom feed chunk. + * + * Determination of which items to send should be done at + * a higher level; don't just shove in a complete feed! + * + * @param string $atom well-formed Atom feed + */ + function push($atom) + { + $headers = array('Content-Type: application/atom+xml'); + if ($this->secret) { + $hmac = sha1($atom . $this->secret); + $headers[] = "X-Hub-Signature: sha1=$hmac"; + } else { + $hmac = '(none)'; + } + common_log(LOG_INFO, "About to push feed to $this->callback for $this->topic, HMAC $hmac"); + try { + $request = new HTTPClient(); + $request->setBody($atom); + $response = $request->post($this->callback, $headers); + + if ($response->isOk()) { + return true; + } + common_log(LOG_ERR, "Error sending PuSH content " . + "to $this->callback for $this->topic: " . + $response->getStatus()); + return false; + + } catch (Exception $e) { + common_log(LOG_ERR, "Error sending PuSH content " . + "to $this->callback for $this->topic: " . + $e->getMessage()); + return false; + } + } +} + diff --git a/plugins/OStatus/extlib/README b/plugins/OStatus/extlib/README new file mode 100644 index 000000000..799b40c47 --- /dev/null +++ b/plugins/OStatus/extlib/README @@ -0,0 +1,9 @@ +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 + 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 new file mode 100755 index 000000000..ffe8220a5 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser.php @@ -0,0 +1,351 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100644 index 000000000..c7e218a1e --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/Atom.php @@ -0,0 +1,365 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100755 index 000000000..063ecb617 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/AtomElement.php @@ -0,0 +1,261 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100755 index 000000000..1e76e3f85 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/Exception.php @@ -0,0 +1,42 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100755 index 000000000..07f38f911 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php @@ -0,0 +1,214 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100755 index 000000000..d41f36e8d --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS09Element.php @@ -0,0 +1,62 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100755 index 000000000..60c9938ba --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php @@ -0,0 +1,277 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100755 index 000000000..3cd1ef15d --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php @@ -0,0 +1,276 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100755 index 000000000..75918beda --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS11Element.php @@ -0,0 +1,151 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100755 index 000000000..8e36d5a9b --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS1Element.php @@ -0,0 +1,116 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100644 index 000000000..0936bd2f5 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php @@ -0,0 +1,335 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100755 index 000000000..6edf910dc --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/RSS2Element.php @@ -0,0 +1,171 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 new file mode 100644 index 000000000..75052619b --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/Parser/Type.php @@ -0,0 +1,467 @@ + + * @copyright 2005 James Stewart + * @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 + * @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: + * PEAR and: + * + * 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 .= '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 new file mode 100755 index 000000000..02e1c5800 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/atom10-entryonly.xml @@ -0,0 +1,28 @@ + + + Atom draft-07 snapshot + + + tag:example.org,2003:3.2397 + 2005-07-10T12:29:29Z + 2003-12-13T08:29:29-04:00 + + Mark Pilgrim + http://example.org/ + f8dy@example.com + + + Sam Ruby + + + Joe Gregorio + + +
+

[Update: The Atom draft is finished.]

+
+
+
\ 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 new file mode 100755 index 000000000..d181d2b6f --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/atom10-example1.xml @@ -0,0 +1,20 @@ + + + + Example Feed + + 2003-12-13T18:30:02Z + + John Doe + + urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6 + + + Atom-Powered Robots Run Amok + + urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a + 2003-12-13T18:30:02Z + Some text. + + + \ 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 new file mode 100755 index 000000000..98abf9d54 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/atom10-example2.xml @@ -0,0 +1,45 @@ + + + dive into mark + + A <em>lot</em> of effort + went into making this effortless + + 2005-07-31T12:29:29Z + tag:example.org,2003:3 + + + Copyright (c) 2003, Mark Pilgrim + + Example Toolkit + + + Atom draft-07 snapshot + + + tag:example.org,2003:3.2397 + 2005-07-31T12:29:29Z + 2003-12-13T08:29:29-04:00 + + Mark Pilgrim + http://example.org/ + f8dy@example.com + + + Sam Ruby + + + Joe Gregorio + + +
+

[Update: The Atom draft is finished.]

+
+
+
+
\ 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 new file mode 100755 index 000000000..32f9fa493 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/delicious.feed @@ -0,0 +1,177 @@ + + + + +del.icio.us/tag/greenbelt +http://del.icio.us/tag/greenbelt +Text + + + + + + + + + + + + + + + + +Greenbelt - Homepage Section +http://www.greenbelt.org.uk/ +jonnybaker +2005-05-16T16:30:38Z +greenbelt + + + + + + + + +Greenbelt festival (uk) +http://www.greenbelt.org.uk/ +sssshhhh +2005-05-14T18:19:40Z +audiology festival gigs greenbelt + + + + + + + + + + + +Natuerlichwien.at - Rundumadum +http://www.natuerlichwien.at/rundumadum/dergruenguertel/ +egmilman47 +2005-05-06T21:33:41Z +Austria Vienna Wien greenbelt nature walking + + + + + + + + + + + + + +Tank - GBMediaWiki +http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars +jystewart +2005-03-21T22:44:11Z +greenbelt + + + + + + + + +Greenbelt homepage +http://www.greenbelt.ca/home.htm +Gooberoo +2005-03-01T22:43:17Z +greenbelt ontario + + + + + + + + + +Pip Wilson bhp ...... blog +http://pipwilsonbhp.blogspot.com/ +sssshhhh +2004-12-27T11:20:51Z +Greenbelt friend ideas links thinking weblog + + + + + + + + + + + + + +maggi dawn +http://maggidawn.typepad.com/maggidawn/ +sssshhhh +2004-12-27T11:20:11Z +Greenbelt ideas links thinking weblog + + + + + + + + + + + + +John Davies +http://www.johndavies.org/ +sssshhhh +2004-12-27T11:18:37Z +Greenbelt ideas links thinking weblog + + + + + + + + + + + + +jonnybaker +http://jonnybaker.blogs.com/ +sssshhhh +2004-12-27T11:18:17Z +Greenbelt event ideas links resources thinking weblog youth + + + + + + + + + + + + + + + diff --git a/plugins/OStatus/extlib/XML/Feed/samples/flickr.feed b/plugins/OStatus/extlib/XML/Feed/samples/flickr.feed new file mode 100755 index 000000000..57e83af57 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/flickr.feed @@ -0,0 +1,184 @@ + + + + jamesstewart - Everyone's Tagged Photos + + + A feed of jamesstewart - Everyone's Tagged Photos + 2005-08-01T18:50:26Z + Flickr + + + Oma and James + + + tag:flickr.com,2004:/photo/30367516 + 2005-08-01T18:50:26Z + 2005-08-01T18:50:26Z + <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> + + kstewart + http://www.flickr.com/people/30484029@N00/ + + jamesstewart oma stoelfamily + + + + + tag:flickr.com,2004:/photo/21376174 + 2005-06-25T02:00:35Z + 2005-06-25T02:00:35Z + <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> + + Lan Rover + http://www.flickr.com/people/buddscreek/ + + amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational rickycarmichael 259 jamesstewart 4 + + + + + tag:flickr.com,2004:/photo/21375650 + 2005-06-25T01:56:24Z + 2005-06-25T01:56:24Z + <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> + + + + Lan Rover + http://www.flickr.com/people/buddscreek/ + + amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart + + + + + tag:flickr.com,2004:/photo/21375345 + 2005-06-25T01:54:11Z + 2005-06-25T01:54:11Z + <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> + + + + Lan Rover + http://www.flickr.com/people/buddscreek/ + + amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart + + + Lunch with Kari & James, café in the crypt of St Martin in the fields + + tag:flickr.com,2004:/photo/16516618 + 2005-05-30T21:56:39Z + 2005-05-30T21:56:39Z + <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> + + + + fidothe + http://www.flickr.com/people/fidothe/ + + nokia7610 london stmartininthefields clarepatterson jamesstewart parvinstewart jimstewart susanstewart + + + Stewart keeping it low over the obstacle. + + tag:flickr.com,2004:/photo/10224728 + 2005-04-21T07:30:29Z + 2005-04-21T07:30:29Z + <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> + + + + pqbon + http://www.flickr.com/people/pqbon/ + + ama hangtown motocross jamesstewart bubba + + + king james stewart + + tag:flickr.com,2004:/photo/7152910 + 2005-03-22T21:53:37Z + 2005-03-22T21:53:37Z + <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> + + jj look + http://www.flickr.com/people/jjlook/ + + dilomar05 eastside austin texas 78702 kingjames stewart jamesstewart borrowed + + + It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson) + + tag:flickr.com,2004:/photo/1586562 + 2004-11-20T09:34:28Z + 2004-11-20T09:34:28Z + <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> + + + + fidothe + http://www.flickr.com/people/fidothe/ + + holiday grandrapids jamesstewart + + + It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson) + + tag:flickr.com,2004:/photo/1586539 + 2004-11-20T09:28:16Z + 2004-11-20T09:28:16Z + <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> + + + + fidothe + http://www.flickr.com/people/fidothe/ + + holiday grandrapids jamesstewart + + + It's a Grind, James and Jim can't decide) + + tag:flickr.com,2004:/photo/1586514 + 2004-11-20T09:25:05Z + 2004-11-20T09:25:05Z + <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> + + + + fidothe + http://www.flickr.com/people/fidothe/ + + holiday grandrapids jamesstewart johnkentish + + + \ 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 new file mode 100755 index 000000000..c351d3c16 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/grwifi-atom.xml @@ -0,0 +1,7 @@ + Updates to Grand Rapids WiFi hotspot details 2005-09-01T15:43:01-05:00 WiFi Hotspots in Grand Rapids, MI http://grwifi.net/atom/locations Creative Commons Attribution-NonCommercial-ShareAlike 2.0 http://creativecommons.org/licenses/by-nc-sa/2.0/ Hotspot Details Updated: Sweetwaters http://grwifi.net/location/sweetwaters 2005-09-01T15:43:01-05:00 The details of the WiFi hotspot at: Sweetwaters have been updated. Find out more at: +http://grwifi.net/location/sweetwaters James http://jystewart.net james@jystewart.net wifi hotspot Hotspot Details Updated: Common Ground Coffee Shop http://grwifi.net/location/common-ground 2005-09-01T15:42:39-05:00 The details of the WiFi hotspot at: Common Ground Coffee Shop have been updated. Find out more at: +http://grwifi.net/location/common-ground James http://jystewart.net james@jystewart.net wifi hotspot Hotspot Details Updated: Grand Rapids Public Library, Main Branch http://grwifi.net/location/grpl-main-branch 2005-09-01T15:42:20-05:00 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 James http://jystewart.net james@jystewart.net wifi hotspot Hotspot Details Updated: Four Friends Coffee House http://grwifi.net/location/four-friends 2005-09-01T15:41:35-05:00 The details of the WiFi hotspot at: Four Friends Coffee House have been updated. Find out more at: +http://grwifi.net/location/four-friends James http://jystewart.net james@jystewart.net wifi hotspot Hotspot Details Updated: Barnes and Noble, Rivertown Crossings http://grwifi.net/location/barnes-noble-rivertown 2005-09-01T15:40:41-05:00 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 James http://jystewart.net james@jystewart.net wifi hotspot Hotspot Details Updated: The Boss Sports Bar & Grille http://grwifi.net/location/boss-sports-bar 2005-09-01T15:40:19-05:00 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 James http://jystewart.net james@jystewart.net wifi hotspot \ 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 new file mode 100755 index 000000000..099463570 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/hoder.xml @@ -0,0 +1,102 @@ + + + + +Editor: Myself (Persian) +http://editormyself.info +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. +en-us +hoder@hotmail.com +2005-10-12T19:45:32-05:00 + +hourly +1 +2000-01-01T12:00+00:00 + + + +لينکدونی‌ | جلسه‌ی امریکن انترپرایز برای تقسیم قومی ایران +http://www.aei.org/events/type.upcoming,eventID.1166,filter.all/event_detail.asp +چطور بعضی‌ها فکر می‌کنند دست راستی‌های آمریکا از خامنه‌ای ملی‌گراترند +14645@http://i.hoder.com/ +iran +2005-10-12T19:45:32-05:00 + + + +لينکدونی‌ | به صبحانه آگهی بدهید +http://www.adbrite.com/mb/commerce/purchase_form.php?opid=24346&afsid=1 +خیلی ارزان و راحت است +14644@http://i.hoder.com/ +media/journalism +2005-10-12T17:23:15-05:00 + + + +لينکدونی‌ | نیروی انتظامی چگونه تابوهای هم‌جنس‌گرایانه را می‌شکند؛ فرنگوپولیس +http://farangeopolis.blogspot.com/2005/10/blog-post_08.html +از پس و پیش و حاشیه‌ی این ماجرا می‌توان یک مستند بی‌نظیر ساخت +14643@http://i.hoder.com/ +soc_popculture +2005-10-12T17:06:40-05:00 + + + +لينکدونی‌ | بازتاب توقیف شد +http://www.baztab.com/news/30201.php +اگر گفتید یک وب‌سایت را چطور توقیف می‌کنند؟ لابد ماوس‌شان را قایم می‌کنند. +14642@http://i.hoder.com/ +media/journalism +2005-10-12T14:41:57-05:00 + + + +لينکدونی‌ | رشد وب در سال 2005 از همیشه بیشتر بوده است" بی.بی.سی +http://news.bbc.co.uk/2/hi/technology/4325918.stm + +14640@http://i.hoder.com/ +tech +2005-10-12T13:04:46-05:00 + + + + + +==قرعه کشی گرین کارد به زودی شروع می‌شود== +http://nice.newsxphotos.biz/05/09/2007_dv_lottery_registration_to_begin_oct_5_14589.php + +14613@http://vagrantly.com +ads03 +2005-09-27T04:49:22-05:00 + + + + + + + + +پروژه‌ی هاروارد، قدم دوم +http://editormyself.info/archives/2005/10/051012_014641.shtml +اگر یادتان باشد چند وقت پیش نوشتم که دانشگاه هاروارد پروژه‌ای دارد با نام آواهای جهانی که در آن به وبلاگ‌های غیر انگلیسی‌زبان می‌پردازد. خواشتم که اگر کسی علاقه دارد ایمیل بزند. تعداد زیادی جواب دادند و ابراز علاقه کردند. حالا وقت قدم دوم است.

+ +

قدم دوم این است که برای اینکه مسوولین پروژه بتوانند تصمیم بگیرند که با چه کسی کار کنند، می‌خواهند نمونه‌ی کارهای علاقمندان مشارکت در این پرزو‌ه را ببینند.

+ +

برای همین از همه‌ی علاقماندان، حتی کسانی که قبلا اعلام آمادگی نکرده بودند، می‌‌خواهم که یک موضوع رایج این روزهای وبلاگستان فارسی را انتخاب کنند و در هفتصد کلمه، به انگلیسی، بنویسند که وبلاگ‌دارهای درباره‌اش چه می‌گویند. لینک به پنج، شش وبلاگ و بازنویسی آنچه آنها از جنبه‌های گوناگون درباره‌ی آن موضوع نوشته‌اند با نقل قول مستقیم از آنها (البته ترجمه شده از فارسی) کافی است. دو سه جمله هم اول کار توضیح دهید که چرا این موضوع مهم است.

+ +

متن نمونه را به آدرس ایمیل من hoder@hoder.com و نیز برای افراد زیر تا روز دوشنبه بفرستید:
+ربکا : rmackinnon@cyber.law.harvard.edu
+هیثم: haitham.sabbah@gmail.com

]]>
+14641@http://editormyself.info +weblog +2005-10-12T14:04:23-05:00 +
+ + + +
+
\ 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 new file mode 100755 index 000000000..612186897 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/illformed_atom10.xml @@ -0,0 +1,13 @@ + + + + + Example author + me@example.com + http://example.com/ + + + + + + +Copyright 1997-1999 UserLand Software, Inc. +Thu, 08 Jul 1999 07:00:00 GMT +Thu, 08 Jul 1999 16:20:26 GMT +http://my.userland.com/stories/storyReader$11 +News and commentary from the cross-platform scripting community. +http://www.scripting.com/ +Scripting News + +http://www.scripting.com/ +Scripting News +http://www.scripting.com/gifs/tinyScriptingNews.gif +40 +78 +What is this used for? + +dave@userland.com (Dave Winer) +dave@userland.com (Dave Winer) +en-us + +6 +7 +8 +9 +10 +11 + + +Sunday + +(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)) + +stuff +http://bar +This is an article about some stuff + + +Search Now! +Enter your search <terms> +find +http://my.site.com/search.cgi + + + \ 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 new file mode 100755 index 000000000..cfe91691f --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss091-international.xml @@ -0,0 +1,30 @@ + + + + +膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ +http://www.mozilla.org +膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ +ja + +NYÒ™Á¢¸»ÌêÛì15285.25´ƒ´‘Á£´Û´—´ÀÁ¹´ê´Ì´éÒ™Ûì¡êçÒÕ‰ÌêÁ£ +http://www.mozilla.org/status/ +This is an item description... + + +‚§±Çç¡ËßÛÂҏéøÓ¸Á£Ë²®Ÿè†Ûè危ÇÌ’¡Íæ—éøë‡Á£ +http://www.mozilla.org/status/ +This is an item description... + + +ËÜË”ïÌëÈšÁ¢È†Ë§æàÀ豎ˉۂÁ¢Ë‚åܼšÛ˜íËüËÁ£ +http://www.mozilla.org/status/ +This is an item description... + + +2000‚øíŠåÁ¢«‘¦éÛ빏ېçéÛ§ÛÂè†ÒæÓ¸Á£Ì¾«…æ—ÕÝéøƒ¸Á£ +http://www.mozilla.org/status/ +This is an item description... + + + \ 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 new file mode 100755 index 000000000..f0964a227 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss091-simple.xml @@ -0,0 +1,15 @@ + + + + +en +News and commentary from the cross-platform scripting community. +http://www.scripting.com/ +Scripting News + +http://www.scripting.com/ +Scripting News +http://www.scripting.com/gifs/tinyScriptingNews.gif + + + \ 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 new file mode 100755 index 000000000..5d75c352b --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss092-sample.xml @@ -0,0 +1,103 @@ + + + + + Dave Winer: Grateful Dead + http://www.scripting.com/blog/categories/gratefulDead.html + 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. + Fri, 13 Apr 2001 19:23:02 GMT + http://backend.userland.com/rss092 + dave@userland.com (Dave Winer) + dave@userland.com (Dave Winer) + + + 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> + + + + + 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> + Scripting News + + + <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. + + + + This is a test of a change I just made. Still diggin.. + + + 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. + + + <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. + + + + 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.." + Scripting News + + + <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> + + + + + <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." + + + + <a href="http://arts.ucsc.edu/gdead/agdl/darkstar.html">Dark Star</a> crashes, pouring its light into ashes. + + + + DaveNet: <a href="http://davenet.userland.com/2001/01/21/theUsBlues">The U.S. Blues</a>. + + + 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> + + + <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> + + + + Grateful Dead: "Tennessee, Tennessee, ain't no place I'd rather be." + + + + 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." + + + + <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." + + + + <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." + + + + <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." + + + + + 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. + + + + 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." + + + A touch of gray, kinda suits you anyway.. + + + + <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> + + + + \ 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 new file mode 100755 index 000000000..0edecf58e --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss10-example1.xml @@ -0,0 +1,62 @@ + + + + + + XML.com + http://xml.com/pub + + XML.com features a rich mix of information and services + for the XML community. + + + + + + + + + + + + + + + + + XML.com + http://www.xml.com + http://xml.com/universal/images/xml_tiny.gif + + + + Processing Inclusions with XSLT + http://xml.com/pub/2000/08/09/xslt/xslt.html + + Processing document inclusions with general XML tools can be + problematic. This article proposes a way of preserving inclusion + information through SAX-based processing. + + + + + Putting RDF to Work + http://xml.com/pub/2000/08/09/rdfdb/index.html + + 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. + + + + + Search XML.com + Search XML.com's XML collection + s + http://search.xml.com + + + \ 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 new file mode 100755 index 000000000..26235f78f --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss10-example2.xml @@ -0,0 +1,67 @@ + + + + + + Meerkat + http://meerkat.oreillynet.com + Meerkat: An Open Wire Service + The O'Reilly Network + Rael Dornfest (mailto:rael@oreilly.com) + Copyright © 2000 O'Reilly & Associates, Inc. + 2000-01-01T12:00+00:00 + hourly + 2 + 2000-01-01T12:00+00:00 + + + + + + + + + + + + + + + Meerkat Powered! + http://meerkat.oreillynet.com/icons/meerkat-powered.jpg + http://meerkat.oreillynet.com + + + + XML: A Disruptive Technology + http://c.moreover.com/click/here.pl?r123 + + XML is placing increasingly heavy loads on the existing technical + infrastructure of the Internet. + + The O'Reilly Network + Simon St.Laurent (mailto:simonstl@simonstl.com) + Copyright © 2000 O'Reilly & Associates, Inc. + XML + XML.com + NASDAQ + XML + + + + Search Meerkat + Search Meerkat's RSS Database... + s + http://meerkat.oreillynet.com/ + search + regex + + + \ 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 new file mode 100755 index 000000000..53483cc51 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/rss2sample.xml @@ -0,0 +1,42 @@ + + + + Liftoff News + http://liftoff.msfc.nasa.gov/ + Liftoff to Space Exploration. + en-us + Tue, 10 Jun 2003 04:00:00 GMT + Tue, 10 Jun 2003 09:41:01 GMT + http://blogs.law.harvard.edu/tech/rss + Weblog Editor 2.0 + editor@example.com + webmaster@example.com + + Star City + http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp + 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>. + Tue, 03 Jun 2003 09:39:21 GMT + http://liftoff.msfc.nasa.gov/2003/06/03.html#item573 + + + 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. + Fri, 30 May 2003 11:06:42 GMT + http://liftoff.msfc.nasa.gov/2003/05/30.html#item572 + + + The Engine That Does More + http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp + 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. + Tue, 27 May 2003 08:37:32 GMT + http://liftoff.msfc.nasa.gov/2003/05/27.html#item571 + Test content

]]>
+
+ + Astronauts' Dirty Laundry + http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp + Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options. + Tue, 20 May 2003 08:56:02 GMT + http://liftoff.msfc.nasa.gov/2003/05/20.html#item570 + +
+
\ 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 new file mode 100755 index 000000000..f8a04bba5 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/sixapart-jp.xml @@ -0,0 +1,226 @@ + + + +Six Apart - News +http://www.sixapart.jp/ + +ja +Copyright 2005 +Fri, 07 Oct 2005 19:09:34 +0900 +http://www.movabletype.org/?v=3.2-ja +http://blogs.law.harvard.edu/tech/rss + + +ファイブ・ディーが、Movable Typeでブログプロモーションをスタート +MIYAZAWAblog_banner.jpg
+ファイブ・ディーは、Movable Typeで構築したプロモーション ブログ『宮沢和史 中南米ツアーblog Latin America 2005』を開設しました。

+ +

9月21日に開設されたこのブログは、ブラジル、ホンジュラス、ニカラグア、メキシコ、キューバの5か国を巡る「Latin America 2005」ツアーに合わせ、そのツアーの模様を同行マネージャーがレポートしていきます。
+さらに今月2日からは宮沢和史自身が日々録音した声をPodcastingするという点でも、ブログを使ったユニークなプロモーションとなっています。

+ +

「宮沢和史 中南米ツアーblog Latin America 2005」

+ +

※シックス・アパートではこうしたブログを使ったプロモーションに最適な製品をご用意しております。
+

]]>
+http://www.sixapart.jp/news/2005/10/07-1909.html +http://www.sixapart.jp/news/2005/10/07-1909.html +news +Fri, 07 Oct 2005 19:09:34 +0900 +
+ +Movable Type 3.2日本語版の提供を開始 +Movable Type Logo

+

シックス・アパートは、Movable Type 3.2日本語版の提供を開始いたしました。
+ベータテストにご協力いただいた多くの皆様に、スタッフ一同、心から感謝いたします。

+

製品概要など、詳しくはプレスリリースをご参照下さい。

+

ご購入のご検討は、Movable Typeのご購入からどうぞ。

]]>
+http://www.sixapart.jp/news/2005/09/29-1530.html +http://www.sixapart.jp/news/2005/09/29-1530.html +news +Thu, 29 Sep 2005 15:30:00 +0900 +
+ +シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始 +<プレスリリース資料>

+ +

シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始 ~ スパムの自動判別機能や新ユーザー・インターフェースで、運用管理の機能を強化 ~

+

2005年9月29日
+シックス・アパート株式会社

+

ブログ・ソフトウェア大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、「Movable Type(ムーバブル・タイプ) 3.2 日本語版」(URL:http://www.sixapart.jp/movabletype/)を9月29日より提供開始いたします。

]]>
+http://www.sixapart.jp/press_releases/2005/09/29-1529.html +http://www.sixapart.jp/press_releases/2005/09/29-1529.html +Press Releases +Thu, 29 Sep 2005 15:29:00 +0900 +
+ +スタッフを募集しています +シックス・アパートはMovable TypeやTypePadの開発エンジニアなど、スタッフを広く募集しています。具体的な募集職種は次の通りです。

+ + + +

拡大を続ける、日本のブログ市場を積極的にリードする人材を、シックス・アパートは募集しています。上記以外の職種につきましても、お気軽にお問い合わせください。詳しい募集要項や応募方法については、求人情報のページをご覧ください。
+

]]>
+http://www.sixapart.jp/news/2005/09/27-0906.html +http://www.sixapart.jp/news/2005/09/27-0906.html +news +Tue, 27 Sep 2005 09:06:10 +0900 +
+ +サイト接続不具合に関するお詫びと復旧のお知らせ +9月24日(土)の14:45ごろから、同日18:30ごろまで、シックス・アパート社のウェブサイトが不安定になっており、断続的に接続できない不具合が発生しておりました。このため、この期間中にウェブサイトの閲覧や製品のダウンロードができませんでした。

+ +

なお現在は不具合は解消しております。みなさまにご迷惑をおかけしたことをお詫びいたします。

]]>
+http://www.sixapart.jp/news/2005/09/26-1000.html +http://www.sixapart.jp/news/2005/09/26-1000.html +news +Mon, 26 Sep 2005 10:00:56 +0900 +
+ +企業ブログ向けパッケージ「TypePad Promotion」を新発売 +シックス・アパートは、ウェブログ・サービスTypePadの企業ブログ向けパッケージ「TypePad Promotion」(タイプパッド・プロモーションの発売を10月下旬から開始いたします。

+ +

詳しくは、プレスリリースをご参照下さい。

]]>
+http://www.sixapart.jp/news/2005/09/20-1500.html +http://www.sixapart.jp/news/2005/09/20-1500.html +news +Tue, 20 Sep 2005 15:00:01 +0900 +
+ +シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売 +<プレスリリース資料>
+印刷用(PDF版)

+ +


+シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売
+~PR/IRサイトやキャンペーンサイトなど企業のプロモーションニーズに特化~
+

+2005年9月20日
+シックス・アパート株式会社

+ +

ブログ・サービス大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、法人向けプロモーションブログ・パッケージ「TypePad Promotion(タイプパッド・プロモーション)」(URL:http://www.sixapart.jp/typepad/typepad_promotion.html)を10月下旬より販売開始いたします。

]]>
+http://www.sixapart.jp/press_releases/2005/09/20-1500.html +http://www.sixapart.jp/press_releases/2005/09/20-1500.html +Press Releases +Tue, 20 Sep 2005 15:00:00 +0900 +
+ +Six [days] Apart Week +本日、9月16日はSix Apartの創業者ミナ・トロットの誕生日です。
+私たちの会社は、創業者のトロット夫妻(ベンとミナ)の誕生日が、6日離れていることからSix [days] Apart →Six Apartという風に名付けられています。本日から22日までの6日間を社名の由来となる Six [days] Apart Weekとして、私たちのプロダクトをご紹介させていただきます。

+ +

今日は、ブログ・サービスのTypePad(タイプパッド)をご紹介します。
+tp-logo.gif

+ +

TypePadは、米国PC MAGAZINE誌の2003年EDITOR'S CHOICE とBEST OF THE YEARに選ばれております。
+pcmag-ad.gif
+

]]>
+http://www.sixapart.jp/news/2005/09/16-1941.html +http://www.sixapart.jp/news/2005/09/16-1941.html +news +Fri, 16 Sep 2005 19:41:47 +0900 +
+ +ハイパーワークスが商用フォントを利用できるMovable Typeホスティングサービスを開始 +ソフト開発会社の有限会社ハイパーワークスは、商用フォントなど多彩なフォントをブログ上で利用できるブログ・サービス「Glyph-On!(グリフォン) Movable Type ホスティング サービス」の提供を開始しました。
+

]]>
+http://www.sixapart.jp/news/2005/09/14-1700.html +http://www.sixapart.jp/news/2005/09/14-1700.html +news +Wed, 14 Sep 2005 17:00:00 +0900 +
+ +Movable Type開発エンジニアの募集 + +勤務形態: フルタイム
+勤務地: 東京 (赤坂)
+職種: ソフトウェア・エンジニア
+職務内容: Movable Typeの開発業務全般
+募集人数: 若干名 +

]]>
+http://www.sixapart.jp/jobs/2005/09/13-0007.html +http://www.sixapart.jp/jobs/2005/09/13-0007.html +Jobs +Tue, 13 Sep 2005 00:07:00 +0900 +
+ +TypePad開発エンジニアの募集 + +勤務形態: フルタイム
+勤務地: 東京 (赤坂)
+職種: アプリケーション・エンジニア
+職務内容: TypePadのカスタマイズ、周辺開発
+募集人数: 若干名 +

]]>
+http://www.sixapart.jp/jobs/2005/09/13-0004.html +http://www.sixapart.jp/jobs/2005/09/13-0004.html +Jobs +Tue, 13 Sep 2005 00:04:00 +0900 +
+ +カスタマーサポート・ディレクターの募集 +勤務形態: フルタイム
+勤務地: 東京(赤坂)
+職種: カスタマーサポート・ディレクター
+職務内容: TypePadやMovable Typeのカスタマーサポート業務の統括
+募集人数: 若干名 +

+]]>
+http://www.sixapart.jp/jobs/2005/09/13-0003.html +http://www.sixapart.jp/jobs/2005/09/13-0003.html +Jobs +Tue, 13 Sep 2005 00:03:30 +0900 +
+ +アルバイト(マーケティング・広報アシスタント)の募集 +勤務形態: アルバイト
+勤務地: 東京(港区)
+職種:マーケティング・PRのアシスタント業務
+募集人数: 若干名
+時給:1000円~(但し、試用期間終了後に応相談)。交通費支給
+時間:平日10時30分~18時30分まで。週3日以上(応相談)
+

]]>
+http://www.sixapart.jp/jobs/2005/09/13-0002.html +http://www.sixapart.jp/jobs/2005/09/13-0002.html +Jobs +Tue, 13 Sep 2005 00:02:00 +0900 +
+ +アルバイト(開発アシスタント)の募集 +勤務形態: アルバイト
+勤務地: 東京(港区)
+職種: アプリケーション開発のアシスタント業務
+募集人数: 若干名
+時給:1000円~(但し、試用期間終了後に応相談)。交通費支給
+時間:平日10時30分~18時30分まで。週3日以上(応相談) +

]]>
+http://www.sixapart.jp/jobs/2005/09/13-0001.html +http://www.sixapart.jp/jobs/2005/09/13-0001.html +Jobs +Tue, 13 Sep 2005 00:01:00 +0900 +
+ +TypePad Japan がバージョンアップしました。 +「TypePad Japan(タイプパッドジャパン)」において、本日、「TypePad 1.6 日本語版」へのバージョンアップを行いました。最新版となる「TypePad 1.6 日本語版」では、ブログデザインの機能強化、ポッドキャスティング対応、モブログ対応に加え、今回新たに大幅な容量アップが行われております。皆様、新しくなったTypePad Japanにどうぞご期待ください。

+ +

なお、TypePadの携帯対応強化に関しましては、本日よりTypePad Japanのお客様を対象にオープン・ベータを開始しております。

+ +

2005年9月5日発表のTypePad日本語版 1.6プレスリリースはこちらをご覧下さい。

]]>
+http://www.sixapart.jp/news/2005/09/12-1953.html +http://www.sixapart.jp/news/2005/09/12-1953.html +news +Mon, 12 Sep 2005 19:53:07 +0900 +
+ + +
+
\ 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 new file mode 100755 index 000000000..6274a32cd --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/samples/technorati.feed @@ -0,0 +1,54 @@ + + + + [Technorati] Tag results for greenbelt + http://www.technorati.com/tag/greenbelt + Posts tagged with "greenbelt" on Technorati. + Mon, 08 Aug 2005 15:15:08 GMT + greenbelt + 2 + 2 + + Technorati v1.0 + + http://static.technorati.com/pix/logos/logo_reverse_sm.gif + Technorati logo + http://www.technorati.com + + + 1 + 7 + 9 + + support@technorati.com (Technorati Support) + http://blogs.law.harvad.edu/tech/rss + 60 + + Greenbelt + http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html + So if the plan goes according to plan (!)... I'll be speaking at Greenbelt at these times: Slot 1... + http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html + Mon, 18 Jul 2005 02:11:42 GMT + James + 2005-07-11 02:08:12 + http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fmaggidawn.typepad.com%2Fmaggidawn%2F2005%2F07%2Fgreenbelt.html + 190 + 237 + maggi dawn + + + + Walking along the Greenbelt + http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html + [IMG] Photo of homeless man walking near the greenbelt in Boise, Idaho Tags: photo homeless greenbelt Boise Idaho picture + http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html + Tue, 28 Jun 2005 01:41:24 GMT + 2005-06-26 17:24:03 + http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fpictureshomeless.blogspot.com%2F2005%2F06%2Fwalking-along-greenbelt.html + 2 + 2 + + + + diff --git a/plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc b/plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc new file mode 100755 index 000000000..e662d2626 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc @@ -0,0 +1,338 @@ +# -*- 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 new file mode 100755 index 000000000..725094788 --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc b/plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc new file mode 100755 index 000000000..c8633766f --- /dev/null +++ b/plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc @@ -0,0 +1,218 @@ + + + + + + + + http://purl.org/net/rss1.1#Channel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://purl.org/net/rss1.1#title + + + + + + + + + + + + + + http://purl.org/net/rss1.1#link + + + + + + + + + + http://purl.org/net/rss1.1#description + + + + + + + + + + + + + http://purl.org/net/rss1.1#image + + + + + + + + + + + + + + + + + + + + + + + + + http://purl.org/net/rss1.1#url + + + + + + + + + + http://purl.org/net/rss1.1#items + + + + + + + + + + + + + + + + + http://purl.org/net/rss1.1#item + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://purl.org/net/rss1.1#Any + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Resource + + + + + Collection + + + + diff --git a/plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch b/plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch new file mode 100644 index 000000000..c53bd9737 --- /dev/null +++ b/plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch @@ -0,0 +1,14 @@ +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/images/24px-Feed-icon.svg.png b/plugins/OStatus/images/24px-Feed-icon.svg.png new file mode 100644 index 000000000..317225814 Binary files /dev/null and b/plugins/OStatus/images/24px-Feed-icon.svg.png differ diff --git a/plugins/OStatus/images/48px-Feed-icon.svg.png b/plugins/OStatus/images/48px-Feed-icon.svg.png new file mode 100644 index 000000000..bd1da4f91 Binary files /dev/null and b/plugins/OStatus/images/48px-Feed-icon.svg.png differ diff --git a/plugins/OStatus/images/96px-Feed-icon.svg.png b/plugins/OStatus/images/96px-Feed-icon.svg.png new file mode 100644 index 000000000..bf16571ec Binary files /dev/null and b/plugins/OStatus/images/96px-Feed-icon.svg.png differ diff --git a/plugins/OStatus/images/README b/plugins/OStatus/images/README new file mode 100644 index 000000000..d9379c23e --- /dev/null +++ b/plugins/OStatus/images/README @@ -0,0 +1,5 @@ +Feed icon rendered from http://commons.wikimedia.org/wiki/File:Feed-icon.svg + +Originally distributed by the Mozilla Foundation under a MPL/GPL/LGPL tri-license: + +http://www.mozilla.org/MPL/boilerplate-1.1/mpl-tri-license-html diff --git a/plugins/OStatus/lib/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php new file mode 100644 index 000000000..9bc7892fb --- /dev/null +++ b/plugins/OStatus/lib/feeddiscovery.php @@ -0,0 +1,221 @@ +. + */ + +/** + * @package FeedSubPlugin + * @maintainer Brion Vibber + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +class FeedSubBadURLException extends FeedSubException +{ +} + +class FeedSubBadResponseException extends FeedSubException +{ +} + +class FeedSubEmptyException extends FeedSubException +{ +} + +class FeedSubBadHTMLException extends FeedSubException +{ +} + +class FeedSubUnrecognizedTypeException extends FeedSubException +{ +} + +class FeedSubNoFeedException extends FeedSubException +{ +} + +/** + * Given a web page or feed URL, discover the final location of the feed + * and return its current contents. + * + * @example + * $feed = new FeedDiscovery(); + * if ($feed->discoverFromURL($url)) { + * print $feed->uri; + * print $feed->type; + * processFeed($feed->body); + * } + */ +class FeedDiscovery +{ + public $uri; + public $type; + public $body; + + + public function feedMunger() + { + require_once 'XML/Feed/Parser.php'; + $feed = new XML_Feed_Parser($this->body, false, false, true); // @fixme + return new FeedMunger($feed, $this->uri); + } + + /** + * @param string $url + * @param bool $htmlOk pass false here if you don't want to follow web pages. + * @return string with validated URL + * @throws FeedSubBadURLException + * @throws FeedSubBadHtmlException + * @throws FeedSubNoFeedException + * @throws FeedSubEmptyException + * @throws FeedSubUnrecognizedTypeException + */ + function discoverFromURL($url, $htmlOk=true) + { + try { + $client = new HTTPClient(); + $response = $client->get($url); + } catch (HTTP_Request2_Exception $e) { + throw new FeedSubBadURLException($e); + } + + if ($htmlOk) { + $type = $response->getHeader('Content-Type'); + $isHtml = preg_match('!^(text/html|application/xhtml\+xml)!i', $type); + if ($isHtml) { + $target = $this->discoverFromHTML($response->getUrl(), $response->getBody()); + if (!$target) { + throw new FeedSubNoFeedException($url); + } + return $this->discoverFromURL($target, false); + } + } + + return $this->initFromResponse($response); + } + + function initFromResponse($response) + { + if (!$response->isOk()) { + throw new FeedSubBadResponseException($response->getCode()); + } + + $sourceurl = $response->getUrl(); + $body = $response->getBody(); + if (!$body) { + throw new FeedSubEmptyException($sourceurl); + } + + $type = $response->getHeader('Content-Type'); + if (preg_match('!^(text/xml|application/xml|application/(rss|atom)\+xml)!i', $type)) { + $this->uri = $sourceurl; + $this->type = $type; + $this->body = $body; + return true; + } else { + common_log(LOG_WARNING, "Unrecognized feed type $type for $sourceurl"); + throw new FeedSubUnrecognizedTypeException($type); + } + } + + /** + * @param string $url source URL, used to resolve relative links + * @param string $body HTML body text + * @return mixed string with URL or false if no target found + */ + function discoverFromHTML($url, $body) + { + // DOMDocument::loadHTML may throw warnings on unrecognized elements. + $old = error_reporting(error_reporting() & ~E_WARNING); + $dom = new DOMDocument(); + $ok = $dom->loadHTML($body); + error_reporting($old); + + if (!$ok) { + throw new FeedSubBadHtmlException(); + } + + // Autodiscovery links may be relative to the page's URL or + $base = false; + $nodes = $dom->getElementsByTagName('base'); + for ($i = 0; $i < $nodes->length; $i++) { + $node = $nodes->item($i); + if ($node->hasAttributes()) { + $href = $node->attributes->getNamedItem('href'); + if ($href) { + $base = trim($href->value); + } + } + } + if ($base) { + $base = $this->resolveURI($base, $url); + } else { + $base = $url; + } + + // Ok... now on to the links! + // @fixme merge with the munger link checks + $nodes = $dom->getElementsByTagName('link'); + for ($i = 0; $i < $nodes->length; $i++) { + $node = $nodes->item($i); + if ($node->hasAttributes()) { + $rel = $node->attributes->getNamedItem('rel'); + $type = $node->attributes->getNamedItem('type'); + $href = $node->attributes->getNamedItem('href'); + if ($rel && $type && $href) { + $rel = trim($rel->value); + $type = trim($type->value); + $href = trim($href->value); + + $feedTypes = array( + 'application/rss+xml', + 'application/atom+xml', + ); + if (trim($rel) == 'alternate' && in_array($type, $feedTypes)) { + return $this->resolveURI($href, $base); + } + } + } + } + + return false; + } + + /** + * Resolve a possibly relative URL against some absolute base URL + * @param string $rel relative or absolute URL + * @param string $base absolute URL + * @return string absolute URL, or original URL if could not be resolved. + */ + function resolveURI($rel, $base) + { + require_once "Net/URL2.php"; + try { + $relUrl = new Net_URL2($rel); + if ($relUrl->isAbsolute()) { + return $rel; + } + $baseUrl = new Net_URL2($base); + $absUrl = $baseUrl->resolve($relUrl); + return $absUrl->getURL(); + } catch (Exception $e) { + common_log(LOG_WARNING, 'Unable to resolve relative link "' . + $rel . '" against base "' . $base . '": ' . $e->getMessage()); + return $rel; + } + } +} diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php new file mode 100644 index 000000000..eeb8d2df3 --- /dev/null +++ b/plugins/OStatus/lib/feedmunger.php @@ -0,0 +1,270 @@ +. + */ + +/** + * @package FeedSubPlugin + * @maintainer Brion Vibber + */ + +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 feedinfo() + { + $feedinfo = new Feedinfo(); + $feedinfo->feeduri = $this->url; + $feedinfo->homeuri = $this->feed->link; + $feedinfo->huburi = $this->getHubLink(); + return $feedinfo; + } + + function getAtomLink($item, $attribs=array()) + { + // XML_Feed_Parser gets confused by multiple elements. + $dom = $item->model; + + // Note that RSS feeds would embed an so this should work for both. + /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds + // + $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 elements. + $dom = $item->model; + + // Note that RSS feeds would embed an so this should work for both. + /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds + // + $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')); + } + + /** + * 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(); + } + + $link = $this->getAltLink($entry); + if (empty($link)) { + if (preg_match('!^https?://!', $entry->id)) { + $link = $entry->id; + common_log(LOG_DEBUG, "No link on entry, using URL from id: $link"); + } + } + $notice->uri = $link; + $notice->url = $link; + $notice->content = $this->noticeFromEntry($entry); + $notice->rendered = common_render_content($notice->content, $notice); + $notice->created = common_sql_date($entry->updated); // @fixme + $notice->is_local = Notice::GATEWAY; + $notice->source = 'feed'; + + return $notice; + } + + /** + * @param XML_Feed_Type $entry + * @return string notice text, within post size limit + */ + function noticeFromEntry($entry) + { + $title = $entry->title; + $link = $entry->link; + + // @todo We can get entries like this: + // $cats = $entry->getCategory('category', array(0, true)); + // but it feels like an awful hack. If it's accessible cleanly, + // try adding #hashtags from the categories/tags on a post. + + // @todo Should we force a language here? + $format = _m('New post: "%1$s" %2$s'); + $title = $entry->title; + $link = $this->getAltLink($entry); + $out = sprintf($format, $title, $link); + + // Trim link if needed... + $max = Notice::maxContent(); + if (mb_strlen($out) > $max) { + $link = common_shorten_url($link); + $out = sprintf($format, $title, $link); + } + + // Trim title if needed... + if (mb_strlen($out) > $max) { + $ellipsis = "\xe2\x80\xa6"; // U+2026 HORIZONTAL ELLIPSIS + $used = mb_strlen($out) - mb_strlen($title); + $available = $max - $used - mb_strlen($ellipsis); + $title = mb_substr($title, 0, $available) . $ellipsis; + $out = sprintf($format, $title, $link); + } + + return $out; + } +} diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php new file mode 100644 index 000000000..126f1355f --- /dev/null +++ b/plugins/OStatus/lib/hubdistribqueuehandler.php @@ -0,0 +1,87 @@ +. + */ + +/** + * Send a PuSH subscription verification from our internal hub. + * Queue up final distribution for + * @package Hub + * @author Brion Vibber + */ +class HubDistribQueueHandler extends QueueHandler +{ + function transport() + { + return 'hubdistrib'; + } + + function handle($notice) + { + assert($notice instanceof Notice); + + // See if there's any PuSH subscriptions, including OStatus clients. + // @fixme handle group subscriptions as well + // http://identi.ca/api/statuses/user_timeline/1.atom + $feed = common_local_url('ApiTimelineUser', + array('id' => $notice->profile_id, + 'format' => 'atom')); + $sub = new HubSub(); + $sub->topic = $feed; + if ($sub->find()) { + common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $feed"); + $qm = QueueManager::get(); + $atom = $this->userFeedForNotice($notice); + while ($sub->fetch()) { + common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $feed"); + $data = array('sub' => clone($sub), + 'atom' => $atom); + $qm->enqueue($data, 'hubout'); + } + } else { + common_log(LOG_INFO, "No PuSH subscribers for $feed"); + } + } + + /** + * Build a single-item version of the sending user's Atom feed. + * @param Notice $notice + * @return string + */ + function userFeedForNotice($notice) + { + // @fixme this feels VERY hacky... + // should probably be a cleaner way to do it + + ob_start(); + $api = new ApiTimelineUserAction(); + $api->prepare(array('id' => $notice->profile_id, + 'format' => 'atom', + 'max_id' => $notice->id, + 'since_id' => $notice->id - 1)); + $api->showTimeline(); + $feed = ob_get_clean(); + + // ...and override the content-type back to something normal... eww! + // hope there's no other headers that got set while we weren't looking. + header('Content-Type: text/html; charset=utf-8'); + + common_log(LOG_DEBUG, $feed); + return $feed; + } +} + diff --git a/plugins/OStatus/lib/huboutqueuehandler.php b/plugins/OStatus/lib/huboutqueuehandler.php new file mode 100644 index 000000000..cb44ad2c4 --- /dev/null +++ b/plugins/OStatus/lib/huboutqueuehandler.php @@ -0,0 +1,52 @@ +. + */ + +/** + * Send a raw PuSH atom update from our internal hub. + * @package Hub + * @author Brion Vibber + */ +class HubOutQueueHandler extends QueueHandler +{ + function transport() + { + return 'hubout'; + } + + function handle($data) + { + $sub = $data['sub']; + $atom = $data['atom']; + + assert($sub instanceof HubSub); + assert(is_string($atom)); + + try { + $sub->push($atom); + } catch (Exception $e) { + common_log(LOG_ERR, "Failed PuSH to $sub->callback for $sub->topic: " . + $e->getMessage()); + // @fixme Reschedule a later delivery? + // Currently we have no way to do this other than 'send NOW' + } + + return true; + } +} + diff --git a/plugins/OStatus/lib/hubverifyqueuehandler.php b/plugins/OStatus/lib/hubverifyqueuehandler.php new file mode 100644 index 000000000..125d13a77 --- /dev/null +++ b/plugins/OStatus/lib/hubverifyqueuehandler.php @@ -0,0 +1,53 @@ +. + */ + +/** + * Send a PuSH subscription verification from our internal hub. + * @package Hub + * @author Brion Vibber + */ +class HubVerifyQueueHandler extends QueueHandler +{ + function transport() + { + return 'hubverify'; + } + + function handle($data) + { + $sub = $data['sub']; + $mode = $data['mode']; + + assert($sub instanceof HubSub); + assert($mode === 'subscribe' || $mode === 'unsubscribe'); + + common_log(LOG_INFO, __METHOD__ . ": $mode $sub->callback $sub->topic"); + try { + $sub->verify($mode); + } catch (Exception $e) { + common_log(LOG_ERR, "Failed PuSH $mode verify to $sub->callback for $sub->topic: " . + $e->getMessage()); + // @fixme schedule retry? + // @fixme just kill it? + } + + return true; + } +} + diff --git a/plugins/OStatus/locale/OStatus.po b/plugins/OStatus/locale/OStatus.po new file mode 100644 index 000000000..dedc018e3 --- /dev/null +++ b/plugins/OStatus/locale/OStatus.po @@ -0,0 +1,104 @@ +# 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 , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2009-12-07 20:38-0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: tests/gettext-speedtest.php:57 FeedSubPlugin.php:76 +msgid "Feeds" +msgstr "" + +#: FeedSubPlugin.php:77 +msgid "Feed subscription options" +msgstr "" + +#: feedmunger.php:215 +#, php-format +msgid "New post: \"%1$s\" %2$s" +msgstr "" + +#: actions/feedsubsettings.php:41 +msgid "Feed subscriptions" +msgstr "" + +#: actions/feedsubsettings.php:52 +msgid "" +"You can subscribe to feeds from other sites; updates will appear in your " +"personal timeline." +msgstr "" + +#: actions/feedsubsettings.php:96 +msgid "Subscribe" +msgstr "" + +#: actions/feedsubsettings.php:98 +msgid "Continue" +msgstr "" + +#: actions/feedsubsettings.php:151 +msgid "Empty feed URL!" +msgstr "" + +#: actions/feedsubsettings.php:161 +msgid "Invalid URL or could not reach server." +msgstr "" + +#: actions/feedsubsettings.php:164 +msgid "Cannot read feed; server returned error." +msgstr "" + +#: actions/feedsubsettings.php:167 +msgid "Cannot read feed; server returned an empty page." +msgstr "" + +#: actions/feedsubsettings.php:170 +msgid "Bad HTML, could not find feed link." +msgstr "" + +#: actions/feedsubsettings.php:173 +msgid "Could not find a feed linked from this URL." +msgstr "" + +#: actions/feedsubsettings.php:176 +msgid "Not a recognized feed type." +msgstr "" + +#: actions/feedsubsettings.php:180 +msgid "Bad feed URL." +msgstr "" + +#: actions/feedsubsettings.php:188 +msgid "Feed is not PuSH-enabled; cannot subscribe." +msgstr "" + +#: actions/feedsubsettings.php:208 +msgid "Feed subscription failed! Bad response from hub." +msgstr "" + +#: actions/feedsubsettings.php:218 +msgid "Already subscribed!" +msgstr "" + +#: actions/feedsubsettings.php:220 +msgid "Feed subscribed!" +msgstr "" + +#: actions/feedsubsettings.php:222 +msgid "Feed subscription failed!" +msgstr "" + +#: actions/feedsubsettings.php:231 +msgid "Previewing feed:" +msgstr "" diff --git a/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po b/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po new file mode 100644 index 000000000..f17dfa50a --- /dev/null +++ b/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po @@ -0,0 +1,106 @@ +# 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 , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2009-12-07 14:14-0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: FeedSubPlugin.php:77 +msgid "Feeds" +msgstr "Flux" + +#: FeedSubPlugin.php:78 +msgid "Feed subscription options" +msgstr "Préférences pour abonnement flux" + +#: feedmunger.php:215 +#, php-format +msgid "New post: \"%1$s\" %2$s" +msgstr "Nouveau: \"%1$s\" %2$s" + +#: actions/feedsubsettings.php:41 +msgid "Feed subscriptions" +msgstr "Abonnements aux fluxes" + +#: actions/feedsubsettings.php:52 +msgid "" +"You can subscribe to feeds from other sites; updates will appear in your " +"personal timeline." +msgstr "" +"Abonner aux fluxes RSS ou Atom des autres sites web; les temps se trouverair" +"en votre flux personnel." + +#: actions/feedsubsettings.php:96 +msgid "Subscribe" +msgstr "Abonner" + +#: actions/feedsubsettings.php:98 +msgid "Continue" +msgstr "Prochaine" + +#: actions/feedsubsettings.php:151 +msgid "Empty feed URL!" +msgstr "" + +#: actions/feedsubsettings.php:161 +msgid "Invalid URL or could not reach server." +msgstr "" + +#: actions/feedsubsettings.php:164 +msgid "Cannot read feed; server returned error." +msgstr "" + +#: actions/feedsubsettings.php:167 +msgid "Cannot read feed; server returned an empty page." +msgstr "" + +#: actions/feedsubsettings.php:170 +msgid "Bad HTML, could not find feed link." +msgstr "" + +#: actions/feedsubsettings.php:173 +msgid "Could not find a feed linked from this URL." +msgstr "" + +#: actions/feedsubsettings.php:176 +msgid "Not a recognized feed type." +msgstr "" + +#: actions/feedsubsettings.php:180 +msgid "Bad feed URL." +msgstr "" + +#: actions/feedsubsettings.php:188 +msgid "Feed is not PuSH-enabled; cannot subscribe." +msgstr "" + +#: actions/feedsubsettings.php:208 +msgid "Feed subscription failed! Bad response from hub." +msgstr "" + +#: actions/feedsubsettings.php:218 +msgid "Already subscribed!" +msgstr "" + +#: actions/feedsubsettings.php:220 +msgid "Feed subscribed!" +msgstr "" + +#: actions/feedsubsettings.php:222 +msgid "Feed subscription failed!" +msgstr "" + +#: actions/feedsubsettings.php:231 +msgid "Previewing feed:" +msgstr "" diff --git a/plugins/OStatus/tests/FeedDiscoveryTest.php b/plugins/OStatus/tests/FeedDiscoveryTest.php new file mode 100644 index 000000000..1c5249701 --- /dev/null +++ b/plugins/OStatus/tests/FeedDiscoveryTest.php @@ -0,0 +1,111 @@ +discoverFromHTML($url, $html); + $this->assertEquals($expected, $url); + } + + static public function provider() + { + $sampleHeader = << + + + + + +leŭksman + + + + + + + + + + + + + + + + + + + + + +END; + return array( + array('http://example.com/', + '', + 'http://example.com/feed/rss'), + array('http://example.com/atom', + '', + 'http://example.com/feed/atom'), + array('http://example.com/empty', + '', + false), + array('http://example.com/tagsoup', + '
',
+                           'http://example.com/feed/rss'),
+                     array('http://example.com/relative/link2',
+                           '',
+                           'http://example.com/feed/rss'),
+                     array('http://example.com/relative/link3',
+                           '',
+                           'http://example.com/feed/rss'),
+                     array('http://example.com/base/link1',
+                           '',
+                           'http://target.example.com/feed/rss'),
+                     array('http://example.com/base/link2',
+                           '',
+                           'http://target.example.com/feed/rss'),
+                     array('http://example.com/base/link3',
+                           '',
+                           'http://target.example.com/feed/rss'),
+                     // Trick question! There's a  but no href on it
+                     array('http://example.com/relative/fauxbase',
+                           '',
+                           'http://example.com/feed/rss'),
+                     // Actual WordPress blog header example
+                     array('http://leuksman.com/log/',
+                           $sampleHeader,
+                           'http://leuksman.com/log/feed/'));
+    }
+}
diff --git a/plugins/OStatus/tests/FeedMungerTest.php b/plugins/OStatus/tests/FeedMungerTest.php
new file mode 100644
index 000000000..0ce24c9fb
--- /dev/null
+++ b/plugins/OStatus/tests/FeedMungerTest.php
@@ -0,0 +1,147 @@
+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('A fairly short titlehttp://example.com/short/link', 0,
+                           'New post: "A fairly short title" http://example.com/short/link'),
+                     // Requires URL shortening ...
+                     array('A fairly short titlehttp://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh', 0,
+                           'New post: "A fairly short title" http://ur1.ca/g2o1'),
+                     array('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...http://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh', 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 . <<
+
+
+	leŭksman
+	
+	http://leuksman.com/log
+	reticula, electronica, & oddities
+
+	Thu, 12 Nov 2009 17:44:42 +0000
+	http://wordpress.org/?v=2.8.6
+	en
+	hourly
+	1
+			
+
+		Compiling PHP on Snow Leopard
+		http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/
+		http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/#comments
+		Thu, 12 Nov 2009 17:44:42 +0000
+		brion
+				
+
+		
+
+		http://leuksman.com/log/?p=649
+		
+			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 should work from here…

+

For reference, here’s the whole configure line I currently use; MySQL is installed from the downloadable installer; other deps from MacPorts:

+

‘./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

+]]>
+ http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/feed/ + 0 +
+
+ +END; + return $samplefeed; + } + + static protected function samplefeedBlogspot() + { + return <<tag:blogger.com,1999:blog-77800835085316971672009-11-19T12:56:11.233-08:00Brion's Cool Test Blogbrionhttp://www.blogger.com/profile/12932299467049762017noreply@blogger.comBlogger2125tag:blogger.com,1999:blog-7780083508531697167.post-84566718790002906772009-11-19T12:55:00.000-08:002009-11-19T12:56:11.241-08:00I love postingIt'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>brionhttp://www.blogger.com/profile/12932299467049762017noreply@blogger.com0tag:blogger.com,1999:blog-7780083508531697167.post-82022969178973466332009-11-18T13:52:00.001-08:002009-11-18T13:52:48.444-08:00Hey dudetestingggggggggg<div class="blogger-post-footer"><img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7780083508531697167-8202296917897346633?l=briontest.blogspot.com' alt='' /></div>brionhttp://www.blogger.com/profile/12932299467049762017noreply@blogger.com0 +END; + } +} diff --git a/plugins/OStatus/tests/gettext-speedtest.php b/plugins/OStatus/tests/gettext-speedtest.php new file mode 100644 index 000000000..8bbdf5e89 --- /dev/null +++ b/plugins/OStatus/tests/gettext-speedtest.php @@ -0,0 +1,78 @@ + $bits) { + list($time, $result) = $bits; + $ms = $time * 1000.0; + printf("%10s %2.4fms %s\n", $func, $ms, $result); +} + + +function fake($str) { + return $str; +} + -- cgit v1.2.3-54-g00ecf From 21c0e75a2e52d63eb46de6f5938b00c4c9ba8323 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 5 Feb 2010 21:39:29 -0800 Subject: Store Twitter screen_name, not name, for foreign_user.nickname when saving Twitter user. --- plugins/TwitterBridge/twitterauthorization.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/TwitterBridge/twitterauthorization.php b/plugins/TwitterBridge/twitterauthorization.php index b2657ff61..dbef438a4 100644 --- a/plugins/TwitterBridge/twitterauthorization.php +++ b/plugins/TwitterBridge/twitterauthorization.php @@ -219,7 +219,7 @@ class TwitterauthorizationAction extends Action $user = common_current_user(); $this->saveForeignLink($user->id, $twitter_user->id, $atok); - save_twitter_user($twitter_user->id, $twitter_user->name); + save_twitter_user($twitter_user->id, $twitter_user->screen_name); } else { -- cgit v1.2.3-54-g00ecf From c83d0b5e98fc6e59632a0fa1335b3586996929e2 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Sat, 6 Feb 2010 06:46:00 +0000 Subject: Delete old Twitter user record when user changes screen name instead of updating. Simpler. --- plugins/TwitterBridge/twitter.php | 54 ++++++--------------------------------- 1 file changed, 8 insertions(+), 46 deletions(-) diff --git a/plugins/TwitterBridge/twitter.php b/plugins/TwitterBridge/twitter.php index 33dfb788b..de30d9ebf 100644 --- a/plugins/TwitterBridge/twitter.php +++ b/plugins/TwitterBridge/twitter.php @@ -26,38 +26,6 @@ define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1 require_once INSTALLDIR . '/plugins/TwitterBridge/twitterbasicauthclient.php'; require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php'; -function updateTwitter_user($twitter_id, $screen_name) -{ - $uri = 'http://twitter.com/' . $screen_name; - $fuser = new Foreign_user(); - - $fuser->query('BEGIN'); - - // Dropping down to SQL because regular DB_DataObject udpate stuff doesn't seem - // to work so good with tables that have multiple column primary keys - - // Any time we update the uri for a forein user we have to make sure there - // are no dupe entries first -- unique constraint on the uri column - - $qry = 'UPDATE foreign_user set uri = \'\' WHERE uri = '; - $qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE; - - $fuser->query($qry); - - // Update the user - - $qry = 'UPDATE foreign_user SET nickname = '; - $qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' '; - $qry .= 'WHERE id = ' . $twitter_id . ' AND service = ' . TWITTER_SERVICE; - - $fuser->query('COMMIT'); - - $fuser->free(); - unset($fuser); - - return true; -} - function add_twitter_user($twitter_id, $screen_name) { @@ -105,7 +73,6 @@ function add_twitter_user($twitter_id, $screen_name) // Creates or Updates a Twitter user function save_twitter_user($twitter_id, $screen_name) { - // Check to see whether the Twitter user is already in the system, // and update its screen name and uri if so. @@ -115,25 +82,20 @@ function save_twitter_user($twitter_id, $screen_name) $result = true; - // Only update if Twitter screen name has changed + // Delete old record if Twitter user changed screen name if ($fuser->nickname != $screen_name) { - $result = updateTwitter_user($twitter_id, $screen_name); - - common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' . - "$fuser->id to $screen_name, was $fuser->nickname"); + $oldname = $fuser->nickname; + $fuser->delete(); + common_log(LOG_INFO, sprintf('Twitter bridge - Updated nickname (and URI) ' . + 'for Twitter user %1$d - %2$s, was %3$s.', + $fuser->id, + $screen_name, + $oldname)); } - return $result; - - } else { return add_twitter_user($twitter_id, $screen_name); } - - $fuser->free(); - unset($fuser); - - return true; } function is_twitter_bound($notice, $flink) { -- cgit v1.2.3-54-g00ecf From 9cac8eaae5315f64e024d22119bc627e9bdd6141 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 1 Feb 2010 13:44:06 -0500 Subject: readme and version for beta5 --- README | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- lib/common.php | 2 +- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/README b/README index 4e576dcdd..9b4147645 100644 --- a/README +++ b/README @@ -2,8 +2,8 @@ README ------ -StatusNet 0.9.0 ("Stand") Beta 4 -27 Jan 2010 +StatusNet 0.9.0 ("Stand") Beta 5 +1 Feb 2010 This is the README file for StatusNet (formerly Laconica), the Open Source microblogging platform. It includes installation instructions, @@ -78,6 +78,11 @@ New this version ================ This is a major feature release since version 0.8.2, released Nov 1 2009. +It is also a security release since 0.9.0beta4 January 27 2010. Beta +users are strongly encouraged to upgrade to deal with a security alert. + +http://status.net/wiki/Security_alert_0000002 + Notable changes this version: - Records of deleted notices are stored without the notice content. @@ -198,6 +203,77 @@ Notable changes this version: - Major refactoring of queue handlers to manage very large hosting site (like status.net) - SubscriptionThrottle plugin to prevent subscription spamming +- Don't enqueue into plugin or SMS queues when disabled (breaks unqueuehandler if SMS queue isn't attached) +- Improve name validation checks on local File references +- fix local file include vulnerability in doc.php +- Reusing fixed selector name for 'processing' in util.js +- Removed hAtom pattern from registration page. +- restructuring of User::registerNew() lost password munging +- Add a script to clear the cache for a given key +- buggy fetch for site owner +- Added missing concat of in Realtime response +- Updated XHR binded events to work better in jQuery 1.4.1. Using .live() for event delegation instead of jQuery.data() and checking to see if an element was previously binded. +- Updated jQuery Form Plugin from v2.17 to v2.36 +- Updated jQuery JavaScript Library from v1.3.2 to v1.4.1 +- move schema.type.php to typeschema.php like other files +- Add Really Simple Discovery (RSD) support +- Add a robots.txt URL to the site root +- error clearing tags for profiles from memcached +- on exceptions, stomp logs the error and reenqueues +- add lat, lon, location and remove closing tag from geocode.php +- Use passed-in lat long in geocode.php +- better handling of null responses from geonames.org +- Globalized form notice data geo values +- Using jQuery chaining in FormNoticeXHR +- Using form object instead of form_id and find(). Slightly faster and easier to read. +- removed describeTable from base class, and fixed it up in pgsql +- getTableDef() mostly working in postgres +- move the schema DDL sql off into seperate files for each db we support +- plugin to limit number of registered users +- add hooks for user registration +- live fast, die young in bash scripts +- for single-user mode, retrieve either site owner or defined nickname +- method to get the site owner +- define a constant for the 'owner' role of a site +- add simple cache getter/setter static functions to Memcached_DataObject +- Adds notice author's name to @title in Realtime response +- Hides .author from XHR response in showstream +- Hides .author from XHR response in showstream +- Fix more fatal errors in queue edge cases +- Don't attempt to resend XMPP messages that can't be broadcast due to the profile being deleted. +- Wrap each bit of distrib queue handler's saving operation in a try/catch; log exceptions but let everything else continue. +- Log exceptions from queuedaemon.php if they're not already caught +- Move sessions settings to its own panel +- Fixes for status_network db object .ini and tag setter script +- Add a script to set tags for sites +- Adjust API authentication to also check for OAuth protocol params in the HTTP Authorization header, as defined in OAuth HTTP Authorization Scheme. +- Last-chance distribution if enqueueing fails +- Manual failover for stomp queues. +- lost config in index.php made all traffic go to master +- "Revert "move RW setup above user get in index.php so remember_me works"" +- Revert "move RW setup above user get in index.php so remember_me works" +- move RW setup above user get in index.php so remember_me works +- hide most DB_DataObject errors +- always set up database_rw, regardless, so cached sessions work +- update mysqltimestamps on insert and update +- additional debugging data for Sessions +- 'Sign in with Twitter' button img +- Update to biz theme +- Remove redundant session token field from form (was already being added by base class). +- 'Sign in with Twitter' button img +- Can now set $config['queue']['stomp_persistent'] = false; to explicitly disable persistence when we queue items +- Showing processing indicator for form_repeat on submit instead of form +- Removed avatar from repeat of username (matches noticelist) +- Removed unused variable assignment for avatar URL and added missing fn +- Don't preemptively close existing DB connections for web views (needed to keep # of conns from going insane on multi-site queue daemons, so just doing for CLI) May, or may not, help with mystery session problems +- dropping the setcookie() call from common_ensure_session() since we're pretty sure it's unnecessary +- append '/' on cookie path for now (may still need some refactoring) +- set session cookie correctly +- Fix for Mapstraction plugin's zoomed map links +- debug log line for control channel sub +- Move faceboookapp.js to the Facebook plugin +- fix for fix for bad realtime JS load +- default 24-hour expiry on Memcached objects where not specified. Prerequisites ============= diff --git a/lib/common.php b/lib/common.php index b482464aa..b95cd1175 100644 --- a/lib/common.php +++ b/lib/common.php @@ -22,7 +22,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } //exit with 200 response, if this is checking fancy from the installer if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') { exit; } -define('STATUSNET_VERSION', '0.9.0beta4'); +define('STATUSNET_VERSION', '0.9.0beta5'); define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility define('STATUSNET_CODENAME', 'Stand'); -- cgit v1.2.3-54-g00ecf From 384387c9b05aefb438f5dbe7e272b1f234ede172 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Feb 2010 14:06:36 -0800 Subject: OStatus cleanup... * Treat linkless feed posts as status updates; drop the "New post:" prefix and quotes on them. * Use stable user IDs for atom/rss2 feed links instead of unstable nicknames * Pull Atom feed preferentially when subscribing -- can now put the remote user's profile page straight into the feed subscription form and get to the right place. * Clean up naming for push endpoints --- actions/showstream.php | 4 +- classes/Notice.php | 4 + lib/util.php | 3 + plugins/OStatus/OStatusPlugin.php | 8 +- plugins/OStatus/actions/feedsubcallback.php | 105 ----------------- plugins/OStatus/actions/hub.php | 176 ---------------------------- plugins/OStatus/actions/pushcallback.php | 105 +++++++++++++++++ plugins/OStatus/actions/pushhub.php | 176 ++++++++++++++++++++++++++++ plugins/OStatus/classes/Feedinfo.php | 2 +- plugins/OStatus/lib/feeddiscovery.php | 22 +++- plugins/OStatus/lib/feedmunger.php | 47 +++++--- 11 files changed, 340 insertions(+), 312 deletions(-) delete mode 100644 plugins/OStatus/actions/feedsubcallback.php delete mode 100644 plugins/OStatus/actions/hub.php create mode 100644 plugins/OStatus/actions/pushcallback.php create mode 100644 plugins/OStatus/actions/pushhub.php diff --git a/actions/showstream.php b/actions/showstream.php index 07cc68b76..f9407e35a 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -131,14 +131,14 @@ class ShowstreamAction extends ProfileAction new Feed(Feed::RSS2, common_local_url('ApiTimelineUser', array( - 'id' => $this->user->nickname, + 'id' => $this->user->id, 'format' => 'rss')), sprintf(_('Notice feed for %s (RSS 2.0)'), $this->user->nickname)), new Feed(Feed::ATOM, common_local_url('ApiTimelineUser', array( - 'id' => $this->user->nickname, + 'id' => $this->user->id, 'format' => 'atom')), sprintf(_('Notice feed for %s (Atom)'), $this->user->nickname)), diff --git a/classes/Notice.php b/classes/Notice.php index f9f386357..fca1c599c 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1176,6 +1176,10 @@ class Notice extends Memcached_DataObject // Figure out who that is. $sender = Profile::staticGet('id', $profile_id); + if (empty($sender)) { + return null; + } + $recipient = common_relative_profile($sender, $nickname, common_sql_now()); if (empty($recipient)) { diff --git a/lib/util.php b/lib/util.php index f0f262dc5..00c21aeb2 100644 --- a/lib/util.php +++ b/lib/util.php @@ -665,6 +665,9 @@ function common_valid_profile_tag($str) function common_at_link($sender_id, $nickname) { $sender = Profile::staticGet($sender_id); + if (!$sender) { + return $nickname; + } $recipient = common_relative_profile($sender, common_canonical_nickname($nickname)); if ($recipient) { $user = User::staticGet('id', $recipient->id); diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 941912112..4e8b892c6 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -53,10 +53,10 @@ class OStatusPlugin extends Plugin */ function onRouterInitialized($m) { - $m->connect('push/hub', array('action' => 'hub')); + $m->connect('main/push/hub', array('action' => 'pushhub')); - $m->connect('feedsub/callback/:feed', - array('action' => 'feedsubcallback'), + $m->connect('main/push/callback/:feed', + array('action' => 'pushcallback'), array('feed' => '[0-9]+')); $m->connect('settings/feedsub', array('action' => 'feedsubsettings')); @@ -97,7 +97,7 @@ class OStatusPlugin extends Plugin // Canonical form of id in URL? // Updates will be handled for our internal PuSH hub. $action->element('link', array('rel' => 'hub', - 'href' => common_local_url('hub'))); + 'href' => common_local_url('pushhub'))); } } return true; diff --git a/plugins/OStatus/actions/feedsubcallback.php b/plugins/OStatus/actions/feedsubcallback.php deleted file mode 100644 index c57ea5b10..000000000 --- a/plugins/OStatus/actions/feedsubcallback.php +++ /dev/null @@ -1,105 +0,0 @@ -. - */ - -/** - * @package FeedSubPlugin - * @maintainer Brion Vibber - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - - -class FeedSubCallbackAction extends Action -{ - function handle() - { - parent::handle(); - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $this->handlePost(); - } else { - $this->handleGet(); - } - } - - /** - * Handler for POST content updates from the hub - */ - function handlePost() - { - $feedid = $this->arg('feed'); - common_log(LOG_INFO, "POST for feed id $feedid"); - if (!$feedid) { - throw new ServerException('Empty or invalid feed id', 400); - } - - $feedinfo = Feedinfo::staticGet('id', $feedid); - if (!$feedinfo) { - throw new ServerException('Unknown feed id ' . $feedid, 400); - } - - $hmac = ''; - if (isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) { - $hmac = $_SERVER['HTTP_X_HUB_SIGNATURE']; - } - - $post = file_get_contents('php://input'); - $feedinfo->postUpdates($post, $hmac); - } - - /** - * Handler for GET verification requests from the hub - */ - function handleGet() - { - $mode = $this->arg('hub_mode'); - $topic = $this->arg('hub_topic'); - $challenge = $this->arg('hub_challenge'); - $lease_seconds = $this->arg('hub_lease_seconds'); - $verify_token = $this->arg('hub_verify_token'); - - if ($mode != 'subscribe' && $mode != 'unsubscribe') { - common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\""); - throw new ServerException("Bogus hub callback: bad mode", 404); - } - - $feedinfo = Feedinfo::staticGet('feeduri', $topic); - if (!$feedinfo) { - common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); - throw new ServerException("Bogus hub callback: unknown feed", 404); - } - - # Can't currently set the token in our sub api - #if ($feedinfo->verify_token !== $verify_token) { - # common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); - # throw new ServerError("Bogus hub callback: bad token", 404); - #} - - // OK! - common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); - $feedinfo->sub_start = common_sql_date(time()); - if ($lease_seconds > 0) { - $feedinfo->sub_end = common_sql_date(time() + $lease_seconds); - } else { - $feedinfo->sub_end = null; - } - $feedinfo->update(); - - print $challenge; - } -} diff --git a/plugins/OStatus/actions/hub.php b/plugins/OStatus/actions/hub.php deleted file mode 100644 index 5caf4b48e..000000000 --- a/plugins/OStatus/actions/hub.php +++ /dev/null @@ -1,176 +0,0 @@ -. - */ - -/** - * Integrated PuSH hub; lets us only ping them what need it. - * @package Hub - * @maintainer Brion Vibber - */ - -/** - - -Things to consider... -* should we purge incomplete subscriptions that never get a verification pingback? -* when can we send subscription renewal checks? - - at next send time probably ok -* when can we handle trimming of subscriptions? - - at next send time probably ok -* should we keep a fail count? - -*/ - - -class HubAction extends Action -{ - function arg($arg, $def=null) - { - // PHP converts '.'s in incoming var names to '_'s. - // It also merges multiple values, which'll break hub.verify and hub.topic for publishing - // @fixme handle multiple args - $arg = str_replace('.', '_', $arg); - return parent::arg($arg, $def); - } - - function prepare($args) - { - StatusNet::setApi(true); // reduce exception reports to aid in debugging - return parent::prepare($args); - } - - function handle() - { - $mode = $this->trimmed('hub.mode'); - switch ($mode) { - case "subscribe": - $this->subscribe(); - break; - case "unsubscribe": - $this->unsubscribe(); - break; - case "publish": - throw new ServerException("Publishing outside feeds not supported.", 400); - default: - throw new ServerException("Unrecognized mode '$mode'.", 400); - } - } - - /** - * Process a PuSH feed subscription request. - * - * HTTP return codes: - * 202 Accepted - request saved and awaiting verification - * 204 No Content - already subscribed - * 403 Forbidden - rejecting this (not specifically spec'd) - */ - function subscribe() - { - $feed = $this->argUrl('hub.topic'); - $callback = $this->argUrl('hub.callback'); - - common_log(LOG_DEBUG, __METHOD__ . ": checking sub'd to $feed $callback"); - if ($this->getSub($feed, $callback)) { - // Already subscribed; return 204 per spec. - header('HTTP/1.1 204 No Content'); - common_log(LOG_DEBUG, __METHOD__ . ': already subscribed'); - return; - } - - common_log(LOG_DEBUG, __METHOD__ . ': setting up'); - $sub = new HubSub(); - $sub->topic = $feed; - $sub->callback = $callback; - $sub->secret = $this->arg('hub.secret', null); - $sub->setLease(intval($this->arg('hub.lease_seconds'))); - - // @fixme check for feeds we don't manage - // @fixme check the verification mode, might want a return immediately? - - common_log(LOG_DEBUG, __METHOD__ . ': inserting'); - $ok = $sub->insert(); - - if (!$ok) { - throw new ServerException("Failed to save subscription record", 500); - } - - // @fixme check errors ;) - - $data = array('sub' => $sub, 'mode' => 'subscribe'); - $qm = QueueManager::get(); - $qm->enqueue($data, 'hubverify'); - - header('HTTP/1.1 202 Accepted'); - common_log(LOG_DEBUG, __METHOD__ . ': done'); - } - - /** - * Process a PuSH feed unsubscription request. - * - * HTTP return codes: - * 202 Accepted - request saved and awaiting verification - * 204 No Content - already subscribed - * 400 Bad Request - invalid params or rejected feed - */ - function unsubscribe() - { - $feed = $this->argUrl('hub.topic'); - $callback = $this->argUrl('hub.callback'); - $sub = $this->getSub($feed, $callback); - - if ($sub) { - if ($sub->verify('unsubscribe')) { - $sub->delete(); - common_log(LOG_INFO, "PuSH unsubscribed $feed for $callback"); - } else { - throw new ServerException("Failed PuSH unsubscription: verification failed! $feed for $callback"); - } - } else { - throw new ServerException("Failed PuSH unsubscription: not subscribed! $feed for $callback"); - } - } - - /** - * Grab and validate a URL from POST parameters. - * @throws ServerException for malformed or non-http/https URLs - */ - protected function argUrl($arg) - { - $url = $this->arg($arg); - $params = array('domain_check' => false, // otherwise breaks my local tests :P - 'allowed_schemes' => array('http', 'https')); - if (Validate::uri($url, $params)) { - return $url; - } else { - throw new ServerException("Invalid URL passed for $arg: '$url'", 400); - } - } - - /** - * Get HubSub subscription record for a given feed & subscriber. - * - * @param string $feed - * @param string $callback - * @return mixed HubSub or false - */ - protected function getSub($feed, $callback) - { - return HubSub::staticGet($feed, $callback); - } -} - diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php new file mode 100644 index 000000000..a5e02e08f --- /dev/null +++ b/plugins/OStatus/actions/pushcallback.php @@ -0,0 +1,105 @@ +. + */ + +/** + * @package FeedSubPlugin + * @maintainer Brion Vibber + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + + +class PushCallbackAction extends Action +{ + function handle() + { + parent::handle(); + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->handlePost(); + } else { + $this->handleGet(); + } + } + + /** + * Handler for POST content updates from the hub + */ + function handlePost() + { + $feedid = $this->arg('feed'); + common_log(LOG_INFO, "POST for feed id $feedid"); + if (!$feedid) { + throw new ServerException('Empty or invalid feed id', 400); + } + + $feedinfo = Feedinfo::staticGet('id', $feedid); + if (!$feedinfo) { + throw new ServerException('Unknown feed id ' . $feedid, 400); + } + + $hmac = ''; + if (isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) { + $hmac = $_SERVER['HTTP_X_HUB_SIGNATURE']; + } + + $post = file_get_contents('php://input'); + $feedinfo->postUpdates($post, $hmac); + } + + /** + * Handler for GET verification requests from the hub + */ + function handleGet() + { + $mode = $this->arg('hub_mode'); + $topic = $this->arg('hub_topic'); + $challenge = $this->arg('hub_challenge'); + $lease_seconds = $this->arg('hub_lease_seconds'); + $verify_token = $this->arg('hub_verify_token'); + + if ($mode != 'subscribe' && $mode != 'unsubscribe') { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\""); + throw new ServerException("Bogus hub callback: bad mode", 404); + } + + $feedinfo = Feedinfo::staticGet('feeduri', $topic); + if (!$feedinfo) { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); + throw new ServerException("Bogus hub callback: unknown feed", 404); + } + + # Can't currently set the token in our sub api + #if ($feedinfo->verify_token !== $verify_token) { + # common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); + # throw new ServerError("Bogus hub callback: bad token", 404); + #} + + // OK! + common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); + $feedinfo->sub_start = common_sql_date(time()); + if ($lease_seconds > 0) { + $feedinfo->sub_end = common_sql_date(time() + $lease_seconds); + } else { + $feedinfo->sub_end = null; + } + $feedinfo->update(); + + print $challenge; + } +} diff --git a/plugins/OStatus/actions/pushhub.php b/plugins/OStatus/actions/pushhub.php new file mode 100644 index 000000000..901c18f70 --- /dev/null +++ b/plugins/OStatus/actions/pushhub.php @@ -0,0 +1,176 @@ +. + */ + +/** + * Integrated PuSH hub; lets us only ping them what need it. + * @package Hub + * @maintainer Brion Vibber + */ + +/** + + +Things to consider... +* should we purge incomplete subscriptions that never get a verification pingback? +* when can we send subscription renewal checks? + - at next send time probably ok +* when can we handle trimming of subscriptions? + - at next send time probably ok +* should we keep a fail count? + +*/ + + +class PushHubAction extends Action +{ + function arg($arg, $def=null) + { + // PHP converts '.'s in incoming var names to '_'s. + // It also merges multiple values, which'll break hub.verify and hub.topic for publishing + // @fixme handle multiple args + $arg = str_replace('.', '_', $arg); + return parent::arg($arg, $def); + } + + function prepare($args) + { + StatusNet::setApi(true); // reduce exception reports to aid in debugging + return parent::prepare($args); + } + + function handle() + { + $mode = $this->trimmed('hub.mode'); + switch ($mode) { + case "subscribe": + $this->subscribe(); + break; + case "unsubscribe": + $this->unsubscribe(); + break; + case "publish": + throw new ServerException("Publishing outside feeds not supported.", 400); + default: + throw new ServerException("Unrecognized mode '$mode'.", 400); + } + } + + /** + * Process a PuSH feed subscription request. + * + * HTTP return codes: + * 202 Accepted - request saved and awaiting verification + * 204 No Content - already subscribed + * 403 Forbidden - rejecting this (not specifically spec'd) + */ + function subscribe() + { + $feed = $this->argUrl('hub.topic'); + $callback = $this->argUrl('hub.callback'); + + common_log(LOG_DEBUG, __METHOD__ . ": checking sub'd to $feed $callback"); + if ($this->getSub($feed, $callback)) { + // Already subscribed; return 204 per spec. + header('HTTP/1.1 204 No Content'); + common_log(LOG_DEBUG, __METHOD__ . ': already subscribed'); + return; + } + + common_log(LOG_DEBUG, __METHOD__ . ': setting up'); + $sub = new HubSub(); + $sub->topic = $feed; + $sub->callback = $callback; + $sub->secret = $this->arg('hub.secret', null); + $sub->setLease(intval($this->arg('hub.lease_seconds'))); + + // @fixme check for feeds we don't manage + // @fixme check the verification mode, might want a return immediately? + + common_log(LOG_DEBUG, __METHOD__ . ': inserting'); + $ok = $sub->insert(); + + if (!$ok) { + throw new ServerException("Failed to save subscription record", 500); + } + + // @fixme check errors ;) + + $data = array('sub' => $sub, 'mode' => 'subscribe'); + $qm = QueueManager::get(); + $qm->enqueue($data, 'hubverify'); + + header('HTTP/1.1 202 Accepted'); + common_log(LOG_DEBUG, __METHOD__ . ': done'); + } + + /** + * Process a PuSH feed unsubscription request. + * + * HTTP return codes: + * 202 Accepted - request saved and awaiting verification + * 204 No Content - already subscribed + * 400 Bad Request - invalid params or rejected feed + */ + function unsubscribe() + { + $feed = $this->argUrl('hub.topic'); + $callback = $this->argUrl('hub.callback'); + $sub = $this->getSub($feed, $callback); + + if ($sub) { + if ($sub->verify('unsubscribe')) { + $sub->delete(); + common_log(LOG_INFO, "PuSH unsubscribed $feed for $callback"); + } else { + throw new ServerException("Failed PuSH unsubscription: verification failed! $feed for $callback"); + } + } else { + throw new ServerException("Failed PuSH unsubscription: not subscribed! $feed for $callback"); + } + } + + /** + * Grab and validate a URL from POST parameters. + * @throws ServerException for malformed or non-http/https URLs + */ + protected function argUrl($arg) + { + $url = $this->arg($arg); + $params = array('domain_check' => false, // otherwise breaks my local tests :P + 'allowed_schemes' => array('http', 'https')); + if (Validate::uri($url, $params)) { + return $url; + } else { + throw new ServerException("Invalid URL passed for $arg: '$url'", 400); + } + } + + /** + * Get HubSub subscription record for a given feed & subscriber. + * + * @param string $feed + * @param string $callback + * @return mixed HubSub or false + */ + protected function getSub($feed, $callback) + { + return HubSub::staticGet($feed, $callback); + } +} + diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php index f29d08cb0..107faf012 100644 --- a/plugins/OStatus/classes/Feedinfo.php +++ b/plugins/OStatus/classes/Feedinfo.php @@ -248,7 +248,7 @@ class Feedinfo extends Memcached_DataObject #$this->verify_token = $token; #$this->update(); // @fixme try { - $callback = common_local_url('feedsubcallback', array('feed' => $this->id)); + $callback = common_local_url('pushcallback', array('feed' => $this->id)); $headers = array('Content-Type: application/x-www-form-urlencoded'); $post = array('hub.mode' => 'subscribe', 'hub.callback' => $callback, diff --git a/plugins/OStatus/lib/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php index 9bc7892fb..39985fc90 100644 --- a/plugins/OStatus/lib/feeddiscovery.php +++ b/plugins/OStatus/lib/feeddiscovery.php @@ -168,7 +168,13 @@ class FeedDiscovery } // Ok... now on to the links! + // Types listed in order of priority -- we'll prefer Atom if available. // @fixme merge with the munger link checks + $feeds = array( + 'application/atom+xml' => false, + 'application/rss+xml' => false, + ); + $nodes = $dom->getElementsByTagName('link'); for ($i = 0; $i < $nodes->length; $i++) { $node = $nodes->item($i); @@ -181,17 +187,21 @@ class FeedDiscovery $type = trim($type->value); $href = trim($href->value); - $feedTypes = array( - 'application/rss+xml', - 'application/atom+xml', - ); - if (trim($rel) == 'alternate' && in_array($type, $feedTypes)) { - return $this->resolveURI($href, $base); + if (trim($rel) == 'alternate' && array_key_exists($type, $feeds) && empty($feeds[$type])) { + // Save the first feed found of each type... + $feeds[$type] = $this->resolveURI($href, $base); } } } } + // Return the highest-priority feed found + foreach ($feeds as $type => $url) { + if ($url) { + return $url; + } + } + return false; } diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index eeb8d2df3..948017702 100644 --- a/plugins/OStatus/lib/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -235,34 +235,45 @@ class FeedMunger */ function noticeFromEntry($entry) { + $max = Notice::maxContent(); + $ellipsis = "\xe2\x80\xa6"; // U+2026 HORIZONTAL ELLIPSIS $title = $entry->title; $link = $entry->link; - + // @todo We can get entries like this: // $cats = $entry->getCategory('category', array(0, true)); // but it feels like an awful hack. If it's accessible cleanly, // try adding #hashtags from the categories/tags on a post. - - // @todo Should we force a language here? - $format = _m('New post: "%1$s" %2$s'); + $title = $entry->title; $link = $this->getAltLink($entry); - $out = sprintf($format, $title, $link); - - // Trim link if needed... - $max = Notice::maxContent(); - if (mb_strlen($out) > $max) { - $link = common_shorten_url($link); + if ($link) { + // Blog post or such... + // @todo Should we force a language here? + $format = _m('New post: "%1$s" %2$s'); $out = sprintf($format, $title, $link); - } - // Trim title if needed... - if (mb_strlen($out) > $max) { - $ellipsis = "\xe2\x80\xa6"; // U+2026 HORIZONTAL ELLIPSIS - $used = mb_strlen($out) - mb_strlen($title); - $available = $max - $used - mb_strlen($ellipsis); - $title = mb_substr($title, 0, $available) . $ellipsis; - $out = sprintf($format, $title, $link); + // Trim link if needed... + if (mb_strlen($out) > $max) { + $link = common_shorten_url($link); + $out = sprintf($format, $title, $link); + } + + // Trim title if needed... + if (mb_strlen($out) > $max) { + $used = mb_strlen($out) - mb_strlen($title); + $available = $max - $used - mb_strlen($ellipsis); + $title = mb_substr($title, 0, $available) . $ellipsis; + $out = sprintf($format, $title, $link); + } + } else { + // No link? Consider a bare status update. + if (mb_strlen($title) > $max) { + $available = $max - mb_strlen($ellipsis); + $out = mb_substr($title, 0, $available) . $ellipsis; + } else { + $out = $title; + } } return $out; -- cgit v1.2.3-54-g00ecf From 96ef4435b61570dbbf15d921a42543bfb13786c0 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Feb 2010 15:32:20 -0800 Subject: Allow scripts/decache.php to blow out cache for objects that don't exist (anymore). May miss keys other than the given or primary key, but should work for a lot of common cases where a bad entry's been removed from DB but lingers in cache. --- scripts/decache.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/decache.php b/scripts/decache.php index 7cabd78ad..094bdb5aa 100644 --- a/scripts/decache.php +++ b/scripts/decache.php @@ -24,6 +24,8 @@ $helptext = << [] Clears the cache for the object in table with id If is specified, use that instead of 'id' + + ENDOFHELP; require_once INSTALLDIR.'/scripts/commandline.inc'; @@ -43,8 +45,10 @@ if (count($args) > 2) { $object = Memcached_DataObject::staticGet($table, $column, $id); if (!$object) { - print "No such '$table' with $column = '$id'.\n"; - exit(1); + print "No such '$table' with $column = '$id'; it's possible some cache keys won't be cleared properly.\n"; + $class = ucfirst($table); + $object = new $class(); + $object->column = $id; } $result = $object->decache(); -- cgit v1.2.3-54-g00ecf From b9b0f0410aa688cc3ee77df1563773527a8d59a9 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Feb 2010 15:46:38 -0800 Subject: Pull GeoRSS locations over OStatus feeds --- plugins/OStatus/lib/feedmunger.php | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index 948017702..cbaec6775 100644 --- a/plugins/OStatus/lib/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -225,10 +225,45 @@ class FeedMunger $notice->created = common_sql_date($entry->updated); // @fixme $notice->is_local = Notice::GATEWAY; $notice->source = 'feed'; - + + $location = $this->getLocation($entry); + if ($location) { + if ($location->location_id) { + $notice->location_ns = $location->location_ns; + $notice->location_id = $location->location_id; + } + $notice->lat = $location->lat; + $notice->lon = $location->lon; + } + return $notice; } + /** + * @param feed item $entry + * @return mixed Location or false + */ + function getLocation($entry) + { + $dom = $entry->model; + $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point'); + + for ($i = 0; $i < $points->length; $i++) { + $point = trim($points->item(0)->textContent); + $coords = explode(' ', $point); + if (count($coords) == 2) { + list($lat, $lon) = $coords; + if (is_numeric($lat) && is_numeric($lon)) { + common_log(LOG_INFO, "Looking up location for $lat $lon from georss"); + return Location::fromLatLon($lat, $lon); + } + } + common_log(LOG_ERR, "Ignoring bogus georss:point value $point"); + } + + return false; + } + /** * @param XML_Feed_Type $entry * @return string notice text, within post size limit -- cgit v1.2.3-54-g00ecf