From dd36c2de23dbdb30f333be1f9d5292d62363b0c8 Mon Sep 17 00:00:00 2001
From: Brion Vibber
Date: Thu, 19 Nov 2009 20:55:38 -0800
Subject: [PATCH] Initial functional version of feed subscription plugin,
currently supporting only PuSH-enabled feeds.
---
db/08to09_pg.sql | 6 +-
db/statusnet_pg.sql | 6 +-
plugins/FeedSub/FeedSubPlugin.php | 117 +++++
plugins/FeedSub/README | 24 +
plugins/FeedSub/actions/feedsubcallback.php | 100 ++++
plugins/FeedSub/actions/feedsubsettings.php | 257 ++++++++++
plugins/FeedSub/extlib/README | 9 +
plugins/FeedSub/extlib/XML/Feed/Parser.php | 351 +++++++++++++
.../FeedSub/extlib/XML/Feed/Parser/Atom.php | 365 ++++++++++++++
.../extlib/XML/Feed/Parser/AtomElement.php | 261 ++++++++++
.../extlib/XML/Feed/Parser/Exception.php | 42 ++
.../FeedSub/extlib/XML/Feed/Parser/RSS09.php | 214 ++++++++
.../extlib/XML/Feed/Parser/RSS09Element.php | 62 +++
.../FeedSub/extlib/XML/Feed/Parser/RSS1.php | 277 +++++++++++
.../FeedSub/extlib/XML/Feed/Parser/RSS11.php | 276 +++++++++++
.../extlib/XML/Feed/Parser/RSS11Element.php | 151 ++++++
.../extlib/XML/Feed/Parser/RSS1Element.php | 116 +++++
.../FeedSub/extlib/XML/Feed/Parser/RSS2.php | 335 +++++++++++++
.../extlib/XML/Feed/Parser/RSS2Element.php | 171 +++++++
.../FeedSub/extlib/XML/Feed/Parser/Type.php | 467 ++++++++++++++++++
.../XML/Feed/samples/atom10-entryonly.xml | 28 ++
.../XML/Feed/samples/atom10-example1.xml | 20 +
.../XML/Feed/samples/atom10-example2.xml | 45 ++
.../extlib/XML/Feed/samples/delicious.feed | 177 +++++++
.../extlib/XML/Feed/samples/flickr.feed | 184 +++++++
.../extlib/XML/Feed/samples/grwifi-atom.xml | 7 +
.../FeedSub/extlib/XML/Feed/samples/hoder.xml | 102 ++++
.../XML/Feed/samples/illformed_atom10.xml | 13 +
.../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 ++++
.../XML/Feed/samples/rss10-example1.xml | 62 +++
.../XML/Feed/samples/rss10-example2.xml | 67 +++
.../extlib/XML/Feed/samples/rss2sample.xml | 42 ++
.../extlib/XML/Feed/samples/sixapart-jp.xml | 226 +++++++++
.../extlib/XML/Feed/samples/technorati.feed | 54 ++
.../FeedSub/extlib/XML/Feed/schemas/atom.rnc | 338 +++++++++++++
.../FeedSub/extlib/XML/Feed/schemas/rss10.rnc | 113 +++++
.../FeedSub/extlib/XML/Feed/schemas/rss11.rnc | 218 ++++++++
.../extlib/xml-feed-parser-bug-16416.patch | 14 +
plugins/FeedSub/feeddiscovery.php | 209 ++++++++
plugins/FeedSub/feedinfo.php | 212 ++++++++
plugins/FeedSub/feedinfo.sql | 14 +
plugins/FeedSub/feedmunger.php | 238 +++++++++
plugins/FeedSub/images/24px-Feed-icon.svg.png | Bin 0 -> 1204 bytes
plugins/FeedSub/images/48px-Feed-icon.svg.png | Bin 0 -> 2434 bytes
plugins/FeedSub/images/96px-Feed-icon.svg.png | Bin 0 -> 5440 bytes
plugins/FeedSub/images/README | 5 +
plugins/FeedSub/tests/FeedDiscoveryTest.php | 111 +++++
plugins/FeedSub/tests/FeedMungerTest.php | 147 ++++++
51 files changed, 6442 insertions(+), 6 deletions(-)
create mode 100644 plugins/FeedSub/FeedSubPlugin.php
create mode 100644 plugins/FeedSub/README
create mode 100644 plugins/FeedSub/actions/feedsubcallback.php
create mode 100644 plugins/FeedSub/actions/feedsubsettings.php
create mode 100644 plugins/FeedSub/extlib/README
create mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser.php
create mode 100644 plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php
create mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php
create mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php
create mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php
create mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php
create mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php
create mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php
create mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php
create mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php
create mode 100644 plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php
create mode 100755 plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php
create mode 100644 plugins/FeedSub/extlib/XML/Feed/Parser/Type.php
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml
create mode 100755 plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed
create mode 100755 plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc
create mode 100755 plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc
create mode 100755 plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc
create mode 100644 plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch
create mode 100644 plugins/FeedSub/feeddiscovery.php
create mode 100644 plugins/FeedSub/feedinfo.php
create mode 100644 plugins/FeedSub/feedinfo.sql
create mode 100644 plugins/FeedSub/feedmunger.php
create mode 100644 plugins/FeedSub/images/24px-Feed-icon.svg.png
create mode 100644 plugins/FeedSub/images/48px-Feed-icon.svg.png
create mode 100644 plugins/FeedSub/images/96px-Feed-icon.svg.png
create mode 100644 plugins/FeedSub/images/README
create mode 100644 plugins/FeedSub/tests/FeedDiscoveryTest.php
create mode 100644 plugins/FeedSub/tests/FeedMungerTest.php
diff --git a/db/08to09_pg.sql b/db/08to09_pg.sql
index d9b57fb841..7aaf3a7a00 100644
--- a/db/08to09_pg.sql
+++ b/db/08to09_pg.sql
@@ -29,13 +29,13 @@ create table config (
);
-create table user_role (
+create table profile_role (
- user_id integer not null /* comment 'user having the role'*/ references "user" (id),
+ profile_id integer not null /* comment 'account having the role'*/ references profile (id),
role varchar(32) not null /* comment 'string representing the role'*/,
created timestamp /* not null comment 'date the role was granted'*/,
- primary key (user_id, role)
+ primary key (profile_id, role)
);
diff --git a/db/statusnet_pg.sql b/db/statusnet_pg.sql
index 8dbaf85981..927925753e 100644
--- a/db/statusnet_pg.sql
+++ b/db/statusnet_pg.sql
@@ -560,13 +560,13 @@ create table config (
);
-create table user_role (
+create table profile_role (
- user_id integer not null /* comment 'user having the role'*/ references "user" (id),
+ profile_id integer not null /* comment 'account having the role'*/ references profile (id),
role varchar(32) not null /* comment 'string representing the role'*/,
created timestamp /* not null comment 'date the role was granted'*/,
- primary key (user_id, role)
+ primary key (profile_id, role)
);
diff --git a/plugins/FeedSub/FeedSubPlugin.php b/plugins/FeedSub/FeedSubPlugin.php
new file mode 100644
index 0000000000..36d4e78023
--- /dev/null
+++ b/plugins/FeedSub/FeedSubPlugin.php
@@ -0,0 +1,117 @@
+
+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'),
+ dgettext('FeebSubPlugin', 'Feeds'),
+ dgettext('FeedSubPlugin', '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;
+ }
+
+ /*
+ // auto increment seems to be broken
+ function onCheckSchema() {
+ $schema = Schema::get();
+ $schema->ensureDataObject('Feedinfo');
+ return true;
+ }
+ */
+}
diff --git a/plugins/FeedSub/README b/plugins/FeedSub/README
new file mode 100644
index 0000000000..cbf3adbb9c
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/actions/feedsubcallback.php b/plugins/FeedSub/actions/feedsubcallback.php
new file mode 100644
index 0000000000..0c4280c1fa
--- /dev/null
+++ b/plugins/FeedSub/actions/feedsubcallback.php
@@ -0,0 +1,100 @@
+.
+ */
+
+/**
+ * @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
new file mode 100644
index 0000000000..242224fac0
--- /dev/null
+++ b/plugins/FeedSub/actions/feedsubsettings.php
@@ -0,0 +1,257 @@
+.
+ */
+
+/**
+ * @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 dgettext('FeedSubPlugin', 'Feed subscriptions');
+ }
+
+ /**
+ * Instructions for use
+ *
+ * @return instructions for use
+ */
+
+ function getInstructions()
+ {
+ return dgettext('FeedSubPlugin',
+ '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', dgettext('FeedSubPlugin', 'Subscribe'));
+ } else {
+ $this->submit('validate', dgettext('FeedSubPlugin', '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(dgettext('FeedSubPlugin',
+ '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(dgettext('FeedSubPlugin', 'Invalid URL or could not reach server.'));
+ return false;
+ } catch (FeedSubBadResponseException $e) {
+ $this->showForm(dgettext('FeedSubPlugin', 'Cannot read feed; server returned error.'));
+ return false;
+ } catch (FeedSubEmptyException $e) {
+ $this->showForm(dgettext('FeedSubPlugin', 'Cannot read feed; server returned an empty page.'));
+ return false;
+ } catch (FeedSubBadHTMLException $e) {
+ $this->showForm(dgettext('FeedSubPlugin', 'Bad HTML, could not find feed link.'));
+ return false;
+ } catch (FeedSubNoFeedException $e) {
+ $this->showForm(dgettext('FeedSubPlugin', 'Could not find a feed linked from this URL.'));
+ return false;
+ } catch (FeedSubUnrecognizedTypeException $e) {
+ $this->showForm(dgettext('FeedSubPlugin', 'Not a recognized feed type.'));
+ return false;
+ } catch (FeedSubException $e) {
+ // Any new ones we forgot about
+ $this->showForm(dgettext('FeedSubPlugin', 'Bad feed URL.'));
+ return false;
+ }
+
+ $this->munger = $discover->feedMunger();
+ $this->feedinfo = $this->munger->feedInfo();
+
+ if ($this->feedinfo->huburi == '') {
+ $this->showForm(dgettext('FeedSubPlugin', '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(dgettext('FeedSubPlugin', '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(dgettext('FeedSubPlugin', 'Already subscribed!'));
+ } elseif ($user->subscribeTo($profile)) {
+ $this->showForm(dgettext('FeedSubPlugin', 'Feed subscribed!'));
+ } else {
+ $this->showForm(dgettext('FeedSubPlugin', 'Feed subscription failed!'));
+ }
+ }
+ }
+
+ function validateAndPreview()
+ {
+ if ($this->validateFeed()) {
+ $this->preview = true;
+ $this->showForm(dgettext('FeedSubPlugin', '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
new file mode 100644
index 0000000000..799b40c478
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser.php b/plugins/FeedSub/extlib/XML/Feed/Parser.php
new file mode 100755
index 0000000000..ffe8220a52
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/Atom.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php
new file mode 100644
index 0000000000..c7e218a1e6
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/AtomElement.php b/plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php
new file mode 100755
index 0000000000..063ecb6177
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/Exception.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php
new file mode 100755
index 0000000000..1e76e3f850
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/RSS09.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php
new file mode 100755
index 0000000000..07f38f911e
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php
new file mode 100755
index 0000000000..d41f36e8d6
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/RSS1.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php
new file mode 100755
index 0000000000..60c9938baa
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/RSS11.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php
new file mode 100755
index 0000000000..3cd1ef15d8
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php
new file mode 100755
index 0000000000..75918beda9
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php
new file mode 100755
index 0000000000..8e36d5a9b9
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/RSS2.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php
new file mode 100644
index 0000000000..0936bd2f5e
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php
new file mode 100755
index 0000000000..6edf910dc4
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/Parser/Type.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Type.php
new file mode 100644
index 0000000000..75052619bd
--- /dev/null
+++ b/plugins/FeedSub/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 .= '' . $node->tagName . '>';
+ }
+
+ return $content;
+ }
+
+ /**
+ * Get content from RSS feeds (atom has its own implementation)
+ *
+ * The official way to include full content in an RSS1 entry is to use
+ * the content module's element 'encoded', and RSS2 feeds often duplicate that.
+ * Often, however, the 'description' element is used instead. We will offer that
+ * as a fallback. Atom uses its own approach and overrides this method.
+ *
+ * @return string|false
+ */
+ protected function getContent()
+ {
+ $options = array('encoded', 'description');
+ foreach ($options as $element) {
+ $test = $this->model->getElementsByTagName($element);
+ if ($test->length == 0) {
+ continue;
+ }
+ if ($test->item(0)->hasChildNodes()) {
+ $value = '';
+ foreach ($test->item(0)->childNodes as $child) {
+ if ($child instanceof DOMText) {
+ $value .= $child->nodeValue;
+ } else {
+ $simple = simplexml_import_dom($child);
+ $value .= $simple->asXML();
+ }
+ }
+ return $value;
+ } else if ($test->length > 0) {
+ return $test->item(0)->nodeValue;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if this element has a particular child element.
+ *
+ * @param String
+ * @param Integer
+ * @return bool
+ **/
+ function hasKey($name, $offset = 0)
+ {
+ $search = $this->model->getElementsByTagName($name);
+ return $search->length > $offset;
+ }
+
+ /**
+ * Return an XML serialization of the feed, should it be required. Most
+ * users however, will already have a serialization that they used when
+ * instantiating the object.
+ *
+ * @return string XML serialization of element
+ */
+ function __toString()
+ {
+ $simple = simplexml_import_dom($this->model);
+ return $simple->asXML();
+ }
+
+ /**
+ * Get directory holding RNG schemas. Method is based on that
+ * found in Contact_AddressBook.
+ *
+ * @return string PEAR data directory.
+ * @access public
+ * @static
+ */
+ static function getSchemaDir()
+ {
+ require_once 'PEAR/Config.php';
+ $config = new PEAR_Config;
+ return $config->get('data_dir') . '/XML_Feed_Parser/schemas';
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml
new file mode 100755
index 0000000000..02e1c58002
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml
new file mode 100755
index 0000000000..d181d2b6f8
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml
new file mode 100755
index 0000000000..98abf9d54f
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/samples/delicious.feed b/plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed
new file mode 100755
index 0000000000..32f9fa4935
--- /dev/null
+++ b/plugins/FeedSub/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/FeedSub/extlib/XML/Feed/samples/flickr.feed b/plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed
new file mode 100755
index 0000000000..57e83af572
--- /dev/null
+++ b/plugins/FeedSub/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 & 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 & 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
new file mode 100755
index 0000000000..c351d3c164
--- /dev/null
+++ b/plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml
@@ -0,0 +1,7 @@
+
Updates to Grand Rapids WiFi hotspot details2005-09-01T15:43:01-05:00WiFi Hotspots in Grand Rapids, MIhttp://grwifi.net/atom/locationsCreative Commons Attribution-NonCommercial-ShareAlike 2.0 http://creativecommons.org/licenses/by-nc-sa/2.0/ Hotspot Details Updated: Sweetwatershttp://grwifi.net/location/sweetwaters2005-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
Jameshttp://jystewart.netjames@jystewart.netwifi hotspotHotspot Details Updated: Common Ground Coffee Shophttp://grwifi.net/location/common-ground2005-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
Jameshttp://jystewart.netjames@jystewart.netwifi hotspotHotspot Details Updated: Grand Rapids Public Library, Main Branchhttp://grwifi.net/location/grpl-main-branch2005-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
Jameshttp://jystewart.netjames@jystewart.netwifi hotspotHotspot Details Updated: Four Friends Coffee Househttp://grwifi.net/location/four-friends2005-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
Jameshttp://jystewart.netjames@jystewart.netwifi hotspotHotspot Details Updated: Barnes and Noble, Rivertown Crossingshttp://grwifi.net/location/barnes-noble-rivertown2005-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
Jameshttp://jystewart.netjames@jystewart.netwifi hotspotHotspot Details Updated: The Boss Sports Bar & Grillehttp://grwifi.net/location/boss-sports-bar2005-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
Jameshttp://jystewart.netjames@jystewart.netwifi 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
new file mode 100755
index 0000000000..0994635707
--- /dev/null
+++ b/plugins/FeedSub/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
9月21日に開設されたこのブログは、ブラジル、ホンジュラス、ニカラグア、メキシコ、キューバの5か国を巡る「Latin America 2005」ツアーに合わせ、そのツアーの模様を同行マネージャーがレポートしていきます。
+さらに今月2日からは宮沢和史自身が日々録音した声をPodcastingするという点でも、ブログを使ったユニークなプロモーションとなっています。
',
+ '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
new file mode 100644
index 0000000000..0ce24c9fb8
--- /dev/null
+++ b/plugins/FeedSub/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: