]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch 'master' into testing
authorBrion Vibber <brion@pobox.com>
Wed, 17 Feb 2010 18:13:35 +0000 (10:13 -0800)
committerBrion Vibber <brion@pobox.com>
Wed, 17 Feb 2010 18:13:35 +0000 (10:13 -0800)
208 files changed:
EVENTS.txt
README
actions/apitimelinefavorites.php
actions/apitimelinefriends.php
actions/apitimelinegroup.php
actions/apitimelinehome.php
actions/apitimelinementions.php
actions/apitimelinepublic.php
actions/apitimelineretweetsofme.php
actions/apitimelinetag.php
actions/apitimelineuser.php
actions/groupmembers.php
actions/makeadmin.php
actions/public.php
actions/rsd.php [new file with mode: 0644]
actions/showgroup.php
actions/showstream.php
actions/userauthorization.php
classes/Avatar.php
classes/Conversation.php [new file with mode: 0755]
classes/Design.php
classes/File.php
classes/Memcached_DataObject.php
classes/Nonce.php
classes/Notice.php
classes/Profile.php
classes/User_group.php
classes/statusnet.ini
classes/statusnet.links.ini
db/statusnet.sql
js/jquery.form.js
js/jquery.js
js/jquery.min.js
js/util.js
lib/action.php
lib/api.php
lib/atom10entry.php [new file with mode: 0644]
lib/atom10feed.php [new file with mode: 0644]
lib/atomgroupnoticefeed.php [new file with mode: 0644]
lib/atomnoticefeed.php [new file with mode: 0644]
lib/atomusernoticefeed.php [new file with mode: 0644]
lib/cache.php
lib/default.php
lib/error.php
lib/grouplist.php
lib/groupsection.php
lib/htmloutputter.php
lib/httpclient.php
lib/mysqlschema.php [new file with mode: 0644]
lib/noticelist.php
lib/noticesection.php
lib/oauthclient.php
lib/pgsqlschema.php [new file with mode: 0644]
lib/profilelist.php
lib/profilesection.php
lib/right.php
lib/router.php
lib/schema.php
lib/statusnet.php
lib/theme.php
lib/userprofile.php
lib/util.php
plugins/FeedSub/FeedSubPlugin.php [deleted file]
plugins/FeedSub/README [deleted file]
plugins/FeedSub/actions/feedsubcallback.php [deleted file]
plugins/FeedSub/actions/feedsubsettings.php [deleted file]
plugins/FeedSub/extlib/README [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/Parser/Type.php [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml [deleted file]
plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed [deleted file]
plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc [deleted file]
plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc [deleted file]
plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc [deleted file]
plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch [deleted file]
plugins/FeedSub/feeddiscovery.php [deleted file]
plugins/FeedSub/feedinfo.php [deleted file]
plugins/FeedSub/feedinfo.sql [deleted file]
plugins/FeedSub/feedmunger.php [deleted file]
plugins/FeedSub/images/24px-Feed-icon.svg.png [deleted file]
plugins/FeedSub/images/48px-Feed-icon.svg.png [deleted file]
plugins/FeedSub/images/96px-Feed-icon.svg.png [deleted file]
plugins/FeedSub/images/README [deleted file]
plugins/FeedSub/locale/FeedSub.po [deleted file]
plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po [deleted file]
plugins/FeedSub/tests/FeedDiscoveryTest.php [deleted file]
plugins/FeedSub/tests/FeedMungerTest.php [deleted file]
plugins/FeedSub/tests/gettext-speedtest.php [deleted file]
plugins/MemcachePlugin.php
plugins/MobileProfile/MobileProfilePlugin.php
plugins/MobileProfile/mp-screen.css
plugins/OStatus/OStatusPlugin.php [new file with mode: 0644]
plugins/OStatus/README [new file with mode: 0644]
plugins/OStatus/actions/feedsubsettings.php [new file with mode: 0644]
plugins/OStatus/actions/hostmeta.php [new file with mode: 0644]
plugins/OStatus/actions/ostatusinit.php [new file with mode: 0644]
plugins/OStatus/actions/ostatussub.php [new file with mode: 0644]
plugins/OStatus/actions/pushcallback.php [new file with mode: 0644]
plugins/OStatus/actions/pushhub.php [new file with mode: 0644]
plugins/OStatus/actions/salmon.php [new file with mode: 0644]
plugins/OStatus/actions/webfinger.php [new file with mode: 0644]
plugins/OStatus/classes/HubSub.php [new file with mode: 0644]
plugins/OStatus/classes/Ostatus_profile.php [new file with mode: 0644]
plugins/OStatus/extlib/README [new file with mode: 0644]
plugins/OStatus/extlib/XML/Feed/Parser.php [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/Parser/Atom.php [new file with mode: 0644]
plugins/OStatus/extlib/XML/Feed/Parser/AtomElement.php [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/Parser/Exception.php [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/Parser/RSS09Element.php [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/Parser/RSS11Element.php [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/Parser/RSS1Element.php [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php [new file with mode: 0644]
plugins/OStatus/extlib/XML/Feed/Parser/RSS2Element.php [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/Parser/Type.php [new file with mode: 0644]
plugins/OStatus/extlib/XML/Feed/samples/atom10-entryonly.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/atom10-example1.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/atom10-example2.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/delicious.feed [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/flickr.feed [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/grwifi-atom.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/hoder.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/illformed_atom10.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/rss091-complete.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/rss091-international.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/rss091-simple.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/rss092-sample.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/rss10-example1.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/rss10-example2.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/rss2sample.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/sixapart-jp.xml [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/samples/technorati.feed [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc [new file with mode: 0755]
plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc [new file with mode: 0755]
plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch [new file with mode: 0644]
plugins/OStatus/images/24px-Feed-icon.svg.png [new file with mode: 0644]
plugins/OStatus/images/48px-Feed-icon.svg.png [new file with mode: 0644]
plugins/OStatus/images/96px-Feed-icon.svg.png [new file with mode: 0644]
plugins/OStatus/images/README [new file with mode: 0644]
plugins/OStatus/js/ostatus.js [new file with mode: 0644]
plugins/OStatus/lib/activity.php [new file with mode: 0644]
plugins/OStatus/lib/feeddiscovery.php [new file with mode: 0644]
plugins/OStatus/lib/feedmunger.php [new file with mode: 0644]
plugins/OStatus/lib/hubdistribqueuehandler.php [new file with mode: 0644]
plugins/OStatus/lib/huboutqueuehandler.php [new file with mode: 0644]
plugins/OStatus/lib/hubverifyqueuehandler.php [new file with mode: 0644]
plugins/OStatus/lib/salmon.php [new file with mode: 0644]
plugins/OStatus/lib/webfinger.php [new file with mode: 0644]
plugins/OStatus/lib/xrd.php [new file with mode: 0644]
plugins/OStatus/locale/OStatus.po [new file with mode: 0644]
plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po [new file with mode: 0644]
plugins/OStatus/tests/ActivityParseTests.php [new file with mode: 0644]
plugins/OStatus/tests/FeedDiscoveryTest.php [new file with mode: 0644]
plugins/OStatus/tests/FeedMungerTest.php [new file with mode: 0644]
plugins/OStatus/tests/gettext-speedtest.php [new file with mode: 0644]
plugins/OStatus/theme/base/css/ostatus.css [new file with mode: 0644]
plugins/OpenID/finishopenidlogin.php
plugins/PostDebug/PostDebugPlugin.php [new file with mode: 0644]
plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php
plugins/Realtime/realtimeupdate.js
plugins/TwitterBridge/twitterauthorization.php
plugins/TwitterBridge/twitteroauthclient.php
plugins/UserFlag/UserFlagPlugin.php
plugins/UserFlag/clearflagform.php
plugins/UserFlag/icon_flag.gif [deleted file]
plugins/UserFlag/userflag.css [deleted file]
scripts/setconfig.php [changed mode: 0644->0755]
theme/base/css/display.css
theme/base/images/icons/icons-01.gif
theme/base/images/icons/twotone/green/against.gif [new file with mode: 0644]
theme/base/images/icons/twotone/green/checkmark.gif [new file with mode: 0644]
theme/base/images/icons/twotone/green/clear.gif [new file with mode: 0644]
theme/base/images/icons/twotone/green/flag.gif [new file with mode: 0644]
theme/base/logo.png
theme/default/css/display.css
theme/default/logo.png
theme/default/mobilelogo.png
theme/identica/css/display.css
theme/identica/mobilelogo.png

index 6bf12bf13fb15db9ea94507971c635266db383ee..90242fa133ca203f2e11da19742b0db28e2e3e4d 100644 (file)
@@ -355,6 +355,14 @@ EndShowHeadElements: Right before the </head> tag; put <script>s here if you nee
 
 CheckSchema: chance to check the schema
 
+StartProfileRemoteSubscribe: Before showing the link to remote subscription
+- $userprofile: UserProfile widget
+- &$profile: the profile being shown
+
+EndProfileRemoteSubscribe: After showing the link to remote subscription
+- $userprofile: UserProfile widget
+- &$profile: the profile being shown
+
 StartProfilePageProfileSection: Starting to show the section of the
                               profile page with the actual profile data;
                               hook to prevent showing the profile (e.g.)
@@ -714,3 +722,10 @@ StartRobotsTxt: Before outputting the robots.txt page
 EndRobotsTxt: After the default robots.txt page (good place for customization)
 - &$action: RobotstxtAction being shown
 
+StartGetProfileUri: When determining the canonical URI for a given profile
+- $profile: the current profile
+- &$uri: the URI
+
+EndGetProfileUri: After determining the canonical URI for a given profile
+- $profile: the current profile
+- &$uri: the URI
diff --git a/README b/README
index 9b4147645b5fe1ce0f67a117480ad5f62402cbf2..75336eb83fb57f28a14776b124db06d3ca06bb45 100644 (file)
--- a/README
+++ b/README
@@ -1192,6 +1192,8 @@ server: If set, defines another server where avatars are stored in the
        typically only make 2 connections to a single server at a
        time <http://ur1.ca/6ih>, so this can parallelize the job.
        Defaults to null.
+ssl:    Whether to access avatars using HTTPS. Defaults to null, meaning
+        to guess based on site-wide SSL settings.
 
 public
 ------
@@ -1221,6 +1223,19 @@ path:    Path part of theme URLs, before the theme name. Relative to the
        (using version numbers as the path) to make sure that all files are
        reloaded by caching clients or proxies. Defaults to null,
        which means to use the site path + '/theme'.
+ssl:   Whether to use SSL for theme elements. Default is null, which means
+       guess based on site SSL settings.
+
+javascript
+----------
+
+server: You can speed up page loading by pointing the
+       theme file lookup to another server (virtual or real).
+       Defaults to NULL, meaning to use the site server.
+path:  Path part of Javascript URLs. Defaults to null,
+       which means to use the site path + '/js/'.
+ssl:   Whether to use SSL for JavaScript files. Default is null, which means
+       guess based on site SSL settings.
 
 xmpp
 ----
@@ -1447,6 +1462,8 @@ server: server name to use when creating URLs for uploaded files.
         a virtual server here can speed up Web performance.
 path: URL path, relative to the server, to find files. Defaults to
       main path + '/file/'.
+ssl: whether to use HTTPS for file URLs. Defaults to null, meaning to
+     guess based on other SSL settings.
 filecommand: command to use for determining the type of a file. May be
              skipped if fileinfo extension is installed. Defaults to
              '/usr/bin/file'.
@@ -1506,6 +1523,8 @@ dir: directory to write backgrounds too. Default is '/background/'
      subdir of install dir.
 path: path to backgrounds. Default is sub-path of install path; note
       that you may need to change this if you change site-path too.
+ssl: Whether or not to use HTTPS for background files. Defaults to
+     null, meaning to guess from site-wide SSL settings.
 
 ping
 ----
index 1027d97d440047f77c3401b9b446b070a974137f..f7f900ddfb5d6382f70c4a1752de799f2473038d 100644 (file)
@@ -100,11 +100,11 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction
 
     function showTimeline()
     {
-        $profile = $this->user->getProfile();
-        $avatar     = $profile->getAvatar(AVATAR_PROFILE_SIZE);
+        $profile  = $this->user->getProfile();
+        $avatar   = $profile->getAvatar(AVATAR_PROFILE_SIZE);
 
-        $sitename   = common_config('site', 'name');
-        $title      = sprintf(
+        $sitename = common_config('site', 'name');
+        $title    = sprintf(
             _('%1$s / Favorites from %2$s'),
             $sitename,
             $this->user->nickname
@@ -112,32 +112,69 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction
 
         $taguribase = common_config('integration', 'taguri');
         $id         = "tag:$taguribase:Favorites:" . $this->user->id;
-        $link       = common_local_url(
-            'favorites',
-            array('nickname' => $this->user->nickname)
-        );
-        $subtitle   = sprintf(
+
+        $subtitle = sprintf(
             _('%1$s updates favorited by %2$s / %2$s.'),
             $sitename,
             $profile->getBestName(),
             $this->user->nickname
         );
-        $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+        $logo = !empty($avatar)
+            ? $avatar->displayUrl()
+            : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
 
         switch($this->format) {
         case 'xml':
             $this->showXmlTimeline($this->notices);
             break;
         case 'rss':
-            $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
+            $link = common_local_url(
+                'showfavorites',
+                array('nickname' => $this->user->nickname)
+            );
+            $this->showRssTimeline(
+                $this->notices,
+                $title,
+                $link,
+                $subtitle,
+                null,
+                $logo
+            );
             break;
         case 'atom':
-            $selfuri = common_root_url() .
-                ltrim($_SERVER['QUERY_STRING'], 'p=');
-            $this->showAtomTimeline(
-                $this->notices, $title, $id, $link, $subtitle,
-                null, $selfuri, $logo
+
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'showfavorites',
+                    array('nickname' => $this->user->nickname)
+                )
+            );
+
+            $id = $this->arg('id');
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
+            }
+
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineFavorites', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
             );
+
+            $atom->addEntryFromNotices($this->notices);
+
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index 4e3827baea61a5bf9f1c947e8f2cad55b86d7f08..0af04fe4fb2cef71c07b1ce90528e4016806a349 100644 (file)
@@ -114,39 +114,71 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction
         $title      = sprintf(_("%s and friends"), $this->user->nickname);
         $taguribase = common_config('integration', 'taguri');
         $id         = "tag:$taguribase:FriendsTimeline:" . $this->user->id;
-        $link       = common_local_url(
-                                       'all', array('nickname' => $this->user->nickname)
-                                       );
-        $subtitle   = sprintf(
-                              _('Updates from %1$s and friends on %2$s!'),
-                              $this->user->nickname, $sitename
-                              );
-        $logo       = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+
+        $subtitle = sprintf(
+            _('Updates from %1$s and friends on %2$s!'),
+            $this->user->nickname, $sitename
+        );
+
+        $logo = (!empty($avatar))
+            ? $avatar->displayUrl()
+            : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
 
         switch($this->format) {
         case 'xml':
             $this->showXmlTimeline($this->notices);
             break;
         case 'rss':
-            $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
+
+            $link = common_local_url(
+                'all', array(
+                    'nickname' => $this->user->nickname
+                )
+            );
+
+            $this->showRssTimeline(
+                $this->notices,
+                $title,
+                $link,
+                $subtitle,
+                null,
+                $logo
+            );
             break;
         case 'atom':
 
-            $target_id = $this->arg('id');
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
 
-            if (isset($target_id)) {
-                $selfuri = common_root_url() .
-                  'api/statuses/friends_timeline/' .
-                  $target_id . '.atom';
-            } else {
-                $selfuri = common_root_url() .
-                  'api/statuses/friends_timeline.atom';
+            $atom->addLink(
+                common_local_url(
+                    'all',
+                    array('nickname' => $this->user->nickname)
+                )
+            );
+
+            $id = $this->arg('id');
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
             }
 
-            $this->showAtomTimeline(
-                                    $this->notices, $title, $id, $link,
-                                    $subtitle, null, $selfuri, $logo
-                                    );
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineFriends', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
+            );
+
+            $atom->addEntryFromNotices($this->notices);
+
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index af414c680403e557361d15ef3c9e1b554353bee4..3c74e36b566ec0c964d16c7a1d5127a0bc6a71cf 100644 (file)
@@ -109,38 +109,82 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction
         $title      = sprintf(_("%s timeline"), $this->group->nickname);
         $taguribase = common_config('integration', 'taguri');
         $id         = "tag:$taguribase:GroupTimeline:" . $this->group->id;
-        $link       = common_local_url(
-            'showgroup',
-            array('nickname' => $this->group->nickname)
-        );
+
         $subtitle   = sprintf(
             _('Updates from %1$s on %2$s!'),
             $this->group->nickname,
             $sitename
         );
-        $logo       = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
+
+        $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
 
         switch($this->format) {
         case 'xml':
             $this->showXmlTimeline($this->notices);
             break;
         case 'rss':
-            $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
-            break;
-        case 'atom':
-            $selfuri = common_root_url() .
-                'api/statusnet/groups/timeline/' .
-                    $this->group->nickname . '.atom';
-            $this->showAtomTimeline(
+                $this->showRssTimeline(
                 $this->notices,
                 $title,
-                $id,
-                $link,
+                $this->group->homeUrl(),
                 $subtitle,
                 null,
-                $selfuri,
                 $logo
             );
+            break;
+        case 'atom':
+
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            try {
+
+                // If this was called using an integer ID, i.e.: using the canonical
+                // URL for this group's feed, then pass the Group object into the feed, 
+                // so the OStatus plugin, and possibly other plugins, can access it. 
+                // Feels sorta hacky. -- Z
+
+                $atom = null;
+                $id = $this->arg('id');
+
+                if (strval(intval($id)) === strval($id)) {
+                    $atom = new AtomGroupNoticeFeed($this->group);
+                } else {
+                    $atom = new AtomGroupNoticeFeed();
+                }
+
+                $atom->setId($id);
+                $atom->setTitle($title);
+                $atom->setSubtitle($subtitle);
+                $atom->setLogo($logo);
+                $atom->setUpdated('now');
+
+                $atom->addAuthorRaw($this->group->asAtomAuthor());
+                $atom->setActivitySubject($this->group->asActivitySubject());
+
+                $atom->addLink($this->group->homeUrl());
+
+                $id = $this->arg('id');
+                $aargs = array('format' => 'atom');
+                if (!empty($id)) {
+                    $aargs['id'] = $id;
+                }
+
+                $atom->addLink(
+                    $this->getSelfUri('ApiTimelineGroup', $aargs),
+                    array('rel' => 'self', 'type' => 'application/atom+xml')
+                );
+
+                $atom->addEntryFromNotices($this->notices);
+
+                $this->raw($atom->getString());
+
+            } catch (Atom10FeedException $e) {
+                $this->serverError(
+                    'Could not generate feed for group - ' . $e->getMessage()
+                );
+                return;
+            }
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index 828eae6cf6f912382f350afe2fe17ba716041bc9..ae41680702fd6484349eba4ff178edd589abdc1d 100644 (file)
@@ -115,39 +115,67 @@ class ApiTimelineHomeAction extends ApiBareAuthAction
         $title      = sprintf(_("%s and friends"), $this->user->nickname);
         $taguribase = common_config('integration', 'taguri');
         $id         = "tag:$taguribase:HomeTimeline:" . $this->user->id;
-        $link       = common_local_url(
-            'all', array('nickname' => $this->user->nickname)
-        );
+
         $subtitle   = sprintf(
             _('Updates from %1$s and friends on %2$s!'),
             $this->user->nickname, $sitename
         );
-        $logo       = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+
+        $logo = (!empty($avatar)) 
+            ? $avatar->displayUrl() 
+            : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
 
         switch($this->format) {
         case 'xml':
             $this->showXmlTimeline($this->notices);
             break;
         case 'rss':
-            $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
+            $link = common_local_url(
+                'all',
+                array('nickname' => $this->user->nickname)
+            );
+            $this->showRssTimeline(
+                $this->notices,
+                $title,
+                $link,
+                $subtitle,
+                null,
+                $logo
+            );
             break;
         case 'atom':
 
-            $target_id = $this->arg('id');
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            $atom = new AtomNoticeFeed();
 
-            if (isset($target_id)) {
-                $selfuri = common_root_url() .
-                    'api/statuses/home_timeline/' .
-                    $target_id . '.atom';
-            } else {
-                $selfuri = common_root_url() .
-                    'api/statuses/home_timeline.atom';
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'all',
+                    array('nickname' => $this->user->nickname)
+                )
+            );
+
+            $id = $this->arg('id');
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
             }
 
-            $this->showAtomTimeline(
-                $this->notices, $title, $id, $link,
-                $subtitle, null, $selfuri, $logo
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineHome', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
             );
+
+            $atom->addEntryFromNotices($this->notices);
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index 9dc2162cc45fa738e8c368c0b8841f19ef3f677a..d2e31d0bdd095fe40e592b98647dd14ee50a5689 100644 (file)
@@ -137,12 +137,36 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction
             $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
             break;
         case 'atom':
-            $selfuri = common_root_url() .
-                ltrim($_SERVER['QUERY_STRING'], 'p=');
-            $this->showAtomTimeline(
-                $this->notices, $title, $id, $link, $subtitle,
-                null, $selfuri, $logo
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'replies',
+                    array('nickname' => $this->user->nickname)
+                )
+            );
+
+            $id = $this->arg('id');
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
+            }
+
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineMentions', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
             );
+
+            $atom->addEntryFromNotices($this->notices);
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index 0fb0788e98795b286daf9b7f37f7d9cf0d92804c..c1fa72a3ee372f72e944931f86c6480bff17aabc 100644 (file)
@@ -74,7 +74,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
         parent::prepare($args);
 
         $this->notices = $this->getNotices();
-        
+
         if ($this->since) {
             throw new ServerException("since parameter is disabled for performance; use since_id", 403);
         }
@@ -122,11 +122,28 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
             $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo);
             break;
         case 'atom':
-            $selfuri = common_root_url() . 'api/statuses/public_timeline.atom';
-            $this->showAtomTimeline(
-                $this->notices, $title, $id, $link,
-                $subtitle, null, $selfuri, $sitelogo
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($sitelogo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(common_local_url('public'));
+
+            $atom->addLink(
+                $this->getSelfUri(
+                    'ApiTimelinePublic', array('format' => 'atom')
+                ),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
             );
+
+            $atom->addEntryFromNotices($this->notices);
+
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index e4b09e9bdaf334b860022482d5d1e0bac6b41bd9..26706a75e7650c1c65717bd79237676e635d36b5 100644 (file)
@@ -99,6 +99,8 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction
 
         $strm = $this->auth_user->repeatsOfMe($offset, $limit, $this->since_id, $this->max_id);
 
+        common_debug(var_export($strm, true));
+
         switch ($this->format) {
         case 'xml':
             $this->showXmlTimeline($strm);
@@ -112,10 +114,38 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction
             $title      = sprintf(_("Repeats of %s"), $this->auth_user->nickname);
             $taguribase = common_config('integration', 'taguri');
             $id         = "tag:$taguribase:RepeatsOfMe:" . $this->auth_user->id;
-            $link       = common_local_url('showstream',
-                                           array('nickname' => $this->auth_user->nickname));
 
-            $this->showAtomTimeline($strm, $title, $id, $link);
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'showstream',
+                    array('nickname' => $this->auth_user->nickname)
+                )
+            );
+
+            $id = $this->arg('id');
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
+            }
+
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineRetweetsOfMe', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
+            );
+
+            $atom->addEntryFromNotices($strm);
+
+            $this->raw($atom->getString());
+
             break;
 
         default:
index 1427d23b6a45d70e0f93f26292f11d25b8c4f1d1..5b6ded4c048d6b08fe77838b2ced98b674484e51 100644 (file)
@@ -100,10 +100,6 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
         $sitename   = common_config('site', 'name');
         $sitelogo   = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png');
         $title      = sprintf(_("Notices tagged with %s"), $this->tag);
-        $link       = common_local_url(
-            'tag',
-            array('tag' => $this->tag)
-        );
         $subtitle   = sprintf(
             _('Updates tagged with %1$s on %2$s!'),
             $this->tag,
@@ -117,22 +113,51 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
             $this->showXmlTimeline($this->notices);
             break;
         case 'rss':
-            $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo);
-            break;
-        case 'atom':
-            $selfuri = common_root_url() .
-                'api/statusnet/tags/timeline/' .
-                    $this->tag . '.atom';
-            $this->showAtomTimeline(
+            $link = common_local_url(
+                'tag',
+                array('tag' => $this->tag)
+            );
+            $this->showRssTimeline(
                 $this->notices,
                 $title,
-                $id,
                 $link,
                 $subtitle,
                 null,
-                $selfuri,
                 $sitelogo
             );
+            break;
+        case 'atom':
+
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'tag',
+                    array('tag' => $this->tag)
+                )
+            );
+
+            $aargs = array('format' => 'atom');
+            if (!empty($this->tag)) {
+                $aargs['tag'] = $this->tag;
+            }
+
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineTag', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
+            );
+
+            $atom->addEntryFromNotices($this->notices);
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index 830b16941dc8898ccfc0453ca23a2eb384203d85..9f7ec4c2363ab7bdac8490f7796c3fd04c377327 100644 (file)
@@ -145,18 +145,60 @@ class ApiTimelineUserAction extends ApiBareAuthAction
             );
             break;
         case 'atom':
-            if (isset($apidata['api_arg'])) {
-                $selfuri = common_root_url() .
-                    'api/statuses/user_timeline/' .
-                    $apidata['api_arg'] . '.atom';
+
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            // If this was called using an integer ID, i.e.: using the canonical
+            // URL for this user's feed, then pass the User object into the feed,
+            // so the OStatus plugin, and possibly other plugins, can access it.
+            // Feels sorta hacky. -- Z
+
+            $atom = null;
+            $id = $this->arg('id');
+
+            if (strval(intval($id)) === strval($id)) {
+                $atom = new AtomUserNoticeFeed($this->user);
             } else {
-                $selfuri = common_root_url() .
-                    'api/statuses/user_timeline.atom';
+                $atom = new AtomUserNoticeFeed();
+            }
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'showstream',
+                    array('nickname' => $this->user->nickname)
+                )
+            );
+
+            $id = $this->arg('id');
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
             }
-            $this->showAtomTimeline(
-                $this->notices, $title, $id, $link,
-                $subtitle, $suplink, $selfuri, $logo
+
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineUser', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
+            );
+
+            $atom->addLink(
+                $suplink,
+                array(
+                    'rel' => 'http://api.friendfeed.com/2008/03#sup',
+                    'type' => 'application/json'
+                )
             );
+
+            $atom->addEntryFromNotices($this->notices);
+
+            #$this->raw($atom->getString());
+            print $atom->getString(); // temporary for output buffering
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index 0f47c268dd30eda6bd4094bc7acbca56c3259031..f16e972a4194a6eee1a0fd60e2c4fad70881b36d 100644 (file)
@@ -192,7 +192,9 @@ class GroupMemberListItem extends ProfileListItem
     {
         $user = common_current_user();
 
-        if (!empty($user) && $user->id != $this->profile->id && $user->isAdmin($this->group) &&
+        if (!empty($user) &&
+            $user->id != $this->profile->id &&
+            ($user->isAdmin($this->group) || $user->hasRight(Right::MAKEGROUPADMIN)) &&
             !$this->profile->isAdmin($this->group)) {
             $this->out->elementStart('li', 'entity_make_admin');
             $maf = new MakeAdminForm($this->out, $this->profile, $this->group,
index 9ad7d6e7c821a247cdc1961e1cbed593dc785991..f19348648d8700f50b0ef8955419dbd7a0b24ecf 100644 (file)
@@ -87,7 +87,8 @@ class MakeadminAction extends Action
             return false;
         }
         $user = common_current_user();
-        if (!$user->isAdmin($this->group)) {
+        if (!$user->isAdmin($this->group) &&
+            !$user->hasRight(Right::MAKEGROUPADMIN)) {
             $this->clientError(_('Only an admin can make another user an admin.'), 401);
             return false;
         }
index 982dfde15700863f6fff8da111a7f04376fdb070..50278bfcedab55a80c9fe2c2ed359ad79399e727 100644 (file)
@@ -131,12 +131,20 @@ class PublicAction extends Action
             return _('Public timeline');
         }
     }
-    
+
     function extraHead()
     {
         parent::extraHead();
         $this->element('meta', array('http-equiv' => 'X-XRDS-Location',
                                            'content' => common_local_url('publicxrds')));
+
+        $rsd = common_local_url('rsd');
+
+        // RSD, http://tales.phrasewise.com/rfc/rsd
+
+        $this->element('link', array('rel' => 'EditURI',
+                                     'type' => 'application/rsd+xml',
+                                     'href' => $rsd));
     }
 
     /**
diff --git a/actions/rsd.php b/actions/rsd.php
new file mode 100644 (file)
index 0000000..f88bf2e
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008-2010, StatusNet, Inc.
+ *
+ * Really Simple Discovery (RSD) for API access
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category API
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ *
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * RSD action class
+ *
+ * Really Simple Discovery (RSD) is a simple (to a fault, maybe)
+ * discovery tool for blog APIs.
+ *
+ * http://tales.phrasewise.com/rfc/rsd
+ *
+ * Anil Dash suggested that RSD be used for services that implement
+ * the Twitter API:
+ *
+ * http://dashes.com/anil/2009/12/the-twitter-api-is-finished.html
+ *
+ * It's in use now for WordPress.com blogs:
+ *
+ * http://matt.wordpress.com/xmlrpc.php?rsd
+ *
+ * I (evan@status.net) have tried to stay faithful to the premise of
+ * RSD, while adding information useful to StatusNet client developers.
+ * In particular:
+ *
+ * - There is a link from each user's profile page to their personal
+ *   RSD feed. A personal rsd.xml includes a 'blogID' element that is
+ *   their username.
+ * - There is a link from the public root to '/rsd.xml', a public RSD
+ *   feed. It's identical to the personal rsd except it doesn't include
+ *   a blogId.
+ * - I've added a setting to the API to indicate that OAuth support is
+ *   available.
+ *
+ * @category API
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ */
+
+class RsdAction extends Action
+{
+    /**
+     * Optional attribute for the personal rsd.xml file.
+     */
+
+    var $user = null;
+
+    /**
+     * Prepare the action for use.
+     *
+     * Check for a nickname; redirect if non-canonical; if
+     * not provided, assume public rsd.xml.
+     *
+     * @param array $args GET, POST, and URI arguments.
+     *
+     * @return boolean success flag
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        // optional argument
+
+        $nickname_arg = $this->arg('nickname');
+
+        if (empty($nickname_arg)) {
+            $this->user = null;
+        } else {
+            $nickname = common_canonical_nickname($nickname_arg);
+
+            // Permanent redirect on non-canonical nickname
+
+            if ($nickname_arg != $nickname) {
+                common_redirect(common_local_url('rsd',
+                                                 array('nickname' => $nickname)),
+                                301);
+                return false;
+            }
+
+            $this->user = User::staticGet('nickname', $nickname);
+
+            if (empty($this->user)) {
+                $this->clientError(_('No such user.'), 404);
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Action handler.
+     *
+     * Outputs the XML format for an RSD file. May include
+     * personal information if this is a personal file
+     * (based on whether $user attribute is set).
+     *
+     * @param array $args array of arguments
+     *
+     * @return nothing
+     */
+
+    function handle($args)
+    {
+        header('Content-Type: application/rsd+xml');
+
+        $this->startXML();
+
+        $rsdNS = 'http://archipelago.phrasewise.com/rsd';
+        $this->elementStart('rsd', array('version' => '1.0',
+                                         'xmlns' => $rsdNS));
+        $this->elementStart('service');
+        $this->element('engineName', null, _('StatusNet'));
+        $this->element('engineLink', null, 'http://status.net/');
+        $this->elementStart('apis');
+        if (Event::handle('StartRsdListApis', array($this, $this->user))) {
+
+            $blogID   = (empty($this->user)) ? '' : $this->user->nickname;
+            $apiAttrs = array('name' => 'Twitter',
+                              'preferred' => 'true',
+                              'apiLink' => $this->_apiRoot(),
+                              'blogID' => $blogID);
+
+            $this->elementStart('api', $apiAttrs);
+            $this->elementStart('settings');
+            $this->element('docs', null,
+                           'http://status.net/wiki/TwitterCompatibleAPI');
+            $this->element('setting', array('name' => 'OAuth'),
+                           'true');
+            $this->elementEnd('settings');
+            $this->elementEnd('api');
+            Event::handle('EndRsdListApis', array($this, $this->user));
+        }
+        $this->elementEnd('apis');
+        $this->elementEnd('service');
+        $this->elementEnd('rsd');
+
+        $this->endXML();
+
+        return true;
+    }
+
+    /**
+     * Returns last-modified date for use in caching
+     *
+     * Per-user rsd.xml is dated to last change of user
+     * (in case of nickname change); public has no date.
+     *
+     * @return string date of last change of this page
+     */
+
+    function lastModified()
+    {
+        if (!empty($this->user)) {
+            return $this->user->modified;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Flag to indicate if this action is read-only
+     *
+     * It is; it doesn't change the DB.
+     *
+     * @param array $args ignored
+     *
+     * @return boolean true
+     */
+
+    function isReadOnly($args)
+    {
+        return true;
+    }
+
+    /**
+     * Return current site's API root
+     *
+     * Varies based on URL parameters, like if fancy URLs are
+     * turned on.
+     *
+     * @return string API root URI for this site
+     */
+
+    private function _apiRoot()
+    {
+        if (common_config('site', 'fancy')) {
+            return common_path('api/', true);
+        } else {
+            return common_path('index.php/api/', true);
+        }
+    }
+}
index 8042a4951339a26f7aebaf4ad56f2e05de3a386b..eb12389029a096dd0373ae7ed523ddc517092d62 100644 (file)
@@ -330,13 +330,13 @@ class ShowgroupAction extends GroupDesignAction
                      new Feed(Feed::RSS2,
                               common_local_url('ApiTimelineGroup',
                                                array('format' => 'rss',
-                                                     'id' => $this->group->nickname)),
+                                                     'id' => $this->group->id)),
                               sprintf(_('Notice feed for %s group (RSS 2.0)'),
                                       $this->group->nickname)),
                      new Feed(Feed::ATOM,
                               common_local_url('ApiTimelineGroup',
                                                array('format' => 'atom',
-                                                     'id' => $this->group->nickname)),
+                                                     'id' => $this->group->id)),
                               sprintf(_('Notice feed for %s group (Atom)'),
                                       $this->group->nickname)),
                      new Feed(Feed::FOAF,
index c529193860ff886d02b937514f9f8474b9b2af3b..f9407e35a1f7890189b817c39dd8900c2c6a1f33 100644 (file)
@@ -131,14 +131,14 @@ class ShowstreamAction extends ProfileAction
                      new Feed(Feed::RSS2,
                               common_local_url('ApiTimelineUser',
                                                array(
-                                                    'id' => $this->user->nickname,
+                                                    'id' => $this->user->id,
                                                     'format' => 'rss')),
                               sprintf(_('Notice feed for %s (RSS 2.0)'),
                                       $this->user->nickname)),
                      new Feed(Feed::ATOM,
                               common_local_url('ApiTimelineUser',
                                                array(
-                                                    'id' => $this->user->nickname,
+                                                    'id' => $this->user->id,
                                                     'format' => 'atom')),
                               sprintf(_('Notice feed for %s (Atom)'),
                                       $this->user->nickname)),
@@ -178,6 +178,15 @@ class ShowstreamAction extends ProfileAction
         $this->element('link', array('rel' => 'microsummary',
                                      'href' => common_local_url('microsummary',
                                                                 array('nickname' => $this->profile->nickname))));
+
+        $rsd = common_local_url('rsd',
+                                array('nickname' => $this->profile->nickname));
+
+        // RSD, http://tales.phrasewise.com/rfc/rsd
+        $this->element('link', array('rel' => 'EditURI',
+                                     'type' => 'application/rsd+xml',
+                                     'href' => $rsd));
+
     }
 
     function showProfile()
index 4321f1302e0ca6735c8307128fe083a00e616946..7f71c60dbe64441adc9515b1ab106362089ce897 100644 (file)
@@ -127,10 +127,10 @@ class UserauthorizationAction extends Action
         $location = $params->getLocation();
         $avatar   = $params->getAvatarURL();
 
-        $this->elementStart('div', array('class' => 'profile'));
         $this->elementStart('div', 'entity_profile vcard');
-        $this->elementStart('a', array('href' => $profile,
-                                            'class' => 'url'));
+        $this->elementStart('dl', 'entity_depiction');
+        $this->element('dt', null, _('Photo'));
+        $this->elementStart('dd');
         if ($avatar) {
             $this->element('img', array('src' => $avatar,
                                         'class' => 'photo avatar',
@@ -138,11 +138,19 @@ class UserauthorizationAction extends Action
                                         'height' => AVATAR_PROFILE_SIZE,
                                         'alt' => $nickname));
         }
+        $this->elementEnd('dd');
+        $this->elementEnd('dl');
+
+        $this->elementStart('dl', 'entity_nickname');
+        $this->element('dt', null, _('Nickname'));
+        $this->elementStart('dd');
         $hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname';
-        $this->elementStart('span', $hasFN);
+        $this->elementStart('a', array('href' => $profile,
+                                       'class' => 'url '.$hasFN));
         $this->raw($nickname);
-        $this->elementEnd('span');
         $this->elementEnd('a');
+        $this->elementEnd('dd');
+        $this->elementEnd('dl');
 
         if (!is_null($fullname)) {
             $this->elementStart('dl', 'entity_fn');
@@ -214,7 +222,6 @@ class UserauthorizationAction extends Action
         $this->elementEnd('li');
         $this->elementEnd('ul');
         $this->elementEnd('div');
-        $this->elementEnd('div');
     }
 
     function sendAuthorization()
@@ -350,4 +357,4 @@ class UserauthorizationAction extends Action
             }
         }
     }
-}
\ No newline at end of file
+}
index 91bde0f0401b6dc144b48a6ba434c312c80c967c..dbe2cd813847eb585f94999b595e2f495ab0d03b 100644 (file)
@@ -82,9 +82,20 @@ class Avatar extends Memcached_DataObject
             $server = common_config('site', 'server');
         }
 
-        // XXX: protocol
+        $ssl = common_config('avatar', 'ssl');
+
+        if (is_null($ssl)) { // null -> guess
+            if (common_config('site', 'ssl') == 'always' &&
+                !common_config('avatar', 'server')) {
+                $ssl = true;
+            } else {
+                $ssl = false;
+            }
+        }
+
+        $protocol = ($ssl) ? 'https' : 'http';
 
-        return 'http://'.$server.$path.$filename;
+        return $protocol.'://'.$server.$path.$filename;
     }
 
     function displayUrl()
diff --git a/classes/Conversation.php b/classes/Conversation.php
new file mode 100755 (executable)
index 0000000..ea8bd87
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Data class for Conversations
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Data
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+require_once INSTALLDIR . '/classes/Memcached_DataObject.php';
+
+class Conversation extends Memcached_DataObject
+{
+    ###START_AUTOCODE
+    /* the code below is auto generated do not remove the above tag */
+
+    public $__table = 'conversation';                    // table name
+    public $id;                              // int(4)  primary_key not_null
+    public $uri;                             // varchar(225)  unique_key
+    public $created;                         // datetime   not_null
+    public $modified;                        // timestamp   not_null default_CURRENT_TIMESTAMP
+
+    /* Static get */
+    function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('conversation',$k,$v); }
+
+    /* the code above is auto generated do not remove the tag below */
+    ###END_AUTOCODE
+
+    /**
+     * Factory method for creating a new conversation
+     *
+     * @return Conversation the new conversation DO
+     */
+    static function create()
+    {
+        $conv = new Conversation();
+        $conv->created = common_sql_now();
+        $id = $conv->insert();
+
+        if (empty($id)) {
+            common_log_db_error($conv, 'INSERT', __FILE__);
+            return null;
+        }
+
+        $orig = clone($conv);
+        $orig->uri = common_local_url('conversation', array('id' => $id));
+        $result = $orig->update($conv);
+
+        if (empty($result)) {
+            common_log_db_error($conv, 'UPDATE', __FILE__);
+            return null;
+        }
+
+        return $conv;
+    }
+
+}
+
index 4e7d7dfb257854a4a898ae44524b9a0739671e72..ff44e010964042f0d6e8b18de603668c2c71d496 100644 (file)
@@ -155,9 +155,20 @@ class Design extends Memcached_DataObject
             $server = common_config('site', 'server');
         }
 
-        // XXX: protocol
+        $ssl = common_config('background', 'ssl');
+
+        if (is_null($ssl)) { // null -> guess
+            if (common_config('site', 'ssl') == 'always' &&
+                !common_config('background', 'server')) {
+                $ssl = true;
+            } else {
+                $ssl = false;
+            }
+        }
+
+        $protocol = ($ssl) ? 'https' : 'http';
 
-        return 'http://'.$server.$path.$filename;
+        return $protocol.'://'.$server.$path.$filename;
     }
 
     function setDisposition($on, $off, $tile)
index ee418a802413a203ed3ea1460afb8fc3d91aeab7..91b12d2e28664e61206aadb9abb4e8301fe21441 100644 (file)
@@ -228,9 +228,20 @@ class File extends Memcached_DataObject
                 $server = common_config('site', 'server');
             }
 
-            // XXX: protocol
+            $ssl = common_config('attachments', 'ssl');
 
-            return 'http://'.$server.$path.$filename;
+            if (is_null($ssl)) { // null -> guess
+                if (common_config('site', 'ssl') == 'always' &&
+                    !common_config('attachments', 'server')) {
+                    $ssl = true;
+                } else {
+                    $ssl = false;
+                }
+            }
+
+            $protocol = ($ssl) ? 'https' : 'http';
+
+            return $protocol.'://'.$server.$path.$filename;
         }
     }
 
index 16c3d906ce30688997572a20e5c9895b3ec42585..40576dc71783aedda343e8ea25414b11f777e70e 100644 (file)
@@ -314,7 +314,7 @@ class Memcached_DataObject extends Safe_DataObject
             $cached[] = clone($inst);
         }
         $inst->free();
-        $c->set($ckey, $cached, MEMCACHE_COMPRESSED, $expiry);
+        $c->set($ckey, $cached, Cache::COMPRESSED, $expiry);
         return new ArrayWrapper($cached);
     }
 
index 486a65a3c7e2aa6d78bc8478f579b27e8d80ab93..2f8ab00b5dc097886687a8e2413bd59df689c59d 100644 (file)
@@ -22,4 +22,19 @@ class Nonce extends Memcached_DataObject
 
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
+
+    /**
+     * Compatibility hack for PHP 5.3
+     *
+     * The statusnet.links.ini entry cannot be read because "," is no longer
+     * allowed in key names when read by parse_ini_file().
+     *
+     * @return   array
+     * @access   public
+     */
+    function links()
+    {
+        return array('consumer_key,token' => 'token:consumer_key,token');
+    }
+
 }
index f9f38635797f44456610cb973baf2611bdebd3a3..b0edb6de60053500ec34c01fbf686a4f8da64cf4 100644 (file)
@@ -309,7 +309,8 @@ class Notice extends Memcached_DataObject
             // the beginning of a new conversation.
 
             if (empty($notice->conversation)) {
-                $notice->conversation = $notice->id;
+                $conv = Conversation::create();
+                $notice->conversation = $conv->id;
                 $changed = true;
             }
 
@@ -331,14 +332,15 @@ class Notice extends Memcached_DataObject
         return $notice;
     }
 
-    function blowOnInsert()
+    function blowOnInsert($conversation = false)
     {
         self::blow('profile:notice_ids:%d', $this->profile_id);
         self::blow('public');
 
-        if ($this->conversation != $this->id) {
-            self::blow('notice:conversation_ids:%d', $this->conversation);
-        }
+        // XXX: Before we were blowing the casche only if the notice id
+        // was not the root of the conversation.  What to do now?
+
+        self::blow('notice:conversation_ids:%d', $this->conversation);
 
         if (!empty($this->repeat_of)) {
             self::blow('notice:repeats:%d', $this->repeat_of);
@@ -783,7 +785,7 @@ class Notice extends Memcached_DataObject
 
             $result = $gi->insert();
 
-            if (!result) {
+            if (!$result) {
                 common_log_db_error($gi, 'INSERT', __FILE__);
                 throw new ServerException(_('Problem saving group inbox.'));
             }
@@ -917,7 +919,7 @@ class Notice extends Memcached_DataObject
     /**
      * Same calculation as saveGroups but without the saving
      * @fixme merge the functions
-     * @return array of Group objects
+     * @return array of Group_inbox objects
      */
     function getGroups()
     {
@@ -957,7 +959,10 @@ class Notice extends Memcached_DataObject
 
         if ($namespace) {
             $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
-                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
+                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
+                           'xmlns:georss' => 'http://www.georss.org/georss',
+                           'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
+                           'xmlns:ostatus' => 'http://ostatus.org/schema/1.0');
         } else {
             $attrs = array();
         }
@@ -983,11 +988,6 @@ class Notice extends Memcached_DataObject
             $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE));
         }
 
-        $xs->elementStart('author');
-        $xs->element('name', null, $profile->nickname);
-        $xs->element('uri', null, $profile->profileurl);
-        $xs->elementEnd('author');
-
         if ($source) {
             $xs->elementEnd('source');
         }
@@ -995,6 +995,9 @@ class Notice extends Memcached_DataObject
         $xs->element('title', null, $this->content);
         $xs->element('summary', null, $this->content);
 
+        $xs->raw($profile->asAtomAuthor());
+        $xs->raw($profile->asActivityActor());
+
         $xs->element('link', array('rel' => 'alternate',
                                    'href' => $this->bestUrl()));
 
@@ -1014,6 +1017,44 @@ class Notice extends Memcached_DataObject
             }
         }
 
+        if (!empty($this->conversation)) {
+
+            $conv = Conversation::staticGet('id', $this->conversation);
+
+            if (!empty($conv)) {
+                $xs->element(
+                    'link', array(
+                        'rel' => 'ostatus:conversation',
+                        'href' => $conv->uri
+                    )
+                );
+            }
+        }
+
+        $reply_ids = $this->getReplies();
+
+        foreach ($reply_ids as $id) {
+            $profile = Profile::staticGet('id', $id);
+           if (!empty($profile)) {
+                $xs->element(
+                    'link', array(
+                        'rel' => 'ostatus:attention',
+                        'href' => $profile->getUri()
+                    )
+                );
+            }
+        }
+
+        if (!empty($this->repeat_of)) {
+            $repeat = Notice::staticGet('id', $this->repeat_of);
+            if (!empty($repeat)) {
+                $xs->element(
+                    'ostatus:forward',
+                     array('ref' => $repeat->uri, 'href' => $repeat->bestUrl())
+                );
+            }
+        }
+
         $xs->element('content', array('type' => 'html'), $this->rendered);
 
         $tag = new Notice_tag();
@@ -1041,9 +1082,7 @@ class Notice extends Memcached_DataObject
         }
 
         if (!empty($this->lat) && !empty($this->lon)) {
-            $xs->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
             $xs->element('georss:point', null, $this->lat . ' ' . $this->lon);
-            $xs->elementEnd('geo');
         }
 
         $xs->elementEnd('entry');
@@ -1176,6 +1215,10 @@ class Notice extends Memcached_DataObject
         // Figure out who that is.
 
         $sender = Profile::staticGet('id', $profile_id);
+        if (empty($sender)) {
+            return null;
+        }
+
         $recipient = common_relative_profile($sender, $nickname, common_sql_now());
 
         if (empty($recipient)) {
index 1076fb2cb3e09f8b95b1915b36f1b6bb5ffbcef6..494c697e425fab0fa9726cba9bf0aac41635e2ff 100644 (file)
@@ -716,6 +716,7 @@ class Profile extends Memcached_DataObject
             switch ($right)
             {
             case Right::DELETEOTHERSNOTICE:
+            case Right::MAKEGROUPADMIN:
             case Right::SANDBOXUSER:
             case Right::SILENCEUSER:
             case Right::DELETEUSER:
@@ -753,4 +754,118 @@ class Profile extends Memcached_DataObject
 
         return !empty($notice);
     }
+
+    /**
+     * Returns an XML string fragment with limited profile information
+     * as an Atom <author> element.
+     *
+     * Assumes that Atom has been previously set up as the base namespace.
+     *
+     * @return string
+     */
+    function asAtomAuthor()
+    {
+        $xs = new XMLStringer(true);
+
+        $xs->elementStart('author');
+        $xs->element('name', null, $this->nickname);
+        $xs->element('uri', null, $this->getUri());
+        $xs->elementEnd('author');
+
+        return $xs->getString();
+    }
+
+    /**
+     * Returns an XML string fragment with profile information as an
+     * Activity Streams <activity:actor> element.
+     *
+     * Assumes that 'activity' namespace has been previously defined.
+     *
+     * @return string
+     */
+    function asActivityActor()
+    {
+        return $this->asActivityNoun('actor');
+    }
+
+    /**
+     * Returns an XML string fragment with profile information as an
+     * Activity Streams noun object with the given element type.
+     *
+     * Assumes that 'activity' namespace has been previously defined.
+     *
+     * @param string $element one of 'actor', 'subject', 'object', 'target'
+     * @return string
+     */
+    function asActivityNoun($element)
+    {
+        $xs = new XMLStringer(true);
+
+        $xs->elementStart('activity:' . $element);
+        $xs->element(
+            'activity:object-type',
+            null,
+            'http://activitystrea.ms/schema/1.0/person'
+        );
+        $xs->element(
+            'id',
+            null,
+            $this->getUri()
+            );
+        $xs->element('title', null, $this->getBestName());
+
+        $avatar = $this->getAvatar(AVATAR_PROFILE_SIZE);
+
+        $xs->element(
+            'link', array(
+                'type' => empty($avatar) ? 'image/png' : $avatar->mediatype,
+                'rel'  => 'avatar',
+                'href' => empty($avatar)
+                ? Avatar::defaultImage(AVATAR_PROFILE_SIZE)
+                : $avatar->displayUrl()
+            ),
+            ''
+        );
+
+        $xs->elementEnd('activity:' . $element);
+
+        return $xs->getString();
+    }
+
+    /**
+     * Returns the best URI for a profile. Plugins may override.
+     *
+     * @return string $uri
+     */
+    function getUri()
+    {
+        $uri = null;
+
+        // check for a local user first
+        $user = User::staticGet('id', $this->id);
+
+        if (!empty($user)) {
+            $uri = common_local_url(
+                'userbyid',
+                array('id' => $user->id)
+            );
+        } else {
+
+            // give plugins a chance to set the URI
+            if (Event::handle('StartGetProfileUri', array($this, &$uri))) {
+
+                // return OMB profile if any
+                $remote = Remote_profile::staticGet('id', $this->id);
+
+                if (!empty($remote)) {
+                    $uri = $remote->uri;
+                }
+
+                Event::handle('EndGetProfileUri', array($this, &$uri));
+            }
+        }
+
+        return $uri;
+    }
+
 }
index c86eadf8fa7fff1e21ca9cb2f496d4aa4ce9acb4..379e6b7219fb6d84eed97b069d3e6c410c12ba36 100644 (file)
@@ -49,12 +49,12 @@ class User_group extends Memcached_DataObject
                                 array('id' => $this->id));
     }
 
-    function getNotices($offset, $limit)
+    function getNotices($offset, $limit, $since_id=null, $max_id=null)
     {
         $ids = Notice::stream(array($this, '_streamDirect'),
                               array(),
                               'user_group:notice_ids:' . $this->id,
-                              $offset, $limit);
+                              $offset, $limit, $since_id, $max_id);
 
         return Notice::getStreamByIds($ids);
     }
@@ -355,6 +355,39 @@ class User_group extends Memcached_DataObject
         return $xs->getString();
     }
 
+    function asAtomAuthor()
+    {
+        $xs = new XMLStringer(true);
+
+        $xs->elementStart('author');
+        $xs->element('name', null, $this->nickname);
+        $xs->element('uri', null, $this->permalink());
+        $xs->elementEnd('author');
+
+        return $xs->getString();
+    }
+
+    function asActivitySubject()
+    {
+        $xs = new XMLStringer(true);
+
+        $xs->elementStart('activity:subject');
+        $xs->element('activity:object', null, 'http://activitystrea.ms/schema/1.0/group');
+        $xs->element('id', null, $this->permalink());
+        $xs->element('title', null, $this->getBestName());
+        $xs->element(
+            'link', array(
+                'rel'  => 'avatar',
+                'href' =>  empty($this->homepage_logo)
+                    ? User_group::defaultLogo(AVATAR_PROFILE_SIZE)
+                    : $this->homepage_logo
+            )
+        );
+        $xs->elementEnd('activity:subject');
+
+        return $xs->getString();
+    }
+
     static function register($fields) {
 
         // MAGICALLY put fields into current scope
index 5f8da7cf51bd5bcf80131eb8b74d362bd38fc2a3..81c1b68b236eedc1b5994c12d7cd96aec11ff0cc 100644 (file)
@@ -47,6 +47,16 @@ modified = 384
 [consumer__keys]
 consumer_key = K
 
+[conversation]
+id = 129
+uri = 2
+created = 142
+modified = 384
+
+[conversation__keys]
+id = N
+uri = U
+
 [deleted_notice]
 id = 129
 profile_id = 129
index 7f233e6760be02689ad98fc5dcf62905c32689a9..b9dd5af0c9b6ab284653422d3ef5c5d3c3f29cb3 100644 (file)
@@ -19,8 +19,11 @@ profile_id = profile:id
 [token]
 consumer_key = consumer:consumer_key
 
-[nonce]
-consumer_key,token = token:consumer_key,token
+; Compatibility hack for PHP 5.3
+; This entry has been moved to the class definition, as commas are no longer
+; considered valid in keys, causing parse_ini_file() to reject the whole file.
+;[nonce]
+;consumer_key,token = token:consumer_key,token
 
 [confirm_address]
 user_id = user:id
index 343464801662d0fd7ff37828632952214f873fb5..97117c80aab6b69898012f695d0902eb6ee1554c 100644 (file)
@@ -633,3 +633,11 @@ create table inbox (
     constraint primary key (user_id)
 
 ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table conversation (
+    id integer auto_increment primary key comment 'unique identifier',
+    uri varchar(225) unique comment 'URI of the conversation',
+    created datetime not null comment 'date this record was created',
+    modified timestamp comment 'date this record was modified'
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
index 936b847abe7bedc9ae44cf68cc75a48b1b94bf5c..dde394270f13cabc96340a834235651863593e8c 100644 (file)
-/*\r
- * jQuery Form Plugin\r
- * version: 2.17 (06-NOV-2008)\r
- * @requires jQuery v1.2.2 or later\r
- *\r
- * Examples and documentation at: http://malsup.com/jquery/form/\r
- * Dual licensed under the MIT and GPL licenses:\r
- *   http://www.opensource.org/licenses/mit-license.php\r
- *   http://www.gnu.org/licenses/gpl.html\r
- *\r
- * Revision: $Id$\r
- */\r
-;(function($) {\r
-\r
-/*\r
-    Usage Note:  \r
-    -----------\r
-    Do not use both ajaxSubmit and ajaxForm on the same form.  These\r
-    functions are intended to be exclusive.  Use ajaxSubmit if you want\r
-    to bind your own submit handler to the form.  For example,\r
-\r
-    $(document).ready(function() {\r
-        $('#myForm').bind('submit', function() {\r
-            $(this).ajaxSubmit({\r
-                target: '#output'\r
-            });\r
-            return false; // <-- important!\r
-        });\r
-    });\r
-\r
-    Use ajaxForm when you want the plugin to manage all the event binding\r
-    for you.  For example,\r
-\r
-    $(document).ready(function() {\r
-        $('#myForm').ajaxForm({\r
-            target: '#output'\r
-        });\r
-    });\r
-        \r
-    When using ajaxForm, the ajaxSubmit function will be invoked for you\r
-    at the appropriate time.  \r
-*/\r
-\r
-/**\r
- * ajaxSubmit() provides a mechanism for immediately submitting \r
- * an HTML form using AJAX.\r
- */\r
-$.fn.ajaxSubmit = function(options) {\r
-    // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)\r
-    if (!this.length) {\r
-        log('ajaxSubmit: skipping submit process - no element selected');\r
-        return this;\r
-    }\r
-\r
-    if (typeof options == 'function')\r
-        options = { success: options };\r
-\r
-    options = $.extend({\r
-        url:  this.attr('action') || window.location.toString(),\r
-        type: this.attr('method') || 'GET'\r
-    }, options || {});\r
-\r
-    // hook for manipulating the form data before it is extracted;\r
-    // convenient for use with rich editors like tinyMCE or FCKEditor\r
-    var veto = {};\r
-    this.trigger('form-pre-serialize', [this, options, veto]);\r
-    if (veto.veto) {\r
-        log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');\r
-        return this;\r
-    }\r
-\r
-    // provide opportunity to alter form data before it is serialized\r
-    if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {\r
-        log('ajaxSubmit: submit aborted via beforeSerialize callback');\r
-        return this;\r
-    }    \r
-   \r
-    var a = this.formToArray(options.semantic);\r
-    if (options.data) {\r
-        options.extraData = options.data;\r
-        for (var n in options.data) {\r
-          if(options.data[n] instanceof Array) {\r
-            for (var k in options.data[n])\r
-              a.push( { name: n, value: options.data[n][k] } )\r
-          }  \r
-          else\r
-             a.push( { name: n, value: options.data[n] } );\r
-        }\r
-    }\r
-\r
-    // give pre-submit callback an opportunity to abort the submit\r
-    if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {\r
-        log('ajaxSubmit: submit aborted via beforeSubmit callback');\r
-        return this;\r
-    }    \r
-\r
-    // fire vetoable 'validate' event\r
-    this.trigger('form-submit-validate', [a, this, options, veto]);\r
-    if (veto.veto) {\r
-        log('ajaxSubmit: submit vetoed via form-submit-validate trigger');\r
-        return this;\r
-    }    \r
-\r
-    var q = $.param(a);\r
-\r
-    if (options.type.toUpperCase() == 'GET') {\r
-        options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;\r
-        options.data = null;  // data is null for 'get'\r
-    }\r
-    else\r
-        options.data = q; // data is the query string for 'post'\r
-\r
-    var $form = this, callbacks = [];\r
-    if (options.resetForm) callbacks.push(function() { $form.resetForm(); });\r
-    if (options.clearForm) callbacks.push(function() { $form.clearForm(); });\r
-\r
-    // perform a load on the target only if dataType is not provided\r
-    if (!options.dataType && options.target) {\r
-        var oldSuccess = options.success || function(){};\r
-        callbacks.push(function(data) {\r
-            $(options.target).html(data).each(oldSuccess, arguments);\r
-        });\r
-    }\r
-    else if (options.success)\r
-        callbacks.push(options.success);\r
-\r
-    options.success = function(data, status) {\r
-        for (var i=0, max=callbacks.length; i < max; i++)\r
-            callbacks[i].apply(options, [data, status, $form]);\r
-    };\r
-\r
-    // are there files to upload?\r
-    var files = $('input:file', this).fieldValue();\r
-    var found = false;\r
-    for (var j=0; j < files.length; j++)\r
-        if (files[j])\r
-            found = true;\r
-\r
-    // options.iframe allows user to force iframe mode\r
-   if (options.iframe || found) { \r
-       // hack to fix Safari hang (thanks to Tim Molendijk for this)\r
-       // see:  http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d\r
-       if ($.browser.safari && options.closeKeepAlive)\r
-           $.get(options.closeKeepAlive, fileUpload);\r
-       else\r
-           fileUpload();\r
-       }\r
-   else\r
-       $.ajax(options);\r
-\r
-    // fire 'notify' event\r
-    this.trigger('form-submit-notify', [this, options]);\r
-    return this;\r
-\r
-\r
-    // private function for handling file uploads (hat tip to YAHOO!)\r
-    function fileUpload() {\r
-        var form = $form[0];\r
-        \r
-        if ($(':input[name=submit]', form).length) {\r
-            alert('Error: Form elements must not be named "submit".');\r
-            return;\r
-        }\r
-        \r
-        var opts = $.extend({}, $.ajaxSettings, options);\r
-               var s = jQuery.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts);\r
-\r
-        var id = 'jqFormIO' + (new Date().getTime());\r
-        var $io = $('<iframe id="' + id + '" name="' + id + '" />');\r
-        var io = $io[0];\r
-\r
-        if ($.browser.msie || $.browser.opera) \r
-            io.src = 'javascript:false;document.write("");';\r
-        $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });\r
-\r
-        var xhr = { // mock object\r
-            aborted: 0,\r
-            responseText: null,\r
-            responseXML: null,\r
-            status: 0,\r
-            statusText: 'n/a',\r
-            getAllResponseHeaders: function() {},\r
-            getResponseHeader: function() {},\r
-            setRequestHeader: function() {},\r
-            abort: function() { \r
-                this.aborted = 1; \r
-                $io.attr('src','about:blank'); // abort op in progress\r
-            }\r
-        };\r
-\r
-        var g = opts.global;\r
-        // trigger ajax global events so that activity/block indicators work like normal\r
-        if (g && ! $.active++) $.event.trigger("ajaxStart");\r
-        if (g) $.event.trigger("ajaxSend", [xhr, opts]);\r
-\r
-               if (s.beforeSend && s.beforeSend(xhr, s) === false) {\r
-                       s.global && jQuery.active--;\r
-                       return;\r
-        }\r
-        if (xhr.aborted)\r
-            return;\r
-        \r
-        var cbInvoked = 0;\r
-        var timedOut = 0;\r
-\r
-        // add submitting element to data if we know it\r
-        var sub = form.clk;\r
-        if (sub) {\r
-            var n = sub.name;\r
-            if (n && !sub.disabled) {\r
-                options.extraData = options.extraData || {};\r
-                options.extraData[n] = sub.value;\r
-                if (sub.type == "image") {\r
-                    options.extraData[name+'.x'] = form.clk_x;\r
-                    options.extraData[name+'.y'] = form.clk_y;\r
-                }\r
-            }\r
-        }\r
-\r
-        // take a breath so that pending repaints get some cpu time before the upload starts\r
-        setTimeout(function() {\r
-            // make sure form attrs are set\r
-            var t = $form.attr('target'), a = $form.attr('action');\r
-            $form.attr({\r
-                target:   id,\r
-                method:   'POST',\r
-                action:   opts.url\r
-            });\r
-            \r
-            // ie borks in some cases when setting encoding\r
-            if (! options.skipEncodingOverride) {\r
-                $form.attr({\r
-                    encoding: 'multipart/form-data',\r
-                    enctype:  'multipart/form-data'\r
-                });\r
-            }\r
-\r
-            // support timout\r
-            if (opts.timeout)\r
-                setTimeout(function() { timedOut = true; cb(); }, opts.timeout);\r
-\r
-            // add "extra" data to form if provided in options\r
-            var extraInputs = [];\r
-            try {\r
-                if (options.extraData)\r
-                    for (var n in options.extraData)\r
-                        extraInputs.push(\r
-                            $('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />')\r
-                                .appendTo(form)[0]);\r
-            \r
-                // add iframe to doc and submit the form\r
-                $io.appendTo('body');\r
-                io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);\r
-                form.submit();\r
-            }\r
-            finally {\r
-                // reset attrs and remove "extra" input elements\r
-                $form.attr('action', a);\r
-                t ? $form.attr('target', t) : $form.removeAttr('target');\r
-                $(extraInputs).remove();\r
-            }\r
-        }, 10);\r
-\r
-        function cb() {\r
-            if (cbInvoked++) return;\r
-            \r
-            io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);\r
-\r
-            var operaHack = 0;\r
-            var ok = true;\r
-            try {\r
-                if (timedOut) throw 'timeout';\r
-                // extract the server response from the iframe\r
-                var data, doc;\r
-\r
-                doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;\r
-                \r
-                if (doc.body == null && !operaHack && $.browser.opera) {\r
-                    // In Opera 9.2.x the iframe DOM is not always traversable when\r
-                    // the onload callback fires so we give Opera 100ms to right itself\r
-                    operaHack = 1;\r
-                    cbInvoked--;\r
-                    setTimeout(cb, 100);\r
-                    return;\r
-                }\r
-                \r
-                xhr.responseText = doc.body ? doc.body.innerHTML : null;\r
-                xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;\r
-                xhr.getResponseHeader = function(header){\r
-                    var headers = {'content-type': opts.dataType};\r
-                    return headers[header];\r
-                };\r
-\r
-                if (opts.dataType == 'json' || opts.dataType == 'script') {\r
-                    var ta = doc.getElementsByTagName('textarea')[0];\r
-                    xhr.responseText = ta ? ta.value : xhr.responseText;\r
-                }\r
-                else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {\r
-                    xhr.responseXML = toXml(xhr.responseText);\r
-                }\r
-                data = $.httpData(xhr, opts.dataType);\r
-            }\r
-            catch(e){\r
-                ok = false;\r
-                $.handleError(opts, xhr, 'error', e);\r
-            }\r
-\r
-            // ordering of these callbacks/triggers is odd, but that's how $.ajax does it\r
-            if (ok) {\r
-                opts.success(data, 'success');\r
-                if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);\r
-            }\r
-            if (g) $.event.trigger("ajaxComplete", [xhr, opts]);\r
-            if (g && ! --$.active) $.event.trigger("ajaxStop");\r
-            if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');\r
-\r
-            // clean up\r
-            setTimeout(function() {\r
-                $io.remove();\r
-                xhr.responseXML = null;\r
-            }, 100);\r
-        };\r
-\r
-        function toXml(s, doc) {\r
-            if (window.ActiveXObject) {\r
-                doc = new ActiveXObject('Microsoft.XMLDOM');\r
-                doc.async = 'false';\r
-                doc.loadXML(s);\r
-            }\r
-            else\r
-                doc = (new DOMParser()).parseFromString(s, 'text/xml');\r
-            return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;\r
-        };\r
-    };\r
-};\r
-\r
-/**\r
- * ajaxForm() provides a mechanism for fully automating form submission.\r
- *\r
- * The advantages of using this method instead of ajaxSubmit() are:\r
- *\r
- * 1: This method will include coordinates for <input type="image" /> elements (if the element\r
- *    is used to submit the form).\r
- * 2. This method will include the submit element's name/value data (for the element that was\r
- *    used to submit the form).\r
- * 3. This method binds the submit() method to the form for you.\r
- *\r
- * The options argument for ajaxForm works exactly as it does for ajaxSubmit.  ajaxForm merely\r
- * passes the options argument along after properly binding events for submit elements and\r
- * the form itself.\r
- */ \r
-$.fn.ajaxForm = function(options) {\r
-    return this.ajaxFormUnbind().bind('submit.form-plugin',function() {\r
-        $(this).ajaxSubmit(options);\r
-        return false;\r
-    }).each(function() {\r
-        // store options in hash\r
-        $(":submit,input:image", this).bind('click.form-plugin',function(e) {\r
-            var form = this.form;\r
-            form.clk = this;\r
-            if (this.type == 'image') {\r
-                if (e.offsetX != undefined) {\r
-                    form.clk_x = e.offsetX;\r
-                    form.clk_y = e.offsetY;\r
-                } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin\r
-                    var offset = $(this).offset();\r
-                    form.clk_x = e.pageX - offset.left;\r
-                    form.clk_y = e.pageY - offset.top;\r
-                } else {\r
-                    form.clk_x = e.pageX - this.offsetLeft;\r
-                    form.clk_y = e.pageY - this.offsetTop;\r
-                }\r
-            }\r
-            // clear form vars\r
-            setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 10);\r
-        });\r
-    });\r
-};\r
-\r
-// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm\r
-$.fn.ajaxFormUnbind = function() {\r
-    this.unbind('submit.form-plugin');\r
-    return this.each(function() {\r
-        $(":submit,input:image", this).unbind('click.form-plugin');\r
-    });\r
-\r
-};\r
-\r
-/**\r
- * formToArray() gathers form element data into an array of objects that can\r
- * be passed to any of the following ajax functions: $.get, $.post, or load.\r
- * Each object in the array has both a 'name' and 'value' property.  An example of\r
- * an array for a simple login form might be:\r
- *\r
- * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]\r
- *\r
- * It is this array that is passed to pre-submit callback functions provided to the\r
- * ajaxSubmit() and ajaxForm() methods.\r
- */\r
-$.fn.formToArray = function(semantic) {\r
-    var a = [];\r
-    if (this.length == 0) return a;\r
-\r
-    var form = this[0];\r
-    var els = semantic ? form.getElementsByTagName('*') : form.elements;\r
-    if (!els) return a;\r
-    for(var i=0, max=els.length; i < max; i++) {\r
-        var el = els[i];\r
-        var n = el.name;\r
-        if (!n) continue;\r
-\r
-        if (semantic && form.clk && el.type == "image") {\r
-            // handle image inputs on the fly when semantic == true\r
-            if(!el.disabled && form.clk == el)\r
-                a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});\r
-            continue;\r
-        }\r
-\r
-        var v = $.fieldValue(el, true);\r
-        if (v && v.constructor == Array) {\r
-            for(var j=0, jmax=v.length; j < jmax; j++)\r
-                a.push({name: n, value: v[j]});\r
-        }\r
-        else if (v !== null && typeof v != 'undefined')\r
-            a.push({name: n, value: v});\r
-    }\r
-\r
-    if (!semantic && form.clk) {\r
-        // input type=='image' are not found in elements array! handle them here\r
-        var inputs = form.getElementsByTagName("input");\r
-        for(var i=0, max=inputs.length; i < max; i++) {\r
-            var input = inputs[i];\r
-            var n = input.name;\r
-            if(n && !input.disabled && input.type == "image" && form.clk == input)\r
-                a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});\r
-        }\r
-    }\r
-    return a;\r
-};\r
-\r
-/**\r
- * Serializes form data into a 'submittable' string. This method will return a string\r
- * in the format: name1=value1&amp;name2=value2\r
- */\r
-$.fn.formSerialize = function(semantic) {\r
-    //hand off to jQuery.param for proper encoding\r
-    return $.param(this.formToArray(semantic));\r
-};\r
-\r
-/**\r
- * Serializes all field elements in the jQuery object into a query string.\r
- * This method will return a string in the format: name1=value1&amp;name2=value2\r
- */\r
-$.fn.fieldSerialize = function(successful) {\r
-    var a = [];\r
-    this.each(function() {\r
-        var n = this.name;\r
-        if (!n) return;\r
-        var v = $.fieldValue(this, successful);\r
-        if (v && v.constructor == Array) {\r
-            for (var i=0,max=v.length; i < max; i++)\r
-                a.push({name: n, value: v[i]});\r
-        }\r
-        else if (v !== null && typeof v != 'undefined')\r
-            a.push({name: this.name, value: v});\r
-    });\r
-    //hand off to jQuery.param for proper encoding\r
-    return $.param(a);\r
-};\r
-\r
-/**\r
- * Returns the value(s) of the element in the matched set.  For example, consider the following form:\r
- *\r
- *  <form><fieldset>\r
- *      <input name="A" type="text" />\r
- *      <input name="A" type="text" />\r
- *      <input name="B" type="checkbox" value="B1" />\r
- *      <input name="B" type="checkbox" value="B2"/>\r
- *      <input name="C" type="radio" value="C1" />\r
- *      <input name="C" type="radio" value="C2" />\r
- *  </fieldset></form>\r
- *\r
- *  var v = $(':text').fieldValue();\r
- *  // if no values are entered into the text inputs\r
- *  v == ['','']\r
- *  // if values entered into the text inputs are 'foo' and 'bar'\r
- *  v == ['foo','bar']\r
- *\r
- *  var v = $(':checkbox').fieldValue();\r
- *  // if neither checkbox is checked\r
- *  v === undefined\r
- *  // if both checkboxes are checked\r
- *  v == ['B1', 'B2']\r
- *\r
- *  var v = $(':radio').fieldValue();\r
- *  // if neither radio is checked\r
- *  v === undefined\r
- *  // if first radio is checked\r
- *  v == ['C1']\r
- *\r
- * The successful argument controls whether or not the field element must be 'successful'\r
- * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).\r
- * The default value of the successful argument is true.  If this value is false the value(s)\r
- * for each element is returned.\r
- *\r
- * Note: This method *always* returns an array.  If no valid value can be determined the\r
- *       array will be empty, otherwise it will contain one or more values.\r
- */\r
-$.fn.fieldValue = function(successful) {\r
-    for (var val=[], i=0, max=this.length; i < max; i++) {\r
-        var el = this[i];\r
-        var v = $.fieldValue(el, successful);\r
-        if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))\r
-            continue;\r
-        v.constructor == Array ? $.merge(val, v) : val.push(v);\r
-    }\r
-    return val;\r
-};\r
-\r
-/**\r
- * Returns the value of the field element.\r
- */\r
-$.fieldValue = function(el, successful) {\r
-    var n = el.name, t = el.type, tag = el.tagName.toLowerCase();\r
-    if (typeof successful == 'undefined') successful = true;\r
-\r
-    if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||\r
-        (t == 'checkbox' || t == 'radio') && !el.checked ||\r
-        (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||\r
-        tag == 'select' && el.selectedIndex == -1))\r
-            return null;\r
-\r
-    if (tag == 'select') {\r
-        var index = el.selectedIndex;\r
-        if (index < 0) return null;\r
-        var a = [], ops = el.options;\r
-        var one = (t == 'select-one');\r
-        var max = (one ? index+1 : ops.length);\r
-        for(var i=(one ? index : 0); i < max; i++) {\r
-            var op = ops[i];\r
-            if (op.selected) {\r
-                // extra pain for IE...\r
-                var v = $.browser.msie && !(op.attributes['value'].specified) ? op.text : op.value;\r
-                if (one) return v;\r
-                a.push(v);\r
-            }\r
-        }\r
-        return a;\r
-    }\r
-    return el.value;\r
-};\r
-\r
-/**\r
- * Clears the form data.  Takes the following actions on the form's input fields:\r
- *  - input text fields will have their 'value' property set to the empty string\r
- *  - select elements will have their 'selectedIndex' property set to -1\r
- *  - checkbox and radio inputs will have their 'checked' property set to false\r
- *  - inputs of type submit, button, reset, and hidden will *not* be effected\r
- *  - button elements will *not* be effected\r
- */\r
-$.fn.clearForm = function() {\r
-    return this.each(function() {\r
-        $('input,select,textarea', this).clearFields();\r
-    });\r
-};\r
-\r
-/**\r
- * Clears the selected form elements.\r
- */\r
-$.fn.clearFields = $.fn.clearInputs = function() {\r
-    return this.each(function() {\r
-        var t = this.type, tag = this.tagName.toLowerCase();\r
-        if (t == 'file' || t == 'text' || t == 'password' || tag == 'textarea')\r
-            this.value = '';\r
-        else if (t == 'checkbox' || t == 'radio')\r
-            this.checked = false;\r
-        else if (tag == 'select')\r
-            this.selectedIndex = -1;\r
-    });\r
-};\r
-\r
-/**\r
- * Resets the form data.  Causes all form elements to be reset to their original value.\r
- */\r
-$.fn.resetForm = function() {\r
-    return this.each(function() {\r
-        // guard against an input with the name of 'reset'\r
-        // note that IE reports the reset function as an 'object'\r
-        if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))\r
-            this.reset();\r
-    });\r
-};\r
-\r
-/**\r
- * Enables or disables any matching elements.\r
- */\r
-$.fn.enable = function(b) { \r
-    if (b == undefined) b = true;\r
-    return this.each(function() { \r
-        this.disabled = !b \r
-    });\r
-};\r
-\r
-/**\r
- * Checks/unchecks any matching checkboxes or radio buttons and\r
- * selects/deselects and matching option elements.\r
- */\r
-$.fn.selected = function(select) {\r
-    if (select == undefined) select = true;\r
-    return this.each(function() { \r
-        var t = this.type;\r
-        if (t == 'checkbox' || t == 'radio')\r
-            this.checked = select;\r
-        else if (this.tagName.toLowerCase() == 'option') {\r
-            var $sel = $(this).parent('select');\r
-            if (select && $sel[0] && $sel[0].type == 'select-one') {\r
-                // deselect all other options\r
-                $sel.find('option').selected(false);\r
-            }\r
-            this.selected = select;\r
-        }\r
-    });\r
-};\r
-\r
-// helper fn for console logging\r
-// set $.fn.ajaxSubmit.debug to true to enable debug logging\r
-function log() {\r
-    if ($.fn.ajaxSubmit.debug && window.console && window.console.log)\r
-        window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,''));\r
-};\r
-\r
-})(jQuery);\r
+/*
+ * jQuery Form Plugin
+ * version: 2.36 (07-NOV-2009)
+ * @requires jQuery v1.2.6 or later
+ *
+ * Examples and documentation at: http://malsup.com/jquery/form/
+ * Dual licensed under the MIT and GPL licenses:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *   http://www.gnu.org/licenses/gpl.html
+ */
+;(function($) {
+
+/*
+       Usage Note:
+       -----------
+       Do not use both ajaxSubmit and ajaxForm on the same form.  These
+       functions are intended to be exclusive.  Use ajaxSubmit if you want
+       to bind your own submit handler to the form.  For example,
+
+       $(document).ready(function() {
+               $('#myForm').bind('submit', function() {
+                       $(this).ajaxSubmit({
+                               target: '#output'
+                       });
+                       return false; // <-- important!
+               });
+       });
+
+       Use ajaxForm when you want the plugin to manage all the event binding
+       for you.  For example,
+
+       $(document).ready(function() {
+               $('#myForm').ajaxForm({
+                       target: '#output'
+               });
+       });
+
+       When using ajaxForm, the ajaxSubmit function will be invoked for you
+       at the appropriate time.
+*/
+
+/**
+ * ajaxSubmit() provides a mechanism for immediately submitting
+ * an HTML form using AJAX.
+ */
+$.fn.ajaxSubmit = function(options) {
+       // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
+       if (!this.length) {
+               log('ajaxSubmit: skipping submit process - no element selected');
+               return this;
+       }
+
+       if (typeof options == 'function')
+               options = { success: options };
+
+       var url = $.trim(this.attr('action'));
+       if (url) {
+               // clean url (don't include hash vaue)
+               url = (url.match(/^([^#]+)/)||[])[1];
+       }
+       url = url || window.location.href || '';
+
+       options = $.extend({
+               url:  url,
+               type: this.attr('method') || 'GET',
+               iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
+       }, options || {});
+
+       // hook for manipulating the form data before it is extracted;
+       // convenient for use with rich editors like tinyMCE or FCKEditor
+       var veto = {};
+       this.trigger('form-pre-serialize', [this, options, veto]);
+       if (veto.veto) {
+               log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
+               return this;
+       }
+
+       // provide opportunity to alter form data before it is serialized
+       if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
+               log('ajaxSubmit: submit aborted via beforeSerialize callback');
+               return this;
+       }
+
+       var a = this.formToArray(options.semantic);
+       if (options.data) {
+               options.extraData = options.data;
+               for (var n in options.data) {
+                 if(options.data[n] instanceof Array) {
+                       for (var k in options.data[n])
+                         a.push( { name: n, value: options.data[n][k] } );
+                 }
+                 else
+                        a.push( { name: n, value: options.data[n] } );
+               }
+       }
+
+       // give pre-submit callback an opportunity to abort the submit
+       if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
+               log('ajaxSubmit: submit aborted via beforeSubmit callback');
+               return this;
+       }
+
+       // fire vetoable 'validate' event
+       this.trigger('form-submit-validate', [a, this, options, veto]);
+       if (veto.veto) {
+               log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
+               return this;
+       }
+
+       var q = $.param(a);
+
+       if (options.type.toUpperCase() == 'GET') {
+               options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
+               options.data = null;  // data is null for 'get'
+       }
+       else
+               options.data = q; // data is the query string for 'post'
+
+       var $form = this, callbacks = [];
+       if (options.resetForm) callbacks.push(function() { $form.resetForm(); });
+       if (options.clearForm) callbacks.push(function() { $form.clearForm(); });
+
+       // perform a load on the target only if dataType is not provided
+       if (!options.dataType && options.target) {
+               var oldSuccess = options.success || function(){};
+               callbacks.push(function(data) {
+                       $(options.target).html(data).each(oldSuccess, arguments);
+               });
+       }
+       else if (options.success)
+               callbacks.push(options.success);
+
+       options.success = function(data, status) {
+               for (var i=0, max=callbacks.length; i < max; i++)
+                       callbacks[i].apply(options, [data, status, $form]);
+       };
+
+       // are there files to upload?
+       var files = $('input:file', this).fieldValue();
+       var found = false;
+       for (var j=0; j < files.length; j++)
+               if (files[j])
+                       found = true;
+
+       var multipart = false;
+//     var mp = 'multipart/form-data';
+//     multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
+
+       // options.iframe allows user to force iframe mode
+       // 06-NOV-09: now defaulting to iframe mode if file input is detected
+   if ((files.length && options.iframe !== false) || options.iframe || found || multipart) {
+          // hack to fix Safari hang (thanks to Tim Molendijk for this)
+          // see:  http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
+          if (options.closeKeepAlive)
+                  $.get(options.closeKeepAlive, fileUpload);
+          else
+                  fileUpload();
+          }
+   else
+          $.ajax(options);
+
+       // fire 'notify' event
+       this.trigger('form-submit-notify', [this, options]);
+       return this;
+
+
+       // private function for handling file uploads (hat tip to YAHOO!)
+       function fileUpload() {
+               var form = $form[0];
+
+               if ($(':input[name=submit]', form).length) {
+                       alert('Error: Form elements must not be named "submit".');
+                       return;
+               }
+
+               var opts = $.extend({}, $.ajaxSettings, options);
+               var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts);
+
+               var id = 'jqFormIO' + (new Date().getTime());
+               var $io = $('<iframe id="' + id + '" name="' + id + '" src="'+ opts.iframeSrc +'" />');
+               var io = $io[0];
+
+               $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
+
+               var xhr = { // mock object
+                       aborted: 0,
+                       responseText: null,
+                       responseXML: null,
+                       status: 0,
+                       statusText: 'n/a',
+                       getAllResponseHeaders: function() {},
+                       getResponseHeader: function() {},
+                       setRequestHeader: function() {},
+                       abort: function() {
+                               this.aborted = 1;
+                               $io.attr('src', opts.iframeSrc); // abort op in progress
+                       }
+               };
+
+               var g = opts.global;
+               // trigger ajax global events so that activity/block indicators work like normal
+               if (g && ! $.active++) $.event.trigger("ajaxStart");
+               if (g) $.event.trigger("ajaxSend", [xhr, opts]);
+
+               if (s.beforeSend && s.beforeSend(xhr, s) === false) {
+                       s.global && $.active--;
+                       return;
+               }
+               if (xhr.aborted)
+                       return;
+
+               var cbInvoked = 0;
+               var timedOut = 0;
+
+               // add submitting element to data if we know it
+               var sub = form.clk;
+               if (sub) {
+                       var n = sub.name;
+                       if (n && !sub.disabled) {
+                               options.extraData = options.extraData || {};
+                               options.extraData[n] = sub.value;
+                               if (sub.type == "image") {
+                                       options.extraData[name+'.x'] = form.clk_x;
+                                       options.extraData[name+'.y'] = form.clk_y;
+                               }
+                       }
+               }
+
+               // take a breath so that pending repaints get some cpu time before the upload starts
+               setTimeout(function() {
+                       // make sure form attrs are set
+                       var t = $form.attr('target'), a = $form.attr('action');
+
+                       // update form attrs in IE friendly way
+                       form.setAttribute('target',id);
+                       if (form.getAttribute('method') != 'POST')
+                               form.setAttribute('method', 'POST');
+                       if (form.getAttribute('action') != opts.url)
+                               form.setAttribute('action', opts.url);
+
+                       // ie borks in some cases when setting encoding
+                       if (! options.skipEncodingOverride) {
+                               $form.attr({
+                                       encoding: 'multipart/form-data',
+                                       enctype:  'multipart/form-data'
+                               });
+                       }
+
+                       // support timout
+                       if (opts.timeout)
+                               setTimeout(function() { timedOut = true; cb(); }, opts.timeout);
+
+                       // add "extra" data to form if provided in options
+                       var extraInputs = [];
+                       try {
+                               if (options.extraData)
+                                       for (var n in options.extraData)
+                                               extraInputs.push(
+                                                       $('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />')
+                                                               .appendTo(form)[0]);
+
+                               // add iframe to doc and submit the form
+                               $io.appendTo('body');
+                               io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
+                               form.submit();
+                       }
+                       finally {
+                               // reset attrs and remove "extra" input elements
+                               form.setAttribute('action',a);
+                               t ? form.setAttribute('target', t) : $form.removeAttr('target');
+                               $(extraInputs).remove();
+                       }
+               }, 10);
+
+               var domCheckCount = 50;
+
+               function cb() {
+                       if (cbInvoked++) return;
+
+                       io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
+
+                       var ok = true;
+                       try {
+                               if (timedOut) throw 'timeout';
+                               // extract the server response from the iframe
+                               var data, doc;
+
+                               doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
+                               
+                               var isXml = opts.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
+                               log('isXml='+isXml);
+                               if (!isXml && (doc.body == null || doc.body.innerHTML == '')) {
+                                       if (--domCheckCount) {
+                                               // in some browsers (Opera) the iframe DOM is not always traversable when
+                                               // the onload callback fires, so we loop a bit to accommodate
+                                               cbInvoked = 0;
+                                               setTimeout(cb, 100);
+                                               return;
+                                       }
+                                       log('Could not access iframe DOM after 50 tries.');
+                                       return;
+                               }
+
+                               xhr.responseText = doc.body ? doc.body.innerHTML : null;
+                               xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
+                               xhr.getResponseHeader = function(header){
+                                       var headers = {'content-type': opts.dataType};
+                                       return headers[header];
+                               };
+
+                               if (opts.dataType == 'json' || opts.dataType == 'script') {
+                                       // see if user embedded response in textarea
+                                       var ta = doc.getElementsByTagName('textarea')[0];
+                                       if (ta)
+                                               xhr.responseText = ta.value;
+                                       else {
+                                               // account for browsers injecting pre around json response
+                                               var pre = doc.getElementsByTagName('pre')[0];
+                                               if (pre)
+                                                       xhr.responseText = pre.innerHTML;
+                                       }                         
+                               }
+                               else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
+                                       xhr.responseXML = toXml(xhr.responseText);
+                               }
+                               data = $.httpData(xhr, opts.dataType);
+                       }
+                       catch(e){
+                               ok = false;
+                               $.handleError(opts, xhr, 'error', e);
+                       }
+
+                       // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
+                       if (ok) {
+                               opts.success(data, 'success');
+                               if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
+                       }
+                       if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
+                       if (g && ! --$.active) $.event.trigger("ajaxStop");
+                       if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
+
+                       // clean up
+                       setTimeout(function() {
+                               $io.remove();
+                               xhr.responseXML = null;
+                       }, 100);
+               };
+
+               function toXml(s, doc) {
+                       if (window.ActiveXObject) {
+                               doc = new ActiveXObject('Microsoft.XMLDOM');
+                               doc.async = 'false';
+                               doc.loadXML(s);
+                       }
+                       else
+                               doc = (new DOMParser()).parseFromString(s, 'text/xml');
+                       return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
+               };
+       };
+};
+
+/**
+ * ajaxForm() provides a mechanism for fully automating form submission.
+ *
+ * The advantages of using this method instead of ajaxSubmit() are:
+ *
+ * 1: This method will include coordinates for <input type="image" /> elements (if the element
+ *     is used to submit the form).
+ * 2. This method will include the submit element's name/value data (for the element that was
+ *     used to submit the form).
+ * 3. This method binds the submit() method to the form for you.
+ *
+ * The options argument for ajaxForm works exactly as it does for ajaxSubmit.  ajaxForm merely
+ * passes the options argument along after properly binding events for submit elements and
+ * the form itself.
+ */
+$.fn.ajaxForm = function(options) {
+       return this.ajaxFormUnbind().bind('submit.form-plugin', function() {
+               $(this).ajaxSubmit(options);
+               return false;
+       }).bind('click.form-plugin', function(e) {
+               var target = e.target;
+               var $el = $(target);
+               if (!($el.is(":submit,input:image"))) {
+                       // is this a child element of the submit el?  (ex: a span within a button)
+                       var t = $el.closest(':submit');
+                       if (t.length == 0)
+                               return;
+                       target = t[0];
+               }
+               var form = this;
+               form.clk = target;
+               if (target.type == 'image') {
+                       if (e.offsetX != undefined) {
+                               form.clk_x = e.offsetX;
+                               form.clk_y = e.offsetY;
+                       } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
+                               var offset = $el.offset();
+                               form.clk_x = e.pageX - offset.left;
+                               form.clk_y = e.pageY - offset.top;
+                       } else {
+                               form.clk_x = e.pageX - target.offsetLeft;
+                               form.clk_y = e.pageY - target.offsetTop;
+                       }
+               }
+               // clear form vars
+               setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
+       });
+};
+
+// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
+$.fn.ajaxFormUnbind = function() {
+       return this.unbind('submit.form-plugin click.form-plugin');
+};
+
+/**
+ * formToArray() gathers form element data into an array of objects that can
+ * be passed to any of the following ajax functions: $.get, $.post, or load.
+ * Each object in the array has both a 'name' and 'value' property.  An example of
+ * an array for a simple login form might be:
+ *
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
+ *
+ * It is this array that is passed to pre-submit callback functions provided to the
+ * ajaxSubmit() and ajaxForm() methods.
+ */
+$.fn.formToArray = function(semantic) {
+       var a = [];
+       if (this.length == 0) return a;
+
+       var form = this[0];
+       var els = semantic ? form.getElementsByTagName('*') : form.elements;
+       if (!els) return a;
+       for(var i=0, max=els.length; i < max; i++) {
+               var el = els[i];
+               var n = el.name;
+               if (!n) continue;
+
+               if (semantic && form.clk && el.type == "image") {
+                       // handle image inputs on the fly when semantic == true
+                       if(!el.disabled && form.clk == el) {
+                               a.push({name: n, value: $(el).val()});
+                               a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+                       }
+                       continue;
+               }
+
+               var v = $.fieldValue(el, true);
+               if (v && v.constructor == Array) {
+                       for(var j=0, jmax=v.length; j < jmax; j++)
+                               a.push({name: n, value: v[j]});
+               }
+               else if (v !== null && typeof v != 'undefined')
+                       a.push({name: n, value: v});
+       }
+
+       if (!semantic && form.clk) {
+               // input type=='image' are not found in elements array! handle it here
+               var $input = $(form.clk), input = $input[0], n = input.name;
+               if (n && !input.disabled && input.type == 'image') {
+                       a.push({name: n, value: $input.val()});
+                       a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+               }
+       }
+       return a;
+};
+
+/**
+ * Serializes form data into a 'submittable' string. This method will return a string
+ * in the format: name1=value1&amp;name2=value2
+ */
+$.fn.formSerialize = function(semantic) {
+       //hand off to jQuery.param for proper encoding
+       return $.param(this.formToArray(semantic));
+};
+
+/**
+ * Serializes all field elements in the jQuery object into a query string.
+ * This method will return a string in the format: name1=value1&amp;name2=value2
+ */
+$.fn.fieldSerialize = function(successful) {
+       var a = [];
+       this.each(function() {
+               var n = this.name;
+               if (!n) return;
+               var v = $.fieldValue(this, successful);
+               if (v && v.constructor == Array) {
+                       for (var i=0,max=v.length; i < max; i++)
+                               a.push({name: n, value: v[i]});
+               }
+               else if (v !== null && typeof v != 'undefined')
+                       a.push({name: this.name, value: v});
+       });
+       //hand off to jQuery.param for proper encoding
+       return $.param(a);
+};
+
+/**
+ * Returns the value(s) of the element in the matched set.  For example, consider the following form:
+ *
+ *  <form><fieldset>
+ *       <input name="A" type="text" />
+ *       <input name="A" type="text" />
+ *       <input name="B" type="checkbox" value="B1" />
+ *       <input name="B" type="checkbox" value="B2"/>
+ *       <input name="C" type="radio" value="C1" />
+ *       <input name="C" type="radio" value="C2" />
+ *  </fieldset></form>
+ *
+ *  var v = $(':text').fieldValue();
+ *  // if no values are entered into the text inputs
+ *  v == ['','']
+ *  // if values entered into the text inputs are 'foo' and 'bar'
+ *  v == ['foo','bar']
+ *
+ *  var v = $(':checkbox').fieldValue();
+ *  // if neither checkbox is checked
+ *  v === undefined
+ *  // if both checkboxes are checked
+ *  v == ['B1', 'B2']
+ *
+ *  var v = $(':radio').fieldValue();
+ *  // if neither radio is checked
+ *  v === undefined
+ *  // if first radio is checked
+ *  v == ['C1']
+ *
+ * The successful argument controls whether or not the field element must be 'successful'
+ * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
+ * The default value of the successful argument is true.  If this value is false the value(s)
+ * for each element is returned.
+ *
+ * Note: This method *always* returns an array.  If no valid value can be determined the
+ *        array will be empty, otherwise it will contain one or more values.
+ */
+$.fn.fieldValue = function(successful) {
+       for (var val=[], i=0, max=this.length; i < max; i++) {
+               var el = this[i];
+               var v = $.fieldValue(el, successful);
+               if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
+                       continue;
+               v.constructor == Array ? $.merge(val, v) : val.push(v);
+       }
+       return val;
+};
+
+/**
+ * Returns the value of the field element.
+ */
+$.fieldValue = function(el, successful) {
+       var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
+       if (typeof successful == 'undefined') successful = true;
+
+       if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
+               (t == 'checkbox' || t == 'radio') && !el.checked ||
+               (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
+               tag == 'select' && el.selectedIndex == -1))
+                       return null;
+
+       if (tag == 'select') {
+               var index = el.selectedIndex;
+               if (index < 0) return null;
+               var a = [], ops = el.options;
+               var one = (t == 'select-one');
+               var max = (one ? index+1 : ops.length);
+               for(var i=(one ? index : 0); i < max; i++) {
+                       var op = ops[i];
+                       if (op.selected) {
+                               var v = op.value;
+                               if (!v) // extra pain for IE...
+                                       v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
+                               if (one) return v;
+                               a.push(v);
+                       }
+               }
+               return a;
+       }
+       return el.value;
+};
+
+/**
+ * Clears the form data.  Takes the following actions on the form's input fields:
+ *  - input text fields will have their 'value' property set to the empty string
+ *  - select elements will have their 'selectedIndex' property set to -1
+ *  - checkbox and radio inputs will have their 'checked' property set to false
+ *  - inputs of type submit, button, reset, and hidden will *not* be effected
+ *  - button elements will *not* be effected
+ */
+$.fn.clearForm = function() {
+       return this.each(function() {
+               $('input,select,textarea', this).clearFields();
+       });
+};
+
+/**
+ * Clears the selected form elements.
+ */
+$.fn.clearFields = $.fn.clearInputs = function() {
+       return this.each(function() {
+               var t = this.type, tag = this.tagName.toLowerCase();
+               if (t == 'text' || t == 'password' || tag == 'textarea')
+                       this.value = '';
+               else if (t == 'checkbox' || t == 'radio')
+                       this.checked = false;
+               else if (tag == 'select')
+                       this.selectedIndex = -1;
+       });
+};
+
+/**
+ * Resets the form data.  Causes all form elements to be reset to their original value.
+ */
+$.fn.resetForm = function() {
+       return this.each(function() {
+               // guard against an input with the name of 'reset'
+               // note that IE reports the reset function as an 'object'
+               if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
+                       this.reset();
+       });
+};
+
+/**
+ * Enables or disables any matching elements.
+ */
+$.fn.enable = function(b) {
+       if (b == undefined) b = true;
+       return this.each(function() {
+               this.disabled = !b;
+       });
+};
+
+/**
+ * Checks/unchecks any matching checkboxes or radio buttons and
+ * selects/deselects and matching option elements.
+ */
+$.fn.selected = function(select) {
+       if (select == undefined) select = true;
+       return this.each(function() {
+               var t = this.type;
+               if (t == 'checkbox' || t == 'radio')
+                       this.checked = select;
+               else if (this.tagName.toLowerCase() == 'option') {
+                       var $sel = $(this).parent('select');
+                       if (select && $sel[0] && $sel[0].type == 'select-one') {
+                               // deselect all other options
+                               $sel.find('option').selected(false);
+                       }
+                       this.selected = select;
+               }
+       });
+};
+
+// helper fn for console logging
+// set $.fn.ajaxSubmit.debug to true to enable debug logging
+function log() {
+       if ($.fn.ajaxSubmit.debug && window.console && window.console.log)
+               window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,''));
+};
+
+})(jQuery);
index 926357433e3e8289757481ee826cee31657982fe..237e1b9081302568b4286f9028e1380b1492db10 100644 (file)
 /*!
- * jQuery JavaScript Library v1.3.2
+ * jQuery JavaScript Library v1.4.1
  * http://jquery.com/
  *
- * Copyright (c) 2009 John Resig
- * Dual licensed under the MIT and GPL licenses.
- * http://docs.jquery.com/License
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
  *
- * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009)
- * Revision: 6246
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2010, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Mon Jan 25 19:43:33 2010 -0500
  */
-(function(){
+(function( window, undefined ) {
+
+// Define a local copy of jQuery
+var jQuery = function( selector, context ) {
+               // The jQuery object is actually just the init constructor 'enhanced'
+               return new jQuery.fn.init( selector, context );
+       },
 
-var 
-       // Will speed up references to window, and allows munging its name.
-       window = this,
-       // Will speed up references to undefined, and allows munging its name.
-       undefined,
        // Map over jQuery in case of overwrite
        _jQuery = window.jQuery,
+
        // Map over the $ in case of overwrite
        _$ = window.$,
 
-       jQuery = window.jQuery = window.$ = function( selector, context ) {
-               // The jQuery object is actually just the init constructor 'enhanced'
-               return new jQuery.fn.init( selector, context );
-       },
+       // Use the correct document accordingly with window argument (sandbox)
+       document = window.document,
+
+       // A central reference to the root jQuery(document)
+       rootjQuery,
 
        // A simple way to check for HTML strings or ID strings
        // (both of which we optimize for)
-       quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,
+       quickExpr = /^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,
+
        // Is it a simple selector
-       isSimple = /^.[^:#\[\.,]*$/;
+       isSimple = /^.[^:#\[\.,]*$/,
+
+       // Check if a string has a non-whitespace character in it
+       rnotwhite = /\S/,
+
+       // Used for trimming whitespace
+       rtrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g,
+
+       // Match a standalone tag
+       rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,
+
+       // Keep a UserAgent string for use with jQuery.browser
+       userAgent = navigator.userAgent,
+
+       // For matching the engine and version of the browser
+       browserMatch,
+       
+       // Has the ready events already been bound?
+       readyBound = false,
+       
+       // The functions to execute on DOM ready
+       readyList = [],
+
+       // The ready event handler
+       DOMContentLoaded,
+
+       // Save a reference to some core methods
+       toString = Object.prototype.toString,
+       hasOwnProperty = Object.prototype.hasOwnProperty,
+       push = Array.prototype.push,
+       slice = Array.prototype.slice,
+       indexOf = Array.prototype.indexOf;
 
 jQuery.fn = jQuery.prototype = {
        init: function( selector, context ) {
-               // Make sure that a selection was provided
-               selector = selector || document;
+               var match, elem, ret, doc;
+
+               // Handle $(""), $(null), or $(undefined)
+               if ( !selector ) {
+                       return this;
+               }
 
                // Handle $(DOMElement)
                if ( selector.nodeType ) {
-                       this[0] = selector;
+                       this.context = this[0] = selector;
                        this.length = 1;
-                       this.context = selector;
                        return this;
                }
+
                // Handle HTML strings
                if ( typeof selector === "string" ) {
                        // Are we dealing with HTML string or an ID?
-                       var match = quickExpr.exec( selector );
+                       match = quickExpr.exec( selector );
 
                        // Verify a match, and that no context was specified for #id
                        if ( match && (match[1] || !context) ) {
 
                                // HANDLE: $(html) -> $(array)
-                               if ( match[1] )
-                                       selector = jQuery.clean( [ match[1] ], context );
+                               if ( match[1] ) {
+                                       doc = (context ? context.ownerDocument || context : document);
+
+                                       // If a single string is passed in and it's a single tag
+                                       // just do a createElement and skip the rest
+                                       ret = rsingleTag.exec( selector );
+
+                                       if ( ret ) {
+                                               if ( jQuery.isPlainObject( context ) ) {
+                                                       selector = [ document.createElement( ret[1] ) ];
+                                                       jQuery.fn.attr.call( selector, context, true );
+
+                                               } else {
+                                                       selector = [ doc.createElement( ret[1] ) ];
+                                               }
+
+                                       } else {
+                                               ret = buildFragment( [ match[1] ], [ doc ] );
+                                               selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes;
+                                       }
 
                                // HANDLE: $("#id")
-                               else {
-                                       var elem = document.getElementById( match[3] );
-
-                                       // Handle the case where IE and Opera return items
-                                       // by name instead of ID
-                                       if ( elem && elem.id != match[3] )
-                                               return jQuery().find( selector );
-
-                                       // Otherwise, we inject the element directly into the jQuery object
-                                       var ret = jQuery( elem || [] );
-                                       ret.context = document;
-                                       ret.selector = selector;
-                                       return ret;
+                               } else {
+                                       elem = document.getElementById( match[2] );
+
+                                       if ( elem ) {
+                                               // Handle the case where IE and Opera return items
+                                               // by name instead of ID
+                                               if ( elem.id !== match[2] ) {
+                                                       return rootjQuery.find( selector );
+                                               }
+
+                                               // Otherwise, we inject the element directly into the jQuery object
+                                               this.length = 1;
+                                               this[0] = elem;
+                                       }
+
+                                       this.context = document;
+                                       this.selector = selector;
+                                       return this;
                                }
 
-                       // HANDLE: $(expr, [context])
-                       // (which is just equivalent to: $(content).find(expr)
-                       } else
+                       // HANDLE: $("TAG")
+                       } else if ( !context && /^\w+$/.test( selector ) ) {
+                               this.selector = selector;
+                               this.context = document;
+                               selector = document.getElementsByTagName( selector );
+
+                       // HANDLE: $(expr, $(...))
+                       } else if ( !context || context.jquery ) {
+                               return (context || rootjQuery).find( selector );
+
+                       // HANDLE: $(expr, context)
+                       // (which is just equivalent to: $(context).find(expr)
+                       } else {
                                return jQuery( context ).find( selector );
+                       }
 
                // HANDLE: $(function)
                // Shortcut for document ready
-               } else if ( jQuery.isFunction( selector ) )
-                       return jQuery( document ).ready( selector );
+               } else if ( jQuery.isFunction( selector ) ) {
+                       return rootjQuery.ready( selector );
+               }
 
-               // Make sure that old selector state is passed along
-               if ( selector.selector && selector.context ) {
+               if (selector.selector !== undefined) {
                        this.selector = selector.selector;
                        this.context = selector.context;
                }
 
-               return this.setArray(jQuery.isArray( selector ) ?
-                       selector :
-                       jQuery.makeArray(selector));
+               return jQuery.isArray( selector ) ?
+                       this.setArray( selector ) :
+                       jQuery.makeArray( selector, this );
        },
 
        // Start with an empty selector
        selector: "",
 
        // The current version of jQuery being used
-       jquery: "1.3.2",
+       jquery: "1.4.1",
+
+       // The default length of a jQuery object is 0
+       length: 0,
 
        // The number of elements contained in the matched element set
        size: function() {
                return this.length;
        },
 
+       toArray: function() {
+               return slice.call( this, 0 );
+       },
+
        // Get the Nth element in the matched element set OR
        // Get the whole matched element set as a clean array
        get: function( num ) {
-               return num === undefined ?
+               return num == null ?
 
                        // Return a 'clean' array
-                       Array.prototype.slice.call( this ) :
+                       this.toArray() :
 
                        // Return just the object
-                       this[ num ];
+                       ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] );
        },
 
        // Take an array of elements and push it onto the stack
        // (returning the new matched element set)
        pushStack: function( elems, name, selector ) {
                // Build a new jQuery matched element set
-               var ret = jQuery( elems );
+               var ret = jQuery( elems || null );
 
                // Add the old object onto the stack (as a reference)
                ret.prevObject = this;
 
                ret.context = this.context;
 
-               if ( name === "find" )
+               if ( name === "find" ) {
                        ret.selector = this.selector + (this.selector ? " " : "") + selector;
-               else if ( name )
+               } else if ( name ) {
                        ret.selector = this.selector + "." + name + "(" + selector + ")";
+               }
 
                // Return the newly-formed element set
                return ret;
@@ -143,7 +228,7 @@ jQuery.fn = jQuery.prototype = {
                // Resetting the length to 0, then using the native Array push
                // is a super-fast way to populate an object with array-like properties
                this.length = 0;
-               Array.prototype.push.apply( this, elems );
+               push.apply( this, elems );
 
                return this;
        },
@@ -154,509 +239,295 @@ jQuery.fn = jQuery.prototype = {
        each: function( callback, args ) {
                return jQuery.each( this, callback, args );
        },
+       
+       ready: function( fn ) {
+               // Attach the listeners
+               jQuery.bindReady();
 
-       // Determine the position of an element within
-       // the matched set of elements
-       index: function( elem ) {
-               // Locate the position of the desired element
-               return jQuery.inArray(
-                       // If it receives a jQuery object, the first element is used
-                       elem && elem.jquery ? elem[0] : elem
-               , this );
-       },
-
-       attr: function( name, value, type ) {
-               var options = name;
-
-               // Look for the case where we're accessing a style value
-               if ( typeof name === "string" )
-                       if ( value === undefined )
-                               return this[0] && jQuery[ type || "attr" ]( this[0], name );
-
-                       else {
-                               options = {};
-                               options[ name ] = value;
-                       }
-
-               // Check to see if we're setting style values
-               return this.each(function(i){
-                       // Set all the styles
-                       for ( name in options )
-                               jQuery.attr(
-                                       type ?
-                                               this.style :
-                                               this,
-                                       name, jQuery.prop( this, options[ name ], type, i, name )
-                               );
-               });
-       },
-
-       css: function( key, value ) {
-               // ignore negative width and height values
-               if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 )
-                       value = undefined;
-               return this.attr( key, value, "curCSS" );
-       },
-
-       text: function( text ) {
-               if ( typeof text !== "object" && text != null )
-                       return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
-
-               var ret = "";
-
-               jQuery.each( text || this, function(){
-                       jQuery.each( this.childNodes, function(){
-                               if ( this.nodeType != 8 )
-                                       ret += this.nodeType != 1 ?
-                                               this.nodeValue :
-                                               jQuery.fn.text( [ this ] );
-                       });
-               });
-
-               return ret;
-       },
-
-       wrapAll: function( html ) {
-               if ( this[0] ) {
-                       // The elements to wrap the target around
-                       var wrap = jQuery( html, this[0].ownerDocument ).clone();
-
-                       if ( this[0].parentNode )
-                               wrap.insertBefore( this[0] );
-
-                       wrap.map(function(){
-                               var elem = this;
-
-                               while ( elem.firstChild )
-                                       elem = elem.firstChild;
+               // If the DOM is already ready
+               if ( jQuery.isReady ) {
+                       // Execute the function immediately
+                       fn.call( document, jQuery );
 
-                               return elem;
-                       }).append(this);
+               // Otherwise, remember the function for later
+               } else if ( readyList ) {
+                       // Add the function to the wait list
+                       readyList.push( fn );
                }
 
                return this;
        },
-
-       wrapInner: function( html ) {
-               return this.each(function(){
-                       jQuery( this ).contents().wrapAll( html );
-               });
-       },
-
-       wrap: function( html ) {
-               return this.each(function(){
-                       jQuery( this ).wrapAll( html );
-               });
+       
+       eq: function( i ) {
+               return i === -1 ?
+                       this.slice( i ) :
+                       this.slice( i, +i + 1 );
        },
 
-       append: function() {
-               return this.domManip(arguments, true, function(elem){
-                       if (this.nodeType == 1)
-                               this.appendChild( elem );
-               });
+       first: function() {
+               return this.eq( 0 );
        },
 
-       prepend: function() {
-               return this.domManip(arguments, true, function(elem){
-                       if (this.nodeType == 1)
-                               this.insertBefore( elem, this.firstChild );
-               });
+       last: function() {
+               return this.eq( -1 );
        },
 
-       before: function() {
-               return this.domManip(arguments, false, function(elem){
-                       this.parentNode.insertBefore( elem, this );
-               });
+       slice: function() {
+               return this.pushStack( slice.apply( this, arguments ),
+                       "slice", slice.call(arguments).join(",") );
        },
 
-       after: function() {
-               return this.domManip(arguments, false, function(elem){
-                       this.parentNode.insertBefore( elem, this.nextSibling );
-               });
+       map: function( callback ) {
+               return this.pushStack( jQuery.map(this, function( elem, i ) {
+                       return callback.call( elem, i, elem );
+               }));
        },
-
+       
        end: function() {
-               return this.prevObject || jQuery( [] );
+               return this.prevObject || jQuery(null);
        },
 
        // For internal use only.
        // Behaves like an Array's method, not like a jQuery method.
-       push: [].push,
+       push: push,
        sort: [].sort,
-       splice: [].splice,
+       splice: [].splice
+};
 
-       find: function( selector ) {
-               if ( this.length === 1 ) {
-                       var ret = this.pushStack( [], "find", selector );
-                       ret.length = 0;
-                       jQuery.find( selector, this[0], ret );
-                       return ret;
-               } else {
-                       return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){
-                               return jQuery.find( selector, elem );
-                       })), "find", selector );
-               }
-       },
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
 
-       clone: function( events ) {
-               // Do the clone
-               var ret = this.map(function(){
-                       if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) {
-                               // IE copies events bound via attachEvent when
-                               // using cloneNode. Calling detachEvent on the
-                               // clone will also remove the events from the orignal
-                               // In order to get around this, we use innerHTML.
-                               // Unfortunately, this means some modifications to
-                               // attributes in IE that are actually only stored
-                               // as properties will not be copied (such as the
-                               // the name attribute on an input).
-                               var html = this.outerHTML;
-                               if ( !html ) {
-                                       var div = this.ownerDocument.createElement("div");
-                                       div.appendChild( this.cloneNode(true) );
-                                       html = div.innerHTML;
-                               }
+jQuery.extend = jQuery.fn.extend = function() {
+       // copy reference to target object
+       var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy;
 
-                               return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0];
-                       } else
-                               return this.cloneNode(true);
-               });
+       // Handle a deep copy situation
+       if ( typeof target === "boolean" ) {
+               deep = target;
+               target = arguments[1] || {};
+               // skip the boolean and the target
+               i = 2;
+       }
 
-               // Copy the events from the original to the clone
-               if ( events === true ) {
-                       var orig = this.find("*").andSelf(), i = 0;
+       // Handle case when target is a string or something (possible in deep copy)
+       if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+               target = {};
+       }
 
-                       ret.find("*").andSelf().each(function(){
-                               if ( this.nodeName !== orig[i].nodeName )
-                                       return;
+       // extend jQuery itself if only one argument is passed
+       if ( length === i ) {
+               target = this;
+               --i;
+       }
 
-                               var events = jQuery.data( orig[i], "events" );
+       for ( ; i < length; i++ ) {
+               // Only deal with non-null/undefined values
+               if ( (options = arguments[ i ]) != null ) {
+                       // Extend the base object
+                       for ( name in options ) {
+                               src = target[ name ];
+                               copy = options[ name ];
 
-                               for ( var type in events ) {
-                                       for ( var handler in events[ type ] ) {
-                                               jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data );
-                                       }
+                               // Prevent never-ending loop
+                               if ( target === copy ) {
+                                       continue;
                                }
 
-                               i++;
-                       });
-               }
+                               // Recurse if we're merging object literal values or arrays
+                               if ( deep && copy && ( jQuery.isPlainObject(copy) || jQuery.isArray(copy) ) ) {
+                                       var clone = src && ( jQuery.isPlainObject(src) || jQuery.isArray(src) ) ? src
+                                               : jQuery.isArray(copy) ? [] : {};
 
-               // Return the cloned set
-               return ret;
-       },
+                                       // Never move original objects, clone them
+                                       target[ name ] = jQuery.extend( deep, clone, copy );
 
-       filter: function( selector ) {
-               return this.pushStack(
-                       jQuery.isFunction( selector ) &&
-                       jQuery.grep(this, function(elem, i){
-                               return selector.call( elem, i );
-                       }) ||
-
-                       jQuery.multiFilter( selector, jQuery.grep(this, function(elem){
-                               return elem.nodeType === 1;
-                       }) ), "filter", selector );
-       },
-
-       closest: function( selector ) {
-               var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null,
-                       closer = 0;
-
-               return this.map(function(){
-                       var cur = this;
-                       while ( cur && cur.ownerDocument ) {
-                               if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) {
-                                       jQuery.data(cur, "closest", closer);
-                                       return cur;
+                               // Don't bring in undefined values
+                               } else if ( copy !== undefined ) {
+                                       target[ name ] = copy;
                                }
-                               cur = cur.parentNode;
-                               closer++;
                        }
-               });
-       },
+               }
+       }
 
-       not: function( selector ) {
-               if ( typeof selector === "string" )
-                       // test special case where just one selector is passed in
-                       if ( isSimple.test( selector ) )
-                               return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector );
-                       else
-                               selector = jQuery.multiFilter( selector, this );
-
-               var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType;
-               return this.filter(function() {
-                       return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector;
-               });
-       },
+       // Return the modified object
+       return target;
+};
 
-       add: function( selector ) {
-               return this.pushStack( jQuery.unique( jQuery.merge(
-                       this.get(),
-                       typeof selector === "string" ?
-                               jQuery( selector ) :
-                               jQuery.makeArray( selector )
-               )));
-       },
+jQuery.extend({
+       noConflict: function( deep ) {
+               window.$ = _$;
 
-       is: function( selector ) {
-               return !!selector && jQuery.multiFilter( selector, this ).length > 0;
-       },
+               if ( deep ) {
+                       window.jQuery = _jQuery;
+               }
 
-       hasClass: function( selector ) {
-               return !!selector && this.is( "." + selector );
+               return jQuery;
        },
+       
+       // Is the DOM ready to be used? Set to true once it occurs.
+       isReady: false,
+       
+       // Handle when the DOM is ready
+       ready: function() {
+               // Make sure that the DOM is not already loaded
+               if ( !jQuery.isReady ) {
+                       // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+                       if ( !document.body ) {
+                               return setTimeout( jQuery.ready, 13 );
+                       }
 
-       val: function( value ) {
-               if ( value === undefined ) {                    
-                       var elem = this[0];
+                       // Remember that the DOM is ready
+                       jQuery.isReady = true;
 
-                       if ( elem ) {
-                               if( jQuery.nodeName( elem, 'option' ) )
-                                       return (elem.attributes.value || {}).specified ? elem.value : elem.text;
-                               
-                               // We need to handle select boxes special
-                               if ( jQuery.nodeName( elem, "select" ) ) {
-                                       var index = elem.selectedIndex,
-                                               values = [],
-                                               options = elem.options,
-                                               one = elem.type == "select-one";
+                       // If there are functions bound, to execute
+                       if ( readyList ) {
+                               // Execute all of them
+                               var fn, i = 0;
+                               while ( (fn = readyList[ i++ ]) ) {
+                                       fn.call( document, jQuery );
+                               }
 
-                                       // Nothing was selected
-                                       if ( index < 0 )
-                                               return null;
+                               // Reset the list of functions
+                               readyList = null;
+                       }
 
-                                       // Loop through all the selected options
-                                       for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
-                                               var option = options[ i ];
+                       // Trigger any bound ready events
+                       if ( jQuery.fn.triggerHandler ) {
+                               jQuery( document ).triggerHandler( "ready" );
+                       }
+               }
+       },
+       
+       bindReady: function() {
+               if ( readyBound ) {
+                       return;
+               }
 
-                                               if ( option.selected ) {
-                                                       // Get the specifc value for the option
-                                                       value = jQuery(option).val();
+               readyBound = true;
 
-                                                       // We don't need an array for one selects
-                                                       if ( one )
-                                                               return value;
+               // Catch cases where $(document).ready() is called after the
+               // browser event has already occurred.
+               if ( document.readyState === "complete" ) {
+                       return jQuery.ready();
+               }
 
-                                                       // Multi-Selects return an array
-                                                       values.push( value );
-                                               }
-                                       }
+               // Mozilla, Opera and webkit nightlies currently support this event
+               if ( document.addEventListener ) {
+                       // Use the handy event callback
+                       document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+                       
+                       // A fallback to window.onload, that will always work
+                       window.addEventListener( "load", jQuery.ready, false );
+
+               // If IE event model is used
+               } else if ( document.attachEvent ) {
+                       // ensure firing before onload,
+                       // maybe late but safe also for iframes
+                       document.attachEvent("onreadystatechange", DOMContentLoaded);
+                       
+                       // A fallback to window.onload, that will always work
+                       window.attachEvent( "onload", jQuery.ready );
 
-                                       return values;                          
-                               }
+                       // If IE and not a frame
+                       // continually check to see if the document is ready
+                       var toplevel = false;
 
-                               // Everything else, we just grab the value
-                               return (elem.value || "").replace(/\r/g, "");
+                       try {
+                               toplevel = window.frameElement == null;
+                       } catch(e) {}
 
+                       if ( document.documentElement.doScroll && toplevel ) {
+                               doScrollCheck();
                        }
-
-                       return undefined;
                }
-
-               if ( typeof value === "number" )
-                       value += '';
-
-               return this.each(function(){
-                       if ( this.nodeType != 1 )
-                               return;
-
-                       if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) )
-                               this.checked = (jQuery.inArray(this.value, value) >= 0 ||
-                                       jQuery.inArray(this.name, value) >= 0);
-
-                       else if ( jQuery.nodeName( this, "select" ) ) {
-                               var values = jQuery.makeArray(value);
-
-                               jQuery( "option", this ).each(function(){
-                                       this.selected = (jQuery.inArray( this.value, values ) >= 0 ||
-                                               jQuery.inArray( this.text, values ) >= 0);
-                               });
-
-                               if ( !values.length )
-                                       this.selectedIndex = -1;
-
-                       } else
-                               this.value = value;
-               });
-       },
-
-       html: function( value ) {
-               return value === undefined ?
-                       (this[0] ?
-                               this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") :
-                               null) :
-                       this.empty().append( value );
        },
 
-       replaceWith: function( value ) {
-               return this.after( value ).remove();
+       // See test/unit/core.js for details concerning isFunction.
+       // Since version 1.3, DOM methods and functions like alert
+       // aren't supported. They return false on IE (#2968).
+       isFunction: function( obj ) {
+               return toString.call(obj) === "[object Function]";
        },
 
-       eq: function( i ) {
-               return this.slice( i, +i + 1 );
+       isArray: function( obj ) {
+               return toString.call(obj) === "[object Array]";
        },
 
-       slice: function() {
-               return this.pushStack( Array.prototype.slice.apply( this, arguments ),
-                       "slice", Array.prototype.slice.call(arguments).join(",") );
+       isPlainObject: function( obj ) {
+               // Must be an Object.
+               // Because of IE, we also have to check the presence of the constructor property.
+               // Make sure that DOM nodes and window objects don't pass through, as well
+               if ( !obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval ) {
+                       return false;
+               }
+               
+               // Not own constructor property must be Object
+               if ( obj.constructor
+                       && !hasOwnProperty.call(obj, "constructor")
+                       && !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf") ) {
+                       return false;
+               }
+               
+               // Own properties are enumerated firstly, so to speed up,
+               // if last one is own, then all properties are own.
+       
+               var key;
+               for ( key in obj ) {}
+               
+               return key === undefined || hasOwnProperty.call( obj, key );
        },
 
-       map: function( callback ) {
-               return this.pushStack( jQuery.map(this, function(elem, i){
-                       return callback.call( elem, i, elem );
-               }));
+       isEmptyObject: function( obj ) {
+               for ( var name in obj ) {
+                       return false;
+               }
+               return true;
        },
-
-       andSelf: function() {
-               return this.add( this.prevObject );
+       
+       error: function( msg ) {
+               throw msg;
        },
-
-       domManip: function( args, table, callback ) {
-               if ( this[0] ) {
-                       var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(),
-                               scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ),
-                               first = fragment.firstChild;
-
-                       if ( first )
-                               for ( var i = 0, l = this.length; i < l; i++ )
-                                       callback.call( root(this[i], first), this.length > 1 || i > 0 ?
-                                                       fragment.cloneNode(true) : fragment );
-               
-                       if ( scripts )
-                               jQuery.each( scripts, evalScript );
+       
+       parseJSON: function( data ) {
+               if ( typeof data !== "string" || !data ) {
+                       return null;
                }
-
-               return this;
                
-               function root( elem, cur ) {
-                       return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ?
-                               (elem.getElementsByTagName("tbody")[0] ||
-                               elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
-                               elem;
-               }
-       }
-};
-
-// Give the init function the jQuery prototype for later instantiation
-jQuery.fn.init.prototype = jQuery.fn;
-
-function evalScript( i, elem ) {
-       if ( elem.src )
-               jQuery.ajax({
-                       url: elem.src,
-                       async: false,
-                       dataType: "script"
-               });
-
-       else
-               jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
-
-       if ( elem.parentNode )
-               elem.parentNode.removeChild( elem );
-}
-
-function now(){
-       return +new Date;
-}
-
-jQuery.extend = jQuery.fn.extend = function() {
-       // copy reference to target object
-       var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;
-
-       // Handle a deep copy situation
-       if ( typeof target === "boolean" ) {
-               deep = target;
-               target = arguments[1] || {};
-               // skip the boolean and the target
-               i = 2;
-       }
-
-       // Handle case when target is a string or something (possible in deep copy)
-       if ( typeof target !== "object" && !jQuery.isFunction(target) )
-               target = {};
-
-       // extend jQuery itself if only one argument is passed
-       if ( length == i ) {
-               target = this;
-               --i;
-       }
-
-       for ( ; i < length; i++ )
-               // Only deal with non-null/undefined values
-               if ( (options = arguments[ i ]) != null )
-                       // Extend the base object
-                       for ( var name in options ) {
-                               var src = target[ name ], copy = options[ name ];
-
-                               // Prevent never-ending loop
-                               if ( target === copy )
-                                       continue;
-
-                               // Recurse if we're merging object values
-                               if ( deep && copy && typeof copy === "object" && !copy.nodeType )
-                                       target[ name ] = jQuery.extend( deep, 
-                                               // Never move original objects, clone them
-                                               src || ( copy.length != null ? [ ] : { } )
-                                       , copy );
-
-                               // Don't bring in undefined values
-                               else if ( copy !== undefined )
-                                       target[ name ] = copy;
-
-                       }
-
-       // Return the modified object
-       return target;
-};
-
-// exclude the following css properties to add px
-var    exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
-       // cache defaultView
-       defaultView = document.defaultView || {},
-       toString = Object.prototype.toString;
-
-jQuery.extend({
-       noConflict: function( deep ) {
-               window.$ = _$;
-
-               if ( deep )
-                       window.jQuery = _jQuery;
-
-               return jQuery;
-       },
+               // Make sure the incoming data is actual JSON
+               // Logic borrowed from http://json.org/json2.js
+               if ( /^[\],:{}\s]*$/.test(data.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@")
+                       .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]")
+                       .replace(/(?:^|:|,)(?:\s*\[)+/g, "")) ) {
 
-       // See test/unit/core.js for details concerning isFunction.
-       // Since version 1.3, DOM methods and functions like alert
-       // aren't supported. They return false on IE (#2968).
-       isFunction: function( obj ) {
-               return toString.call(obj) === "[object Function]";
-       },
+                       // Try to use the native JSON parser first
+                       return window.JSON && window.JSON.parse ?
+                               window.JSON.parse( data ) :
+                               (new Function("return " + data))();
 
-       isArray: function( obj ) {
-               return toString.call(obj) === "[object Array]";
+               } else {
+                       jQuery.error( "Invalid JSON: " + data );
+               }
        },
 
-       // check if an element is in a (or is an) XML document
-       isXMLDoc: function( elem ) {
-               return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
-                       !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument );
-       },
+       noop: function() {},
 
        // Evalulates a script in a global context
        globalEval: function( data ) {
-               if ( data && /\S/.test(data) ) {
+               if ( data && rnotwhite.test(data) ) {
                        // Inspired by code by Andrea Giammarchi
                        // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
                        var head = document.getElementsByTagName("head")[0] || document.documentElement,
                                script = document.createElement("script");
 
                        script.type = "text/javascript";
-                       if ( jQuery.support.scriptEval )
+
+                       if ( jQuery.support.scriptEval ) {
                                script.appendChild( document.createTextNode( data ) );
-                       else
+                       } else {
                                script.text = data;
+                       }
 
-                       // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
+                       // Use insertBefore instead of appendChild to circumvent an IE6 bug.
                        // This arises when a base node is used (#2709).
                        head.insertBefore( script, head.firstChild );
                        head.removeChild( script );
@@ -664,951 +535,2224 @@ jQuery.extend({
        },
 
        nodeName: function( elem, name ) {
-               return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase();
+               return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
        },
 
        // args is for internal usage only
        each: function( object, callback, args ) {
-               var name, i = 0, length = object.length;
+               var name, i = 0,
+                       length = object.length,
+                       isObj = length === undefined || jQuery.isFunction(object);
 
                if ( args ) {
-                       if ( length === undefined ) {
-                               for ( name in object )
-                                       if ( callback.apply( object[ name ], args ) === false )
+                       if ( isObj ) {
+                               for ( name in object ) {
+                                       if ( callback.apply( object[ name ], args ) === false ) {
                                                break;
-                       } else
-                               for ( ; i < length; )
-                                       if ( callback.apply( object[ i++ ], args ) === false )
+                                       }
+                               }
+                       } else {
+                               for ( ; i < length; ) {
+                                       if ( callback.apply( object[ i++ ], args ) === false ) {
                                                break;
+                                       }
+                               }
+                       }
 
                // A special, fast, case for the most common use of each
                } else {
-                       if ( length === undefined ) {
-                               for ( name in object )
-                                       if ( callback.call( object[ name ], name, object[ name ] ) === false )
+                       if ( isObj ) {
+                               for ( name in object ) {
+                                       if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
                                                break;
-                       } else
+                                       }
+                               }
+                       } else {
                                for ( var value = object[0];
-                                       i < length && callback.call( value, i, value ) !== false; value = object[++i] ){}
+                                       i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {}
+                       }
                }
 
                return object;
        },
 
-       prop: function( elem, value, type, i, name ) {
-               // Handle executable functions
-               if ( jQuery.isFunction( value ) )
-                       value = value.call( elem, i );
-
-               // Handle passing in a number to a CSS property
-               return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ?
-                       value + "px" :
-                       value;
+       trim: function( text ) {
+               return (text || "").replace( rtrim, "" );
        },
 
-       className: {
-               // internal only, use addClass("class")
-               add: function( elem, classNames ) {
-                       jQuery.each((classNames || "").split(/\s+/), function(i, className){
-                               if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) )
-                                       elem.className += (elem.className ? " " : "") + className;
-                       });
-               },
-
-               // internal only, use removeClass("class")
-               remove: function( elem, classNames ) {
-                       if (elem.nodeType == 1)
-                               elem.className = classNames !== undefined ?
-                                       jQuery.grep(elem.className.split(/\s+/), function(className){
-                                               return !jQuery.className.has( classNames, className );
-                                       }).join(" ") :
-                                       "";
-               },
+       // results is for internal usage only
+       makeArray: function( array, results ) {
+               var ret = results || [];
 
-               // internal only, use hasClass("class")
-               has: function( elem, className ) {
-                       return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1;
+               if ( array != null ) {
+                       // The window, strings (and functions) also have 'length'
+                       // The extra typeof function check is to prevent crashes
+                       // in Safari 2 (See: #3039)
+                       if ( array.length == null || typeof array === "string" || jQuery.isFunction(array) || (typeof array !== "function" && array.setInterval) ) {
+                               push.call( ret, array );
+                       } else {
+                               jQuery.merge( ret, array );
+                       }
                }
+
+               return ret;
        },
 
-       // A method for quickly swapping in/out CSS properties to get correct calculations
-       swap: function( elem, options, callback ) {
-               var old = {};
-               // Remember the old values, and insert the new ones
-               for ( var name in options ) {
-                       old[ name ] = elem.style[ name ];
-                       elem.style[ name ] = options[ name ];
+       inArray: function( elem, array ) {
+               if ( array.indexOf ) {
+                       return array.indexOf( elem );
                }
 
-               callback.call( elem );
+               for ( var i = 0, length = array.length; i < length; i++ ) {
+                       if ( array[ i ] === elem ) {
+                               return i;
+                       }
+               }
 
-               // Revert the old values
-               for ( var name in options )
-                       elem.style[ name ] = old[ name ];
+               return -1;
        },
 
-       css: function( elem, name, force, extra ) {
-               if ( name == "width" || name == "height" ) {
-                       var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ];
+       merge: function( first, second ) {
+               var i = first.length, j = 0;
 
-                       function getWH() {
-                               val = name == "width" ? elem.offsetWidth : elem.offsetHeight;
+               if ( typeof second.length === "number" ) {
+                       for ( var l = second.length; j < l; j++ ) {
+                               first[ i++ ] = second[ j ];
+                       }
+               } else {
+                       while ( second[j] !== undefined ) {
+                               first[ i++ ] = second[ j++ ];
+                       }
+               }
 
-                               if ( extra === "border" )
-                                       return;
+               first.length = i;
 
-                               jQuery.each( which, function() {
-                                       if ( !extra )
-                                               val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
-                                       if ( extra === "margin" )
-                                               val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0;
-                                       else
-                                               val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
-                               });
-                       }
+               return first;
+       },
 
-                       if ( elem.offsetWidth !== 0 )
-                               getWH();
-                       else
-                               jQuery.swap( elem, props, getWH );
+       grep: function( elems, callback, inv ) {
+               var ret = [];
 
-                       return Math.max(0, Math.round(val));
+               // Go through the array, only saving the items
+               // that pass the validator function
+               for ( var i = 0, length = elems.length; i < length; i++ ) {
+                       if ( !inv !== !callback( elems[ i ], i ) ) {
+                               ret.push( elems[ i ] );
+                       }
                }
 
-               return jQuery.curCSS( elem, name, force );
+               return ret;
        },
 
-       curCSS: function( elem, name, force ) {
-               var ret, style = elem.style;
+       // arg is for internal usage only
+       map: function( elems, callback, arg ) {
+               var ret = [], value;
 
-               // We need to handle opacity special in IE
-               if ( name == "opacity" && !jQuery.support.opacity ) {
-                       ret = jQuery.attr( style, "opacity" );
+               // Go through the array, translating each of the items to their
+               // new value (or values).
+               for ( var i = 0, length = elems.length; i < length; i++ ) {
+                       value = callback( elems[ i ], i, arg );
 
-                       return ret == "" ?
-                               "1" :
-                               ret;
+                       if ( value != null ) {
+                               ret[ ret.length ] = value;
+                       }
                }
 
-               // Make sure we're using the right name for getting the float value
-               if ( name.match( /float/i ) )
-                       name = styleFloat;
+               return ret.concat.apply( [], ret );
+       },
 
-               if ( !force && style && style[ name ] )
-                       ret = style[ name ];
+       // A global GUID counter for objects
+       guid: 1,
 
-               else if ( defaultView.getComputedStyle ) {
+       proxy: function( fn, proxy, thisObject ) {
+               if ( arguments.length === 2 ) {
+                       if ( typeof proxy === "string" ) {
+                               thisObject = fn;
+                               fn = thisObject[ proxy ];
+                               proxy = undefined;
 
-                       // Only "float" is needed here
-                       if ( name.match( /float/i ) )
-                               name = "float";
+                       } else if ( proxy && !jQuery.isFunction( proxy ) ) {
+                               thisObject = proxy;
+                               proxy = undefined;
+                       }
+               }
 
-                       name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase();
+               if ( !proxy && fn ) {
+                       proxy = function() {
+                               return fn.apply( thisObject || this, arguments );
+                       };
+               }
 
-                       var computedStyle = defaultView.getComputedStyle( elem, null );
+               // Set the guid of unique handler to the same of original handler, so it can be removed
+               if ( fn ) {
+                       proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
+               }
 
-                       if ( computedStyle )
-                               ret = computedStyle.getPropertyValue( name );
+               // So proxy can be declared as an argument
+               return proxy;
+       },
 
-                       // We should always get a number back from opacity
-                       if ( name == "opacity" && ret == "" )
-                               ret = "1";
+       // Use of jQuery.browser is frowned upon.
+       // More details: http://docs.jquery.com/Utilities/jQuery.browser
+       uaMatch: function( ua ) {
+               ua = ua.toLowerCase();
 
-               } else if ( elem.currentStyle ) {
-                       var camelCase = name.replace(/\-(\w)/g, function(all, letter){
-                               return letter.toUpperCase();
-                       });
+               var match = /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
+                       /(opera)(?:.*version)?[ \/]([\w.]+)/.exec( ua ) ||
+                       /(msie) ([\w.]+)/.exec( ua ) ||
+                       !/compatible/.test( ua ) && /(mozilla)(?:.*? rv:([\w.]+))?/.exec( ua ) ||
+                       [];
 
-                       ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
+               return { browser: match[1] || "", version: match[2] || "0" };
+       },
 
-                       // From the awesome hack by Dean Edwards
-                       // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+       browser: {}
+});
 
-                       // If we're not dealing with a regular pixel number
-                       // but a number that has a weird ending, we need to convert it to pixels
-                       if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
-                               // Remember the original values
-                               var left = style.left, rsLeft = elem.runtimeStyle.left;
+browserMatch = jQuery.uaMatch( userAgent );
+if ( browserMatch.browser ) {
+       jQuery.browser[ browserMatch.browser ] = true;
+       jQuery.browser.version = browserMatch.version;
+}
 
-                               // Put in the new values to get a computed value out
-                               elem.runtimeStyle.left = elem.currentStyle.left;
-                               style.left = ret || 0;
-                               ret = style.pixelLeft + "px";
+// Deprecated, use jQuery.browser.webkit instead
+if ( jQuery.browser.webkit ) {
+       jQuery.browser.safari = true;
+}
 
-                               // Revert the changed values
-                               style.left = left;
-                               elem.runtimeStyle.left = rsLeft;
-                       }
-               }
+if ( indexOf ) {
+       jQuery.inArray = function( elem, array ) {
+               return indexOf.call( array, elem );
+       };
+}
 
-               return ret;
-       },
+// All jQuery objects should point back to these
+rootjQuery = jQuery(document);
 
-       clean: function( elems, context, fragment ) {
-               context = context || document;
-
-               // !context.createElement fails in IE with an error but returns typeof 'object'
-               if ( typeof context.createElement === "undefined" )
-                       context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
+// Cleanup functions for the document ready method
+if ( document.addEventListener ) {
+       DOMContentLoaded = function() {
+               document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+               jQuery.ready();
+       };
 
-               // If a single string is passed in and it's a single tag
-               // just do a createElement and skip the rest
-               if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) {
-                       var match = /^<(\w+)\s*\/?>$/.exec(elems[0]);
-                       if ( match )
-                               return [ context.createElement( match[1] ) ];
+} else if ( document.attachEvent ) {
+       DOMContentLoaded = function() {
+               // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+               if ( document.readyState === "complete" ) {
+                       document.detachEvent( "onreadystatechange", DOMContentLoaded );
+                       jQuery.ready();
                }
+       };
+}
 
-               var ret = [], scripts = [], div = context.createElement("div");
-
-               jQuery.each(elems, function(i, elem){
-                       if ( typeof elem === "number" )
-                               elem += '';
+// The DOM ready check for Internet Explorer
+function doScrollCheck() {
+       if ( jQuery.isReady ) {
+               return;
+       }
 
-                       if ( !elem )
-                               return;
+       try {
+               // If IE is used, use the trick by Diego Perini
+               // http://javascript.nwbox.com/IEContentLoaded/
+               document.documentElement.doScroll("left");
+       } catch( error ) {
+               setTimeout( doScrollCheck, 1 );
+               return;
+       }
 
-                       // Convert html string into DOM nodes
-                       if ( typeof elem === "string" ) {
-                               // Fix "XHTML"-style tags in all browsers
-                               elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
-                                       return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
-                                               all :
-                                               front + "></" + tag + ">";
-                               });
+       // and execute any waiting functions
+       jQuery.ready();
+}
 
-                               // Trim whitespace, otherwise indexOf won't work as expected
-                               var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase();
+function evalScript( i, elem ) {
+       if ( elem.src ) {
+               jQuery.ajax({
+                       url: elem.src,
+                       async: false,
+                       dataType: "script"
+               });
+       } else {
+               jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
+       }
 
-                               var wrap =
-                                       // option or optgroup
-                                       !tags.indexOf("<opt") &&
-                                       [ 1, "<select multiple='multiple'>", "</select>" ] ||
+       if ( elem.parentNode ) {
+               elem.parentNode.removeChild( elem );
+       }
+}
 
-                                       !tags.indexOf("<leg") &&
-                                       [ 1, "<fieldset>", "</fieldset>" ] ||
+// Mutifunctional method to get and set values to a collection
+// The value/s can be optionally by executed if its a function
+function access( elems, key, value, exec, fn, pass ) {
+       var length = elems.length;
+       
+       // Setting many attributes
+       if ( typeof key === "object" ) {
+               for ( var k in key ) {
+                       access( elems, k, key[k], exec, fn, value );
+               }
+               return elems;
+       }
+       
+       // Setting one attribute
+       if ( value !== undefined ) {
+               // Optionally, function values get executed if exec is true
+               exec = !pass && exec && jQuery.isFunction(value);
+               
+               for ( var i = 0; i < length; i++ ) {
+                       fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
+               }
+               
+               return elems;
+       }
+       
+       // Getting an attribute
+       return length ? fn( elems[0], key ) : null;
+}
 
-                                       tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
-                                       [ 1, "<table>", "</table>" ] ||
+function now() {
+       return (new Date).getTime();
+}
+(function() {
 
-                                       !tags.indexOf("<tr") &&
-                                       [ 2, "<table><tbody>", "</tbody></table>" ] ||
+       jQuery.support = {};
 
-                                       // <thead> matched above
-                                       (!tags.indexOf("<td") || !tags.indexOf("<th")) &&
-                                       [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||
+       var root = document.documentElement,
+               script = document.createElement("script"),
+               div = document.createElement("div"),
+               id = "script" + now();
 
-                                       !tags.indexOf("<col") &&
-                                       [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||
+       div.style.display = "none";
+       div.innerHTML = "   <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";
 
-                                       // IE can't serialize <link> and <script> tags normally
-                                       !jQuery.support.htmlSerialize &&
-                                       [ 1, "div<div>", "</div>" ] ||
+       var all = div.getElementsByTagName("*"),
+               a = div.getElementsByTagName("a")[0];
 
-                                       [ 0, "", "" ];
+       // Can't get basic test support
+       if ( !all || !all.length || !a ) {
+               return;
+       }
 
-                               // Go to html and back, then peel off extra wrappers
-                               div.innerHTML = wrap[1] + elem + wrap[2];
+       jQuery.support = {
+               // IE strips leading whitespace when .innerHTML is used
+               leadingWhitespace: div.firstChild.nodeType === 3,
 
-                               // Move to the right depth
-                               while ( wrap[0]-- )
-                                       div = div.lastChild;
+               // Make sure that tbody elements aren't automatically inserted
+               // IE will insert them into empty tables
+               tbody: !div.getElementsByTagName("tbody").length,
 
-                               // Remove IE's autoinserted <tbody> from table fragments
-                               if ( !jQuery.support.tbody ) {
+               // Make sure that link elements get serialized correctly by innerHTML
+               // This requires a wrapper element in IE
+               htmlSerialize: !!div.getElementsByTagName("link").length,
 
-                                       // String was a <table>, *may* have spurious <tbody>
-                                       var hasBody = /<tbody/i.test(elem),
-                                               tbody = !tags.indexOf("<table") && !hasBody ?
-                                                       div.firstChild && div.firstChild.childNodes :
+               // Get the style information from getAttribute
+               // (IE uses .cssText insted)
+               style: /red/.test( a.getAttribute("style") ),
 
-                                               // String was a bare <thead> or <tfoot>
-                                               wrap[1] == "<table>" && !hasBody ?
-                                                       div.childNodes :
-                                                       [];
+               // Make sure that URLs aren't manipulated
+               // (IE normalizes it by default)
+               hrefNormalized: a.getAttribute("href") === "/a",
 
-                                       for ( var j = tbody.length - 1; j >= 0 ; --j )
-                                               if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length )
-                                                       tbody[ j ].parentNode.removeChild( tbody[ j ] );
+               // Make sure that element opacity exists
+               // (IE uses filter instead)
+               // Use a regex to work around a WebKit issue. See #5145
+               opacity: /^0.55$/.test( a.style.opacity ),
 
-                                       }
+               // Verify style float existence
+               // (IE uses styleFloat instead of cssFloat)
+               cssFloat: !!a.style.cssFloat,
 
-                               // IE completely kills leading whitespace when innerHTML is used
-                               if ( !jQuery.support.leadingWhitespace && /^\s/.test( elem ) )
-                                       div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild );
-                               
-                               elem = jQuery.makeArray( div.childNodes );
-                       }
+               // Make sure that if no value is specified for a checkbox
+               // that it defaults to "on".
+               // (WebKit defaults to "" instead)
+               checkOn: div.getElementsByTagName("input")[0].value === "on",
 
-                       if ( elem.nodeType )
-                               ret.push( elem );
-                       else
-                               ret = jQuery.merge( ret, elem );
+               // Make sure that a selected-by-default option has a working selected property.
+               // (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
+               optSelected: document.createElement("select").appendChild( document.createElement("option") ).selected,
 
-               });
+               // Will be defined later
+               checkClone: false,
+               scriptEval: false,
+               noCloneEvent: true,
+               boxModel: null
+       };
 
-               if ( fragment ) {
-                       for ( var i = 0; ret[i]; i++ ) {
-                               if ( jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
-                                       scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
-                               } else {
-                                       if ( ret[i].nodeType === 1 )
-                                               ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
-                                       fragment.appendChild( ret[i] );
-                               }
-                       }
-                       
-                       return scripts;
-               }
+       script.type = "text/javascript";
+       try {
+               script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
+       } catch(e) {}
 
-               return ret;
-       },
+       root.insertBefore( script, root.firstChild );
 
-       attr: function( elem, name, value ) {
-               // don't set attributes on text and comment nodes
-               if (!elem || elem.nodeType == 3 || elem.nodeType == 8)
-                       return undefined;
+       // Make sure that the execution of code works by injecting a script
+       // tag with appendChild/createTextNode
+       // (IE doesn't support this, fails, and uses .text instead)
+       if ( window[ id ] ) {
+               jQuery.support.scriptEval = true;
+               delete window[ id ];
+       }
 
-               var notxml = !jQuery.isXMLDoc( elem ),
-                       // Whether we are setting (or getting)
-                       set = value !== undefined;
+       root.removeChild( script );
 
-               // Try to normalize/fix the name
-               name = notxml && jQuery.props[ name ] || name;
+       if ( div.attachEvent && div.fireEvent ) {
+               div.attachEvent("onclick", function click() {
+                       // Cloning a node shouldn't copy over any
+                       // bound event handlers (IE does this)
+                       jQuery.support.noCloneEvent = false;
+                       div.detachEvent("onclick", click);
+               });
+               div.cloneNode(true).fireEvent("onclick");
+       }
 
-               // Only do all the following if this is a node (faster for style)
-               // IE elem.getAttribute passes even for style
-               if ( elem.tagName ) {
+       div = document.createElement("div");
+       div.innerHTML = "<input type='radio' name='radiotest' checked='checked'/>";
 
-                       // These attributes require special treatment
-                       var special = /href|src|style/.test( name );
+       var fragment = document.createDocumentFragment();
+       fragment.appendChild( div.firstChild );
 
-                       // Safari mis-reports the default selected property of a hidden option
-                       // Accessing the parent's selectedIndex property fixes it
-                       if ( name == "selected" && elem.parentNode )
-                               elem.parentNode.selectedIndex;
+       // WebKit doesn't clone checked state correctly in fragments
+       jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked;
 
-                       // If applicable, access the attribute via the DOM 0 way
-                       if ( name in elem && notxml && !special ) {
-                               if ( set ){
-                                       // We can't allow the type property to be changed (since it causes problems in IE)
-                                       if ( name == "type" && jQuery.nodeName( elem, "input" ) && elem.parentNode )
-                                               throw "type property can't be changed";
+       // Figure out if the W3C box model works as expected
+       // document.body must exist before we can do this
+       jQuery(function() {
+               var div = document.createElement("div");
+               div.style.width = div.style.paddingLeft = "1px";
 
-                                       elem[ name ] = value;
-                               }
+               document.body.appendChild( div );
+               jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2;
+               document.body.removeChild( div ).style.display = 'none';
+               div = null;
+       });
 
-                               // browsers index elements by id/name on forms, give priority to attributes.
-                               if( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) )
-                                       return elem.getAttributeNode( name ).nodeValue;
+       // Technique from Juriy Zaytsev
+       // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
+       var eventSupported = function( eventName ) { 
+               var el = document.createElement("div"); 
+               eventName = "on" + eventName; 
 
-                               // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
-                               // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
-                               if ( name == "tabIndex" ) {
-                                       var attributeNode = elem.getAttributeNode( "tabIndex" );
-                                       return attributeNode && attributeNode.specified
-                                               ? attributeNode.value
-                                               : elem.nodeName.match(/(button|input|object|select|textarea)/i)
-                                                       ? 0
-                                                       : elem.nodeName.match(/^(a|area)$/i) && elem.href
-                                                               ? 0
-                                                               : undefined;
-                               }
+               var isSupported = (eventName in el); 
+               if ( !isSupported ) { 
+                       el.setAttribute(eventName, "return;"); 
+                       isSupported = typeof el[eventName] === "function"; 
+               } 
+               el = null; 
 
-                               return elem[ name ];
-                       }
+               return isSupported; 
+       };
+       
+       jQuery.support.submitBubbles = eventSupported("submit");
+       jQuery.support.changeBubbles = eventSupported("change");
 
-                       if ( !jQuery.support.style && notxml &&  name == "style" )
-                               return jQuery.attr( elem.style, "cssText", value );
+       // release memory in IE
+       root = script = div = all = a = null;
+})();
 
-                       if ( set )
-                               // convert the value to a string (all browsers do this but IE) see #1070
-                               elem.setAttribute( name, "" + value );
+jQuery.props = {
+       "for": "htmlFor",
+       "class": "className",
+       readonly: "readOnly",
+       maxlength: "maxLength",
+       cellspacing: "cellSpacing",
+       rowspan: "rowSpan",
+       colspan: "colSpan",
+       tabindex: "tabIndex",
+       usemap: "useMap",
+       frameborder: "frameBorder"
+};
+var expando = "jQuery" + now(), uuid = 0, windowData = {};
+var emptyObject = {};
 
-                       var attr = !jQuery.support.hrefNormalized && notxml && special
-                                       // Some attributes require a special call on IE
-                                       ? elem.getAttribute( name, 2 )
-                                       : elem.getAttribute( name );
+jQuery.extend({
+       cache: {},
+       
+       expando:expando,
+
+       // The following elements throw uncatchable exceptions if you
+       // attempt to add expando properties to them.
+       noData: {
+               "embed": true,
+               "object": true,
+               "applet": true
+       },
 
-                       // Non-existent attributes return null, we normalize to undefined
-                       return attr === null ? undefined : attr;
+       data: function( elem, name, data ) {
+               if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
+                       return;
                }
 
-               // elem is actually elem.style ... set the style
+               elem = elem == window ?
+                       windowData :
+                       elem;
 
-               // IE uses filters for opacity
-               if ( !jQuery.support.opacity && name == "opacity" ) {
-                       if ( set ) {
-                               // IE has trouble with opacity if it does not have layout
-                               // Force it by setting the zoom level
-                               elem.zoom = 1;
+               var id = elem[ expando ], cache = jQuery.cache, thisCache;
 
-                               // Set the alpha filter to set the opacity
-                               elem.filter = (elem.filter || "").replace( /alpha\([^)]*\)/, "" ) +
-                                       (parseInt( value ) + '' == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")");
-                       }
+               // Handle the case where there's no name immediately
+               if ( !name && !id ) {
+                       return null;
+               }
 
-                       return elem.filter && elem.filter.indexOf("opacity=") >= 0 ?
-                               (parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100) + '':
-                               "";
+               // Compute a unique ID for the element
+               if ( !id ) { 
+                       id = ++uuid;
                }
 
-               name = name.replace(/-([a-z])/ig, function(all, letter){
-                       return letter.toUpperCase();
-               });
+               // Avoid generating a new cache unless none exists and we
+               // want to manipulate it.
+               if ( typeof name === "object" ) {
+                       elem[ expando ] = id;
+                       thisCache = cache[ id ] = jQuery.extend(true, {}, name);
+               } else if ( cache[ id ] ) {
+                       thisCache = cache[ id ];
+               } else if ( typeof data === "undefined" ) {
+                       thisCache = emptyObject;
+               } else {
+                       thisCache = cache[ id ] = {};
+               }
 
-               if ( set )
-                       elem[ name ] = value;
+               // Prevent overriding the named cache with undefined values
+               if ( data !== undefined ) {
+                       elem[ expando ] = id;
+                       thisCache[ name ] = data;
+               }
 
-               return elem[ name ];
+               return typeof name === "string" ? thisCache[ name ] : thisCache;
        },
 
-       trim: function( text ) {
-               return (text || "").replace( /^\s+|\s+$/g, "" );
-       },
+       removeData: function( elem, name ) {
+               if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
+                       return;
+               }
 
-       makeArray: function( array ) {
-               var ret = [];
+               elem = elem == window ?
+                       windowData :
+                       elem;
 
-               if( array != null ){
-                       var i = array.length;
-                       // The window, strings (and functions) also have 'length'
-                       if( i == null || typeof array === "string" || jQuery.isFunction(array) || array.setInterval )
-                               ret[0] = array;
-                       else
-                               while( i )
-                                       ret[--i] = array[i];
-               }
+               var id = elem[ expando ], cache = jQuery.cache, thisCache = cache[ id ];
 
-               return ret;
-       },
+               // If we want to remove a specific section of the element's data
+               if ( name ) {
+                       if ( thisCache ) {
+                               // Remove the section of cache data
+                               delete thisCache[ name ];
 
-       inArray: function( elem, array ) {
-               for ( var i = 0, length = array.length; i < length; i++ )
-               // Use === because on IE, window == document
-                       if ( array[ i ] === elem )
-                               return i;
+                               // If we've removed all the data, remove the element's cache
+                               if ( jQuery.isEmptyObject(thisCache) ) {
+                                       jQuery.removeData( elem );
+                               }
+                       }
 
-               return -1;
-       },
+               // Otherwise, we want to remove all of the element's data
+               } else {
+                       // Clean up the element expando
+                       try {
+                               delete elem[ expando ];
+                       } catch( e ) {
+                               // IE has trouble directly removing the expando
+                               // but it's ok with using removeAttribute
+                               if ( elem.removeAttribute ) {
+                                       elem.removeAttribute( expando );
+                               }
+                       }
 
-       merge: function( first, second ) {
-               // We have to loop this way because IE & Opera overwrite the length
-               // expando of getElementsByTagName
-               var i = 0, elem, pos = first.length;
-               // Also, we need to make sure that the correct elements are being returned
-               // (IE returns comment nodes in a '*' query)
-               if ( !jQuery.support.getAll ) {
-                       while ( (elem = second[ i++ ]) != null )
-                               if ( elem.nodeType != 8 )
-                                       first[ pos++ ] = elem;
-
-               } else
-                       while ( (elem = second[ i++ ]) != null )
-                               first[ pos++ ] = elem;
+                       // Completely remove the data cache
+                       delete cache[ id ];
+               }
+       }
+});
 
-               return first;
-       },
+jQuery.fn.extend({
+       data: function( key, value ) {
+               if ( typeof key === "undefined" && this.length ) {
+                       return jQuery.data( this[0] );
 
-       unique: function( array ) {
-               var ret = [], done = {};
+               } else if ( typeof key === "object" ) {
+                       return this.each(function() {
+                               jQuery.data( this, key );
+                       });
+               }
 
-               try {
+               var parts = key.split(".");
+               parts[1] = parts[1] ? "." + parts[1] : "";
 
-                       for ( var i = 0, length = array.length; i < length; i++ ) {
-                               var id = jQuery.data( array[ i ] );
+               if ( value === undefined ) {
+                       var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
 
-                               if ( !done[ id ] ) {
-                                       done[ id ] = true;
-                                       ret.push( array[ i ] );
-                               }
+                       if ( data === undefined && this.length ) {
+                               data = jQuery.data( this[0], key );
                        }
+                       return data === undefined && parts[1] ?
+                               this.data( parts[0] ) :
+                               data;
+               } else {
+                       return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function() {
+                               jQuery.data( this, key, value );
+                       });
+               }
+       },
 
-               } catch( e ) {
-                       ret = array;
+       removeData: function( key ) {
+               return this.each(function() {
+                       jQuery.removeData( this, key );
+               });
+       }
+});
+jQuery.extend({
+       queue: function( elem, type, data ) {
+               if ( !elem ) {
+                       return;
                }
 
-               return ret;
-       },
+               type = (type || "fx") + "queue";
+               var q = jQuery.data( elem, type );
 
-       grep: function( elems, callback, inv ) {
-               var ret = [];
+               // Speed up dequeue by getting out quickly if this is just a lookup
+               if ( !data ) {
+                       return q || [];
+               }
 
-               // Go through the array, only saving the items
-               // that pass the validator function
-               for ( var i = 0, length = elems.length; i < length; i++ )
-                       if ( !inv != !callback( elems[ i ], i ) )
-                               ret.push( elems[ i ] );
+               if ( !q || jQuery.isArray(data) ) {
+                       q = jQuery.data( elem, type, jQuery.makeArray(data) );
 
-               return ret;
+               } else {
+                       q.push( data );
+               }
+
+               return q;
        },
 
-       map: function( elems, callback ) {
-               var ret = [];
+       dequeue: function( elem, type ) {
+               type = type || "fx";
 
-               // Go through the array, translating each of the items to their
-               // new value (or values).
-               for ( var i = 0, length = elems.length; i < length; i++ ) {
-                       var value = callback( elems[ i ], i );
+               var queue = jQuery.queue( elem, type ), fn = queue.shift();
 
-                       if ( value != null )
-                               ret[ ret.length ] = value;
+               // If the fx queue is dequeued, always remove the progress sentinel
+               if ( fn === "inprogress" ) {
+                       fn = queue.shift();
                }
 
-               return ret.concat.apply( [], ret );
+               if ( fn ) {
+                       // Add a progress sentinel to prevent the fx queue from being
+                       // automatically dequeued
+                       if ( type === "fx" ) {
+                               queue.unshift("inprogress");
+                       }
+
+                       fn.call(elem, function() {
+                               jQuery.dequeue(elem, type);
+                       });
+               }
        }
 });
 
-// Use of jQuery.browser is deprecated.
-// It's included for backwards compatibility and plugins,
-// although they should work to migrate away.
-
-var userAgent = navigator.userAgent.toLowerCase();
+jQuery.fn.extend({
+       queue: function( type, data ) {
+               if ( typeof type !== "string" ) {
+                       data = type;
+                       type = "fx";
+               }
 
-// Figure out what browser is being used
-jQuery.browser = {
-       version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1],
-       safari: /webkit/.test( userAgent ),
-       opera: /opera/.test( userAgent ),
-       msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
-       mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
-};
+               if ( data === undefined ) {
+                       return jQuery.queue( this[0], type );
+               }
+               return this.each(function( i, elem ) {
+                       var queue = jQuery.queue( this, type, data );
 
-jQuery.each({
-       parent: function(elem){return elem.parentNode;},
-       parents: function(elem){return jQuery.dir(elem,"parentNode");},
-       next: function(elem){return jQuery.nth(elem,2,"nextSibling");},
-       prev: function(elem){return jQuery.nth(elem,2,"previousSibling");},
-       nextAll: function(elem){return jQuery.dir(elem,"nextSibling");},
-       prevAll: function(elem){return jQuery.dir(elem,"previousSibling");},
-       siblings: function(elem){return jQuery.sibling(elem.parentNode.firstChild,elem);},
-       children: function(elem){return jQuery.sibling(elem.firstChild);},
-       contents: function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.makeArray(elem.childNodes);}
-}, function(name, fn){
-       jQuery.fn[ name ] = function( selector ) {
-               var ret = jQuery.map( this, fn );
+                       if ( type === "fx" && queue[0] !== "inprogress" ) {
+                               jQuery.dequeue( this, type );
+                       }
+               });
+       },
+       dequeue: function( type ) {
+               return this.each(function() {
+                       jQuery.dequeue( this, type );
+               });
+       },
 
-               if ( selector && typeof selector == "string" )
-                       ret = jQuery.multiFilter( selector, ret );
+       // Based off of the plugin by Clint Helfers, with permission.
+       // http://blindsignals.com/index.php/2009/07/jquery-delay/
+       delay: function( time, type ) {
+               time = jQuery.fx ? jQuery.fx.speeds[time] || time : time;
+               type = type || "fx";
+
+               return this.queue( type, function() {
+                       var elem = this;
+                       setTimeout(function() {
+                               jQuery.dequeue( elem, type );
+                       }, time );
+               });
+       },
 
-               return this.pushStack( jQuery.unique( ret ), name, selector );
-       };
+       clearQueue: function( type ) {
+               return this.queue( type || "fx", [] );
+       }
 });
+var rclass = /[\n\t]/g,
+       rspace = /\s+/,
+       rreturn = /\r/g,
+       rspecialurl = /href|src|style/,
+       rtype = /(button|input)/i,
+       rfocusable = /(button|input|object|select|textarea)/i,
+       rclickable = /^(a|area)$/i,
+       rradiocheck = /radio|checkbox/;
 
-jQuery.each({
-       appendTo: "append",
-       prependTo: "prepend",
-       insertBefore: "before",
-       insertAfter: "after",
-       replaceAll: "replaceWith"
-}, function(name, original){
-       jQuery.fn[ name ] = function( selector ) {
-               var ret = [], insert = jQuery( selector );
+jQuery.fn.extend({
+       attr: function( name, value ) {
+               return access( this, name, value, true, jQuery.attr );
+       },
 
-               for ( var i = 0, l = insert.length; i < l; i++ ) {
-                       var elems = (i > 0 ? this.clone(true) : this).get();
-                       jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
-                       ret = ret.concat( elems );
+       removeAttr: function( name, fn ) {
+               return this.each(function(){
+                       jQuery.attr( this, name, "" );
+                       if ( this.nodeType === 1 ) {
+                               this.removeAttribute( name );
+                       }
+               });
+       },
+
+       addClass: function( value ) {
+               if ( jQuery.isFunction(value) ) {
+                       return this.each(function(i) {
+                               var self = jQuery(this);
+                               self.addClass( value.call(this, i, self.attr("class")) );
+                       });
                }
 
-               return this.pushStack( ret, name, selector );
-       };
-});
+               if ( value && typeof value === "string" ) {
+                       var classNames = (value || "").split( rspace );
 
-jQuery.each({
-       removeAttr: function( name ) {
-               jQuery.attr( this, name, "" );
-               if (this.nodeType == 1)
-                       this.removeAttribute( name );
-       },
+                       for ( var i = 0, l = this.length; i < l; i++ ) {
+                               var elem = this[i];
 
-       addClass: function( classNames ) {
-               jQuery.className.add( this, classNames );
-       },
+                               if ( elem.nodeType === 1 ) {
+                                       if ( !elem.className ) {
+                                               elem.className = value;
 
-       removeClass: function( classNames ) {
-               jQuery.className.remove( this, classNames );
-       },
+                                       } else {
+                                               var className = " " + elem.className + " ";
+                                               for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
+                                                       if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) {
+                                                               elem.className += " " + classNames[c];
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
 
-       toggleClass: function( classNames, state ) {
-               if( typeof state !== "boolean" )
-                       state = !jQuery.className.has( this, classNames );
-               jQuery.className[ state ? "add" : "remove" ]( this, classNames );
+               return this;
        },
 
-       remove: function( selector ) {
-               if ( !selector || jQuery.filter( selector, [ this ] ).length ) {
-                       // Prevent memory leaks
-                       jQuery( "*", this ).add([this]).each(function(){
-                               jQuery.event.remove(this);
-                               jQuery.removeData(this);
+       removeClass: function( value ) {
+               if ( jQuery.isFunction(value) ) {
+                       return this.each(function(i) {
+                               var self = jQuery(this);
+                               self.removeClass( value.call(this, i, self.attr("class")) );
                        });
-                       if (this.parentNode)
-                               this.parentNode.removeChild( this );
                }
-       },
 
-       empty: function() {
-               // Remove element nodes and prevent memory leaks
-               jQuery(this).children().remove();
+               if ( (value && typeof value === "string") || value === undefined ) {
+                       var classNames = (value || "").split(rspace);
 
-               // Remove any remaining nodes
-               while ( this.firstChild )
-                       this.removeChild( this.firstChild );
-       }
-}, function(name, fn){
-       jQuery.fn[ name ] = function(){
-               return this.each( fn, arguments );
-       };
-});
+                       for ( var i = 0, l = this.length; i < l; i++ ) {
+                               var elem = this[i];
 
-// Helper function used by the dimensions and offset modules
-function num(elem, prop) {
-       return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0;
-}
-var expando = "jQuery" + now(), uuid = 0, windowData = {};\r
-\r
-jQuery.extend({\r
-       cache: {},\r
-\r
-       data: function( elem, name, data ) {\r
-               elem = elem == window ?\r
-                       windowData :\r
-                       elem;\r
-\r
-               var id = elem[ expando ];\r
-\r
-               // Compute a unique ID for the element\r
-               if ( !id )\r
-                       id = elem[ expando ] = ++uuid;\r
-\r
-               // Only generate the data cache if we're\r
-               // trying to access or manipulate it\r
-               if ( name && !jQuery.cache[ id ] )\r
-                       jQuery.cache[ id ] = {};\r
-\r
-               // Prevent overriding the named cache with undefined values\r
-               if ( data !== undefined )\r
-                       jQuery.cache[ id ][ name ] = data;\r
-\r
-               // Return the named cache data, or the ID for the element\r
-               return name ?\r
-                       jQuery.cache[ id ][ name ] :\r
-                       id;\r
-       },\r
-\r
-       removeData: function( elem, name ) {\r
-               elem = elem == window ?\r
-                       windowData :\r
-                       elem;\r
-\r
-               var id = elem[ expando ];\r
-\r
-               // If we want to remove a specific section of the element's data\r
-               if ( name ) {\r
-                       if ( jQuery.cache[ id ] ) {\r
-                               // Remove the section of cache data\r
-                               delete jQuery.cache[ id ][ name ];\r
-\r
-                               // If we've removed all the data, remove the element's cache\r
-                               name = "";\r
-\r
-                               for ( name in jQuery.cache[ id ] )\r
-                                       break;\r
-\r
-                               if ( !name )\r
-                                       jQuery.removeData( elem );\r
-                       }\r
-\r
-               // Otherwise, we want to remove all of the element's data\r
-               } else {\r
-                       // Clean up the element expando\r
-                       try {\r
-                               delete elem[ expando ];\r
-                       } catch(e){\r
-                               // IE has trouble directly removing the expando\r
-                               // but it's ok with using removeAttribute\r
-                               if ( elem.removeAttribute )\r
-                                       elem.removeAttribute( expando );\r
-                       }\r
-\r
-                       // Completely remove the data cache\r
-                       delete jQuery.cache[ id ];\r
-               }\r
-       },\r
-       queue: function( elem, type, data ) {\r
-               if ( elem ){\r
-       \r
-                       type = (type || "fx") + "queue";\r
-       \r
-                       var q = jQuery.data( elem, type );\r
-       \r
-                       if ( !q || jQuery.isArray(data) )\r
-                               q = jQuery.data( elem, type, jQuery.makeArray(data) );\r
-                       else if( data )\r
-                               q.push( data );\r
-       \r
-               }\r
-               return q;\r
-       },\r
-\r
-       dequeue: function( elem, type ){\r
-               var queue = jQuery.queue( elem, type ),\r
-                       fn = queue.shift();\r
-               \r
-               if( !type || type === "fx" )\r
-                       fn = queue[0];\r
-                       \r
-               if( fn !== undefined )\r
-                       fn.call(elem);\r
-       }\r
-});\r
-\r
-jQuery.fn.extend({\r
-       data: function( key, value ){\r
-               var parts = key.split(".");\r
-               parts[1] = parts[1] ? "." + parts[1] : "";\r
-\r
-               if ( value === undefined ) {\r
-                       var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);\r
-\r
-                       if ( data === undefined && this.length )\r
-                               data = jQuery.data( this[0], key );\r
-\r
-                       return data === undefined && parts[1] ?\r
-                               this.data( parts[0] ) :\r
-                               data;\r
-               } else\r
-                       return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){\r
-                               jQuery.data( this, key, value );\r
-                       });\r
-       },\r
-\r
-       removeData: function( key ){\r
-               return this.each(function(){\r
-                       jQuery.removeData( this, key );\r
-               });\r
-       },\r
-       queue: function(type, data){\r
-               if ( typeof type !== "string" ) {\r
-                       data = type;\r
-                       type = "fx";\r
-               }\r
-\r
-               if ( data === undefined )\r
-                       return jQuery.queue( this[0], type );\r
-\r
-               return this.each(function(){\r
-                       var queue = jQuery.queue( this, type, data );\r
-                       \r
-                        if( type == "fx" && queue.length == 1 )\r
-                               queue[0].call(this);\r
-               });\r
-       },\r
-       dequeue: function(type){\r
-               return this.each(function(){\r
-                       jQuery.dequeue( this, type );\r
-               });\r
-       }\r
-});/*!
- * Sizzle CSS Selector Engine - v0.9.3
- *  Copyright 2009, The Dojo Foundation
- *  Released under the MIT, BSD, and GPL Licenses.
- *  More information: http://sizzlejs.com/
- */
-(function(){
+                               if ( elem.nodeType === 1 && elem.className ) {
+                                       if ( value ) {
+                                               var className = (" " + elem.className + " ").replace(rclass, " ");
+                                               for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
+                                                       className = className.replace(" " + classNames[c] + " ", " ");
+                                               }
+                                               elem.className = className.substring(1, className.length - 1);
 
-var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,
-       done = 0,
-       toString = Object.prototype.toString;
+                                       } else {
+                                               elem.className = "";
+                                       }
+                               }
+                       }
+               }
 
-var Sizzle = function(selector, context, results, seed) {
-       results = results || [];
-       context = context || document;
+               return this;
+       },
 
-       if ( context.nodeType !== 1 && context.nodeType !== 9 )
-               return [];
-       
-       if ( !selector || typeof selector !== "string" ) {
-               return results;
-       }
+       toggleClass: function( value, stateVal ) {
+               var type = typeof value, isBool = typeof stateVal === "boolean";
 
-       var parts = [], m, set, checkSet, check, mode, extra, prune = true;
-       
-       // Reset the position of the chunker regexp (start from head)
-       chunker.lastIndex = 0;
-       
-       while ( (m = chunker.exec(selector)) !== null ) {
-               parts.push( m[1] );
-               
-               if ( m[2] ) {
-                       extra = RegExp.rightContext;
-                       break;
+               if ( jQuery.isFunction( value ) ) {
+                       return this.each(function(i) {
+                               var self = jQuery(this);
+                               self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal );
+                       });
                }
-       }
 
-       if ( parts.length > 1 && origPOS.exec( selector ) ) {
-               if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
-                       set = posProcess( parts[0] + parts[1], context );
-               } else {
-                       set = Expr.relative[ parts[0] ] ?
-                               [ context ] :
-                               Sizzle( parts.shift(), context );
+               return this.each(function() {
+                       if ( type === "string" ) {
+                               // toggle individual class names
+                               var className, i = 0, self = jQuery(this),
+                                       state = stateVal,
+                                       classNames = value.split( rspace );
+
+                               while ( (className = classNames[ i++ ]) ) {
+                                       // check each className given, space seperated list
+                                       state = isBool ? state : !self.hasClass( className );
+                                       self[ state ? "addClass" : "removeClass" ]( className );
+                               }
 
-                       while ( parts.length ) {
-                               selector = parts.shift();
+                       } else if ( type === "undefined" || type === "boolean" ) {
+                               if ( this.className ) {
+                                       // store className if set
+                                       jQuery.data( this, "__className__", this.className );
+                               }
 
-                               if ( Expr.relative[ selector ] )
-                                       selector += parts.shift();
+                               // toggle whole className
+                               this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || "";
+                       }
+               });
+       },
 
-                               set = posProcess( selector, set );
+       hasClass: function( selector ) {
+               var className = " " + selector + " ";
+               for ( var i = 0, l = this.length; i < l; i++ ) {
+                       if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {
+                               return true;
                        }
                }
-       } else {
-               var ret = seed ?
-                       { expr: parts.pop(), set: makeArray(seed) } :
-                       Sizzle.find( parts.pop(), parts.length === 1 && context.parentNode ? context.parentNode : context, isXML(context) );
-               set = Sizzle.filter( ret.expr, ret.set );
 
-               if ( parts.length > 0 ) {
-                       checkSet = makeArray(set);
-               } else {
-                       prune = false;
-               }
+               return false;
+       },
 
-               while ( parts.length ) {
-                       var cur = parts.pop(), pop = cur;
+       val: function( value ) {
+               if ( value === undefined ) {
+                       var elem = this[0];
 
-                       if ( !Expr.relative[ cur ] ) {
-                               cur = "";
-                       } else {
-                               pop = parts.pop();
-                       }
+                       if ( elem ) {
+                               if ( jQuery.nodeName( elem, "option" ) ) {
+                                       return (elem.attributes.value || {}).specified ? elem.value : elem.text;
+                               }
 
-                       if ( pop == null ) {
-                               pop = context;
-                       }
+                               // We need to handle select boxes special
+                               if ( jQuery.nodeName( elem, "select" ) ) {
+                                       var index = elem.selectedIndex,
+                                               values = [],
+                                               options = elem.options,
+                                               one = elem.type === "select-one";
 
-                       Expr.relative[ cur ]( checkSet, pop, isXML(context) );
-               }
-       }
+                                       // Nothing was selected
+                                       if ( index < 0 ) {
+                                               return null;
+                                       }
 
-       if ( !checkSet ) {
-               checkSet = set;
-       }
+                                       // Loop through all the selected options
+                                       for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
+                                               var option = options[ i ];
 
-       if ( !checkSet ) {
-               throw "Syntax error, unrecognized expression: " + (cur || selector);
-       }
+                                               if ( option.selected ) {
+                                                       // Get the specifc value for the option
+                                                       value = jQuery(option).val();
 
-       if ( toString.call(checkSet) === "[object Array]" ) {
-               if ( !prune ) {
-                       results.push.apply( results, checkSet );
-               } else if ( context.nodeType === 1 ) {
-                       for ( var i = 0; checkSet[i] != null; i++ ) {
-                               if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) {
-                                       results.push( set[i] );
+                                                       // We don't need an array for one selects
+                                                       if ( one ) {
+                                                               return value;
+                                                       }
+
+                                                       // Multi-Selects return an array
+                                                       values.push( value );
+                                               }
+                                       }
+
+                                       return values;
                                }
-                       }
-               } else {
-                       for ( var i = 0; checkSet[i] != null; i++ ) {
-                               if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
-                                       results.push( set[i] );
+
+                               // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
+                               if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) {
+                                       return elem.getAttribute("value") === null ? "on" : elem.value;
                                }
+                               
+
+                               // Everything else, we just grab the value
+                               return (elem.value || "").replace(rreturn, "");
+
                        }
+
+                       return undefined;
                }
-       } else {
-               makeArray( checkSet, results );
-       }
 
-       if ( extra ) {
-               Sizzle( extra, context, results, seed );
+               var isFunction = jQuery.isFunction(value);
 
-               if ( sortOrder ) {
-                       hasDuplicate = false;
-                       results.sort(sortOrder);
+               return this.each(function(i) {
+                       var self = jQuery(this), val = value;
 
-                       if ( hasDuplicate ) {
-                               for ( var i = 1; i < results.length; i++ ) {
-                                       if ( results[i] === results[i-1] ) {
-                                               results.splice(i--, 1);
-                                       }
-                               }
+                       if ( this.nodeType !== 1 ) {
+                               return;
                        }
-               }
-       }
 
-       return results;
-};
+                       if ( isFunction ) {
+                               val = value.call(this, i, self.val());
+                       }
 
-Sizzle.matches = function(expr, set){
-       return Sizzle(expr, null, null, set);
-};
+                       // Typecast each time if the value is a Function and the appended
+                       // value is therefore different each time.
+                       if ( typeof val === "number" ) {
+                               val += "";
+                       }
 
-Sizzle.find = function(expr, context, isXML){
-       var set, match;
+                       if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) {
+                               this.checked = jQuery.inArray( self.val(), val ) >= 0;
 
-       if ( !expr ) {
-               return [];
-       }
+                       } else if ( jQuery.nodeName( this, "select" ) ) {
+                               var values = jQuery.makeArray(val);
 
-       for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
-               var type = Expr.order[i], match;
-               
-               if ( (match = Expr.match[ type ].exec( expr )) ) {
-                       var left = RegExp.leftContext;
+                               jQuery( "option", this ).each(function() {
+                                       this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
+                               });
 
-                       if ( left.substr( left.length - 1 ) !== "\\" ) {
-                               match[1] = (match[1] || "").replace(/\\/g, "");
-                               set = Expr.find[ type ]( match, context, isXML );
-                               if ( set != null ) {
-                                       expr = expr.replace( Expr.match[ type ], "" );
-                                       break;
+                               if ( !values.length ) {
+                                       this.selectedIndex = -1;
                                }
+
+                       } else {
+                               this.value = val;
                        }
-               }
+               });
        }
+});
 
-       if ( !set ) {
-               set = context.getElementsByTagName("*");
-       }
+jQuery.extend({
+       attrFn: {
+               val: true,
+               css: true,
+               html: true,
+               text: true,
+               data: true,
+               width: true,
+               height: true,
+               offset: true
+       },
+               
+       attr: function( elem, name, value, pass ) {
+               // don't set attributes on text and comment nodes
+               if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
+                       return undefined;
+               }
 
-       return {set: set, expr: expr};
-};
+               if ( pass && name in jQuery.attrFn ) {
+                       return jQuery(elem)[name](value);
+               }
 
-Sizzle.filter = function(expr, set, inplace, not){
-       var old = expr, result = [], curLoop = set, match, anyFound,
-               isXMLFilter = set && set[0] && isXML(set[0]);
+               var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ),
+                       // Whether we are setting (or getting)
+                       set = value !== undefined;
 
-       while ( expr && set.length ) {
-               for ( var type in Expr.filter ) {
-                       if ( (match = Expr.match[ type ].exec( expr )) != null ) {
-                               var filter = Expr.filter[ type ], found, item;
-                               anyFound = false;
+               // Try to normalize/fix the name
+               name = notxml && jQuery.props[ name ] || name;
 
-                               if ( curLoop == result ) {
-                                       result = [];
-                               }
+               // Only do all the following if this is a node (faster for style)
+               if ( elem.nodeType === 1 ) {
+                       // These attributes require special treatment
+                       var special = rspecialurl.test( name );
 
-                               if ( Expr.preFilter[ type ] ) {
-                                       match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
+                       // Safari mis-reports the default selected property of an option
+                       // Accessing the parent's selectedIndex property fixes it
+                       if ( name === "selected" && !jQuery.support.optSelected ) {
+                               var parent = elem.parentNode;
+                               if ( parent ) {
+                                       parent.selectedIndex;
+       
+                                       // Make sure that it also works with optgroups, see #5701
+                                       if ( parent.parentNode ) {
+                                               parent.parentNode.selectedIndex;
+                                       }
+                               }
+                       }
 
-                                       if ( !match ) {
-                                               anyFound = found = true;
-                                       } else if ( match === true ) {
-                                               continue;
+                       // If applicable, access the attribute via the DOM 0 way
+                       if ( name in elem && notxml && !special ) {
+                               if ( set ) {
+                                       // We can't allow the type property to be changed (since it causes problems in IE)
+                                       if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) {
+                                               jQuery.error( "type property can't be changed" );
                                        }
+
+                                       elem[ name ] = value;
                                }
 
-                               if ( match ) {
-                                       for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
-                                               if ( item ) {
-                                                       found = filter( item, match, i, curLoop );
-                                                       var pass = not ^ !!found;
+                               // browsers index elements by id/name on forms, give priority to attributes.
+                               if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) {
+                                       return elem.getAttributeNode( name ).nodeValue;
+                               }
 
-                                                       if ( inplace && found != null ) {
-                                                               if ( pass ) {
-                                                                       anyFound = true;
+                               // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
+                               // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+                               if ( name === "tabIndex" ) {
+                                       var attributeNode = elem.getAttributeNode( "tabIndex" );
+
+                                       return attributeNode && attributeNode.specified ?
+                                               attributeNode.value :
+                                               rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
+                                                       0 :
+                                                       undefined;
+                               }
+
+                               return elem[ name ];
+                       }
+
+                       if ( !jQuery.support.style && notxml && name === "style" ) {
+                               if ( set ) {
+                                       elem.style.cssText = "" + value;
+                               }
+
+                               return elem.style.cssText;
+                       }
+
+                       if ( set ) {
+                               // convert the value to a string (all browsers do this but IE) see #1070
+                               elem.setAttribute( name, "" + value );
+                       }
+
+                       var attr = !jQuery.support.hrefNormalized && notxml && special ?
+                                       // Some attributes require a special call on IE
+                                       elem.getAttribute( name, 2 ) :
+                                       elem.getAttribute( name );
+
+                       // Non-existent attributes return null, we normalize to undefined
+                       return attr === null ? undefined : attr;
+               }
+
+               // elem is actually elem.style ... set the style
+               // Using attr for specific style information is now deprecated. Use style insead.
+               return jQuery.style( elem, name, value );
+       }
+});
+var fcleanup = function( nm ) {
+       return nm.replace(/[^\w\s\.\|`]/g, function( ch ) {
+               return "\\" + ch;
+       });
+};
+
+/*
+ * A number of helper functions used for managing events.
+ * Many of the ideas behind this code originated from
+ * Dean Edwards' addEvent library.
+ */
+jQuery.event = {
+
+       // Bind an event to an element
+       // Original by Dean Edwards
+       add: function( elem, types, handler, data ) {
+               if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+                       return;
+               }
+
+               // For whatever reason, IE has trouble passing the window object
+               // around, causing it to be cloned in the process
+               if ( elem.setInterval && ( elem !== window && !elem.frameElement ) ) {
+                       elem = window;
+               }
+
+               // Make sure that the function being executed has a unique ID
+               if ( !handler.guid ) {
+                       handler.guid = jQuery.guid++;
+               }
+
+               // if data is passed, bind to handler
+               if ( data !== undefined ) {
+                       // Create temporary function pointer to original handler
+                       var fn = handler;
+
+                       // Create unique handler function, wrapped around original handler
+                       handler = jQuery.proxy( fn );
+
+                       // Store data in unique handler
+                       handler.data = data;
+               }
+
+               // Init the element's event structure
+               var events = jQuery.data( elem, "events" ) || jQuery.data( elem, "events", {} ),
+                       handle = jQuery.data( elem, "handle" ), eventHandle;
+
+               if ( !handle ) {
+                       eventHandle = function() {
+                               // Handle the second event of a trigger and when
+                               // an event is called after a page has unloaded
+                               return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
+                                       jQuery.event.handle.apply( eventHandle.elem, arguments ) :
+                                       undefined;
+                       };
+
+                       handle = jQuery.data( elem, "handle", eventHandle );
+               }
+
+               // If no handle is found then we must be trying to bind to one of the
+               // banned noData elements
+               if ( !handle ) {
+                       return;
+               }
+
+               // Add elem as a property of the handle function
+               // This is to prevent a memory leak with non-native
+               // event in IE.
+               handle.elem = elem;
+
+               // Handle multiple events separated by a space
+               // jQuery(...).bind("mouseover mouseout", fn);
+               types = types.split( /\s+/ );
+
+               var type, i = 0;
+
+               while ( (type = types[ i++ ]) ) {
+                       // Namespaced event handlers
+                       var namespaces = type.split(".");
+                       type = namespaces.shift();
+
+                       if ( i > 1 ) {
+                               handler = jQuery.proxy( handler );
+
+                               if ( data !== undefined ) {
+                                       handler.data = data;
+                               }
+                       }
+
+                       handler.type = namespaces.slice(0).sort().join(".");
+
+                       // Get the current list of functions bound to this event
+                       var handlers = events[ type ],
+                               special = this.special[ type ] || {};
+
+                       // Init the event handler queue
+                       if ( !handlers ) {
+                               handlers = events[ type ] = {};
+
+                               // Check for a special event handler
+                               // Only use addEventListener/attachEvent if the special
+                               // events handler returns false
+                               if ( !special.setup || special.setup.call( elem, data, namespaces, handler) === false ) {
+                                       // Bind the global event handler to the element
+                                       if ( elem.addEventListener ) {
+                                               elem.addEventListener( type, handle, false );
+                                       } else if ( elem.attachEvent ) {
+                                               elem.attachEvent( "on" + type, handle );
+                                       }
+                               }
+                       }
+                       
+                       if ( special.add ) { 
+                               var modifiedHandler = special.add.call( elem, handler, data, namespaces, handlers ); 
+                               if ( modifiedHandler && jQuery.isFunction( modifiedHandler ) ) { 
+                                       modifiedHandler.guid = modifiedHandler.guid || handler.guid; 
+                                       modifiedHandler.data = modifiedHandler.data || handler.data; 
+                                       modifiedHandler.type = modifiedHandler.type || handler.type; 
+                                       handler = modifiedHandler; 
+                               } 
+                       } 
+                       
+                       // Add the function to the element's handler list
+                       handlers[ handler.guid ] = handler;
+
+                       // Keep track of which events have been used, for global triggering
+                       this.global[ type ] = true;
+               }
+
+               // Nullify elem to prevent memory leaks in IE
+               elem = null;
+       },
+
+       global: {},
+
+       // Detach an event or set of events from an element
+       remove: function( elem, types, handler ) {
+               // don't do events on text and comment nodes
+               if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+                       return;
+               }
+
+               var events = jQuery.data( elem, "events" ), ret, type, fn;
+
+               if ( events ) {
+                       // Unbind all events for the element
+                       if ( types === undefined || (typeof types === "string" && types.charAt(0) === ".") ) {
+                               for ( type in events ) {
+                                       this.remove( elem, type + (types || "") );
+                               }
+                       } else {
+                               // types is actually an event object here
+                               if ( types.type ) {
+                                       handler = types.handler;
+                                       types = types.type;
+                               }
+
+                               // Handle multiple events separated by a space
+                               // jQuery(...).unbind("mouseover mouseout", fn);
+                               types = types.split(/\s+/);
+                               var i = 0;
+                               while ( (type = types[ i++ ]) ) {
+                                       // Namespaced event handlers
+                                       var namespaces = type.split(".");
+                                       type = namespaces.shift();
+                                       var all = !namespaces.length,
+                                               cleaned = jQuery.map( namespaces.slice(0).sort(), fcleanup ),
+                                               namespace = new RegExp("(^|\\.)" + cleaned.join("\\.(?:.*\\.)?") + "(\\.|$)"),
+                                               special = this.special[ type ] || {};
+
+                                       if ( events[ type ] ) {
+                                               // remove the given handler for the given type
+                                               if ( handler ) {
+                                                       fn = events[ type ][ handler.guid ];
+                                                       delete events[ type ][ handler.guid ];
+
+                                               // remove all handlers for the given type
+                                               } else {
+                                                       for ( var handle in events[ type ] ) {
+                                                               // Handle the removal of namespaced events
+                                                               if ( all || namespace.test( events[ type ][ handle ].type ) ) {
+                                                                       delete events[ type ][ handle ];
+                                                               }
+                                                       }
+                                               }
+
+                                               if ( special.remove ) {
+                                                       special.remove.call( elem, namespaces, fn);
+                                               }
+
+                                               // remove generic event handler if no more handlers exist
+                                               for ( ret in events[ type ] ) {
+                                                       break;
+                                               }
+                                               if ( !ret ) {
+                                                       if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
+                                                               if ( elem.removeEventListener ) {
+                                                                       elem.removeEventListener( type, jQuery.data( elem, "handle" ), false );
+                                                               } else if ( elem.detachEvent ) {
+                                                                       elem.detachEvent( "on" + type, jQuery.data( elem, "handle" ) );
+                                                               }
+                                                       }
+                                                       ret = null;
+                                                       delete events[ type ];
+                                               }
+                                       }
+                               }
+                       }
+
+                       // Remove the expando if it's no longer used
+                       for ( ret in events ) {
+                               break;
+                       }
+                       if ( !ret ) {
+                               var handle = jQuery.data( elem, "handle" );
+                               if ( handle ) {
+                                       handle.elem = null;
+                               }
+                               jQuery.removeData( elem, "events" );
+                               jQuery.removeData( elem, "handle" );
+                       }
+               }
+       },
+
+       // bubbling is internal
+       trigger: function( event, data, elem /*, bubbling */ ) {
+               // Event object or event type
+               var type = event.type || event,
+                       bubbling = arguments[3];
+
+               if ( !bubbling ) {
+                       event = typeof event === "object" ?
+                               // jQuery.Event object
+                               event[expando] ? event :
+                               // Object literal
+                               jQuery.extend( jQuery.Event(type), event ) :
+                               // Just the event type (string)
+                               jQuery.Event(type);
+
+                       if ( type.indexOf("!") >= 0 ) {
+                               event.type = type = type.slice(0, -1);
+                               event.exclusive = true;
+                       }
+
+                       // Handle a global trigger
+                       if ( !elem ) {
+                               // Don't bubble custom events when global (to avoid too much overhead)
+                               event.stopPropagation();
+
+                               // Only trigger if we've ever bound an event for it
+                               if ( this.global[ type ] ) {
+                                       jQuery.each( jQuery.cache, function() {
+                                               if ( this.events && this.events[type] ) {
+                                                       jQuery.event.trigger( event, data, this.handle.elem );
+                                               }
+                                       });
+                               }
+                       }
+
+                       // Handle triggering a single element
+
+                       // don't do events on text and comment nodes
+                       if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
+                               return undefined;
+                       }
+
+                       // Clean up in case it is reused
+                       event.result = undefined;
+                       event.target = elem;
+
+                       // Clone the incoming data, if any
+                       data = jQuery.makeArray( data );
+                       data.unshift( event );
+               }
+
+               event.currentTarget = elem;
+
+               // Trigger the event, it is assumed that "handle" is a function
+               var handle = jQuery.data( elem, "handle" );
+               if ( handle ) {
+                       handle.apply( elem, data );
+               }
+
+               var parent = elem.parentNode || elem.ownerDocument;
+
+               // Trigger an inline bound script
+               try {
+                       if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) {
+                               if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) {
+                                       event.result = false;
+                               }
+                       }
+
+               // prevent IE from throwing an error for some elements with some event types, see #3533
+               } catch (e) {}
+
+               if ( !event.isPropagationStopped() && parent ) {
+                       jQuery.event.trigger( event, data, parent, true );
+
+               } else if ( !event.isDefaultPrevented() ) {
+                       var target = event.target, old,
+                               isClick = jQuery.nodeName(target, "a") && type === "click";
+
+                       if ( !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) {
+                               try {
+                                       if ( target[ type ] ) {
+                                               // Make sure that we don't accidentally re-trigger the onFOO events
+                                               old = target[ "on" + type ];
+
+                                               if ( old ) {
+                                                       target[ "on" + type ] = null;
+                                               }
+
+                                               this.triggered = true;
+                                               target[ type ]();
+                                       }
+
+                               // prevent IE from throwing an error for some elements with some event types, see #3533
+                               } catch (e) {}
+
+                               if ( old ) {
+                                       target[ "on" + type ] = old;
+                               }
+
+                               this.triggered = false;
+                       }
+               }
+       },
+
+       handle: function( event ) {
+               // returned undefined or false
+               var all, handlers;
+
+               event = arguments[0] = jQuery.event.fix( event || window.event );
+               event.currentTarget = this;
+
+               // Namespaced event handlers
+               var namespaces = event.type.split(".");
+               event.type = namespaces.shift();
+
+               // Cache this now, all = true means, any handler
+               all = !namespaces.length && !event.exclusive;
+
+               var namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)");
+
+               handlers = ( jQuery.data(this, "events") || {} )[ event.type ];
+
+               for ( var j in handlers ) {
+                       var handler = handlers[ j ];
+
+                       // Filter the functions by class
+                       if ( all || namespace.test(handler.type) ) {
+                               // Pass in a reference to the handler function itself
+                               // So that we can later remove it
+                               event.handler = handler;
+                               event.data = handler.data;
+
+                               var ret = handler.apply( this, arguments );
+
+                               if ( ret !== undefined ) {
+                                       event.result = ret;
+                                       if ( ret === false ) {
+                                               event.preventDefault();
+                                               event.stopPropagation();
+                                       }
+                               }
+
+                               if ( event.isImmediatePropagationStopped() ) {
+                                       break;
+                               }
+
+                       }
+               }
+
+               return event.result;
+       },
+
+       props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
+
+       fix: function( event ) {
+               if ( event[ expando ] ) {
+                       return event;
+               }
+
+               // store a copy of the original event object
+               // and "clone" to set read-only properties
+               var originalEvent = event;
+               event = jQuery.Event( originalEvent );
+
+               for ( var i = this.props.length, prop; i; ) {
+                       prop = this.props[ --i ];
+                       event[ prop ] = originalEvent[ prop ];
+               }
+
+               // Fix target property, if necessary
+               if ( !event.target ) {
+                       event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
+               }
+
+               // check if target is a textnode (safari)
+               if ( event.target.nodeType === 3 ) {
+                       event.target = event.target.parentNode;
+               }
+
+               // Add relatedTarget, if necessary
+               if ( !event.relatedTarget && event.fromElement ) {
+                       event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
+               }
+
+               // Calculate pageX/Y if missing and clientX/Y available
+               if ( event.pageX == null && event.clientX != null ) {
+                       var doc = document.documentElement, body = document.body;
+                       event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
+                       event.pageY = event.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
+               }
+
+               // Add which for key events
+               if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) ) {
+                       event.which = event.charCode || event.keyCode;
+               }
+
+               // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
+               if ( !event.metaKey && event.ctrlKey ) {
+                       event.metaKey = event.ctrlKey;
+               }
+
+               // Add which for click: 1 === left; 2 === middle; 3 === right
+               // Note: button is not normalized, so don't use it
+               if ( !event.which && event.button !== undefined ) {
+                       event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
+               }
+
+               return event;
+       },
+
+       // Deprecated, use jQuery.guid instead
+       guid: 1E8,
+
+       // Deprecated, use jQuery.proxy instead
+       proxy: jQuery.proxy,
+
+       special: {
+               ready: {
+                       // Make sure the ready event is setup
+                       setup: jQuery.bindReady,
+                       teardown: jQuery.noop
+               },
+
+               live: {
+                       add: function( proxy, data, namespaces, live ) {
+                               jQuery.extend( proxy, data || {} );
+
+                               proxy.guid += data.selector + data.live; 
+                               data.liveProxy = proxy;
+
+                               jQuery.event.add( this, data.live, liveHandler, data ); 
+                               
+                       },
+
+                       remove: function( namespaces ) {
+                               if ( namespaces.length ) {
+                                       var remove = 0, name = new RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)");
+
+                                       jQuery.each( (jQuery.data(this, "events").live || {}), function() {
+                                               if ( name.test(this.type) ) {
+                                                       remove++;
+                                               }
+                                       });
+
+                                       if ( remove < 1 ) {
+                                               jQuery.event.remove( this, namespaces[0], liveHandler );
+                                       }
+                               }
+                       },
+                       special: {}
+               },
+               beforeunload: {
+                       setup: function( data, namespaces, fn ) {
+                               // We only want to do this special case on windows
+                               if ( this.setInterval ) {
+                                       this.onbeforeunload = fn;
+                               }
+
+                               return false;
+                       },
+                       teardown: function( namespaces, fn ) {
+                               if ( this.onbeforeunload === fn ) {
+                                       this.onbeforeunload = null;
+                               }
+                       }
+               }
+       }
+};
+
+jQuery.Event = function( src ) {
+       // Allow instantiation without the 'new' keyword
+       if ( !this.preventDefault ) {
+               return new jQuery.Event( src );
+       }
+
+       // Event object
+       if ( src && src.type ) {
+               this.originalEvent = src;
+               this.type = src.type;
+       // Event type
+       } else {
+               this.type = src;
+       }
+
+       // timeStamp is buggy for some events on Firefox(#3843)
+       // So we won't rely on the native value
+       this.timeStamp = now();
+
+       // Mark it as fixed
+       this[ expando ] = true;
+};
+
+function returnFalse() {
+       return false;
+}
+function returnTrue() {
+       return true;
+}
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+       preventDefault: function() {
+               this.isDefaultPrevented = returnTrue;
+
+               var e = this.originalEvent;
+               if ( !e ) {
+                       return;
+               }
+               
+               // if preventDefault exists run it on the original event
+               if ( e.preventDefault ) {
+                       e.preventDefault();
+               }
+               // otherwise set the returnValue property of the original event to false (IE)
+               e.returnValue = false;
+       },
+       stopPropagation: function() {
+               this.isPropagationStopped = returnTrue;
+
+               var e = this.originalEvent;
+               if ( !e ) {
+                       return;
+               }
+               // if stopPropagation exists run it on the original event
+               if ( e.stopPropagation ) {
+                       e.stopPropagation();
+               }
+               // otherwise set the cancelBubble property of the original event to true (IE)
+               e.cancelBubble = true;
+       },
+       stopImmediatePropagation: function() {
+               this.isImmediatePropagationStopped = returnTrue;
+               this.stopPropagation();
+       },
+       isDefaultPrevented: returnFalse,
+       isPropagationStopped: returnFalse,
+       isImmediatePropagationStopped: returnFalse
+};
+
+// Checks if an event happened on an element within another element
+// Used in jQuery.event.special.mouseenter and mouseleave handlers
+var withinElement = function( event ) {
+       // Check if mouse(over|out) are still within the same parent element
+       var parent = event.relatedTarget;
+
+       // Traverse up the tree
+       while ( parent && parent !== this ) {
+               // Firefox sometimes assigns relatedTarget a XUL element
+               // which we cannot access the parentNode property of
+               try {
+                       parent = parent.parentNode;
+
+               // assuming we've left the element since we most likely mousedover a xul element
+               } catch(e) {
+                       break;
+               }
+       }
+
+       if ( parent !== this ) {
+               // set the correct event type
+               event.type = event.data;
+
+               // handle event if we actually just moused on to a non sub-element
+               jQuery.event.handle.apply( this, arguments );
+       }
+
+},
+
+// In case of event delegation, we only need to rename the event.type,
+// liveHandler will take care of the rest.
+delegate = function( event ) {
+       event.type = event.data;
+       jQuery.event.handle.apply( this, arguments );
+};
+
+// Create mouseenter and mouseleave events
+jQuery.each({
+       mouseenter: "mouseover",
+       mouseleave: "mouseout"
+}, function( orig, fix ) {
+       jQuery.event.special[ orig ] = {
+               setup: function( data ) {
+                       jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig );
+               },
+               teardown: function( data ) {
+                       jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement );
+               }
+       };
+});
+
+// submit delegation
+if ( !jQuery.support.submitBubbles ) {
+
+jQuery.event.special.submit = {
+       setup: function( data, namespaces, fn ) {
+               if ( this.nodeName.toLowerCase() !== "form" ) {
+                       jQuery.event.add(this, "click.specialSubmit." + fn.guid, function( e ) {
+                               var elem = e.target, type = elem.type;
+
+                               if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) {
+                                       return trigger( "submit", this, arguments );
+                               }
+                       });
+        
+                       jQuery.event.add(this, "keypress.specialSubmit." + fn.guid, function( e ) {
+                               var elem = e.target, type = elem.type;
+
+                               if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) {
+                                       return trigger( "submit", this, arguments );
+                               }
+                       });
+
+               } else {
+                       return false;
+               }
+       },
+
+       remove: function( namespaces, fn ) {
+               jQuery.event.remove( this, "click.specialSubmit" + (fn ? "."+fn.guid : "") );
+               jQuery.event.remove( this, "keypress.specialSubmit" + (fn ? "."+fn.guid : "") );
+       }
+};
+
+}
+
+// change delegation, happens here so we have bind.
+if ( !jQuery.support.changeBubbles ) {
+
+var formElems = /textarea|input|select/i;
+
+function getVal( elem ) {
+       var type = elem.type, val = elem.value;
+
+       if ( type === "radio" || type === "checkbox" ) {
+               val = elem.checked;
+
+       } else if ( type === "select-multiple" ) {
+               val = elem.selectedIndex > -1 ?
+                       jQuery.map( elem.options, function( elem ) {
+                               return elem.selected;
+                       }).join("-") :
+                       "";
+
+       } else if ( elem.nodeName.toLowerCase() === "select" ) {
+               val = elem.selectedIndex;
+       }
+
+       return val;
+}
+
+function testChange( e ) {
+               var elem = e.target, data, val;
+
+               if ( !formElems.test( elem.nodeName ) || elem.readOnly ) {
+                       return;
+               }
+
+               data = jQuery.data( elem, "_change_data" );
+               val = getVal(elem);
+
+               // the current data will be also retrieved by beforeactivate
+               if ( e.type !== "focusout" || elem.type !== "radio" ) {
+                       jQuery.data( elem, "_change_data", val );
+               }
+               
+               if ( data === undefined || val === data ) {
+                       return;
+               }
+
+               if ( data != null || val ) {
+                       e.type = "change";
+                       return jQuery.event.trigger( e, arguments[1], elem );
+               }
+}
+
+jQuery.event.special.change = {
+       filters: {
+               focusout: testChange, 
+
+               click: function( e ) {
+                       var elem = e.target, type = elem.type;
+
+                       if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) {
+                               return testChange.call( this, e );
+                       }
+               },
+
+               // Change has to be called before submit
+               // Keydown will be called before keypress, which is used in submit-event delegation
+               keydown: function( e ) {
+                       var elem = e.target, type = elem.type;
+
+                       if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") ||
+                               (e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
+                               type === "select-multiple" ) {
+                               return testChange.call( this, e );
+                       }
+               },
+
+               // Beforeactivate happens also before the previous element is blurred
+               // with this event you can't trigger a change event, but you can store
+               // information/focus[in] is not needed anymore
+               beforeactivate: function( e ) {
+                       var elem = e.target;
+
+                       if ( elem.nodeName.toLowerCase() === "input" && elem.type === "radio" ) {
+                               jQuery.data( elem, "_change_data", getVal(elem) );
+                       }
+               }
+       },
+       setup: function( data, namespaces, fn ) {
+               for ( var type in changeFilters ) {
+                       jQuery.event.add( this, type + ".specialChange." + fn.guid, changeFilters[type] );
+               }
+
+               return formElems.test( this.nodeName );
+       },
+       remove: function( namespaces, fn ) {
+               for ( var type in changeFilters ) {
+                       jQuery.event.remove( this, type + ".specialChange" + (fn ? "."+fn.guid : ""), changeFilters[type] );
+               }
+
+               return formElems.test( this.nodeName );
+       }
+};
+
+var changeFilters = jQuery.event.special.change.filters;
+
+}
+
+function trigger( type, elem, args ) {
+       args[0].type = type;
+       return jQuery.event.handle.apply( elem, args );
+}
+
+// Create "bubbling" focus and blur events
+if ( document.addEventListener ) {
+       jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+               jQuery.event.special[ fix ] = {
+                       setup: function() {
+                               this.addEventListener( orig, handler, true );
+                       }, 
+                       teardown: function() { 
+                               this.removeEventListener( orig, handler, true );
+                       }
+               };
+
+               function handler( e ) { 
+                       e = jQuery.event.fix( e );
+                       e.type = fix;
+                       return jQuery.event.handle.call( this, e );
+               }
+       });
+}
+
+jQuery.each(["bind", "one"], function( i, name ) {
+       jQuery.fn[ name ] = function( type, data, fn ) {
+               // Handle object literals
+               if ( typeof type === "object" ) {
+                       for ( var key in type ) {
+                               this[ name ](key, data, type[key], fn);
+                       }
+                       return this;
+               }
+               
+               if ( jQuery.isFunction( data ) ) {
+                       fn = data;
+                       data = undefined;
+               }
+
+               var handler = name === "one" ? jQuery.proxy( fn, function( event ) {
+                       jQuery( this ).unbind( event, handler );
+                       return fn.apply( this, arguments );
+               }) : fn;
+
+               return type === "unload" && name !== "one" ?
+                       this.one( type, data, fn ) :
+                       this.each(function() {
+                               jQuery.event.add( this, type, handler, data );
+                       });
+       };
+});
+
+jQuery.fn.extend({
+       unbind: function( type, fn ) {
+               // Handle object literals
+               if ( typeof type === "object" && !type.preventDefault ) {
+                       for ( var key in type ) {
+                               this.unbind(key, type[key]);
+                       }
+                       return this;
+               }
+
+               return this.each(function() {
+                       jQuery.event.remove( this, type, fn );
+               });
+       },
+       trigger: function( type, data ) {
+               return this.each(function() {
+                       jQuery.event.trigger( type, data, this );
+               });
+       },
+
+       triggerHandler: function( type, data ) {
+               if ( this[0] ) {
+                       var event = jQuery.Event( type );
+                       event.preventDefault();
+                       event.stopPropagation();
+                       jQuery.event.trigger( event, data, this[0] );
+                       return event.result;
+               }
+       },
+
+       toggle: function( fn ) {
+               // Save reference to arguments for access in closure
+               var args = arguments, i = 1;
+
+               // link all the functions, so any of them can unbind this click handler
+               while ( i < args.length ) {
+                       jQuery.proxy( fn, args[ i++ ] );
+               }
+
+               return this.click( jQuery.proxy( fn, function( event ) {
+                       // Figure out which function to execute
+                       var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i;
+                       jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 );
+
+                       // Make sure that clicks stop
+                       event.preventDefault();
+
+                       // and execute the function
+                       return args[ lastToggle ].apply( this, arguments ) || false;
+               }));
+       },
+
+       hover: function( fnOver, fnOut ) {
+               return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+       }
+});
+
+jQuery.each(["live", "die"], function( i, name ) {
+       jQuery.fn[ name ] = function( types, data, fn ) {
+               var type, i = 0;
+
+               if ( jQuery.isFunction( data ) ) {
+                       fn = data;
+                       data = undefined;
+               }
+
+               types = (types || "").split( /\s+/ );
+
+               while ( (type = types[ i++ ]) != null ) {
+                       type = type === "focus" ? "focusin" : // focus --> focusin
+                                       type === "blur" ? "focusout" : // blur --> focusout
+                                       type === "hover" ? types.push("mouseleave") && "mouseenter" : // hover support
+                                       type;
+                       
+                       if ( name === "live" ) {
+                               // bind live handler
+                               jQuery( this.context ).bind( liveConvert( type, this.selector ), {
+                                       data: data, selector: this.selector, live: type
+                               }, fn );
+
+                       } else {
+                               // unbind live handler
+                               jQuery( this.context ).unbind( liveConvert( type, this.selector ), fn ? { guid: fn.guid + this.selector + type } : null );
+                       }
+               }
+               
+               return this;
+       }
+});
+
+function liveHandler( event ) {
+       var stop, elems = [], selectors = [], args = arguments,
+               related, match, fn, elem, j, i, l, data,
+               live = jQuery.extend({}, jQuery.data( this, "events" ).live);
+
+       // Make sure we avoid non-left-click bubbling in Firefox (#3861)
+       if ( event.button && event.type === "click" ) {
+               return;
+       }
+
+       for ( j in live ) {
+               fn = live[j];
+               if ( fn.live === event.type ||
+                               fn.altLive && jQuery.inArray(event.type, fn.altLive) > -1 ) {
+
+                       data = fn.data;
+                       if ( !(data.beforeFilter && data.beforeFilter[event.type] && 
+                                       !data.beforeFilter[event.type](event)) ) {
+                               selectors.push( fn.selector );
+                       }
+               } else {
+                       delete live[j];
+               }
+       }
+
+       match = jQuery( event.target ).closest( selectors, event.currentTarget );
+
+       for ( i = 0, l = match.length; i < l; i++ ) {
+               for ( j in live ) {
+                       fn = live[j];
+                       elem = match[i].elem;
+                       related = null;
+
+                       if ( match[i].selector === fn.selector ) {
+                               // Those two events require additional checking
+                               if ( fn.live === "mouseenter" || fn.live === "mouseleave" ) {
+                                       related = jQuery( event.relatedTarget ).closest( fn.selector )[0];
+                               }
+
+                               if ( !related || related !== elem ) {
+                                       elems.push({ elem: elem, fn: fn });
+                               }
+                       }
+               }
+       }
+
+       for ( i = 0, l = elems.length; i < l; i++ ) {
+               match = elems[i];
+               event.currentTarget = match.elem;
+               event.data = match.fn.data;
+               if ( match.fn.apply( match.elem, args ) === false ) {
+                       stop = false;
+                       break;
+               }
+       }
+
+       return stop;
+}
+
+function liveConvert( type, selector ) {
+       return "live." + (type ? type + "." : "") + selector.replace(/\./g, "`").replace(/ /g, "&");
+}
+
+jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
+       "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+       "change select submit keydown keypress keyup error").split(" "), function( i, name ) {
+
+       // Handle event binding
+       jQuery.fn[ name ] = function( fn ) {
+               return fn ? this.bind( name, fn ) : this.trigger( name );
+       };
+
+       if ( jQuery.attrFn ) {
+               jQuery.attrFn[ name ] = true;
+       }
+});
+
+// Prevent memory leaks in IE
+// Window isn't included so as not to unbind existing unload events
+// More info:
+//  - http://isaacschlueter.com/2006/10/msie-memory-leaks/
+if ( window.attachEvent && !window.addEventListener ) {
+       window.attachEvent("onunload", function() {
+               for ( var id in jQuery.cache ) {
+                       if ( jQuery.cache[ id ].handle ) {
+                               // Try/Catch is to handle iframes being unloaded, see #4280
+                               try {
+                                       jQuery.event.remove( jQuery.cache[ id ].handle.elem );
+                               } catch(e) {}
+                       }
+               }
+       });
+}
+/*!
+ * Sizzle CSS Selector Engine - v1.0
+ *  Copyright 2009, The Dojo Foundation
+ *  Released under the MIT, BSD, and GPL Licenses.
+ *  More information: http://sizzlejs.com/
+ */
+(function(){
+
+var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
+       done = 0,
+       toString = Object.prototype.toString,
+       hasDuplicate = false,
+       baseHasDuplicate = true;
+
+// Here we check if the JavaScript engine is using some sort of
+// optimization where it does not always call our comparision
+// function. If that is the case, discard the hasDuplicate value.
+//   Thus far that includes Google Chrome.
+[0, 0].sort(function(){
+       baseHasDuplicate = false;
+       return 0;
+});
+
+var Sizzle = function(selector, context, results, seed) {
+       results = results || [];
+       var origContext = context = context || document;
+
+       if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
+               return [];
+       }
+       
+       if ( !selector || typeof selector !== "string" ) {
+               return results;
+       }
+
+       var parts = [], m, set, checkSet, extra, prune = true, contextXML = isXML(context),
+               soFar = selector;
+       
+       // Reset the position of the chunker regexp (start from head)
+       while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) {
+               soFar = m[3];
+               
+               parts.push( m[1] );
+               
+               if ( m[2] ) {
+                       extra = m[3];
+                       break;
+               }
+       }
+
+       if ( parts.length > 1 && origPOS.exec( selector ) ) {
+               if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
+                       set = posProcess( parts[0] + parts[1], context );
+               } else {
+                       set = Expr.relative[ parts[0] ] ?
+                               [ context ] :
+                               Sizzle( parts.shift(), context );
+
+                       while ( parts.length ) {
+                               selector = parts.shift();
+
+                               if ( Expr.relative[ selector ] ) {
+                                       selector += parts.shift();
+                               }
+                               
+                               set = posProcess( selector, set );
+                       }
+               }
+       } else {
+               // Take a shortcut and set the context if the root selector is an ID
+               // (but not if it'll be faster if the inner selector is an ID)
+               if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
+                               Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
+                       var ret = Sizzle.find( parts.shift(), context, contextXML );
+                       context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0];
+               }
+
+               if ( context ) {
+                       var ret = seed ?
+                               { expr: parts.pop(), set: makeArray(seed) } :
+                               Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
+                       set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set;
+
+                       if ( parts.length > 0 ) {
+                               checkSet = makeArray(set);
+                       } else {
+                               prune = false;
+                       }
+
+                       while ( parts.length ) {
+                               var cur = parts.pop(), pop = cur;
+
+                               if ( !Expr.relative[ cur ] ) {
+                                       cur = "";
+                               } else {
+                                       pop = parts.pop();
+                               }
+
+                               if ( pop == null ) {
+                                       pop = context;
+                               }
+
+                               Expr.relative[ cur ]( checkSet, pop, contextXML );
+                       }
+               } else {
+                       checkSet = parts = [];
+               }
+       }
+
+       if ( !checkSet ) {
+               checkSet = set;
+       }
+
+       if ( !checkSet ) {
+               Sizzle.error( cur || selector );
+       }
+
+       if ( toString.call(checkSet) === "[object Array]" ) {
+               if ( !prune ) {
+                       results.push.apply( results, checkSet );
+               } else if ( context && context.nodeType === 1 ) {
+                       for ( var i = 0; checkSet[i] != null; i++ ) {
+                               if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) {
+                                       results.push( set[i] );
+                               }
+                       }
+               } else {
+                       for ( var i = 0; checkSet[i] != null; i++ ) {
+                               if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
+                                       results.push( set[i] );
+                               }
+                       }
+               }
+       } else {
+               makeArray( checkSet, results );
+       }
+
+       if ( extra ) {
+               Sizzle( extra, origContext, results, seed );
+               Sizzle.uniqueSort( results );
+       }
+
+       return results;
+};
+
+Sizzle.uniqueSort = function(results){
+       if ( sortOrder ) {
+               hasDuplicate = baseHasDuplicate;
+               results.sort(sortOrder);
+
+               if ( hasDuplicate ) {
+                       for ( var i = 1; i < results.length; i++ ) {
+                               if ( results[i] === results[i-1] ) {
+                                       results.splice(i--, 1);
+                               }
+                       }
+               }
+       }
+
+       return results;
+};
+
+Sizzle.matches = function(expr, set){
+       return Sizzle(expr, null, null, set);
+};
+
+Sizzle.find = function(expr, context, isXML){
+       var set, match;
+
+       if ( !expr ) {
+               return [];
+       }
+
+       for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
+               var type = Expr.order[i], match;
+               
+               if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
+                       var left = match[1];
+                       match.splice(1,1);
+
+                       if ( left.substr( left.length - 1 ) !== "\\" ) {
+                               match[1] = (match[1] || "").replace(/\\/g, "");
+                               set = Expr.find[ type ]( match, context, isXML );
+                               if ( set != null ) {
+                                       expr = expr.replace( Expr.match[ type ], "" );
+                                       break;
+                               }
+                       }
+               }
+       }
+
+       if ( !set ) {
+               set = context.getElementsByTagName("*");
+       }
+
+       return {set: set, expr: expr};
+};
+
+Sizzle.filter = function(expr, set, inplace, not){
+       var old = expr, result = [], curLoop = set, match, anyFound,
+               isXMLFilter = set && set[0] && isXML(set[0]);
+
+       while ( expr && set.length ) {
+               for ( var type in Expr.filter ) {
+                       if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
+                               var filter = Expr.filter[ type ], found, item, left = match[1];
+                               anyFound = false;
+
+                               match.splice(1,1);
+
+                               if ( left.substr( left.length - 1 ) === "\\" ) {
+                                       continue;
+                               }
+
+                               if ( curLoop === result ) {
+                                       result = [];
+                               }
+
+                               if ( Expr.preFilter[ type ] ) {
+                                       match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
+
+                                       if ( !match ) {
+                                               anyFound = found = true;
+                                       } else if ( match === true ) {
+                                               continue;
+                                       }
+                               }
+
+                               if ( match ) {
+                                       for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
+                                               if ( item ) {
+                                                       found = filter( item, match, i, curLoop );
+                                                       var pass = not ^ !!found;
+
+                                                       if ( inplace && found != null ) {
+                                                               if ( pass ) {
+                                                                       anyFound = true;
                                                                } else {
                                                                        curLoop[i] = false;
                                                                }
@@ -1637,9 +2781,9 @@ Sizzle.filter = function(expr, set, inplace, not){
                }
 
                // Improper expression
-               if ( expr == old ) {
+               if ( expr === old ) {
                        if ( anyFound == null ) {
-                               throw "Syntax error, unrecognized expression: " + expr;
+                               Sizzle.error( expr );
                        } else {
                                break;
                        }
@@ -1651,18 +2795,23 @@ Sizzle.filter = function(expr, set, inplace, not){
        return curLoop;
 };
 
+Sizzle.error = function( msg ) {
+       throw "Syntax error, unrecognized expression: " + msg;
+};
+
 var Expr = Sizzle.selectors = {
        order: [ "ID", "NAME", "TAG" ],
        match: {
-               ID: /#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,
-               CLASS: /\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,
-               NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,
-               ATTR: /\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
-               TAG: /^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,
+               ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
+               CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
+               NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,
+               ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
+               TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,
                CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,
                POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,
-               PSEUDO: /:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/
+               PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
        },
+       leftMatch: {},
        attrMap: {
                "class": "className",
                "for": "htmlFor"
@@ -1673,20 +2822,20 @@ var Expr = Sizzle.selectors = {
                }
        },
        relative: {
-               "+": function(checkSet, part, isXML){
+               "+": function(checkSet, part){
                        var isPartStr = typeof part === "string",
                                isTag = isPartStr && !/\W/.test(part),
                                isPartStrNotTag = isPartStr && !isTag;
 
-                       if ( isTag && !isXML ) {
-                               part = part.toUpperCase();
+                       if ( isTag ) {
+                               part = part.toLowerCase();
                        }
 
                        for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
                                if ( (elem = checkSet[i]) ) {
                                        while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
 
-                                       checkSet[i] = isPartStrNotTag || elem && elem.nodeName === part ?
+                                       checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
                                                elem || false :
                                                elem === part;
                                }
@@ -1696,17 +2845,17 @@ var Expr = Sizzle.selectors = {
                                Sizzle.filter( part, checkSet, true );
                        }
                },
-               ">": function(checkSet, part, isXML){
+               ">": function(checkSet, part){
                        var isPartStr = typeof part === "string";
 
                        if ( isPartStr && !/\W/.test(part) ) {
-                               part = isXML ? part : part.toUpperCase();
+                               part = part.toLowerCase();
 
                                for ( var i = 0, l = checkSet.length; i < l; i++ ) {
                                        var elem = checkSet[i];
                                        if ( elem ) {
                                                var parent = elem.parentNode;
-                                               checkSet[i] = parent.nodeName === part ? parent : false;
+                                               checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
                                        }
                                }
                        } else {
@@ -1727,8 +2876,8 @@ var Expr = Sizzle.selectors = {
                "": function(checkSet, part, isXML){
                        var doneName = done++, checkFn = dirCheck;
 
-                       if ( !part.match(/\W/) ) {
-                               var nodeCheck = part = isXML ? part : part.toUpperCase();
+                       if ( typeof part === "string" && !/\W/.test(part) ) {
+                               var nodeCheck = part = part.toLowerCase();
                                checkFn = dirNodeCheck;
                        }
 
@@ -1737,8 +2886,8 @@ var Expr = Sizzle.selectors = {
                "~": function(checkSet, part, isXML){
                        var doneName = done++, checkFn = dirCheck;
 
-                       if ( typeof part === "string" && !part.match(/\W/) ) {
-                               var nodeCheck = part = isXML ? part : part.toUpperCase();
+                       if ( typeof part === "string" && !/\W/.test(part) ) {
+                               var nodeCheck = part = part.toLowerCase();
                                checkFn = dirNodeCheck;
                        }
 
@@ -1752,7 +2901,7 @@ var Expr = Sizzle.selectors = {
                                return m ? [m] : [];
                        }
                },
-               NAME: function(match, context, isXML){
+               NAME: function(match, context){
                        if ( typeof context.getElementsByName !== "undefined" ) {
                                var ret = [], results = context.getElementsByName(match[1]);
 
@@ -1779,9 +2928,10 @@ var Expr = Sizzle.selectors = {
 
                        for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
                                if ( elem ) {
-                                       if ( not ^ (elem.className && (" " + elem.className + " ").indexOf(match) >= 0) ) {
-                                               if ( !inplace )
+                                       if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) {
+                                               if ( !inplace ) {
                                                        result.push( elem );
+                                               }
                                        } else if ( inplace ) {
                                                curLoop[i] = false;
                                        }
@@ -1794,14 +2944,13 @@ var Expr = Sizzle.selectors = {
                        return match[1].replace(/\\/g, "");
                },
                TAG: function(match, curLoop){
-                       for ( var i = 0; curLoop[i] === false; i++ ){}
-                       return curLoop[i] && isXML(curLoop[i]) ? match[1] : match[1].toUpperCase();
+                       return match[1].toLowerCase();
                },
                CHILD: function(match){
-                       if ( match[1] == "nth" ) {
+                       if ( match[1] === "nth" ) {
                                // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
                                var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
-                                       match[2] == "even" && "2n" || match[2] == "odd" && "2n+1" ||
+                                       match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
                                        !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
 
                                // calculate the numbers (first)n+(last) including if they are negative
@@ -1830,7 +2979,7 @@ var Expr = Sizzle.selectors = {
                PSEUDO: function(match, curLoop, inplace, result, not){
                        if ( match[1] === "not" ) {
                                // If we're dealing with a complex expression, or a simple one
-                               if ( match[3].match(chunker).length > 1 || /^\w/.test(match[3]) ) {
+                               if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
                                        match[3] = Sizzle(match[3], null, null, curLoop);
                                } else {
                                        var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
@@ -1903,7 +3052,7 @@ var Expr = Sizzle.selectors = {
                        return "reset" === elem.type;
                },
                button: function(elem){
-                       return "button" === elem.type || elem.nodeName.toUpperCase() === "BUTTON";
+                       return "button" === elem.type || elem.nodeName.toLowerCase() === "button";
                },
                input: function(elem){
                        return /input|select|textarea|button/i.test(elem.nodeName);
@@ -1929,10 +3078,10 @@ var Expr = Sizzle.selectors = {
                        return i > match[3] - 0;
                },
                nth: function(elem, i, match){
-                       return match[3] - 0 == i;
+                       return match[3] - 0 === i;
                },
                eq: function(elem, i, match){
-                       return match[3] - 0 == i;
+                       return match[3] - 0 === i;
                }
        },
        filter: {
@@ -1942,7 +3091,7 @@ var Expr = Sizzle.selectors = {
                        if ( filter ) {
                                return filter( elem, i, match, array );
                        } else if ( name === "contains" ) {
-                               return (elem.textContent || elem.innerText || "").indexOf(match[3]) >= 0;
+                               return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0;
                        } else if ( name === "not" ) {
                                var not = match[3];
 
@@ -1953,6 +3102,8 @@ var Expr = Sizzle.selectors = {
                                }
 
                                return true;
+                       } else {
+                               Sizzle.error( "Syntax error, unrecognized expression: " + name );
                        }
                },
                CHILD: function(elem, match){
@@ -1960,20 +3111,26 @@ var Expr = Sizzle.selectors = {
                        switch (type) {
                                case 'only':
                                case 'first':
-                                       while (node = node.previousSibling)  {
-                                               if ( node.nodeType === 1 ) return false;
+                                       while ( (node = node.previousSibling) )  {
+                                               if ( node.nodeType === 1 ) { 
+                                                       return false; 
+                                               }
+                                       }
+                                       if ( type === "first" ) { 
+                                               return true; 
                                        }
-                                       if ( type == 'first') return true;
                                        node = elem;
                                case 'last':
-                                       while (node = node.nextSibling)  {
-                                               if ( node.nodeType === 1 ) return false;
+                                       while ( (node = node.nextSibling) )      {
+                                               if ( node.nodeType === 1 ) { 
+                                                       return false; 
+                                               }
                                        }
                                        return true;
                                case 'nth':
                                        var first = match[2], last = match[3];
 
-                                       if ( first == 1 && last == 0 ) {
+                                       if ( first === 1 && last === 0 ) {
                                                return true;
                                        }
                                        
@@ -1991,10 +3148,10 @@ var Expr = Sizzle.selectors = {
                                        }
                                        
                                        var diff = elem.nodeIndex - last;
-                                       if ( first == 0 ) {
-                                               return diff == 0;
+                                       if ( first === 0 ) {
+                                               return diff === 0;
                                        } else {
-                                               return ( diff % first == 0 && diff / first >= 0 );
+                                               return ( diff % first === 0 && diff / first >= 0 );
                                        }
                        }
                },
@@ -2002,7 +3159,7 @@ var Expr = Sizzle.selectors = {
                        return elem.nodeType === 1 && elem.getAttribute("id") === match;
                },
                TAG: function(elem, match){
-                       return (match === "*" && elem.nodeType === 1) || elem.nodeName === match;
+                       return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match;
                },
                CLASS: function(elem, match){
                        return (" " + (elem.className || elem.getAttribute("class")) + " ")
@@ -2030,7 +3187,7 @@ var Expr = Sizzle.selectors = {
                                !check ?
                                value && result !== false :
                                type === "!=" ?
-                               value != check :
+                               value !== check :
                                type === "^=" ?
                                value.indexOf(check) === 0 :
                                type === "$=" ?
@@ -2052,11 +3209,14 @@ var Expr = Sizzle.selectors = {
 var origPOS = Expr.match.POS;
 
 for ( var type in Expr.match ) {
-       Expr.match[ type ] = RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source );
+       Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source );
+       Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, function(all, num){
+               return "\\" + (num - 0 + 1);
+       }));
 }
 
 var makeArray = function(array, results) {
-       array = Array.prototype.slice.call( array );
+       array = Array.prototype.slice.call( array, 0 );
 
        if ( results ) {
                results.push.apply( results, array );
@@ -2069,7 +3229,7 @@ var makeArray = function(array, results) {
 // Perform a simple check to determine if the browser is capable of
 // converting a NodeList to an array using builtin methods.
 try {
-       Array.prototype.slice.call( document.documentElement.childNodes );
+       Array.prototype.slice.call( document.documentElement.childNodes, 0 );
 
 // Provide a fallback method if it does not work
 } catch(e){
@@ -2098,6 +3258,13 @@ var sortOrder;
 
 if ( document.documentElement.compareDocumentPosition ) {
        sortOrder = function( a, b ) {
+               if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
+                       if ( a == b ) {
+                               hasDuplicate = true;
+                       }
+                       return a.compareDocumentPosition ? -1 : 1;
+               }
+
                var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1;
                if ( ret === 0 ) {
                        hasDuplicate = true;
@@ -2106,6 +3273,13 @@ if ( document.documentElement.compareDocumentPosition ) {
        };
 } else if ( "sourceIndex" in document.documentElement ) {
        sortOrder = function( a, b ) {
+               if ( !a.sourceIndex || !b.sourceIndex ) {
+                       if ( a == b ) {
+                               hasDuplicate = true;
+                       }
+                       return a.sourceIndex ? -1 : 1;
+               }
+
                var ret = a.sourceIndex - b.sourceIndex;
                if ( ret === 0 ) {
                        hasDuplicate = true;
@@ -2114,11 +3288,18 @@ if ( document.documentElement.compareDocumentPosition ) {
        };
 } else if ( document.createRange ) {
        sortOrder = function( a, b ) {
+               if ( !a.ownerDocument || !b.ownerDocument ) {
+                       if ( a == b ) {
+                               hasDuplicate = true;
+                       }
+                       return a.ownerDocument ? -1 : 1;
+               }
+
                var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange();
-               aRange.selectNode(a);
-               aRange.collapse(true);
-               bRange.selectNode(b);
-               bRange.collapse(true);
+               aRange.setStart(a, 0);
+               aRange.setEnd(a, 0);
+               bRange.setStart(b, 0);
+               bRange.setEnd(b, 0);
                var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
                if ( ret === 0 ) {
                        hasDuplicate = true;
@@ -2127,13 +3308,33 @@ if ( document.documentElement.compareDocumentPosition ) {
        };
 }
 
+// Utility function for retreiving the text value of an array of DOM nodes
+function getText( elems ) {
+       var ret = "", elem;
+
+       for ( var i = 0; elems[i]; i++ ) {
+               elem = elems[i];
+
+               // Get the text from text nodes and CDATA nodes
+               if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
+                       ret += elem.nodeValue;
+
+               // Traverse everything else, except comment nodes
+               } else if ( elem.nodeType !== 8 ) {
+                       ret += getText( elem.childNodes );
+               }
+       }
+
+       return ret;
+}
+
 // Check to see if the browser returns elements by name when
 // querying by getElementById (and provide a workaround)
 (function(){
        // We're going to inject a fake input element with a specified name
-       var form = document.createElement("form"),
+       var form = document.createElement("div"),
                id = "script" + (new Date).getTime();
-       form.innerHTML = "<input name='" + id + "'/>";
+       form.innerHTML = "<a name='" + id + "'/>";
 
        // Inject it into the root element, check its status, and remove it quickly
        var root = document.documentElement;
@@ -2141,7 +3342,7 @@ if ( document.documentElement.compareDocumentPosition ) {
 
        // The workaround has to do additional checks after a getElementById
        // Which slows things down for other browsers (hence the branching)
-       if ( !!document.getElementById( id ) ) {
+       if ( document.getElementById( id ) ) {
                Expr.find.ID = function(match, context, isXML){
                        if ( typeof context.getElementById !== "undefined" && !isXML ) {
                                var m = context.getElementById(match[1]);
@@ -2156,6 +3357,7 @@ if ( document.documentElement.compareDocumentPosition ) {
        }
 
        root.removeChild( form );
+       root = form = null; // release memory in IE
 })();
 
 (function(){
@@ -2196,69 +3398,75 @@ if ( document.documentElement.compareDocumentPosition ) {
                        return elem.getAttribute("href", 2);
                };
        }
+
+       div = null; // release memory in IE
 })();
 
-if ( document.querySelectorAll ) (function(){
-       var oldSizzle = Sizzle, div = document.createElement("div");
-       div.innerHTML = "<p class='TEST'></p>";
+if ( document.querySelectorAll ) {
+       (function(){
+               var oldSizzle = Sizzle, div = document.createElement("div");
+               div.innerHTML = "<p class='TEST'></p>";
 
-       // Safari can't handle uppercase or unicode characters when
-       // in quirks mode.
-       if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
-               return;
-       }
+               // Safari can't handle uppercase or unicode characters when
+               // in quirks mode.
+               if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
+                       return;
+               }
        
-       Sizzle = function(query, context, extra, seed){
-               context = context || document;
+               Sizzle = function(query, context, extra, seed){
+                       context = context || document;
+
+                       // Only use querySelectorAll on non-XML documents
+                       // (ID selectors don't work in non-HTML documents)
+                       if ( !seed && context.nodeType === 9 && !isXML(context) ) {
+                               try {
+                                       return makeArray( context.querySelectorAll(query), extra );
+                               } catch(e){}
+                       }
+               
+                       return oldSizzle(query, context, extra, seed);
+               };
 
-               // Only use querySelectorAll on non-XML documents
-               // (ID selectors don't work in non-HTML documents)
-               if ( !seed && context.nodeType === 9 && !isXML(context) ) {
-                       try {
-                               return makeArray( context.querySelectorAll(query), extra );
-                       } catch(e){}
+               for ( var prop in oldSizzle ) {
+                       Sizzle[ prop ] = oldSizzle[ prop ];
                }
-               
-               return oldSizzle(query, context, extra, seed);
-       };
 
-       Sizzle.find = oldSizzle.find;
-       Sizzle.filter = oldSizzle.filter;
-       Sizzle.selectors = oldSizzle.selectors;
-       Sizzle.matches = oldSizzle.matches;
-})();
+               div = null; // release memory in IE
+       })();
+}
 
-if ( document.getElementsByClassName && document.documentElement.getElementsByClassName ) (function(){
+(function(){
        var div = document.createElement("div");
+
        div.innerHTML = "<div class='test e'></div><div class='test'></div>";
 
        // Opera can't find a second classname (in 9.6)
-       if ( div.getElementsByClassName("e").length === 0 )
+       // Also, make sure that getElementsByClassName actually exists
+       if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
                return;
+       }
 
        // Safari caches class attributes, doesn't catch changes (in 3.2)
        div.lastChild.className = "e";
 
-       if ( div.getElementsByClassName("e").length === 1 )
+       if ( div.getElementsByClassName("e").length === 1 ) {
                return;
-
+       }
+       
        Expr.order.splice(1, 0, "CLASS");
        Expr.find.CLASS = function(match, context, isXML) {
                if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
                        return context.getElementsByClassName(match[1]);
                }
        };
+
+       div = null; // release memory in IE
 })();
 
 function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
-       var sibDir = dir == "previousSibling" && !isXML;
        for ( var i = 0, l = checkSet.length; i < l; i++ ) {
                var elem = checkSet[i];
                if ( elem ) {
-                       if ( sibDir && elem.nodeType === 1 ){
-                               elem.sizcache = doneName;
-                               elem.sizset = i;
-                       }
                        elem = elem[dir];
                        var match = false;
 
@@ -2273,7 +3481,7 @@ function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
                                        elem.sizset = i;
                                }
 
-                               if ( elem.nodeName === cur ) {
+                               if ( elem.nodeName.toLowerCase() === cur ) {
                                        match = elem;
                                        break;
                                }
@@ -2287,14 +3495,9 @@ function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
 }
 
 function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
-       var sibDir = dir == "previousSibling" && !isXML;
        for ( var i = 0, l = checkSet.length; i < l; i++ ) {
                var elem = checkSet[i];
                if ( elem ) {
-                       if ( sibDir && elem.nodeType === 1 ) {
-                               elem.sizcache = doneName;
-                               elem.sizset = i;
-                       }
                        elem = elem[dir];
                        var match = false;
 
@@ -2329,15 +3532,17 @@ function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
        }
 }
 
-var contains = document.compareDocumentPosition ?  function(a, b){
+var contains = document.compareDocumentPosition ? function(a, b){
        return a.compareDocumentPosition(b) & 16;
 } : function(a, b){
        return a !== b && (a.contains ? a.contains(b) : true);
 };
 
 var isXML = function(elem){
-       return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
-               !!elem.ownerDocument && isXML( elem.ownerDocument );
+       // documentElement is verified for cases where it doesn't yet exist
+       // (such as loading iframes in IE - #4833) 
+       var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
+       return documentElement ? documentElement.nodeName !== "HTML" : false;
 };
 
 var posProcess = function(selector, context){
@@ -2362,873 +3567,1068 @@ var posProcess = function(selector, context){
 
 // EXPOSE
 jQuery.find = Sizzle;
-jQuery.filter = Sizzle.filter;
 jQuery.expr = Sizzle.selectors;
 jQuery.expr[":"] = jQuery.expr.filters;
+jQuery.unique = Sizzle.uniqueSort;
+jQuery.getText = getText;
+jQuery.isXMLDoc = isXML;
+jQuery.contains = contains;
+
+return;
 
-Sizzle.selectors.filters.hidden = function(elem){
-       return elem.offsetWidth === 0 || elem.offsetHeight === 0;
+window.Sizzle = Sizzle;
+
+})();
+var runtil = /Until$/,
+       rparentsprev = /^(?:parents|prevUntil|prevAll)/,
+       // Note: This RegExp should be improved, or likely pulled from Sizzle
+       rmultiselector = /,/,
+       slice = Array.prototype.slice;
+
+// Implement the identical functionality for filter and not
+var winnow = function( elements, qualifier, keep ) {
+       if ( jQuery.isFunction( qualifier ) ) {
+               return jQuery.grep(elements, function( elem, i ) {
+                       return !!qualifier.call( elem, i, elem ) === keep;
+               });
+
+       } else if ( qualifier.nodeType ) {
+               return jQuery.grep(elements, function( elem, i ) {
+                       return (elem === qualifier) === keep;
+               });
+
+       } else if ( typeof qualifier === "string" ) {
+               var filtered = jQuery.grep(elements, function( elem ) {
+                       return elem.nodeType === 1;
+               });
+
+               if ( isSimple.test( qualifier ) ) {
+                       return jQuery.filter(qualifier, filtered, !keep);
+               } else {
+                       qualifier = jQuery.filter( qualifier, filtered );
+               }
+       }
+
+       return jQuery.grep(elements, function( elem, i ) {
+               return (jQuery.inArray( elem, qualifier ) >= 0) === keep;
+       });
 };
 
-Sizzle.selectors.filters.visible = function(elem){
-       return elem.offsetWidth > 0 || elem.offsetHeight > 0;
-};
+jQuery.fn.extend({
+       find: function( selector ) {
+               var ret = this.pushStack( "", "find", selector ), length = 0;
+
+               for ( var i = 0, l = this.length; i < l; i++ ) {
+                       length = ret.length;
+                       jQuery.find( selector, this[i], ret );
+
+                       if ( i > 0 ) {
+                               // Make sure that the results are unique
+                               for ( var n = length; n < ret.length; n++ ) {
+                                       for ( var r = 0; r < length; r++ ) {
+                                               if ( ret[r] === ret[n] ) {
+                                                       ret.splice(n--, 1);
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               return ret;
+       },
+
+       has: function( target ) {
+               var targets = jQuery( target );
+               return this.filter(function() {
+                       for ( var i = 0, l = targets.length; i < l; i++ ) {
+                               if ( jQuery.contains( this, targets[i] ) ) {
+                                       return true;
+                               }
+                       }
+               });
+       },
+
+       not: function( selector ) {
+               return this.pushStack( winnow(this, selector, false), "not", selector);
+       },
+
+       filter: function( selector ) {
+               return this.pushStack( winnow(this, selector, true), "filter", selector );
+       },
+       
+       is: function( selector ) {
+               return !!selector && jQuery.filter( selector, this ).length > 0;
+       },
+
+       closest: function( selectors, context ) {
+               if ( jQuery.isArray( selectors ) ) {
+                       var ret = [], cur = this[0], match, matches = {}, selector;
+
+                       if ( cur && selectors.length ) {
+                               for ( var i = 0, l = selectors.length; i < l; i++ ) {
+                                       selector = selectors[i];
+
+                                       if ( !matches[selector] ) {
+                                               matches[selector] = jQuery.expr.match.POS.test( selector ) ? 
+                                                       jQuery( selector, context || this.context ) :
+                                                       selector;
+                                       }
+                               }
+
+                               while ( cur && cur.ownerDocument && cur !== context ) {
+                                       for ( selector in matches ) {
+                                               match = matches[selector];
+
+                                               if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) {
+                                                       ret.push({ selector: selector, elem: cur });
+                                                       delete matches[selector];
+                                               }
+                                       }
+                                       cur = cur.parentNode;
+                               }
+                       }
+
+                       return ret;
+               }
+
+               var pos = jQuery.expr.match.POS.test( selectors ) ? 
+                       jQuery( selectors, context || this.context ) : null;
+
+               return this.map(function( i, cur ) {
+                       while ( cur && cur.ownerDocument && cur !== context ) {
+                               if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selectors) ) {
+                                       return cur;
+                               }
+                               cur = cur.parentNode;
+                       }
+                       return null;
+               });
+       },
+       
+       // Determine the position of an element within
+       // the matched set of elements
+       index: function( elem ) {
+               if ( !elem || typeof elem === "string" ) {
+                       return jQuery.inArray( this[0],
+                               // If it receives a string, the selector is used
+                               // If it receives nothing, the siblings are used
+                               elem ? jQuery( elem ) : this.parent().children() );
+               }
+               // Locate the position of the desired element
+               return jQuery.inArray(
+                       // If it receives a jQuery object, the first element is used
+                       elem.jquery ? elem[0] : elem, this );
+       },
+
+       add: function( selector, context ) {
+               var set = typeof selector === "string" ?
+                               jQuery( selector, context || this.context ) :
+                               jQuery.makeArray( selector ),
+                       all = jQuery.merge( this.get(), set );
+
+               return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
+                       all :
+                       jQuery.unique( all ) );
+       },
+
+       andSelf: function() {
+               return this.add( this.prevObject );
+       }
+});
+
+// A painfully simple check to see if an element is disconnected
+// from a document (should be improved, where feasible).
+function isDisconnected( node ) {
+       return !node || !node.parentNode || node.parentNode.nodeType === 11;
+}
+
+jQuery.each({
+       parent: function( elem ) {
+               var parent = elem.parentNode;
+               return parent && parent.nodeType !== 11 ? parent : null;
+       },
+       parents: function( elem ) {
+               return jQuery.dir( elem, "parentNode" );
+       },
+       parentsUntil: function( elem, i, until ) {
+               return jQuery.dir( elem, "parentNode", until );
+       },
+       next: function( elem ) {
+               return jQuery.nth( elem, 2, "nextSibling" );
+       },
+       prev: function( elem ) {
+               return jQuery.nth( elem, 2, "previousSibling" );
+       },
+       nextAll: function( elem ) {
+               return jQuery.dir( elem, "nextSibling" );
+       },
+       prevAll: function( elem ) {
+               return jQuery.dir( elem, "previousSibling" );
+       },
+       nextUntil: function( elem, i, until ) {
+               return jQuery.dir( elem, "nextSibling", until );
+       },
+       prevUntil: function( elem, i, until ) {
+               return jQuery.dir( elem, "previousSibling", until );
+       },
+       siblings: function( elem ) {
+               return jQuery.sibling( elem.parentNode.firstChild, elem );
+       },
+       children: function( elem ) {
+               return jQuery.sibling( elem.firstChild );
+       },
+       contents: function( elem ) {
+               return jQuery.nodeName( elem, "iframe" ) ?
+                       elem.contentDocument || elem.contentWindow.document :
+                       jQuery.makeArray( elem.childNodes );
+       }
+}, function( name, fn ) {
+       jQuery.fn[ name ] = function( until, selector ) {
+               var ret = jQuery.map( this, fn, until );
+               
+               if ( !runtil.test( name ) ) {
+                       selector = until;
+               }
+
+               if ( selector && typeof selector === "string" ) {
+                       ret = jQuery.filter( selector, ret );
+               }
+
+               ret = this.length > 1 ? jQuery.unique( ret ) : ret;
+
+               if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+                       ret = ret.reverse();
+               }
+
+               return this.pushStack( ret, name, slice.call(arguments).join(",") );
+       };
+});
+
+jQuery.extend({
+       filter: function( expr, elems, not ) {
+               if ( not ) {
+                       expr = ":not(" + expr + ")";
+               }
+
+               return jQuery.find.matches(expr, elems);
+       },
+       
+       dir: function( elem, dir, until ) {
+               var matched = [], cur = elem[dir];
+               while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
+                       if ( cur.nodeType === 1 ) {
+                               matched.push( cur );
+                       }
+                       cur = cur[dir];
+               }
+               return matched;
+       },
+
+       nth: function( cur, result, dir, elem ) {
+               result = result || 1;
+               var num = 0;
+
+               for ( ; cur; cur = cur[dir] ) {
+                       if ( cur.nodeType === 1 && ++num === result ) {
+                               break;
+                       }
+               }
+
+               return cur;
+       },
+
+       sibling: function( n, elem ) {
+               var r = [];
+
+               for ( ; n; n = n.nextSibling ) {
+                       if ( n.nodeType === 1 && n !== elem ) {
+                               r.push( n );
+                       }
+               }
+
+               return r;
+       }
+});
+var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,
+       rleadingWhitespace = /^\s+/,
+       rxhtmlTag = /(<([\w:]+)[^>]*?)\/>/g,
+       rselfClosing = /^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,
+       rtagName = /<([\w:]+)/,
+       rtbody = /<tbody/i,
+       rhtml = /<|&\w+;/,
+       rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,  // checked="checked" or checked (html5)
+       fcloseTag = function( all, front, tag ) {
+               return rselfClosing.test( tag ) ?
+                       all :
+                       front + "></" + tag + ">";
+       },
+       wrapMap = {
+               option: [ 1, "<select multiple='multiple'>", "</select>" ],
+               legend: [ 1, "<fieldset>", "</fieldset>" ],
+               thead: [ 1, "<table>", "</table>" ],
+               tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+               td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+               col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
+               area: [ 1, "<map>", "</map>" ],
+               _default: [ 0, "", "" ]
+       };
+
+wrapMap.optgroup = wrapMap.option;
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+// IE can't serialize <link> and <script> tags normally
+if ( !jQuery.support.htmlSerialize ) {
+       wrapMap._default = [ 1, "div<div>", "</div>" ];
+}
+
+jQuery.fn.extend({
+       text: function( text ) {
+               if ( jQuery.isFunction(text) ) {
+                       return this.each(function(i) {
+                               var self = jQuery(this);
+                               self.text( text.call(this, i, self.text()) );
+                       });
+               }
+
+               if ( typeof text !== "object" && text !== undefined ) {
+                       return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
+               }
+
+               return jQuery.getText( this );
+       },
+
+       wrapAll: function( html ) {
+               if ( jQuery.isFunction( html ) ) {
+                       return this.each(function(i) {
+                               jQuery(this).wrapAll( html.call(this, i) );
+                       });
+               }
 
-Sizzle.selectors.filters.animated = function(elem){
-       return jQuery.grep(jQuery.timers, function(fn){
-               return elem === fn.elem;
-       }).length;
-};
+               if ( this[0] ) {
+                       // The elements to wrap the target around
+                       var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
 
-jQuery.multiFilter = function( expr, elems, not ) {
-       if ( not ) {
-               expr = ":not(" + expr + ")";
-       }
+                       if ( this[0].parentNode ) {
+                               wrap.insertBefore( this[0] );
+                       }
 
-       return Sizzle.matches(expr, elems);
-};
+                       wrap.map(function() {
+                               var elem = this;
 
-jQuery.dir = function( elem, dir ){
-       var matched = [], cur = elem[dir];
-       while ( cur && cur != document ) {
-               if ( cur.nodeType == 1 )
-                       matched.push( cur );
-               cur = cur[dir];
-       }
-       return matched;
-};
+                               while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
+                                       elem = elem.firstChild;
+                               }
 
-jQuery.nth = function(cur, result, dir, elem){
-       result = result || 1;
-       var num = 0;
+                               return elem;
+                       }).append(this);
+               }
 
-       for ( ; cur; cur = cur[dir] )
-               if ( cur.nodeType == 1 && ++num == result )
-                       break;
+               return this;
+       },
 
-       return cur;
-};
+       wrapInner: function( html ) {
+               if ( jQuery.isFunction( html ) ) {
+                       return this.each(function(i) {
+                               jQuery(this).wrapInner( html.call(this, i) );
+                       });
+               }
 
-jQuery.sibling = function(n, elem){
-       var r = [];
+               return this.each(function() {
+                       var self = jQuery( this ), contents = self.contents();
 
-       for ( ; n; n = n.nextSibling ) {
-               if ( n.nodeType == 1 && n != elem )
-                       r.push( n );
-       }
+                       if ( contents.length ) {
+                               contents.wrapAll( html );
 
-       return r;
-};
+                       } else {
+                               self.append( html );
+                       }
+               });
+       },
 
-return;
+       wrap: function( html ) {
+               return this.each(function() {
+                       jQuery( this ).wrapAll( html );
+               });
+       },
 
-window.Sizzle = Sizzle;
+       unwrap: function() {
+               return this.parent().each(function() {
+                       if ( !jQuery.nodeName( this, "body" ) ) {
+                               jQuery( this ).replaceWith( this.childNodes );
+                       }
+               }).end();
+       },
 
-})();
-/*
- * A number of helper functions used for managing events.
- * Many of the ideas behind this code originated from
- * Dean Edwards' addEvent library.
- */
-jQuery.event = {
+       append: function() {
+               return this.domManip(arguments, true, function( elem ) {
+                       if ( this.nodeType === 1 ) {
+                               this.appendChild( elem );
+                       }
+               });
+       },
 
-       // Bind an event to an element
-       // Original by Dean Edwards
-       add: function(elem, types, handler, data) {
-               if ( elem.nodeType == 3 || elem.nodeType == 8 )
-                       return;
+       prepend: function() {
+               return this.domManip(arguments, true, function( elem ) {
+                       if ( this.nodeType === 1 ) {
+                               this.insertBefore( elem, this.firstChild );
+                       }
+               });
+       },
 
-               // For whatever reason, IE has trouble passing the window object
-               // around, causing it to be cloned in the process
-               if ( elem.setInterval && elem != window )
-                       elem = window;
+       before: function() {
+               if ( this[0] && this[0].parentNode ) {
+                       return this.domManip(arguments, false, function( elem ) {
+                               this.parentNode.insertBefore( elem, this );
+                       });
+               } else if ( arguments.length ) {
+                       var set = jQuery(arguments[0]);
+                       set.push.apply( set, this.toArray() );
+                       return this.pushStack( set, "before", arguments );
+               }
+       },
 
-               // Make sure that the function being executed has a unique ID
-               if ( !handler.guid )
-                       handler.guid = this.guid++;
+       after: function() {
+               if ( this[0] && this[0].parentNode ) {
+                       return this.domManip(arguments, false, function( elem ) {
+                               this.parentNode.insertBefore( elem, this.nextSibling );
+                       });
+               } else if ( arguments.length ) {
+                       var set = this.pushStack( this, "after", arguments );
+                       set.push.apply( set, jQuery(arguments[0]).toArray() );
+                       return set;
+               }
+       },
 
-               // if data is passed, bind to handler
-               if ( data !== undefined ) {
-                       // Create temporary function pointer to original handler
-                       var fn = handler;
+       clone: function( events ) {
+               // Do the clone
+               var ret = this.map(function() {
+                       if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) {
+                               // IE copies events bound via attachEvent when
+                               // using cloneNode. Calling detachEvent on the
+                               // clone will also remove the events from the orignal
+                               // In order to get around this, we use innerHTML.
+                               // Unfortunately, this means some modifications to
+                               // attributes in IE that are actually only stored
+                               // as properties will not be copied (such as the
+                               // the name attribute on an input).
+                               var html = this.outerHTML, ownerDocument = this.ownerDocument;
+                               if ( !html ) {
+                                       var div = ownerDocument.createElement("div");
+                                       div.appendChild( this.cloneNode(true) );
+                                       html = div.innerHTML;
+                               }
 
-                       // Create unique handler function, wrapped around original handler
-                       handler = this.proxy( fn );
+                               return jQuery.clean([html.replace(rinlinejQuery, "")
+                                       .replace(rleadingWhitespace, "")], ownerDocument)[0];
+                       } else {
+                               return this.cloneNode(true);
+                       }
+               });
 
-                       // Store data in unique handler
-                       handler.data = data;
+               // Copy the events from the original to the clone
+               if ( events === true ) {
+                       cloneCopyEvent( this, ret );
+                       cloneCopyEvent( this.find("*"), ret.find("*") );
                }
 
-               // Init the element's event structure
-               var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
-                       handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
-                               // Handle the second event of a trigger and when
-                               // an event is called after a page has unloaded
-                               return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
-                                       jQuery.event.handle.apply(arguments.callee.elem, arguments) :
-                                       undefined;
-                       });
-               // Add elem as a property of the handle function
-               // This is to prevent a memory leak with non-native
-               // event in IE.
-               handle.elem = elem;
+               // Return the cloned set
+               return ret;
+       },
 
-               // Handle multiple events separated by a space
-               // jQuery(...).bind("mouseover mouseout", fn);
-               jQuery.each(types.split(/\s+/), function(index, type) {
-                       // Namespaced event handlers
-                       var namespaces = type.split(".");
-                       type = namespaces.shift();
-                       handler.type = namespaces.slice().sort().join(".");
+       html: function( value ) {
+               if ( value === undefined ) {
+                       return this[0] && this[0].nodeType === 1 ?
+                               this[0].innerHTML.replace(rinlinejQuery, "") :
+                               null;
 
-                       // Get the current list of functions bound to this event
-                       var handlers = events[type];
-                       
-                       if ( jQuery.event.specialAll[type] )
-                               jQuery.event.specialAll[type].setup.call(elem, data, namespaces);
+               // See if we can take a shortcut and just use innerHTML
+               } else if ( typeof value === "string" && !/<script/i.test( value ) &&
+                       (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
+                       !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {
 
-                       // Init the event handler queue
-                       if (!handlers) {
-                               handlers = events[type] = {};
+                       value = value.replace(rxhtmlTag, fcloseTag);
 
-                               // Check for a special event handler
-                               // Only use addEventListener/attachEvent if the special
-                               // events handler returns false
-                               if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem, data, namespaces) === false ) {
-                                       // Bind the global event handler to the element
-                                       if (elem.addEventListener)
-                                               elem.addEventListener(type, handle, false);
-                                       else if (elem.attachEvent)
-                                               elem.attachEvent("on" + type, handle);
+                       try {
+                               for ( var i = 0, l = this.length; i < l; i++ ) {
+                                       // Remove element nodes and prevent memory leaks
+                                       if ( this[i].nodeType === 1 ) {
+                                               jQuery.cleanData( this[i].getElementsByTagName("*") );
+                                               this[i].innerHTML = value;
+                                       }
                                }
+
+                       // If using innerHTML throws an exception, use the fallback method
+                       } catch(e) {
+                               this.empty().append( value );
                        }
 
-                       // Add the function to the element's handler list
-                       handlers[handler.guid] = handler;
+               } else if ( jQuery.isFunction( value ) ) {
+                       this.each(function(i){
+                               var self = jQuery(this), old = self.html();
+                               self.empty().append(function(){
+                                       return value.call( this, i, old );
+                               });
+                       });
 
-                       // Keep track of which events have been used, for global triggering
-                       jQuery.event.global[type] = true;
-               });
+               } else {
+                       this.empty().append( value );
+               }
 
-               // Nullify elem to prevent memory leaks in IE
-               elem = null;
+               return this;
        },
 
-       guid: 1,
-       global: {},
+       replaceWith: function( value ) {
+               if ( this[0] && this[0].parentNode ) {
+                       // Make sure that the elements are removed from the DOM before they are inserted
+                       // this can help fix replacing a parent with child elements
+                       if ( !jQuery.isFunction( value ) ) {
+                               value = jQuery( value ).detach();
 
-       // Detach an event or set of events from an element
-       remove: function(elem, types, handler) {
-               // don't do events on text and comment nodes
-               if ( elem.nodeType == 3 || elem.nodeType == 8 )
-                       return;
+                       } else {
+                               return this.each(function(i) {
+                                       var self = jQuery(this), old = self.html();
+                                       self.replaceWith( value.call( this, i, old ) );
+                               });
+                       }
 
-               var events = jQuery.data(elem, "events"), ret, index;
+                       return this.each(function() {
+                               var next = this.nextSibling, parent = this.parentNode;
 
-               if ( events ) {
-                       // Unbind all events for the element
-                       if ( types === undefined || (typeof types === "string" && types.charAt(0) == ".") )
-                               for ( var type in events )
-                                       this.remove( elem, type + (types || "") );
-                       else {
-                               // types is actually an event object here
-                               if ( types.type ) {
-                                       handler = types.handler;
-                                       types = types.type;
+                               jQuery(this).remove();
+
+                               if ( next ) {
+                                       jQuery(next).before( value );
+                               } else {
+                                       jQuery(parent).append( value );
                                }
+                       });
+               } else {
+                       return this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value );
+               }
+       },
 
-                               // Handle multiple events seperated by a space
-                               // jQuery(...).unbind("mouseover mouseout", fn);
-                               jQuery.each(types.split(/\s+/), function(index, type){
-                                       // Namespaced event handlers
-                                       var namespaces = type.split(".");
-                                       type = namespaces.shift();
-                                       var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
+       detach: function( selector ) {
+               return this.remove( selector, true );
+       },
 
-                                       if ( events[type] ) {
-                                               // remove the given handler for the given type
-                                               if ( handler )
-                                                       delete events[type][handler.guid];
+       domManip: function( args, table, callback ) {
+               var results, first, value = args[0], scripts = [];
 
-                                               // remove all handlers for the given type
-                                               else
-                                                       for ( var handle in events[type] )
-                                                               // Handle the removal of namespaced events
-                                                               if ( namespace.test(events[type][handle].type) )
-                                                                       delete events[type][handle];
-                                                                       
-                                               if ( jQuery.event.specialAll[type] )
-                                                       jQuery.event.specialAll[type].teardown.call(elem, namespaces);
+               // We can't cloneNode fragments that contain checked, in WebKit
+               if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
+                       return this.each(function() {
+                               jQuery(this).domManip( args, table, callback, true );
+                       });
+               }
 
-                                               // remove generic event handler if no more handlers exist
-                                               for ( ret in events[type] ) break;
-                                               if ( !ret ) {
-                                                       if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem, namespaces) === false ) {
-                                                               if (elem.removeEventListener)
-                                                                       elem.removeEventListener(type, jQuery.data(elem, "handle"), false);
-                                                               else if (elem.detachEvent)
-                                                                       elem.detachEvent("on" + type, jQuery.data(elem, "handle"));
-                                                       }
-                                                       ret = null;
-                                                       delete events[type];
-                                               }
-                                       }
-                               });
-                       }
+               if ( jQuery.isFunction(value) ) {
+                       return this.each(function(i) {
+                               var self = jQuery(this);
+                               args[0] = value.call(this, i, table ? self.html() : undefined);
+                               self.domManip( args, table, callback );
+                       });
+               }
 
-                       // Remove the expando if it's no longer used
-                       for ( ret in events ) break;
-                       if ( !ret ) {
-                               var handle = jQuery.data( elem, "handle" );
-                               if ( handle ) handle.elem = null;
-                               jQuery.removeData( elem, "events" );
-                               jQuery.removeData( elem, "handle" );
+               if ( this[0] ) {
+                       // If we're in a fragment, just use that instead of building a new one
+                       if ( args[0] && args[0].parentNode && args[0].parentNode.nodeType === 11 ) {
+                               results = { fragment: args[0].parentNode };
+                       } else {
+                               results = buildFragment( args, this, scripts );
                        }
-               }
-       },
 
-       // bubbling is internal
-       trigger: function( event, data, elem, bubbling ) {
-               // Event object or event type
-               var type = event.type || event;
+                       first = results.fragment.firstChild;
 
-               if( !bubbling ){
-                       event = typeof event === "object" ?
-                               // jQuery.Event object
-                               event[expando] ? event :
-                               // Object literal
-                               jQuery.extend( jQuery.Event(type), event ) :
-                               // Just the event type (string)
-                               jQuery.Event(type);
+                       if ( first ) {
+                               table = table && jQuery.nodeName( first, "tr" );
 
-                       if ( type.indexOf("!") >= 0 ) {
-                               event.type = type = type.slice(0, -1);
-                               event.exclusive = true;
+                               for ( var i = 0, l = this.length; i < l; i++ ) {
+                                       callback.call(
+                                               table ?
+                                                       root(this[i], first) :
+                                                       this[i],
+                                               results.cacheable || this.length > 1 || i > 0 ?
+                                                       results.fragment.cloneNode(true) :
+                                                       results.fragment
+                                       );
+                               }
                        }
 
-                       // Handle a global trigger
-                       if ( !elem ) {
-                               // Don't bubble custom events when global (to avoid too much overhead)
-                               event.stopPropagation();
-                               // Only trigger if we've ever bound an event for it
-                               if ( this.global[type] )
-                                       jQuery.each( jQuery.cache, function(){
-                                               if ( this.events && this.events[type] )
-                                                       jQuery.event.trigger( event, data, this.handle.elem );
-                                       });
+                       if ( scripts ) {
+                               jQuery.each( scripts, evalScript );
                        }
-
-                       // Handle triggering a single element
-
-                       // don't do events on text and comment nodes
-                       if ( !elem || elem.nodeType == 3 || elem.nodeType == 8 )
-                               return undefined;
-                       
-                       // Clean up in case it is reused
-                       event.result = undefined;
-                       event.target = elem;
-                       
-                       // Clone the incoming data, if any
-                       data = jQuery.makeArray(data);
-                       data.unshift( event );
                }
 
-               event.currentTarget = elem;
+               return this;
 
-               // Trigger the event, it is assumed that "handle" is a function
-               var handle = jQuery.data(elem, "handle");
-               if ( handle )
-                       handle.apply( elem, data );
+               function root( elem, cur ) {
+                       return jQuery.nodeName(elem, "table") ?
+                               (elem.getElementsByTagName("tbody")[0] ||
+                               elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
+                               elem;
+               }
+       }
+});
 
-               // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)
-               if ( (!elem[type] || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )
-                       event.result = false;
+function cloneCopyEvent(orig, ret) {
+       var i = 0;
 
-               // Trigger the native events (except for clicks on links)
-               if ( !bubbling && elem[type] && !event.isDefaultPrevented() && !(jQuery.nodeName(elem, 'a') && type == "click") ) {
-                       this.triggered = true;
-                       try {
-                               elem[ type ]();
-                       // prevent IE from throwing an error for some hidden elements
-                       } catch (e) {}
+       ret.each(function() {
+               if ( this.nodeName !== (orig[i] && orig[i].nodeName) ) {
+                       return;
                }
 
-               this.triggered = false;
+               var oldData = jQuery.data( orig[i++] ), curData = jQuery.data( this, oldData ), events = oldData && oldData.events;
+
+               if ( events ) {
+                       delete curData.handle;
+                       curData.events = {};
 
-               if ( !event.isPropagationStopped() ) {
-                       var parent = elem.parentNode || elem.ownerDocument;
-                       if ( parent )
-                               jQuery.event.trigger(event, data, parent, true);
+                       for ( var type in events ) {
+                               for ( var handler in events[ type ] ) {
+                                       jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data );
+                               }
+                       }
                }
-       },
+       });
+}
 
-       handle: function(event) {
-               // returned undefined or false
-               var all, handlers;
+function buildFragment( args, nodes, scripts ) {
+       var fragment, cacheable, cacheresults, doc;
 
-               event = arguments[0] = jQuery.event.fix( event || window.event );
-               event.currentTarget = this;
-               
-               // Namespaced event handlers
-               var namespaces = event.type.split(".");
-               event.type = namespaces.shift();
+       // webkit does not clone 'checked' attribute of radio inputs on cloneNode, so don't cache if string has a checked
+       if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && args[0].indexOf("<option") < 0 && (jQuery.support.checkClone || !rchecked.test( args[0] )) ) {
+               cacheable = true;
+               cacheresults = jQuery.fragments[ args[0] ];
+               if ( cacheresults ) {
+                       if ( cacheresults !== 1 ) {
+                               fragment = cacheresults;
+                       }
+               }
+       }
 
-               // Cache this now, all = true means, any handler
-               all = !namespaces.length && !event.exclusive;
-               
-               var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
+       if ( !fragment ) {
+               doc = (nodes && nodes[0] ? nodes[0].ownerDocument || nodes[0] : document);
+               fragment = doc.createDocumentFragment();
+               jQuery.clean( args, doc, fragment, scripts );
+       }
 
-               handlers = ( jQuery.data(this, "events") || {} )[event.type];
+       if ( cacheable ) {
+               jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1;
+       }
 
-               for ( var j in handlers ) {
-                       var handler = handlers[j];
+       return { fragment: fragment, cacheable: cacheable };
+}
 
-                       // Filter the functions by class
-                       if ( all || namespace.test(handler.type) ) {
-                               // Pass in a reference to the handler function itself
-                               // So that we can later remove it
-                               event.handler = handler;
-                               event.data = handler.data;
+jQuery.fragments = {};
 
-                               var ret = handler.apply(this, arguments);
+jQuery.each({
+       appendTo: "append",
+       prependTo: "prepend",
+       insertBefore: "before",
+       insertAfter: "after",
+       replaceAll: "replaceWith"
+}, function( name, original ) {
+       jQuery.fn[ name ] = function( selector ) {
+               var ret = [], insert = jQuery( selector );
 
-                               if( ret !== undefined ){
-                                       event.result = ret;
-                                       if ( ret === false ) {
-                                               event.preventDefault();
-                                               event.stopPropagation();
-                                       }
-                               }
+               for ( var i = 0, l = insert.length; i < l; i++ ) {
+                       var elems = (i > 0 ? this.clone(true) : this).get();
+                       jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
+                       ret = ret.concat( elems );
+               }
+               return this.pushStack( ret, name, insert.selector );
+       };
+});
 
-                               if( event.isImmediatePropagationStopped() )
-                                       break;
+jQuery.each({
+       // keepData is for internal use only--do not document
+       remove: function( selector, keepData ) {
+               if ( !selector || jQuery.filter( selector, [ this ] ).length ) {
+                       if ( !keepData && this.nodeType === 1 ) {
+                               jQuery.cleanData( this.getElementsByTagName("*") );
+                               jQuery.cleanData( [ this ] );
+                       }
 
+                       if ( this.parentNode ) {
+                                this.parentNode.removeChild( this );
                        }
                }
        },
 
-       props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
+       empty: function() {
+               // Remove element nodes and prevent memory leaks
+               if ( this.nodeType === 1 ) {
+                       jQuery.cleanData( this.getElementsByTagName("*") );
+               }
 
-       fix: function(event) {
-               if ( event[expando] )
-                       return event;
+               // Remove any remaining nodes
+               while ( this.firstChild ) {
+                       this.removeChild( this.firstChild );
+               }
+       }
+}, function( name, fn ) {
+       jQuery.fn[ name ] = function() {
+               return this.each( fn, arguments );
+       };
+});
 
-               // store a copy of the original event object
-               // and "clone" to set read-only properties
-               var originalEvent = event;
-               event = jQuery.Event( originalEvent );
+jQuery.extend({
+       clean: function( elems, context, fragment, scripts ) {
+               context = context || document;
 
-               for ( var i = this.props.length, prop; i; ){
-                       prop = this.props[ --i ];
-                       event[ prop ] = originalEvent[ prop ];
+               // !context.createElement fails in IE with an error but returns typeof 'object'
+               if ( typeof context.createElement === "undefined" ) {
+                       context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
                }
 
-               // Fix target property, if necessary
-               if ( !event.target )
-                       event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
+               var ret = [];
 
-               // check if target is a textnode (safari)
-               if ( event.target.nodeType == 3 )
-                       event.target = event.target.parentNode;
+               jQuery.each(elems, function( i, elem ) {
+                       if ( typeof elem === "number" ) {
+                               elem += "";
+                       }
 
-               // Add relatedTarget, if necessary
-               if ( !event.relatedTarget && event.fromElement )
-                       event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
+                       if ( !elem ) {
+                               return;
+                       }
 
-               // Calculate pageX/Y if missing and clientX/Y available
-               if ( event.pageX == null && event.clientX != null ) {
-                       var doc = document.documentElement, body = document.body;
-                       event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
-                       event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
-               }
+                       // Convert html string into DOM nodes
+                       if ( typeof elem === "string" && !rhtml.test( elem ) ) {
+                               elem = context.createTextNode( elem );
 
-               // Add which for key events
-               if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) )
-                       event.which = event.charCode || event.keyCode;
+                       } else if ( typeof elem === "string" ) {
+                               // Fix "XHTML"-style tags in all browsers
+                               elem = elem.replace(rxhtmlTag, fcloseTag);
 
-               // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
-               if ( !event.metaKey && event.ctrlKey )
-                       event.metaKey = event.ctrlKey;
+                               // Trim whitespace, otherwise indexOf won't work as expected
+                               var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(),
+                                       wrap = wrapMap[ tag ] || wrapMap._default,
+                                       depth = wrap[0],
+                                       div = context.createElement("div");
 
-               // Add which for click: 1 == left; 2 == middle; 3 == right
-               // Note: button is not normalized, so don't use it
-               if ( !event.which && event.button )
-                       event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
+                               // Go to html and back, then peel off extra wrappers
+                               div.innerHTML = wrap[1] + elem + wrap[2];
 
-               return event;
-       },
+                               // Move to the right depth
+                               while ( depth-- ) {
+                                       div = div.lastChild;
+                               }
 
-       proxy: function( fn, proxy ){
-               proxy = proxy || function(){ return fn.apply(this, arguments); };
-               // Set the guid of unique handler to the same of original handler, so it can be removed
-               proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
-               // So proxy can be declared as an argument
-               return proxy;
-       },
+                               // Remove IE's autoinserted <tbody> from table fragments
+                               if ( !jQuery.support.tbody ) {
 
-       special: {
-               ready: {
-                       // Make sure the ready event is setup
-                       setup: bindReady,
-                       teardown: function() {}
-               }
-       },
-       
-       specialAll: {
-               live: {
-                       setup: function( selector, namespaces ){
-                               jQuery.event.add( this, namespaces[0], liveHandler );
-                       },
-                       teardown:  function( namespaces ){
-                               if ( namespaces.length ) {
-                                       var remove = 0, name = RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)");
-                                       
-                                       jQuery.each( (jQuery.data(this, "events").live || {}), function(){
-                                               if ( name.test(this.type) )
-                                                       remove++;
-                                       });
-                                       
-                                       if ( remove < 1 )
-                                               jQuery.event.remove( this, namespaces[0], liveHandler );
-                               }
-                       }
-               }
-       }
-};
+                                       // String was a <table>, *may* have spurious <tbody>
+                                       var hasBody = rtbody.test(elem),
+                                               tbody = tag === "table" && !hasBody ?
+                                                       div.firstChild && div.firstChild.childNodes :
 
-jQuery.Event = function( src ){
-       // Allow instantiation without the 'new' keyword
-       if( !this.preventDefault )
-               return new jQuery.Event(src);
-       
-       // Event object
-       if( src && src.type ){
-               this.originalEvent = src;
-               this.type = src.type;
-       // Event type
-       }else
-               this.type = src;
+                                                       // String was a bare <thead> or <tfoot>
+                                                       wrap[1] === "<table>" && !hasBody ?
+                                                               div.childNodes :
+                                                               [];
 
-       // timeStamp is buggy for some events on Firefox(#3843)
-       // So we won't rely on the native value
-       this.timeStamp = now();
-       
-       // Mark it as fixed
-       this[expando] = true;
-};
+                                       for ( var j = tbody.length - 1; j >= 0 ; --j ) {
+                                               if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
+                                                       tbody[ j ].parentNode.removeChild( tbody[ j ] );
+                                               }
+                                       }
 
-function returnFalse(){
-       return false;
-}
-function returnTrue(){
-       return true;
-}
+                               }
 
-// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
-// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
-jQuery.Event.prototype = {
-       preventDefault: function() {
-               this.isDefaultPrevented = returnTrue;
+                               // IE completely kills leading whitespace when innerHTML is used
+                               if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
+                                       div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
+                               }
 
-               var e = this.originalEvent;
-               if( !e )
-                       return;
-               // if preventDefault exists run it on the original event
-               if (e.preventDefault)
-                       e.preventDefault();
-               // otherwise set the returnValue property of the original event to false (IE)
-               e.returnValue = false;
-       },
-       stopPropagation: function() {
-               this.isPropagationStopped = returnTrue;
+                               elem = jQuery.makeArray( div.childNodes );
+                       }
 
-               var e = this.originalEvent;
-               if( !e )
-                       return;
-               // if stopPropagation exists run it on the original event
-               if (e.stopPropagation)
-                       e.stopPropagation();
-               // otherwise set the cancelBubble property of the original event to true (IE)
-               e.cancelBubble = true;
-       },
-       stopImmediatePropagation:function(){
-               this.isImmediatePropagationStopped = returnTrue;
-               this.stopPropagation();
-       },
-       isDefaultPrevented: returnFalse,
-       isPropagationStopped: returnFalse,
-       isImmediatePropagationStopped: returnFalse
-};
-// Checks if an event happened on an element within another element
-// Used in jQuery.event.special.mouseenter and mouseleave handlers
-var withinElement = function(event) {
-       // Check if mouse(over|out) are still within the same parent element
-       var parent = event.relatedTarget;
-       // Traverse up the tree
-       while ( parent && parent != this )
-               try { parent = parent.parentNode; }
-               catch(e) { parent = this; }
-       
-       if( parent != this ){
-               // set the correct event type
-               event.type = event.data;
-               // handle event if we actually just moused on to a non sub-element
-               jQuery.event.handle.apply( this, arguments );
-       }
-};
-       
-jQuery.each({ 
-       mouseover: 'mouseenter', 
-       mouseout: 'mouseleave'
-}, function( orig, fix ){
-       jQuery.event.special[ fix ] = {
-               setup: function(){
-                       jQuery.event.add( this, orig, withinElement, fix );
-               },
-               teardown: function(){
-                       jQuery.event.remove( this, orig, withinElement );
-               }
-       };                         
-});
+                       if ( elem.nodeType ) {
+                               ret.push( elem );
+                       } else {
+                               ret = jQuery.merge( ret, elem );
+                       }
 
-jQuery.fn.extend({
-       bind: function( type, data, fn ) {
-               return type == "unload" ? this.one(type, data, fn) : this.each(function(){
-                       jQuery.event.add( this, type, fn || data, fn && data );
                });
-       },
 
-       one: function( type, data, fn ) {
-               var one = jQuery.event.proxy( fn || data, function(event) {
-                       jQuery(this).unbind(event, one);
-                       return (fn || data).apply( this, arguments );
-               });
-               return this.each(function(){
-                       jQuery.event.add( this, type, one, fn && data);
-               });
-       },
+               if ( fragment ) {
+                       for ( var i = 0; ret[i]; i++ ) {
+                               if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
+                                       scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
+                               } else {
+                                       if ( ret[i].nodeType === 1 ) {
+                                               ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
+                                       }
+                                       fragment.appendChild( ret[i] );
+                               }
+                       }
+               }
 
-       unbind: function( type, fn ) {
-               return this.each(function(){
-                       jQuery.event.remove( this, type, fn );
-               });
+               return ret;
        },
+       
+       cleanData: function( elems ) {
+               for ( var i = 0, elem, id; (elem = elems[i]) != null; i++ ) {
+                       jQuery.event.remove( elem );
+                       jQuery.removeData( elem );
+               }
+       }
+});
+// exclude the following css properties to add px
+var rexclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
+       ralpha = /alpha\([^)]*\)/,
+       ropacity = /opacity=([^)]*)/,
+       rfloat = /float/i,
+       rdashAlpha = /-([a-z])/ig,
+       rupper = /([A-Z])/g,
+       rnumpx = /^-?\d+(?:px)?$/i,
+       rnum = /^-?\d/,
+
+       cssShow = { position: "absolute", visibility: "hidden", display:"block" },
+       cssWidth = [ "Left", "Right" ],
+       cssHeight = [ "Top", "Bottom" ],
+
+       // cache check for defaultView.getComputedStyle
+       getComputedStyle = document.defaultView && document.defaultView.getComputedStyle,
+       // normalize float css property
+       styleFloat = jQuery.support.cssFloat ? "cssFloat" : "styleFloat",
+       fcamelCase = function( all, letter ) {
+               return letter.toUpperCase();
+       };
 
-       trigger: function( type, data ) {
-               return this.each(function(){
-                       jQuery.event.trigger( type, data, this );
-               });
-       },
+jQuery.fn.css = function( name, value ) {
+       return access( this, name, value, true, function( elem, name, value ) {
+               if ( value === undefined ) {
+                       return jQuery.curCSS( elem, name );
+               }
+               
+               if ( typeof value === "number" && !rexclude.test(name) ) {
+                       value += "px";
+               }
 
-       triggerHandler: function( type, data ) {
-               if( this[0] ){
-                       var event = jQuery.Event(type);
-                       event.preventDefault();
-                       event.stopPropagation();
-                       jQuery.event.trigger( event, data, this[0] );
-                       return event.result;
-               }               
-       },
+               jQuery.style( elem, name, value );
+       });
+};
 
-       toggle: function( fn ) {
-               // Save reference to arguments for access in closure
-               var args = arguments, i = 1;
+jQuery.extend({
+       style: function( elem, name, value ) {
+               // don't set styles on text and comment nodes
+               if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
+                       return undefined;
+               }
 
-               // link all the functions, so any of them can unbind this click handler
-               while( i < args.length )
-                       jQuery.event.proxy( fn, args[i++] );
+               // ignore negative width and height values #1599
+               if ( (name === "width" || name === "height") && parseFloat(value) < 0 ) {
+                       value = undefined;
+               }
 
-               return this.click( jQuery.event.proxy( fn, function(event) {
-                       // Figure out which function to execute
-                       this.lastToggle = ( this.lastToggle || 0 ) % i;
+               var style = elem.style || elem, set = value !== undefined;
 
-                       // Make sure that clicks stop
-                       event.preventDefault();
+               // IE uses filters for opacity
+               if ( !jQuery.support.opacity && name === "opacity" ) {
+                       if ( set ) {
+                               // IE has trouble with opacity if it does not have layout
+                               // Force it by setting the zoom level
+                               style.zoom = 1;
 
-                       // and execute the function
-                       return args[ this.lastToggle++ ].apply( this, arguments ) || false;
-               }));
-       },
+                               // Set the alpha filter to set the opacity
+                               var opacity = parseInt( value, 10 ) + "" === "NaN" ? "" : "alpha(opacity=" + value * 100 + ")";
+                               var filter = style.filter || jQuery.curCSS( elem, "filter" ) || "";
+                               style.filter = ralpha.test(filter) ? filter.replace(ralpha, opacity) : opacity;
+                       }
 
-       hover: function(fnOver, fnOut) {
-               return this.mouseenter(fnOver).mouseleave(fnOut);
-       },
+                       return style.filter && style.filter.indexOf("opacity=") >= 0 ?
+                               (parseFloat( ropacity.exec(style.filter)[1] ) / 100) + "":
+                               "";
+               }
 
-       ready: function(fn) {
-               // Attach the listeners
-               bindReady();
+               // Make sure we're using the right name for getting the float value
+               if ( rfloat.test( name ) ) {
+                       name = styleFloat;
+               }
 
-               // If the DOM is already ready
-               if ( jQuery.isReady )
-                       // Execute the function immediately
-                       fn.call( document, jQuery );
+               name = name.replace(rdashAlpha, fcamelCase);
 
-               // Otherwise, remember the function for later
-               else
-                       // Add the function to the wait list
-                       jQuery.readyList.push( fn );
+               if ( set ) {
+                       style[ name ] = value;
+               }
 
-               return this;
+               return style[ name ];
        },
-       
-       live: function( type, fn ){
-               var proxy = jQuery.event.proxy( fn );
-               proxy.guid += this.selector + type;
 
-               jQuery(document).bind( liveConvert(type, this.selector), this.selector, proxy );
+       css: function( elem, name, force, extra ) {
+               if ( name === "width" || name === "height" ) {
+                       var val, props = cssShow, which = name === "width" ? cssWidth : cssHeight;
 
-               return this;
-       },
-       
-       die: function( type, fn ){
-               jQuery(document).unbind( liveConvert(type, this.selector), fn ? { guid: fn.guid + this.selector + type } : null );
-               return this;
-       }
-});
+                       function getWH() {
+                               val = name === "width" ? elem.offsetWidth : elem.offsetHeight;
 
-function liveHandler( event ){
-       var check = RegExp("(^|\\.)" + event.type + "(\\.|$)"),
-               stop = true,
-               elems = [];
+                               if ( extra === "border" ) {
+                                       return;
+                               }
 
-       jQuery.each(jQuery.data(this, "events").live || [], function(i, fn){
-               if ( check.test(fn.type) ) {
-                       var elem = jQuery(event.target).closest(fn.data)[0];
-                       if ( elem )
-                               elems.push({ elem: elem, fn: fn });
-               }
-       });
+                               jQuery.each( which, function() {
+                                       if ( !extra ) {
+                                               val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
+                                       }
 
-       elems.sort(function(a,b) {
-               return jQuery.data(a.elem, "closest") - jQuery.data(b.elem, "closest");
-       });
-       
-       jQuery.each(elems, function(){
-               if ( this.fn.call(this.elem, event, this.fn.data) === false )
-                       return (stop = false);
-       });
+                                       if ( extra === "margin" ) {
+                                               val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0;
+                                       } else {
+                                               val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
+                                       }
+                               });
+                       }
 
-       return stop;
-}
+                       if ( elem.offsetWidth !== 0 ) {
+                               getWH();
+                       } else {
+                               jQuery.swap( elem, props, getWH );
+                       }
 
-function liveConvert(type, selector){
-       return ["live", type, selector.replace(/\./g, "`").replace(/ /g, "|")].join(".");
-}
+                       return Math.max(0, Math.round(val));
+               }
 
-jQuery.extend({
-       isReady: false,
-       readyList: [],
-       // Handle when the DOM is ready
-       ready: function() {
-               // Make sure that the DOM is not already loaded
-               if ( !jQuery.isReady ) {
-                       // Remember that the DOM is ready
-                       jQuery.isReady = true;
+               return jQuery.curCSS( elem, name, force );
+       },
 
-                       // If there are functions bound, to execute
-                       if ( jQuery.readyList ) {
-                               // Execute all of them
-                               jQuery.each( jQuery.readyList, function(){
-                                       this.call( document, jQuery );
-                               });
+       curCSS: function( elem, name, force ) {
+               var ret, style = elem.style, filter;
 
-                               // Reset the list of functions
-                               jQuery.readyList = null;
-                       }
+               // IE uses filters for opacity
+               if ( !jQuery.support.opacity && name === "opacity" && elem.currentStyle ) {
+                       ret = ropacity.test(elem.currentStyle.filter || "") ?
+                               (parseFloat(RegExp.$1) / 100) + "" :
+                               "";
 
-                       // Trigger any bound ready events
-                       jQuery(document).triggerHandler("ready");
+                       return ret === "" ?
+                               "1" :
+                               ret;
                }
-       }
-});
 
-var readyBound = false;
+               // Make sure we're using the right name for getting the float value
+               if ( rfloat.test( name ) ) {
+                       name = styleFloat;
+               }
 
-function bindReady(){
-       if ( readyBound ) return;
-       readyBound = true;
+               if ( !force && style && style[ name ] ) {
+                       ret = style[ name ];
 
-       // Mozilla, Opera and webkit nightlies currently support this event
-       if ( document.addEventListener ) {
-               // Use the handy event callback
-               document.addEventListener( "DOMContentLoaded", function(){
-                       document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
-                       jQuery.ready();
-               }, false );
+               } else if ( getComputedStyle ) {
 
-       // If IE event model is used
-       } else if ( document.attachEvent ) {
-               // ensure firing before onload,
-               // maybe late but safe also for iframes
-               document.attachEvent("onreadystatechange", function(){
-                       if ( document.readyState === "complete" ) {
-                               document.detachEvent( "onreadystatechange", arguments.callee );
-                               jQuery.ready();
+                       // Only "float" is needed here
+                       if ( rfloat.test( name ) ) {
+                               name = "float";
                        }
-               });
 
-               // If IE and not an iframe
-               // continually check to see if the document is ready
-               if ( document.documentElement.doScroll && window == window.top ) (function(){
-                       if ( jQuery.isReady ) return;
+                       name = name.replace( rupper, "-$1" ).toLowerCase();
 
-                       try {
-                               // If IE is used, use the trick by Diego Perini
-                               // http://javascript.nwbox.com/IEContentLoaded/
-                               document.documentElement.doScroll("left");
-                       } catch( error ) {
-                               setTimeout( arguments.callee, 0 );
-                               return;
-                       }
+                       var defaultView = elem.ownerDocument.defaultView;
 
-                       // and execute any waiting functions
-                       jQuery.ready();
-               })();
-       }
+                       if ( !defaultView ) {
+                               return null;
+                       }
 
-       // A fallback to window.onload, that will always work
-       jQuery.event.add( window, "load", jQuery.ready );
-}
+                       var computedStyle = defaultView.getComputedStyle( elem, null );
 
-jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
-       "mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave," +
-       "change,select,submit,keydown,keypress,keyup,error").split(","), function(i, name){
+                       if ( computedStyle ) {
+                               ret = computedStyle.getPropertyValue( name );
+                       }
 
-       // Handle event binding
-       jQuery.fn[name] = function(fn){
-               return fn ? this.bind(name, fn) : this.trigger(name);
-       };
-});
+                       // We should always get a number back from opacity
+                       if ( name === "opacity" && ret === "" ) {
+                               ret = "1";
+                       }
 
-// Prevent memory leaks in IE
-// And prevent errors on refresh with events like mouseover in other browsers
-// Window isn't included so as not to unbind existing unload events
-jQuery( window ).bind( 'unload', function(){ 
-       for ( var id in jQuery.cache )
-               // Skip the window
-               if ( id != 1 && jQuery.cache[ id ].handle )
-                       jQuery.event.remove( jQuery.cache[ id ].handle.elem );
-}); 
-(function(){
+               } else if ( elem.currentStyle ) {
+                       var camelCase = name.replace(rdashAlpha, fcamelCase);
 
-       jQuery.support = {};
+                       ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
 
-       var root = document.documentElement,
-               script = document.createElement("script"),
-               div = document.createElement("div"),
-               id = "script" + (new Date).getTime();
+                       // From the awesome hack by Dean Edwards
+                       // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
 
-       div.style.display = "none";
-       div.innerHTML = '   <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';
+                       // If we're not dealing with a regular pixel number
+                       // but a number that has a weird ending, we need to convert it to pixels
+                       if ( !rnumpx.test( ret ) && rnum.test( ret ) ) {
+                               // Remember the original values
+                               var left = style.left, rsLeft = elem.runtimeStyle.left;
 
-       var all = div.getElementsByTagName("*"),
-               a = div.getElementsByTagName("a")[0];
+                               // Put in the new values to get a computed value out
+                               elem.runtimeStyle.left = elem.currentStyle.left;
+                               style.left = camelCase === "fontSize" ? "1em" : (ret || 0);
+                               ret = style.pixelLeft + "px";
 
-       // Can't get basic test support
-       if ( !all || !all.length || !a ) {
-               return;
-       }
+                               // Revert the changed values
+                               style.left = left;
+                               elem.runtimeStyle.left = rsLeft;
+                       }
+               }
 
-       jQuery.support = {
-               // IE strips leading whitespace when .innerHTML is used
-               leadingWhitespace: div.firstChild.nodeType == 3,
-               
-               // Make sure that tbody elements aren't automatically inserted
-               // IE will insert them into empty tables
-               tbody: !div.getElementsByTagName("tbody").length,
-               
-               // Make sure that you can get all elements in an <object> element
-               // IE 7 always returns no results
-               objectAll: !!div.getElementsByTagName("object")[0]
-                       .getElementsByTagName("*").length,
-               
-               // Make sure that link elements get serialized correctly by innerHTML
-               // This requires a wrapper element in IE
-               htmlSerialize: !!div.getElementsByTagName("link").length,
-               
-               // Get the style information from getAttribute
-               // (IE uses .cssText insted)
-               style: /red/.test( a.getAttribute("style") ),
-               
-               // Make sure that URLs aren't manipulated
-               // (IE normalizes it by default)
-               hrefNormalized: a.getAttribute("href") === "/a",
-               
-               // Make sure that element opacity exists
-               // (IE uses filter instead)
-               opacity: a.style.opacity === "0.5",
-               
-               // Verify style float existence
-               // (IE uses styleFloat instead of cssFloat)
-               cssFloat: !!a.style.cssFloat,
+               return ret;
+       },
 
-               // Will be defined later
-               scriptEval: false,
-               noCloneEvent: true,
-               boxModel: null
-       };
-       
-       script.type = "text/javascript";
-       try {
-               script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
-       } catch(e){}
+       // A method for quickly swapping in/out CSS properties to get correct calculations
+       swap: function( elem, options, callback ) {
+               var old = {};
 
-       root.insertBefore( script, root.firstChild );
-       
-       // Make sure that the execution of code works by injecting a script
-       // tag with appendChild/createTextNode
-       // (IE doesn't support this, fails, and uses .text instead)
-       if ( window[ id ] ) {
-               jQuery.support.scriptEval = true;
-               delete window[ id ];
-       }
+               // Remember the old values, and insert the new ones
+               for ( var name in options ) {
+                       old[ name ] = elem.style[ name ];
+                       elem.style[ name ] = options[ name ];
+               }
 
-       root.removeChild( script );
+               callback.call( elem );
 
-       if ( div.attachEvent && div.fireEvent ) {
-               div.attachEvent("onclick", function(){
-                       // Cloning a node shouldn't copy over any
-                       // bound event handlers (IE does this)
-                       jQuery.support.noCloneEvent = false;
-                       div.detachEvent("onclick", arguments.callee);
-               });
-               div.cloneNode(true).fireEvent("onclick");
+               // Revert the old values
+               for ( var name in options ) {
+                       elem.style[ name ] = old[ name ];
+               }
        }
+});
 
-       // Figure out if the W3C box model works as expected
-       // document.body must exist before we can do this
-       jQuery(function(){
-               var div = document.createElement("div");
-               div.style.width = div.style.paddingLeft = "1px";
+if ( jQuery.expr && jQuery.expr.filters ) {
+       jQuery.expr.filters.hidden = function( elem ) {
+               var width = elem.offsetWidth, height = elem.offsetHeight,
+                       skip = elem.nodeName.toLowerCase() === "tr";
 
-               document.body.appendChild( div );
-               jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2;
-               document.body.removeChild( div ).style.display = 'none';
-       });
-})();
+               return width === 0 && height === 0 && !skip ?
+                       true :
+                       width > 0 && height > 0 && !skip ?
+                               false :
+                               jQuery.curCSS(elem, "display") === "none";
+       };
 
-var styleFloat = jQuery.support.cssFloat ? "cssFloat" : "styleFloat";
+       jQuery.expr.filters.visible = function( elem ) {
+               return !jQuery.expr.filters.hidden( elem );
+       };
+}
+var jsc = now(),
+       rscript = /<script(.|\s)*?\/script>/gi,
+       rselectTextarea = /select|textarea/i,
+       rinput = /color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,
+       jsre = /=\?(&|$)/,
+       rquery = /\?/,
+       rts = /(\?|&)_=.*?(&|$)/,
+       rurl = /^(\w+:)?\/\/([^\/?#]+)/,
+       r20 = /%20/g;
 
-jQuery.props = {
-       "for": "htmlFor",
-       "class": "className",
-       "float": styleFloat,
-       cssFloat: styleFloat,
-       styleFloat: styleFloat,
-       readonly: "readOnly",
-       maxlength: "maxLength",
-       cellspacing: "cellSpacing",
-       rowspan: "rowSpan",
-       tabindex: "tabIndex"
-};
 jQuery.fn.extend({
        // Keep a copy of the old load
        _load: jQuery.fn.load,
 
        load: function( url, params, callback ) {
-               if ( typeof url !== "string" )
+               if ( typeof url !== "string" ) {
                        return this._load( url );
 
+               // Don't do a request if no elements are being requested
+               } else if ( !this.length ) {
+                       return this;
+               }
+
                var off = url.indexOf(" ");
                if ( off >= 0 ) {
                        var selector = url.slice(off, url.length);
@@ -3239,7 +4639,7 @@ jQuery.fn.extend({
                var type = "GET";
 
                // If the second parameter was provided
-               if ( params )
+               if ( params ) {
                        // If it's a function
                        if ( jQuery.isFunction( params ) ) {
                                // We assume that it's the callback
@@ -3247,10 +4647,11 @@ jQuery.fn.extend({
                                params = null;
 
                        // Otherwise, build a param string
-                       } else if( typeof params === "object" ) {
-                               params = jQuery.param( params );
+                       } else if ( typeof params === "object" ) {
+                               params = jQuery.param( params, jQuery.ajaxSettings.traditional );
                                type = "POST";
                        }
+               }
 
                var self = this;
 
@@ -3260,27 +4661,30 @@ jQuery.fn.extend({
                        type: type,
                        dataType: "html",
                        data: params,
-                       complete: function(res, status){
+                       complete: function( res, status ) {
                                // If successful, inject the HTML into all the matched elements
-                               if ( status == "success" || status == "notmodified" )
+                               if ( status === "success" || status === "notmodified" ) {
                                        // See if a selector was specified
                                        self.html( selector ?
                                                // Create a dummy div to hold the results
-                                               jQuery("<div/>")
+                                               jQuery("<div />")
                                                        // inject the contents of the document in, removing the scripts
                                                        // to avoid any 'Permission Denied' errors in IE
-                                                       .append(res.responseText.replace(/<script(.|\s)*?\/script>/g, ""))
+                                                       .append(res.responseText.replace(rscript, ""))
 
                                                        // Locate the specified elements
                                                        .find(selector) :
 
                                                // If not, just inject the full result
                                                res.responseText );
+                               }
 
-                               if( callback )
+                               if ( callback ) {
                                        self.each( callback, [res.responseText, status, res] );
+                               }
                        }
                });
+
                return this;
        },
 
@@ -3288,40 +4692,41 @@ jQuery.fn.extend({
                return jQuery.param(this.serializeArray());
        },
        serializeArray: function() {
-               return this.map(function(){
+               return this.map(function() {
                        return this.elements ? jQuery.makeArray(this.elements) : this;
                })
-               .filter(function(){
+               .filter(function() {
                        return this.name && !this.disabled &&
-                               (this.checked || /select|textarea/i.test(this.nodeName) ||
-                                       /text|hidden|password|search/i.test(this.type));
+                               (this.checked || rselectTextarea.test(this.nodeName) ||
+                                       rinput.test(this.type));
                })
-               .map(function(i, elem){
+               .map(function( i, elem ) {
                        var val = jQuery(this).val();
-                       return val == null ? null :
+
+                       return val == null ?
+                               null :
                                jQuery.isArray(val) ?
-                                       jQuery.map( val, function(val, i){
-                                               return {name: elem.name, value: val};
+                                       jQuery.map( val, function( val, i ) {
+                                               return { name: elem.name, value: val };
                                        }) :
-                                       {name: elem.name, value: val};
+                                       { name: elem.name, value: val };
                }).get();
        }
 });
 
 // Attach a bunch of functions for handling common AJAX events
-jQuery.each( "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","), function(i,o){
-       jQuery.fn[o] = function(f){
+jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), function( i, o ) {
+       jQuery.fn[o] = function( f ) {
                return this.bind(o, f);
        };
 });
 
-var jsc = now();
-
 jQuery.extend({
-  
+
        get: function( url, data, callback, type ) {
-               // shift arguments if data argument was ommited
+               // shift arguments if data argument was omited
                if ( jQuery.isFunction( data ) ) {
+                       type = type || callback;
                        callback = data;
                        data = null;
                }
@@ -3344,7 +4749,9 @@ jQuery.extend({
        },
 
        post: function( url, data, callback, type ) {
+               // shift arguments if data argument was omited
                if ( jQuery.isFunction( data ) ) {
+                       type = type || callback;
                        callback = data;
                        data = {};
                }
@@ -3374,13 +4781,21 @@ jQuery.extend({
                data: null,
                username: null,
                password: null,
+               traditional: false,
                */
                // Create the request object; Microsoft failed to properly
-               // implement the XMLHttpRequest in IE7, so we use the ActiveXObject when it is available
+               // implement the XMLHttpRequest in IE7 (can't request local files),
+               // so we use the ActiveXObject when it is available
                // This function can be overriden by calling jQuery.ajaxSetup
-               xhr:function(){
-                       return window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
-               },
+               xhr: window.XMLHttpRequest && (window.location.protocol !== "file:" || !window.ActiveXObject) ?
+                       function() {
+                               return new window.XMLHttpRequest();
+                       } :
+                       function() {
+                               try {
+                                       return new window.ActiveXObject("Microsoft.XMLHTTP");
+                               } catch(e) {}
+                       },
                accepts: {
                        xml: "application/xml, text/xml",
                        html: "text/html",
@@ -3393,36 +4808,41 @@ jQuery.extend({
 
        // Last-Modified header cache for next request
        lastModified: {},
+       etag: {},
 
-       ajax: function( s ) {
-               // Extend the settings, but re-extend 's' so that it can be
-               // checked again later (in the test suite, specifically)
-               s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));
-
-               var jsonp, jsre = /=\?(&|$)/g, status, data,
+       ajax: function( origSettings ) {
+               var s = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings);
+               
+               var jsonp, status, data,
+                       callbackContext = origSettings && origSettings.context || s,
                        type = s.type.toUpperCase();
 
                // convert data if not already a string
-               if ( s.data && s.processData && typeof s.data !== "string" )
-                       s.data = jQuery.param(s.data);
+               if ( s.data && s.processData && typeof s.data !== "string" ) {
+                       s.data = jQuery.param( s.data, s.traditional );
+               }
 
                // Handle JSONP Parameter Callbacks
-               if ( s.dataType == "jsonp" ) {
-                       if ( type == "GET" ) {
-                               if ( !s.url.match(jsre) )
-                                       s.url += (s.url.match(/\?/) ? "&" : "?") + (s.jsonp || "callback") + "=?";
-                       } else if ( !s.data || !s.data.match(jsre) )
+               if ( s.dataType === "jsonp" ) {
+                       if ( type === "GET" ) {
+                               if ( !jsre.test( s.url ) ) {
+                                       s.url += (rquery.test( s.url ) ? "&" : "?") + (s.jsonp || "callback") + "=?";
+                               }
+                       } else if ( !s.data || !jsre.test(s.data) ) {
                                s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
+                       }
                        s.dataType = "json";
                }
 
                // Build temporary JSONP function
-               if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) {
-                       jsonp = "jsonp" + jsc++;
+               if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) {
+                       jsonp = s.jsonpCallback || ("jsonp" + jsc++);
 
                        // Replace the =? sequence both in the query string and the data
-                       if ( s.data )
+                       if ( s.data ) {
                                s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
+                       }
+
                        s.url = s.url.replace(jsre, "=" + jsonp + "$1");
 
                        // We need to make sure
@@ -3430,75 +4850,85 @@ jQuery.extend({
                        s.dataType = "script";
 
                        // Handle JSONP-style loading
-                       window[ jsonp ] = function(tmp){
+                       window[ jsonp ] = window[ jsonp ] || function( tmp ) {
                                data = tmp;
                                success();
                                complete();
                                // Garbage collect
                                window[ jsonp ] = undefined;
-                               try{ delete window[ jsonp ]; } catch(e){}
-                               if ( head )
+
+                               try {
+                                       delete window[ jsonp ];
+                               } catch(e) {}
+
+                               if ( head ) {
                                        head.removeChild( script );
+                               }
                        };
                }
 
-               if ( s.dataType == "script" && s.cache == null )
+               if ( s.dataType === "script" && s.cache === null ) {
                        s.cache = false;
+               }
 
-               if ( s.cache === false && type == "GET" ) {
+               if ( s.cache === false && type === "GET" ) {
                        var ts = now();
+
                        // try replacing _= if it is there
-                       var ret = s.url.replace(/(\?|&)_=.*?(&|$)/, "$1_=" + ts + "$2");
+                       var ret = s.url.replace(rts, "$1_=" + ts + "$2");
+
                        // if nothing was replaced, add timestamp to the end
-                       s.url = ret + ((ret == s.url) ? (s.url.match(/\?/) ? "&" : "?") + "_=" + ts : "");
+                       s.url = ret + ((ret === s.url) ? (rquery.test(s.url) ? "&" : "?") + "_=" + ts : "");
                }
 
                // If data is available, append data to url for get requests
-               if ( s.data && type == "GET" ) {
-                       s.url += (s.url.match(/\?/) ? "&" : "?") + s.data;
-
-                       // IE likes to send both get and post data, prevent this
-                       s.data = null;
+               if ( s.data && type === "GET" ) {
+                       s.url += (rquery.test(s.url) ? "&" : "?") + s.data;
                }
 
                // Watch for a new set of requests
-               if ( s.global && ! jQuery.active++ )
+               if ( s.global && ! jQuery.active++ ) {
                        jQuery.event.trigger( "ajaxStart" );
+               }
 
                // Matches an absolute URL, and saves the domain
-               var parts = /^(\w+:)?\/\/([^\/?#]+)/.exec( s.url );
+               var parts = rurl.exec( s.url ),
+                       remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
 
                // If we're requesting a remote document
                // and trying to load JSON or Script with a GET
-               if ( s.dataType == "script" && type == "GET" && parts
-                       && ( parts[1] && parts[1] != location.protocol || parts[2] != location.host )){
-
-                       var head = document.getElementsByTagName("head")[0];
+               if ( s.dataType === "script" && type === "GET" && remote ) {
+                       var head = document.getElementsByTagName("head")[0] || document.documentElement;
                        var script = document.createElement("script");
                        script.src = s.url;
-                       if (s.scriptCharset)
+                       if ( s.scriptCharset ) {
                                script.charset = s.scriptCharset;
+                       }
 
                        // Handle Script loading
                        if ( !jsonp ) {
                                var done = false;
 
                                // Attach handlers for all browsers
-                               script.onload = script.onreadystatechange = function(){
+                               script.onload = script.onreadystatechange = function() {
                                        if ( !done && (!this.readyState ||
-                                                       this.readyState == "loaded" || this.readyState == "complete") ) {
+                                                       this.readyState === "loaded" || this.readyState === "complete") ) {
                                                done = true;
                                                success();
                                                complete();
 
                                                // Handle memory leak in IE
                                                script.onload = script.onreadystatechange = null;
-                                               head.removeChild( script );
+                                               if ( head && script.parentNode ) {
+                                                       head.removeChild( script );
+                                               }
                                        }
                                };
                        }
 
-                       head.appendChild(script);
+                       // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
+                       // This arises when a base node is used (#2709 and #4378).
+                       head.insertBefore( script, head.firstChild );
 
                        // We handle everything using the script element injection
                        return undefined;
@@ -3509,158 +4939,197 @@ jQuery.extend({
                // Create the request object
                var xhr = s.xhr();
 
+               if ( !xhr ) {
+                       return;
+               }
+
                // Open the socket
                // Passing null username, generates a login popup on Opera (#2865)
-               if( s.username )
+               if ( s.username ) {
                        xhr.open(type, s.url, s.async, s.username, s.password);
-               else
+               } else {
                        xhr.open(type, s.url, s.async);
+               }
 
                // Need an extra try/catch for cross domain requests in Firefox 3
                try {
                        // Set the correct header, if data is being sent
-                       if ( s.data )
+                       if ( s.data || origSettings && origSettings.contentType ) {
                                xhr.setRequestHeader("Content-Type", s.contentType);
+                       }
+
+                       // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+                       if ( s.ifModified ) {
+                               if ( jQuery.lastModified[s.url] ) {
+                                       xhr.setRequestHeader("If-Modified-Since", jQuery.lastModified[s.url]);
+                               }
 
-                       // Set the If-Modified-Since header, if ifModified mode.
-                       if ( s.ifModified )
-                               xhr.setRequestHeader("If-Modified-Since",
-                                       jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );
+                               if ( jQuery.etag[s.url] ) {
+                                       xhr.setRequestHeader("If-None-Match", jQuery.etag[s.url]);
+                               }
+                       }
 
                        // Set header so the called script knows that it's an XMLHttpRequest
-                       xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+                       // Only send the header if it's not a remote XHR
+                       if ( !remote ) {
+                               xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+                       }
 
                        // Set the Accepts header for the server, depending on the dataType
                        xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
                                s.accepts[ s.dataType ] + ", */*" :
                                s.accepts._default );
-               } catch(e){}
+               } catch(e) {}
 
                // Allow custom headers/mimetypes and early abort
-               if ( s.beforeSend && s.beforeSend(xhr, s) === false ) {
+               if ( s.beforeSend && s.beforeSend.call(callbackContext, xhr, s) === false ) {
                        // Handle the global AJAX counter
-                       if ( s.global && ! --jQuery.active )
+                       if ( s.global && ! --jQuery.active ) {
                                jQuery.event.trigger( "ajaxStop" );
+                       }
+
                        // close opended socket
                        xhr.abort();
                        return false;
                }
 
-               if ( s.global )
-                       jQuery.event.trigger("ajaxSend", [xhr, s]);
+               if ( s.global ) {
+                       trigger("ajaxSend", [xhr, s]);
+               }
 
                // Wait for a response to come back
-               var onreadystatechange = function(isTimeout){
-                       // The request was aborted, clear the interval and decrement jQuery.active
-                       if (xhr.readyState == 0) {
-                               if (ival) {
-                                       // clear poll interval
-                                       clearInterval(ival);
-                                       ival = null;
-                                       // Handle the global AJAX counter
-                                       if ( s.global && ! --jQuery.active )
-                                               jQuery.event.trigger( "ajaxStop" );
+               var onreadystatechange = xhr.onreadystatechange = function( isTimeout ) {
+                       // The request was aborted
+                       if ( !xhr || xhr.readyState === 0 || isTimeout === "abort" ) {
+                               // Opera doesn't call onreadystatechange before this point
+                               // so we simulate the call
+                               if ( !requestDone ) {
+                                       complete();
+                               }
+
+                               requestDone = true;
+                               if ( xhr ) {
+                                       xhr.onreadystatechange = jQuery.noop;
                                }
+
                        // The transfer is complete and the data is available, or the request timed out
-                       } else if ( !requestDone && xhr && (xhr.readyState == 4 || isTimeout == "timeout") ) {
+                       } else if ( !requestDone && xhr && (xhr.readyState === 4 || isTimeout === "timeout") ) {
                                requestDone = true;
+                               xhr.onreadystatechange = jQuery.noop;
 
-                               // clear poll interval
-                               if (ival) {
-                                       clearInterval(ival);
-                                       ival = null;
-                               }
+                               status = isTimeout === "timeout" ?
+                                       "timeout" :
+                                       !jQuery.httpSuccess( xhr ) ?
+                                               "error" :
+                                               s.ifModified && jQuery.httpNotModified( xhr, s.url ) ?
+                                                       "notmodified" :
+                                                       "success";
 
-                               status = isTimeout == "timeout" ? "timeout" :
-                                       !jQuery.httpSuccess( xhr ) ? "error" :
-                                       s.ifModified && jQuery.httpNotModified( xhr, s.url ) ? "notmodified" :
-                                       "success";
+                               var errMsg;
 
-                               if ( status == "success" ) {
+                               if ( status === "success" ) {
                                        // Watch for, and catch, XML document parse errors
                                        try {
                                                // process the data (runs the xml through httpData regardless of callback)
                                                data = jQuery.httpData( xhr, s.dataType, s );
-                                       } catch(e) {
+                                       } catch(err) {
                                                status = "parsererror";
+                                               errMsg = err;
                                        }
                                }
 
                                // Make sure that the request was successful or notmodified
-                               if ( status == "success" ) {
-                                       // Cache Last-Modified header, if ifModified mode.
-                                       var modRes;
-                                       try {
-                                               modRes = xhr.getResponseHeader("Last-Modified");
-                                       } catch(e) {} // swallow exception thrown by FF if header is not available
-
-                                       if ( s.ifModified && modRes )
-                                               jQuery.lastModified[s.url] = modRes;
-
+                               if ( status === "success" || status === "notmodified" ) {
                                        // JSONP handles its own success callback
-                                       if ( !jsonp )
+                                       if ( !jsonp ) {
                                                success();
-                               } else
-                                       jQuery.handleError(s, xhr, status);
+                                       }
+                               } else {
+                                       jQuery.handleError(s, xhr, status, errMsg);
+                               }
 
                                // Fire the complete handlers
                                complete();
 
-                               if ( isTimeout )
+                               if ( isTimeout === "timeout" ) {
                                        xhr.abort();
+                               }
 
                                // Stop memory leaks
-                               if ( s.async )
+                               if ( s.async ) {
                                        xhr = null;
+                               }
                        }
                };
 
-               if ( s.async ) {
-                       // don't attach the handler to the request, just poll it instead
-                       var ival = setInterval(onreadystatechange, 13);
+               // Override the abort handler, if we can (IE doesn't allow it, but that's OK)
+               // Opera doesn't fire onreadystatechange at all on abort
+               try {
+                       var oldAbort = xhr.abort;
+                       xhr.abort = function() {
+                               if ( xhr ) {
+                                       oldAbort.call( xhr );
+                               }
 
-                       // Timeout checker
-                       if ( s.timeout > 0 )
-                               setTimeout(function(){
-                                       // Check to see if the request is still happening
-                                       if ( xhr && !requestDone )
-                                               onreadystatechange( "timeout" );
-                               }, s.timeout);
+                               onreadystatechange( "abort" );
+                       };
+               } catch(e) { }
+
+               // Timeout checker
+               if ( s.async && s.timeout > 0 ) {
+                       setTimeout(function() {
+                               // Check to see if the request is still happening
+                               if ( xhr && !requestDone ) {
+                                       onreadystatechange( "timeout" );
+                               }
+                       }, s.timeout);
                }
 
                // Send the data
                try {
-                       xhr.send(s.data);
+                       xhr.send( type === "POST" || type === "PUT" || type === "DELETE" ? s.data : null );
                } catch(e) {
                        jQuery.handleError(s, xhr, null, e);
+                       // Fire the complete handlers
+                       complete();
                }
 
                // firefox 1.5 doesn't fire statechange for sync requests
-               if ( !s.async )
+               if ( !s.async ) {
                        onreadystatechange();
+               }
 
-               function success(){
+               function success() {
                        // If a local callback was specified, fire it and pass it the data
-                       if ( s.success )
-                               s.success( data, status );
+                       if ( s.success ) {
+                               s.success.call( callbackContext, data, status, xhr );
+                       }
 
                        // Fire the global callback
-                       if ( s.global )
-                               jQuery.event.trigger( "ajaxSuccess", [xhr, s] );
+                       if ( s.global ) {
+                               trigger( "ajaxSuccess", [xhr, s] );
+                       }
                }
 
-               function complete(){
+               function complete() {
                        // Process result
-                       if ( s.complete )
-                               s.complete(xhr, status);
+                       if ( s.complete ) {
+                               s.complete.call( callbackContext, xhr, status);
+                       }
 
                        // The request was completed
-                       if ( s.global )
-                               jQuery.event.trigger( "ajaxComplete", [xhr, s] );
+                       if ( s.global ) {
+                               trigger( "ajaxComplete", [xhr, s] );
+                       }
 
                        // Handle the global AJAX counter
-                       if ( s.global && ! --jQuery.active )
+                       if ( s.global && ! --jQuery.active ) {
                                jQuery.event.trigger( "ajaxStop" );
+                       }
+               }
+               
+               function trigger(type, args) {
+                       (s.context ? jQuery(s.context) : jQuery.event).trigger(type, args);
                }
 
                // return XMLHttpRequest to allow aborting the request etc.
@@ -3669,11 +5138,14 @@ jQuery.extend({
 
        handleError: function( s, xhr, status, e ) {
                // If a local callback was specified, fire it
-               if ( s.error ) s.error( xhr, status, e );
+               if ( s.error ) {
+                       s.error.call( s.context || s, xhr, status, e );
+               }
 
                // Fire the global callback
-               if ( s.global )
-                       jQuery.event.trigger( "ajaxError", [xhr, s, e] );
+               if ( s.global ) {
+                       (s.context ? jQuery(s.context) : jQuery.event).trigger( "ajaxError", [xhr, s, e] );
+               }
        },
 
        // Counter for holding the number of active queries
@@ -3683,86 +5155,131 @@ jQuery.extend({
        httpSuccess: function( xhr ) {
                try {
                        // IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450
-                       return !xhr.status && location.protocol == "file:" ||
-                               ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status == 304 || xhr.status == 1223;
-               } catch(e){}
+                       return !xhr.status && location.protocol === "file:" ||
+                               // Opera returns 0 when status is 304
+                               ( xhr.status >= 200 && xhr.status < 300 ) ||
+                               xhr.status === 304 || xhr.status === 1223 || xhr.status === 0;
+               } catch(e) {}
+
                return false;
        },
 
        // Determines if an XMLHttpRequest returns NotModified
        httpNotModified: function( xhr, url ) {
-               try {
-                       var xhrRes = xhr.getResponseHeader("Last-Modified");
+               var lastModified = xhr.getResponseHeader("Last-Modified"),
+                       etag = xhr.getResponseHeader("Etag");
 
-                       // Firefox always returns 200. check Last-Modified date
-                       return xhr.status == 304 || xhrRes == jQuery.lastModified[url];
-               } catch(e){}
-               return false;
+               if ( lastModified ) {
+                       jQuery.lastModified[url] = lastModified;
+               }
+
+               if ( etag ) {
+                       jQuery.etag[url] = etag;
+               }
+
+               // Opera returns 0 when status is 304
+               return xhr.status === 304 || xhr.status === 0;
        },
 
        httpData: function( xhr, type, s ) {
-               var ct = xhr.getResponseHeader("content-type"),
-                       xml = type == "xml" || !type && ct && ct.indexOf("xml") >= 0,
+               var ct = xhr.getResponseHeader("content-type") || "",
+                       xml = type === "xml" || !type && ct.indexOf("xml") >= 0,
                        data = xml ? xhr.responseXML : xhr.responseText;
 
-               if ( xml && data.documentElement.tagName == "parsererror" )
-                       throw "parsererror";
-                       
+               if ( xml && data.documentElement.nodeName === "parsererror" ) {
+                       jQuery.error( "parsererror" );
+               }
+
                // Allow a pre-filtering function to sanitize the response
-               // s != null is checked to keep backwards compatibility
-               if( s && s.dataFilter )
+               // s is checked to keep backwards compatibility
+               if ( s && s.dataFilter ) {
                        data = s.dataFilter( data, type );
+               }
 
                // The filter can actually parse the response
-               if( typeof data === "string" ){
+               if ( typeof data === "string" ) {
+                       // Get the JavaScript object, if JSON is used.
+                       if ( type === "json" || !type && ct.indexOf("json") >= 0 ) {
+                               data = jQuery.parseJSON( data );
 
                        // If the type is "script", eval it in global context
-                       if ( type == "script" )
+                       } else if ( type === "script" || !type && ct.indexOf("javascript") >= 0 ) {
                                jQuery.globalEval( data );
-
-                       // Get the JavaScript object, if JSON is used.
-                       if ( type == "json" )
-                               data = window["eval"]("(" + data + ")");
+                       }
                }
-               
+
                return data;
        },
 
        // Serialize an array of form elements or a set of
        // key/values into a query string
-       param: function( a ) {
-               var s = [ ];
-
-               function add( key, value ){
-                       s[ s.length ] = encodeURIComponent(key) + '=' + encodeURIComponent(value);
-               };
-
-               // If an array was passed in, assume that it is an array
-               // of form elements
-               if ( jQuery.isArray(a) || a.jquery )
+       param: function( a, traditional ) {
+               var s = [];
+               
+               // Set traditional to true for jQuery <= 1.3.2 behavior.
+               if ( traditional === undefined ) {
+                       traditional = jQuery.ajaxSettings.traditional;
+               }
+               
+               // If an array was passed in, assume that it is an array of form elements.
+               if ( jQuery.isArray(a) || a.jquery ) {
                        // Serialize the form elements
-                       jQuery.each( a, function(){
+                       jQuery.each( a, function() {
                                add( this.name, this.value );
                        });
-
-               // Otherwise, assume that it's an object of key/value pairs
-               else
-                       // Serialize the key/values
-                       for ( var j in a )
-                               // If the value is an array then the key names need to be repeated
-                               if ( jQuery.isArray(a[j]) )
-                                       jQuery.each( a[j], function(){
-                                               add( j, this );
-                                       });
-                               else
-                                       add( j, jQuery.isFunction(a[j]) ? a[j]() : a[j] );
+                       
+               } else {
+                       // If traditional, encode the "old" way (the way 1.3.2 or older
+                       // did it), otherwise encode params recursively.
+                       for ( var prefix in a ) {
+                               buildParams( prefix, a[prefix] );
+                       }
+               }
 
                // Return the resulting serialization
-               return s.join("&").replace(/%20/g, "+");
-       }
+               return s.join("&").replace(r20, "+");
+
+               function buildParams( prefix, obj ) {
+                       if ( jQuery.isArray(obj) ) {
+                               // Serialize array item.
+                               jQuery.each( obj, function( i, v ) {
+                                       if ( traditional ) {
+                                               // Treat each array item as a scalar.
+                                               add( prefix, v );
+                                       } else {
+                                               // If array item is non-scalar (array or object), encode its
+                                               // numeric index to resolve deserialization ambiguity issues.
+                                               // Note that rack (as of 1.0.0) can't currently deserialize
+                                               // nested arrays properly, and attempting to do so may cause
+                                               // a server error. Possible fixes are to modify rack's
+                                               // deserialization algorithm or to provide an option or flag
+                                               // to force array serialization to be shallow.
+                                               buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v );
+                                       }
+                               });
+                                       
+                       } else if ( !traditional && obj != null && typeof obj === "object" ) {
+                               // Serialize object item.
+                               jQuery.each( obj, function( k, v ) {
+                                       buildParams( prefix + "[" + k + "]", v );
+                               });
+                                       
+                       } else {
+                               // Serialize scalar item.
+                               add( prefix, obj );
+                       }
+               }
 
+               function add( key, value ) {
+                       // If value is a function, invoke it and return its value
+                       value = jQuery.isFunction(value) ? value() : value;
+                       s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
+               }
+       }
 });
 var elemdisplay = {},
+       rfxtypes = /toggle|show|hide/,
+       rfxnum = /^([+-]=)?([\d+-.]+)(.*)$/,
        timerId,
        fxAttrs = [
                // height animations
@@ -3773,69 +5290,67 @@ var elemdisplay = {},
                [ "opacity" ]
        ];
 
-function genFx( type, num ){
-       var obj = {};
-       jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function(){
-               obj[ this ] = type;
-       });
-       return obj;
-}
-
 jQuery.fn.extend({
-       show: function(speed,callback){
-               if ( speed ) {
+       show: function( speed, callback ) {
+               if ( speed || speed === 0) {
                        return this.animate( genFx("show", 3), speed, callback);
+
                } else {
-                       for ( var i = 0, l = this.length; i < l; i++ ){
+                       for ( var i = 0, l = this.length; i < l; i++ ) {
                                var old = jQuery.data(this[i], "olddisplay");
-                               
+
                                this[i].style.display = old || "";
-                               
+
                                if ( jQuery.css(this[i], "display") === "none" ) {
-                                       var tagName = this[i].tagName, display;
-                                       
-                                       if ( elemdisplay[ tagName ] ) {
-                                               display = elemdisplay[ tagName ];
+                                       var nodeName = this[i].nodeName, display;
+
+                                       if ( elemdisplay[ nodeName ] ) {
+                                               display = elemdisplay[ nodeName ];
+
                                        } else {
-                                               var elem = jQuery("<" + tagName + " />").appendTo("body");
-                                               
+                                               var elem = jQuery("<" + nodeName + " />").appendTo("body");
+
                                                display = elem.css("display");
-                                               if ( display === "none" )
+
+                                               if ( display === "none" ) {
                                                        display = "block";
-                                               
+                                               }
+
                                                elem.remove();
-                                               
-                                               elemdisplay[ tagName ] = display;
+
+                                               elemdisplay[ nodeName ] = display;
                                        }
-                                       
+
                                        jQuery.data(this[i], "olddisplay", display);
                                }
                        }
 
                        // Set the display of the elements in a second loop
                        // to avoid the constant reflow
-                       for ( var i = 0, l = this.length; i < l; i++ ){
-                               this[i].style.display = jQuery.data(this[i], "olddisplay") || "";
+                       for ( var j = 0, k = this.length; j < k; j++ ) {
+                               this[j].style.display = jQuery.data(this[j], "olddisplay") || "";
                        }
-                       
+
                        return this;
                }
        },
 
-       hide: function(speed,callback){
-               if ( speed ) {
+       hide: function( speed, callback ) {
+               if ( speed || speed === 0 ) {
                        return this.animate( genFx("hide", 3), speed, callback);
+
                } else {
-                       for ( var i = 0, l = this.length; i < l; i++ ){
+                       for ( var i = 0, l = this.length; i < l; i++ ) {
                                var old = jQuery.data(this[i], "olddisplay");
-                               if ( !old && old !== "none" )
+                               if ( !old && old !== "none" ) {
                                        jQuery.data(this[i], "olddisplay", jQuery.css(this[i], "display"));
+                               }
                        }
 
                        // Set the display of the elements in a second loop
                        // to avoid the constant reflow
-                       for ( var i = 0, l = this.length; i < l; i++ ){
-                               this[i].style.display = "none";
+                       for ( var j = 0, k = this.length; j < k; j++ ) {
+                               this[j].style.display = "none";
                        }
 
                        return this;
@@ -3845,77 +5360,107 @@ jQuery.fn.extend({
        // Save the old toggle function
        _toggle: jQuery.fn.toggle,
 
-       toggle: function( fn, fn2 ){
+       toggle: function( fn, fn2 ) {
                var bool = typeof fn === "boolean";
 
-               return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ?
-                       this._toggle.apply( this, arguments ) :
-                       fn == null || bool ?
-                               this.each(function(){
-                                       var state = bool ? fn : jQuery(this).is(":hidden");
-                                       jQuery(this)[ state ? "show" : "hide" ]();
-                               }) :
-                               this.animate(genFx("toggle", 3), fn, fn2);
+               if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) {
+                       this._toggle.apply( this, arguments );
+
+               } else if ( fn == null || bool ) {
+                       this.each(function() {
+                               var state = bool ? fn : jQuery(this).is(":hidden");
+                               jQuery(this)[ state ? "show" : "hide" ]();
+                       });
+
+               } else {
+                       this.animate(genFx("toggle", 3), fn, fn2);
+               }
+
+               return this;
        },
 
-       fadeTo: function(speed,to,callback){
-               return this.animate({opacity: to}, speed, callback);
+       fadeTo: function( speed, to, callback ) {
+               return this.filter(":hidden").css("opacity", 0).show().end()
+                                       .animate({opacity: to}, speed, callback);
        },
 
        animate: function( prop, speed, easing, callback ) {
                var optall = jQuery.speed(speed, easing, callback);
 
-               return this[ optall.queue === false ? "each" : "queue" ](function(){
-               
+               if ( jQuery.isEmptyObject( prop ) ) {
+                       return this.each( optall.complete );
+               }
+
+               return this[ optall.queue === false ? "each" : "queue" ](function() {
                        var opt = jQuery.extend({}, optall), p,
-                               hidden = this.nodeType == 1 && jQuery(this).is(":hidden"),
+                               hidden = this.nodeType === 1 && jQuery(this).is(":hidden"),
                                self = this;
-       
+
                        for ( p in prop ) {
-                               if ( prop[p] == "hide" && hidden || prop[p] == "show" && !hidden )
+                               var name = p.replace(rdashAlpha, fcamelCase);
+
+                               if ( p !== name ) {
+                                       prop[ name ] = prop[ p ];
+                                       delete prop[ p ];
+                                       p = name;
+                               }
+
+                               if ( prop[p] === "hide" && hidden || prop[p] === "show" && !hidden ) {
                                        return opt.complete.call(this);
+                               }
 
-                               if ( ( p == "height" || p == "width" ) && this.style ) {
+                               if ( ( p === "height" || p === "width" ) && this.style ) {
                                        // Store display property
                                        opt.display = jQuery.css(this, "display");
 
                                        // Make sure that nothing sneaks out
                                        opt.overflow = this.style.overflow;
                                }
+
+                               if ( jQuery.isArray( prop[p] ) ) {
+                                       // Create (if needed) and add to specialEasing
+                                       (opt.specialEasing = opt.specialEasing || {})[p] = prop[p][1];
+                                       prop[p] = prop[p][0];
+                               }
                        }
 
-                       if ( opt.overflow != null )
+                       if ( opt.overflow != null ) {
                                this.style.overflow = "hidden";
+                       }
 
                        opt.curAnim = jQuery.extend({}, prop);
 
-                       jQuery.each( prop, function(name, val){
+                       jQuery.each( prop, function( name, val ) {
                                var e = new jQuery.fx( self, opt, name );
 
-                               if ( /toggle|show|hide/.test(val) )
-                                       e[ val == "toggle" ? hidden ? "show" : "hide" : val ]( prop );
-                               else {
-                                       var parts = val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),
+                               if ( rfxtypes.test(val) ) {
+                                       e[ val === "toggle" ? hidden ? "show" : "hide" : val ]( prop );
+
+                               } else {
+                                       var parts = rfxnum.exec(val),
                                                start = e.cur(true) || 0;
 
                                        if ( parts ) {
-                                               var end = parseFloat(parts[2]),
+                                               var end = parseFloat( parts[2] ),
                                                        unit = parts[3] || "px";
 
                                                // We need to compute starting value
-                                               if ( unit != "px" ) {
+                                               if ( unit !== "px" ) {
                                                        self.style[ name ] = (end || 1) + unit;
                                                        start = ((end || 1) / e.cur(true)) * start;
                                                        self.style[ name ] = start + unit;
                                                }
 
                                                // If a +=/-= token was provided, we're doing a relative animation
-                                               if ( parts[1] )
-                                                       end = ((parts[1] == "-=" ? -1 : 1) * end) + start;
+                                               if ( parts[1] ) {
+                                                       end = ((parts[1] === "-=" ? -1 : 1) * end) + start;
+                                               }
 
                                                e.custom( start, end, unit );
-                                       } else
+
+                                       } else {
                                                e.custom( start, val, "" );
+                                       }
                                }
                        });
 
@@ -3924,26 +5469,31 @@ jQuery.fn.extend({
                });
        },
 
-       stop: function(clearQueue, gotoEnd){
+       stop: function( clearQueue, gotoEnd ) {
                var timers = jQuery.timers;
 
-               if (clearQueue)
+               if ( clearQueue ) {
                        this.queue([]);
+               }
 
-               this.each(function(){
+               this.each(function() {
                        // go in reverse order so anything added to the queue during the loop is ignored
-                       for ( var i = timers.length - 1; i >= 0; i-- )
-                               if ( timers[i].elem == this ) {
-                                       if (gotoEnd)
+                       for ( var i = timers.length - 1; i >= 0; i-- ) {
+                               if ( timers[i].elem === this ) {
+                                       if (gotoEnd) {
                                                // force the next step to be the last
                                                timers[i](true);
+                                       }
+
                                        timers.splice(i, 1);
                                }
+                       }
                });
 
                // start the next in the queue if the last step wasn't forced
-               if (!gotoEnd)
+               if ( !gotoEnd ) {
                        this.dequeue();
+               }
 
                return this;
        }
@@ -3957,16 +5507,15 @@ jQuery.each({
        slideToggle: genFx("toggle", 1),
        fadeIn: { opacity: "show" },
        fadeOut: { opacity: "hide" }
-}, function( name, props ){
-       jQuery.fn[ name ] = function( speed, callback ){
+}, function( name, props ) {
+       jQuery.fn[ name ] = function( speed, callback ) {
                return this.animate( props, speed, callback );
        };
 });
 
 jQuery.extend({
-
-       speed: function(speed, easing, fn) {
-               var opt = typeof speed === "object" ? speed : {
+       speed: function( speed, easing, fn ) {
+               var opt = speed && typeof speed === "object" ? speed : {
                        complete: fn || !fn && easing ||
                                jQuery.isFunction( speed ) && speed,
                        duration: speed,
@@ -3978,11 +5527,13 @@ jQuery.extend({
 
                // Queueing
                opt.old = opt.complete;
-               opt.complete = function(){
-                       if ( opt.queue !== false )
+               opt.complete = function() {
+                       if ( opt.queue !== false ) {
                                jQuery(this).dequeue();
-                       if ( jQuery.isFunction( opt.old ) )
+                       }
+                       if ( jQuery.isFunction( opt.old ) ) {
                                opt.old.call( this );
+                       }
                };
 
                return opt;
@@ -3999,42 +5550,45 @@ jQuery.extend({
 
        timers: [],
 
-       fx: function( elem, options, prop ){
+       fx: function( elem, options, prop ) {
                this.options = options;
                this.elem = elem;
                this.prop = prop;
 
-               if ( !options.orig )
+               if ( !options.orig ) {
                        options.orig = {};
+               }
        }
 
 });
 
 jQuery.fx.prototype = {
-
        // Simple function for setting a style value
-       update: function(){
-               if ( this.options.step )
+       update: function() {
+               if ( this.options.step ) {
                        this.options.step.call( this.elem, this.now, this );
+               }
 
                (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );
 
                // Set display property to block for height/width animations
-               if ( ( this.prop == "height" || this.prop == "width" ) && this.elem.style )
+               if ( ( this.prop === "height" || this.prop === "width" ) && this.elem.style ) {
                        this.elem.style.display = "block";
+               }
        },
 
        // Get the current size
-       cur: function(force){
-               if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) )
+       cur: function( force ) {
+               if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) {
                        return this.elem[ this.prop ];
+               }
 
                var r = parseFloat(jQuery.css(this.elem, this.prop, force));
                return r && r > -10000 ? r : parseFloat(jQuery.curCSS(this.elem, this.prop)) || 0;
        },
 
        // Start an animation from one number to another
-       custom: function(from, to, unit){
+       custom: function( from, to, unit ) {
                this.startTime = now();
                this.start = from;
                this.end = to;
@@ -4043,47 +5597,36 @@ jQuery.fx.prototype = {
                this.pos = this.state = 0;
 
                var self = this;
-               function t(gotoEnd){
+               function t( gotoEnd ) {
                        return self.step(gotoEnd);
                }
 
                t.elem = this.elem;
 
                if ( t() && jQuery.timers.push(t) && !timerId ) {
-                       timerId = setInterval(function(){
-                               var timers = jQuery.timers;
-
-                               for ( var i = 0; i < timers.length; i++ )
-                                       if ( !timers[i]() )
-                                               timers.splice(i--, 1);
-
-                               if ( !timers.length ) {
-                                       clearInterval( timerId );
-                                       timerId = undefined;
-                               }
-                       }, 13);
+                       timerId = setInterval(jQuery.fx.tick, 13);
                }
        },
 
        // Simple 'show' function
-       show: function(){
+       show: function() {
                // Remember where we started, so that we can go back to it later
-               this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
+               this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
                this.options.show = true;
 
                // Begin the animation
                // Make sure that we start at a small width/height to avoid any
                // flash of content
-               this.custom(this.prop == "width" || this.prop == "height" ? 1 : 0, this.cur());
+               this.custom(this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur());
 
                // Start by showing the element
-               jQuery(this.elem).show();
+               jQuery( this.elem ).show();
        },
 
        // Simple 'hide' function
-       hide: function(){
+       hide: function() {
                // Remember where we started, so that we can go back to it later
-               this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
+               this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
                this.options.hide = true;
 
                // Begin the animation
@@ -4091,8 +5634,8 @@ jQuery.fx.prototype = {
        },
 
        // Each step of an animation
-       step: function(gotoEnd){
-               var t = now();
+       step: function( gotoEnd ) {
+               var t = now(), done = true;
 
                if ( gotoEnd || t >= this.options.duration + this.startTime ) {
                        this.now = this.end;
@@ -4101,10 +5644,11 @@ jQuery.fx.prototype = {
 
                        this.options.curAnim[ this.prop ] = true;
 
-                       var done = true;
-                       for ( var i in this.options.curAnim )
-                               if ( this.options.curAnim[i] !== true )
+                       for ( var i in this.options.curAnim ) {
+                               if ( this.options.curAnim[i] !== true ) {
                                        done = false;
+                               }
+                       }
 
                        if ( done ) {
                                if ( this.options.display != null ) {
@@ -4112,31 +5656,40 @@ jQuery.fx.prototype = {
                                        this.elem.style.overflow = this.options.overflow;
 
                                        // Reset the display
-                                       this.elem.style.display = this.options.display;
-                                       if ( jQuery.css(this.elem, "display") == "none" )
+                                       var old = jQuery.data(this.elem, "olddisplay");
+                                       this.elem.style.display = old ? old : this.options.display;
+
+                                       if ( jQuery.css(this.elem, "display") === "none" ) {
                                                this.elem.style.display = "block";
+                                       }
                                }
 
                                // Hide the element if the "hide" operation was done
-                               if ( this.options.hide )
+                               if ( this.options.hide ) {
                                        jQuery(this.elem).hide();
+                               }
 
                                // Reset the properties, if the item has been hidden or shown
-                               if ( this.options.hide || this.options.show )
-                                       for ( var p in this.options.curAnim )
-                                               jQuery.attr(this.elem.style, p, this.options.orig[p]);
-                                       
+                               if ( this.options.hide || this.options.show ) {
+                                       for ( var p in this.options.curAnim ) {
+                                               jQuery.style(this.elem, p, this.options.orig[p]);
+                                       }
+                               }
+
                                // Execute the complete function
                                this.options.complete.call( this.elem );
                        }
 
                        return false;
+
                } else {
                        var n = t - this.startTime;
                        this.state = n / this.options.duration;
 
                        // Perform the easing function, defaults to swing
-                       this.pos = jQuery.easing[this.options.easing || (jQuery.easing.swing ? "swing" : "linear")](this.state, n, 0, 1, this.options.duration);
+                       var specialEasing = this.options.specialEasing && this.options.specialEasing[this.prop];
+                       var defaultEasing = this.options.easing || (jQuery.easing.swing ? "swing" : "linear");
+                       this.pos = jQuery.easing[specialEasing || defaultEasing](this.state, n, 0, 1, this.options.duration);
                        this.now = this.start + ((this.end - this.start) * this.pos);
 
                        // Perform the next step of the animation
@@ -4145,232 +5698,382 @@ jQuery.fx.prototype = {
 
                return true;
        }
-
 };
 
 jQuery.extend( jQuery.fx, {
-       speeds:{
+       tick: function() {
+               var timers = jQuery.timers;
+
+               for ( var i = 0; i < timers.length; i++ ) {
+                       if ( !timers[i]() ) {
+                               timers.splice(i--, 1);
+                       }
+               }
+
+               if ( !timers.length ) {
+                       jQuery.fx.stop();
+               }
+       },
+               
+       stop: function() {
+               clearInterval( timerId );
+               timerId = null;
+       },
+       
+       speeds: {
                slow: 600,
                fast: 200,
                // Default speed
                _default: 400
        },
-       step: {
 
-               opacity: function(fx){
-                       jQuery.attr(fx.elem.style, "opacity", fx.now);
+       step: {
+               opacity: function( fx ) {
+                       jQuery.style(fx.elem, "opacity", fx.now);
                },
 
-               _default: function(fx){
-                       if ( fx.elem.style && fx.elem.style[ fx.prop ] != null )
-                               fx.elem.style[ fx.prop ] = fx.now + fx.unit;
-                       else
+               _default: function( fx ) {
+                       if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) {
+                               fx.elem.style[ fx.prop ] = (fx.prop === "width" || fx.prop === "height" ? Math.max(0, fx.now) : fx.now) + fx.unit;
+                       } else {
                                fx.elem[ fx.prop ] = fx.now;
+                       }
                }
        }
 });
-if ( document.documentElement["getBoundingClientRect"] )
-       jQuery.fn.offset = function() {
-               if ( !this[0] ) return { top: 0, left: 0 };
-               if ( this[0] === this[0].ownerDocument.body ) return jQuery.offset.bodyOffset( this[0] );
-               var box  = this[0].getBoundingClientRect(), doc = this[0].ownerDocument, body = doc.body, docElem = doc.documentElement,
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+       jQuery.expr.filters.animated = function( elem ) {
+               return jQuery.grep(jQuery.timers, function( fn ) {
+                       return elem === fn.elem;
+               }).length;
+       };
+}
+
+function genFx( type, num ) {
+       var obj = {};
+
+       jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function() {
+               obj[ this ] = type;
+       });
+
+       return obj;
+}
+if ( "getBoundingClientRect" in document.documentElement ) {
+       jQuery.fn.offset = function( options ) {
+               var elem = this[0];
+
+               if ( options ) { 
+                       return this.each(function( i ) {
+                               jQuery.offset.setOffset( this, options, i );
+                       });
+               }
+
+               if ( !elem || !elem.ownerDocument ) {
+                       return null;
+               }
+
+               if ( elem === elem.ownerDocument.body ) {
+                       return jQuery.offset.bodyOffset( elem );
+               }
+
+               var box = elem.getBoundingClientRect(), doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement,
                        clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0,
-                       top  = box.top  + (self.pageYOffset || jQuery.boxModel && docElem.scrollTop  || body.scrollTop ) - clientTop,
-                       left = box.left + (self.pageXOffset || jQuery.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft;
+                       top  = box.top  + (self.pageYOffset || jQuery.support.boxModel && docElem.scrollTop  || body.scrollTop ) - clientTop,
+                       left = box.left + (self.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft;
+
                return { top: top, left: left };
        };
-else 
-       jQuery.fn.offset = function() {
-               if ( !this[0] ) return { top: 0, left: 0 };
-               if ( this[0] === this[0].ownerDocument.body ) return jQuery.offset.bodyOffset( this[0] );
-               jQuery.offset.initialized || jQuery.offset.initialize();
 
-               var elem = this[0], offsetParent = elem.offsetParent, prevOffsetParent = elem,
+} else {
+       jQuery.fn.offset = function( options ) {
+               var elem = this[0];
+
+               if ( options ) { 
+                       return this.each(function( i ) {
+                               jQuery.offset.setOffset( this, options, i );
+                       });
+               }
+
+               if ( !elem || !elem.ownerDocument ) {
+                       return null;
+               }
+
+               if ( elem === elem.ownerDocument.body ) {
+                       return jQuery.offset.bodyOffset( elem );
+               }
+
+               jQuery.offset.initialize();
+
+               var offsetParent = elem.offsetParent, prevOffsetParent = elem,
                        doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement,
                        body = doc.body, defaultView = doc.defaultView,
-                       prevComputedStyle = defaultView.getComputedStyle(elem, null),
+                       prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle,
                        top = elem.offsetTop, left = elem.offsetLeft;
 
                while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) {
-                       computedStyle = defaultView.getComputedStyle(elem, null);
-                       top -= elem.scrollTop, left -= elem.scrollLeft;
+                       if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
+                               break;
+                       }
+
+                       computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle;
+                       top  -= elem.scrollTop;
+                       left -= elem.scrollLeft;
+
                        if ( elem === offsetParent ) {
-                               top += elem.offsetTop, left += elem.offsetLeft;
-                               if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.tagName)) )
-                                       top  += parseInt( computedStyle.borderTopWidth,  10) || 0,
-                                       left += parseInt( computedStyle.borderLeftWidth, 10) || 0;
+                               top  += elem.offsetTop;
+                               left += elem.offsetLeft;
+
+                               if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.nodeName)) ) {
+                                       top  += parseFloat( computedStyle.borderTopWidth  ) || 0;
+                                       left += parseFloat( computedStyle.borderLeftWidth ) || 0;
+                               }
+
                                prevOffsetParent = offsetParent, offsetParent = elem.offsetParent;
                        }
-                       if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" )
-                               top  += parseInt( computedStyle.borderTopWidth,  10) || 0,
-                               left += parseInt( computedStyle.borderLeftWidth, 10) || 0;
+
+                       if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) {
+                               top  += parseFloat( computedStyle.borderTopWidth  ) || 0;
+                               left += parseFloat( computedStyle.borderLeftWidth ) || 0;
+                       }
+
                        prevComputedStyle = computedStyle;
                }
 
-               if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" )
-                       top  += body.offsetTop,
+               if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) {
+                       top  += body.offsetTop;
                        left += body.offsetLeft;
+               }
 
-               if ( prevComputedStyle.position === "fixed" )
-                       top  += Math.max(docElem.scrollTop, body.scrollTop),
-                       left += Math.max(docElem.scrollLeft, body.scrollLeft);
+               if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
+                       top  += Math.max( docElem.scrollTop, body.scrollTop );
+                       left += Math.max( docElem.scrollLeft, body.scrollLeft );
+               }
 
                return { top: top, left: left };
        };
+}
 
 jQuery.offset = {
        initialize: function() {
-               if ( this.initialized ) return;
-               var body = document.body, container = document.createElement('div'), innerDiv, checkDiv, table, td, rules, prop, bodyMarginTop = body.style.marginTop,
-                       html = '<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" cellpadding="0" cellspacing="0"><tr><td></td></tr></table>';
+               var body = document.body, container = document.createElement("div"), innerDiv, checkDiv, table, td, bodyMarginTop = parseFloat( jQuery.curCSS(body, "marginTop", true) ) || 0,
+                       html = "<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";
 
-               rules = { position: 'absolute', top: 0, left: 0, margin: 0, border: 0, width: '1px', height: '1px', visibility: 'hidden' };
-               for ( prop in rules ) container.style[prop] = rules[prop];
+               jQuery.extend( container.style, { position: "absolute", top: 0, left: 0, margin: 0, border: 0, width: "1px", height: "1px", visibility: "hidden" } );
 
                container.innerHTML = html;
-               body.insertBefore(container, body.firstChild);
-               innerDiv = container.firstChild, checkDiv = innerDiv.firstChild, td = innerDiv.nextSibling.firstChild.firstChild;
+               body.insertBefore( container, body.firstChild );
+               innerDiv = container.firstChild;
+               checkDiv = innerDiv.firstChild;
+               td = innerDiv.nextSibling.firstChild.firstChild;
 
                this.doesNotAddBorder = (checkDiv.offsetTop !== 5);
                this.doesAddBorderForTableAndCells = (td.offsetTop === 5);
 
-               innerDiv.style.overflow = 'hidden', innerDiv.style.position = 'relative';
+               checkDiv.style.position = "fixed", checkDiv.style.top = "20px";
+               // safari subtracts parent border width here which is 5px
+               this.supportsFixedPosition = (checkDiv.offsetTop === 20 || checkDiv.offsetTop === 15);
+               checkDiv.style.position = checkDiv.style.top = "";
+
+               innerDiv.style.overflow = "hidden", innerDiv.style.position = "relative";
                this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5);
 
-               body.style.marginTop = '1px';
-               this.doesNotIncludeMarginInBodyOffset = (body.offsetTop === 0);
-               body.style.marginTop = bodyMarginTop;
+               this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginTop);
 
-               body.removeChild(container);
-               this.initialized = true;
+               body.removeChild( container );
+               body = container = innerDiv = checkDiv = table = td = null;
+               jQuery.offset.initialize = jQuery.noop;
        },
 
-       bodyOffset: function(body) {
-               jQuery.offset.initialized || jQuery.offset.initialize();
+       bodyOffset: function( body ) {
                var top = body.offsetTop, left = body.offsetLeft;
-               if ( jQuery.offset.doesNotIncludeMarginInBodyOffset )
-                       top  += parseInt( jQuery.curCSS(body, 'marginTop',  true), 10 ) || 0,
-                       left += parseInt( jQuery.curCSS(body, 'marginLeft', true), 10 ) || 0;
+
+               jQuery.offset.initialize();
+
+               if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) {
+                       top  += parseFloat( jQuery.curCSS(body, "marginTop",  true) ) || 0;
+                       left += parseFloat( jQuery.curCSS(body, "marginLeft", true) ) || 0;
+               }
+
                return { top: top, left: left };
+       },
+       
+       setOffset: function( elem, options, i ) {
+               // set position first, in-case top/left are set even on static elem
+               if ( /static/.test( jQuery.curCSS( elem, "position" ) ) ) {
+                       elem.style.position = "relative";
+               }
+               var curElem   = jQuery( elem ),
+                       curOffset = curElem.offset(),
+                       curTop    = parseInt( jQuery.curCSS( elem, "top",  true ), 10 ) || 0,
+                       curLeft   = parseInt( jQuery.curCSS( elem, "left", true ), 10 ) || 0;
+
+               if ( jQuery.isFunction( options ) ) {
+                       options = options.call( elem, i, curOffset );
+               }
+
+               var props = {
+                       top:  (options.top  - curOffset.top)  + curTop,
+                       left: (options.left - curOffset.left) + curLeft
+               };
+               
+               if ( "using" in options ) {
+                       options.using.call( elem, props );
+               } else {
+                       curElem.css( props );
+               }
        }
 };
 
 
 jQuery.fn.extend({
        position: function() {
-               var left = 0, top = 0, results;
-
-               if ( this[0] ) {
-                       // Get *real* offsetParent
-                       var offsetParent = this.offsetParent(),
-
-                       // Get correct offsets
-                       offset       = this.offset(),
-                       parentOffset = /^body|html$/i.test(offsetParent[0].tagName) ? { top: 0, left: 0 } : offsetParent.offset();
-
-                       // Subtract element margins
-                       // note: when an element has margin: auto the offsetLeft and marginLeft 
-                       // are the same in Safari causing offset.left to incorrectly be 0
-                       offset.top  -= num( this, 'marginTop'  );
-                       offset.left -= num( this, 'marginLeft' );
-
-                       // Add offsetParent borders
-                       parentOffset.top  += num( offsetParent, 'borderTopWidth'  );
-                       parentOffset.left += num( offsetParent, 'borderLeftWidth' );
-
-                       // Subtract the two offsets
-                       results = {
-                               top:  offset.top  - parentOffset.top,
-                               left: offset.left - parentOffset.left
-                       };
+               if ( !this[0] ) {
+                       return null;
                }
 
-               return results;
+               var elem = this[0],
+
+               // Get *real* offsetParent
+               offsetParent = this.offsetParent(),
+
+               // Get correct offsets
+               offset       = this.offset(),
+               parentOffset = /^body|html$/i.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();
+
+               // Subtract element margins
+               // note: when an element has margin: auto the offsetLeft and marginLeft
+               // are the same in Safari causing offset.left to incorrectly be 0
+               offset.top  -= parseFloat( jQuery.curCSS(elem, "marginTop",  true) ) || 0;
+               offset.left -= parseFloat( jQuery.curCSS(elem, "marginLeft", true) ) || 0;
+
+               // Add offsetParent borders
+               parentOffset.top  += parseFloat( jQuery.curCSS(offsetParent[0], "borderTopWidth",  true) ) || 0;
+               parentOffset.left += parseFloat( jQuery.curCSS(offsetParent[0], "borderLeftWidth", true) ) || 0;
+
+               // Subtract the two offsets
+               return {
+                       top:  offset.top  - parentOffset.top,
+                       left: offset.left - parentOffset.left
+               };
        },
 
        offsetParent: function() {
-               var offsetParent = this[0].offsetParent || document.body;
-               while ( offsetParent && (!/^body|html$/i.test(offsetParent.tagName) && jQuery.css(offsetParent, 'position') == 'static') )
-                       offsetParent = offsetParent.offsetParent;
-               return jQuery(offsetParent);
+               return this.map(function() {
+                       var offsetParent = this.offsetParent || document.body;
+                       while ( offsetParent && (!/^body|html$/i.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) {
+                               offsetParent = offsetParent.offsetParent;
+                       }
+                       return offsetParent;
+               });
        }
 });
 
 
 // Create scrollLeft and scrollTop methods
-jQuery.each( ['Left', 'Top'], function(i, name) {
-       var method = 'scroll' + name;
-       
-       jQuery.fn[ method ] = function(val) {
-               if (!this[0]) return null;
+jQuery.each( ["Left", "Top"], function( i, name ) {
+       var method = "scroll" + name;
 
-               return val !== undefined ?
+       jQuery.fn[ method ] = function(val) {
+               var elem = this[0], win;
+               
+               if ( !elem ) {
+                       return null;
+               }
 
+               if ( val !== undefined ) {
                        // Set the scroll offset
-                       this.each(function() {
-                               this == window || this == document ?
-                                       window.scrollTo(
-                                               !i ? val : jQuery(window).scrollLeft(),
-                                                i ? val : jQuery(window).scrollTop()
-                                       ) :
+                       return this.each(function() {
+                               win = getWindow( this );
+
+                               if ( win ) {
+                                       win.scrollTo(
+                                               !i ? val : jQuery(win).scrollLeft(),
+                                                i ? val : jQuery(win).scrollTop()
+                                       );
+
+                               } else {
                                        this[ method ] = val;
-                       }) :
+                               }
+                       });
+               } else {
+                       win = getWindow( elem );
 
                        // Return the scroll offset
-                       this[0] == window || this[0] == document ?
-                               self[ i ? 'pageYOffset' : 'pageXOffset' ] ||
-                                       jQuery.boxModel && document.documentElement[ method ] ||
-                                       document.body[ method ] :
-                               this[0][ method ];
+                       return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] :
+                               jQuery.support.boxModel && win.document.documentElement[ method ] ||
+                                       win.document.body[ method ] :
+                               elem[ method ];
+               }
        };
 });
+
+function getWindow( elem ) {
+       return ("scrollTo" in elem && elem.document) ?
+               elem :
+               elem.nodeType === 9 ?
+                       elem.defaultView || elem.parentWindow :
+                       false;
+}
 // Create innerHeight, innerWidth, outerHeight and outerWidth methods
-jQuery.each([ "Height", "Width" ], function(i, name){
+jQuery.each([ "Height", "Width" ], function( i, name ) {
 
-       var tl = i ? "Left"  : "Top",  // top or left
-               br = i ? "Right" : "Bottom", // bottom or right
-               lower = name.toLowerCase();
+       var type = name.toLowerCase();
 
        // innerHeight and innerWidth
-       jQuery.fn["inner" + name] = function(){
+       jQuery.fn["inner" + name] = function() {
                return this[0] ?
-                       jQuery.css( this[0], lower, false, "padding" ) :
+                       jQuery.css( this[0], type, false, "padding" ) :
                        null;
        };
 
        // outerHeight and outerWidth
-       jQuery.fn["outer" + name] = function(margin) {
+       jQuery.fn["outer" + name] = function( margin ) {
                return this[0] ?
-                       jQuery.css( this[0], lower, false, margin ? "margin" : "border" ) :
+                       jQuery.css( this[0], type, false, margin ? "margin" : "border" ) :
                        null;
        };
-       
-       var type = name.toLowerCase();
 
        jQuery.fn[ type ] = function( size ) {
                // Get window width or height
-               return this[0] == window ?
+               var elem = this[0];
+               if ( !elem ) {
+                       return size == null ? null : this;
+               }
+               
+               if ( jQuery.isFunction( size ) ) {
+                       return this.each(function( i ) {
+                               var self = jQuery( this );
+                               self[ type ]( size.call( this, i, self[ type ]() ) );
+                       });
+               }
+
+               return ("scrollTo" in elem && elem.document) ? // does it walk and quack like a window?
                        // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
-                       document.compatMode == "CSS1Compat" && document.documentElement[ "client" + name ] ||
-                       document.body[ "client" + name ] :
+                       elem.document.compatMode === "CSS1Compat" && elem.document.documentElement[ "client" + name ] ||
+                       elem.document.body[ "client" + name ] :
 
                        // Get document width or height
-                       this[0] == document ?
+                       (elem.nodeType === 9) ? // is it a document
                                // Either scroll[Width/Height] or offset[Width/Height], whichever is greater
                                Math.max(
-                                       document.documentElement["client" + name],
-                                       document.body["scroll" + name], document.documentElement["scroll" + name],
-                                       document.body["offset" + name], document.documentElement["offset" + name]
+                                       elem.documentElement["client" + name],
+                                       elem.body["scroll" + name], elem.documentElement["scroll" + name],
+                                       elem.body["offset" + name], elem.documentElement["offset" + name]
                                ) :
 
                                // Get or set width or height on the element
                                size === undefined ?
                                        // Get width or height on the element
-                                       (this.length ? jQuery.css( this[0], type ) : null) :
+                                       jQuery.css( elem, type ) :
 
                                        // Set the width or height on the element (default to pixels if value is unitless)
                                        this.css( type, typeof size === "string" ? size : size + "px" );
        };
 
 });
-})();
+// Expose jQuery to the global object
+window.jQuery = window.$ = jQuery;
+
+})(window);
+
index b1ae21d8b23f25359f21b6c69f0eb2dd29016466..950198f4770f8135ea7708da4e6a73b33c0d40ec 100644 (file)
-/*
- * jQuery JavaScript Library v1.3.2
+/*!
+ * jQuery JavaScript Library v1.4.1
  * http://jquery.com/
  *
- * Copyright (c) 2009 John Resig
- * Dual licensed under the MIT and GPL licenses.
- * http://docs.jquery.com/License
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
  *
- * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009)
- * Revision: 6246
- */
-(function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var F=o(I||[]);F.context=document;F.selector=E;return F}}else{return o(H).find(E)}}else{if(o.isFunction(E)){return o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return this.setArray(o.isArray(E)?E:o.makeArray(E))},selector:"",jquery:"1.3.2",size:function(){return this.length},get:function(E){return E===g?Array.prototype.slice.call(this):this[E]},pushStack:function(F,H,E){var G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" ":"")+E}else{if(H){G.selector=this.selector+"."+H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return this},each:function(F,E){return o.each(this,F,E)},index:function(E){return o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return E},wrapAll:function(E){if(this[0]){var F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var G=this;while(G.firstChild){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return this.each(function(){o(this).wrapAll(E)})},append:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return this.prevObject||o([])},push:[].push,sort:[].sort,splice:[].splice,find:function(E){if(this.length===1){var F=this.pushStack([],"find",E);F.length=0;o.find(E,this[0],F);return F}else{return this.pushStack(o.unique(o.map(this,function(G){return o.find(E,G)})),"find",E)}},clone:function(G){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var I=this.outerHTML;if(!I){var J=this.ownerDocument.createElement("div");J.appendChild(this.cloneNode(true));I=J.innerHTML}return o.clean([I.replace(/ jQuery\d+="(?:\d+|null)"/g,"").replace(/^\s*/,"")])[0]}else{return this.cloneNode(true)}});if(G===true){var H=this.find("*").andSelf(),F=0;E.find("*").andSelf().each(function(){if(this.nodeName!==H[F].nodeName){return}var I=o.data(H[F],"events");for(var K in I){for(var J in I[K]){o.event.add(this,K,I[K][J],I[K][J].data)}}F++})}return E},filter:function(E){return this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return F.nodeType===1})),"filter",E)},closest:function(E){var G=o.expr.match.POS.test(E)?o(E):null,F=0;return this.map(function(){var H=this;while(H&&H.ownerDocument){if(G?G.index(H)>-1:o(H).is(E)){o.data(H,"closest",F);return H}H=H.parentNode;F++}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return this.pushStack(o.multiFilter(E,this,true),"not",E)}else{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return !!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return !!E&&this.is("."+E)},val:function(K){if(K===g){var E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var F=H?I:0,J=H?I+1:M.length;F<J;F++){var G=M[F];if(G.selected){K=o(G).val();if(H){return K}L.push(K)}}return L}return(E.value||"").replace(/\r/g,"")}return g}if(typeof K==="number"){K+=""}return this.each(function(){if(this.nodeType!=1){return}if(o.isArray(K)&&/radio|checkbox/.test(this.type)){this.checked=(o.inArray(this.value,K)>=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(this,"select")){var N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return E===g?(this[0]?this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g,""):null):this.empty().append(E)},replaceWith:function(E){return this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=o.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild;if(H){for(var G=0,E=this.length;G<E;G++){L.call(K(this[G],H),this.length>1||G>0?I.cloneNode(true):I)}}if(F){o.each(F,z)}}return this;function K(N,O){return M&&o.nodeName(N,"table")&&o.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};o.fn.init.prototype=o.fn;function z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function e(){return +new Date}o.extend=o.fn.extend=function(){var J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H<I;H++){if((G=arguments[H])!=null){for(var F in G){var K=J[F],L=G[F];if(J===L){continue}if(E&&L&&typeof L==="object"&&!L.nodeType){J[F]=o.extend(E,K||(L.length!=null?[]:{}),L)}else{if(L!==g){J[F]=L}}}}}return J};var b=/z-?index|font-?weight|opacity|zoom|line-?height/i,q=document.defaultView||{},s=Object.prototype.toString;o.extend({noConflict:function(E){l.$=p;if(E){l.jQuery=y}return o},isFunction:function(E){return s.call(E)==="[object Function]"},isArray:function(E){return s.call(E)==="[object Array]"},isXMLDoc:function(E){return E.nodeType===9&&E.documentElement.nodeName!=="HTML"||!!E.ownerDocument&&o.isXMLDoc(E.ownerDocument)},globalEval:function(G){if(G&&/\S/.test(G)){var F=document.getElementsByTagName("head")[0]||document.documentElement,E=document.createElement("script");E.type="text/javascript";if(o.support.scriptEval){E.appendChild(document.createTextNode(G))}else{E.text=G}F.insertBefore(E,F.firstChild);F.removeChild(E)}},nodeName:function(F,E){return F.nodeName&&F.nodeName.toUpperCase()==E.toUpperCase()},each:function(G,K,F){var E,H=0,I=G.length;if(F){if(I===g){for(E in G){if(K.apply(G[E],F)===false){break}}}else{for(;H<I;){if(K.apply(G[H++],F)===false){break}}}}else{if(I===g){for(E in G){if(K.call(G[E],E,G[E])===false){break}}}else{for(var J=G[0];H<I&&K.call(J,H,J)!==false;J=G[++H]){}}}return G},prop:function(H,I,G,F,E){if(o.isFunction(I)){I=I.call(H,F)}return typeof I==="number"&&G=="curCSS"&&!b.test(E)?I+"px":I},className:{add:function(E,F){o.each((F||"").split(/\s+/),function(G,H){if(E.nodeType==1&&!o.className.has(E.className,H)){E.className+=(E.className?" ":"")+H}})},remove:function(E,F){if(E.nodeType==1){E.className=F!==g?o.grep(E.className.split(/\s+/),function(G){return !o.className.has(F,G)}).join(" "):""}},has:function(F,E){return F&&o.inArray(E,(F.className||F).toString().split(/\s+/))>-1}},swap:function(H,G,I){var E={};for(var F in G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in G){H.style[F]=E[F]}},css:function(H,F,J,E){if(F=="width"||F=="height"){var L,G={position:"absolute",visibility:"hidden",display:"block"},K=F=="width"?["Left","Right"]:["Top","Bottom"];function I(){L=F=="width"?H.offsetWidth:H.offsetHeight;if(E==="border"){return}o.each(K,function(){if(!E){L-=parseFloat(o.curCSS(H,"padding"+this,true))||0}if(E==="margin"){L+=parseFloat(o.curCSS(H,"margin"+this,true))||0}else{L-=parseFloat(o.curCSS(H,"border"+this+"Width",true))||0}})}if(H.offsetWidth!==0){I()}else{o.swap(H,G,I)}return Math.max(0,Math.round(L))}return o.curCSS(H,F,J)},curCSS:function(I,F,G){var L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var J=F.replace(/\-(\w)/g,function(N,O){return O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return L},clean:function(F,K,I){K=K||document;if(typeof K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.createElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,S){if(typeof S==="number"){S+=""}if(!S){return}if(typeof S==="string"){S=S.replace(/(<(\w+)[^>]*?)\/>/g,function(U,V,T){return T.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?U:V+"></"+T+">"});var O=S.replace(/^\s+/,"").substring(0,10).toLowerCase();var Q=!O.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!O.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!O.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!O.indexOf("<td")||!O.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!O.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||!o.support.htmlSerialize&&[1,"div<div>","</div>"]||[0,"",""];L.innerHTML=Q[1]+S+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var R=/<tbody/i.test(S),N=!O.indexOf("<table")&&!R?L.firstChild&&L.firstChild.childNodes:Q[1]=="<table>"&&!R?L.childNodes:[];for(var M=N.length-1;M>=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(S)){L.insertBefore(K.createTextNode(S.match(/^\s*/)[0]),L.firstChild)}S=o.makeArray(L.childNodes)}if(S.nodeType){G.push(S)}else{G=o.merge(G,S)}});if(I){for(var J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type property can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return N.toUpperCase()});if(L){J[G]=K}return J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var F=G.length;if(F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return E},inArray:function(G,H){for(var E=0,F=H.length;E<F;E++){if(H[E]===G){return E}}return -1},merge:function(H,E){var F=0,G,I=H.length;if(!o.support.getAll){while((G=E[F++])!=null){if(G.nodeType!=8){H[I++]=G}}}else{while((G=E[F++])!=null){H[I++]=G}}return H},unique:function(K){var F=[],E={};try{for(var G=0,H=K.length;G<H;G++){var J=o.data(K[G]);if(!E[J]){E[J]=true;F.push(K[G])}}}catch(I){F=K}return F},grep:function(F,J,E){var G=[];for(var H=0,I=F.length;H<I;H++){if(!E!=!J(F[H],H)){G.push(F[H])}}return G},map:function(E,J){var F=[];for(var G=0,H=E.length;G<H;G++){var I=J(E[G],G);if(I!=null){F[F.length]=I}}return F.concat.apply([],F)}});var C=navigator.userAgent.toLowerCase();o.browser={version:(C.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[0,"0"])[1],safari:/webkit/.test(C),opera:/opera/.test(C),msie:/msie/.test(C)&&!/opera/.test(C),mozilla:/mozilla/.test(C)&&!/(compatible|webkit)/.test(C)};o.each({parent:function(E){return E.parentNode},parents:function(E){return o.dir(E,"parentNode")},next:function(E){return o.nth(E,2,"nextSibling")},prev:function(E){return o.nth(E,2,"previousSibling")},nextAll:function(E){return o.dir(E,"nextSibling")},prevAll:function(E){return o.dir(E,"previousSibling")},siblings:function(E){return o.sibling(E.parentNode.firstChild,E)},children:function(E){return o.sibling(E.firstChild)},contents:function(E){return o.nodeName(E,"iframe")?E.contentDocument||E.contentWindow.document:o.makeArray(E.childNodes)}},function(E,F){o.fn[E]=function(G){var H=o.map(this,F);if(G&&typeof G=="string"){H=o.multiFilter(G,H)}return this.pushStack(o.unique(H),E,G)}});o.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(E,F){o.fn[E]=function(G){var J=[],L=o(G);for(var K=0,H=L.length;K<H;K++){var I=(K>0?this.clone(true):this).get();o.fn[F].apply(o(L[K]),I);J=J.concat(I)}return this.pushStack(J,E,G)}});o.each({removeAttr:function(E){o.attr(this,E,"");if(this.nodeType==1){this.removeAttribute(E)}},addClass:function(E){o.className.add(this,E)},removeClass:function(E){o.className.remove(this,E)},toggleClass:function(F,E){if(typeof E!=="boolean"){E=!o.className.has(this,F)}o.className[E?"add":"remove"](this,F)},remove:function(E){if(!E||o.filter(E,[this]).length){o("*",this).add([this]).each(function(){o.event.remove(this);o.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){o(this).children().remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete o.cache[H][E];E="";for(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return G},dequeue:function(H,G){var E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return F===g&&H[1]?this.data(H[0]):F}else{return this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){return this.each(function(){o.dequeue(this,E)})}});
-/*
- * Sizzle CSS Selector Engine - v0.9.3
- *  Copyright 2009, The Dojo Foundation
- *  Released under the MIT, BSD, and GPL Licenses.
- *  More information: http://sizzlejs.com/
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2010, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Mon Jan 25 19:43:33 2010 -0500
  */
-(function(){var R=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,L=0,H=Object.prototype.toString;var F=function(Y,U,ab,ac){ab=ab||[];U=U||document;if(U.nodeType!==1&&U.nodeType!==9){return[]}if(!Y||typeof Y!=="string"){return ab}var Z=[],W,af,ai,T,ad,V,X=true;R.lastIndex=0;while((W=R.exec(Y))!==null){Z.push(W[1]);if(W[2]){V=RegExp.rightContext;break}}if(Z.length>1&&M.exec(Y)){if(Z.length===2&&I.relative[Z[0]]){af=J(Z[0]+Z[1],U)}else{af=I.relative[Z[0]]?[U]:F(Z.shift(),U);while(Z.length){Y=Z.shift();if(I.relative[Y]){Y+=Z.shift()}af=J(Y,af)}}}else{var ae=ac?{expr:Z.pop(),set:E(ac)}:F.find(Z.pop(),Z.length===1&&U.parentNode?U.parentNode:U,Q(U));af=F.filter(ae.expr,ae.set);if(Z.length>0){ai=E(af)}else{X=false}while(Z.length){var ah=Z.pop(),ag=ah;if(!I.relative[ah]){ah=""}else{ag=Z.pop()}if(ag==null){ag=U}I.relative[ah](ai,ag,Q(U))}}if(!ai){ai=af}if(!ai){throw"Syntax error, unrecognized expression: "+(ah||Y)}if(H.call(ai)==="[object Array]"){if(!X){ab.push.apply(ab,ai)}else{if(U.nodeType===1){for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&(ai[aa]===true||ai[aa].nodeType===1&&K(U,ai[aa]))){ab.push(af[aa])}}}else{for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&ai[aa].nodeType===1){ab.push(af[aa])}}}}}else{E(ai,ab)}if(V){F(V,U,ab,ac);if(G){hasDuplicate=false;ab.sort(G);if(hasDuplicate){for(var aa=1;aa<ab.length;aa++){if(ab[aa]===ab[aa-1]){ab.splice(aa--,1)}}}}}return ab};F.matches=function(T,U){return F(T,null,null,U)};F.find=function(aa,T,ab){var Z,X;if(!aa){return[]}for(var W=0,V=I.order.length;W<V;W++){var Y=I.order[W],X;if((X=I.match[Y].exec(aa))){var U=RegExp.leftContext;if(U.substr(U.length-1)!=="\\"){X[1]=(X[1]||"").replace(/\\/g,"");Z=I.find[Y](X,T,ab);if(Z!=null){aa=aa.replace(I.match[Y],"");break}}}}if(!Z){Z=T.getElementsByTagName("*")}return{set:Z,expr:aa}};F.filter=function(ad,ac,ag,W){var V=ad,ai=[],aa=ac,Y,T,Z=ac&&ac[0]&&Q(ac[0]);while(ad&&ac.length){for(var ab in I.filter){if((Y=I.match[ab].exec(ad))!=null){var U=I.filter[ab],ah,af;T=false;if(aa==ai){ai=[]}if(I.preFilter[ab]){Y=I.preFilter[ab](Y,aa,ag,ai,W,Z);if(!Y){T=ah=true}else{if(Y===true){continue}}}if(Y){for(var X=0;(af=aa[X])!=null;X++){if(af){ah=U(af,Y,X,aa);var ae=W^!!ah;if(ag&&ah!=null){if(ae){T=true}else{aa[X]=false}}else{if(ae){ai.push(af);T=true}}}}}if(ah!==g){if(!ag){aa=ai}ad=ad.replace(I.match[ab],"");if(!T){return[]}break}}}if(ad==V){if(T==null){throw"Syntax error, unrecognized expression: "+ad}else{break}}V=ad}return aa};var I=F.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(T){return T.getAttribute("href")}},relative:{"+":function(aa,T,Z){var X=typeof T==="string",ab=X&&!/\W/.test(T),Y=X&&!ab;if(ab&&!Z){T=T.toUpperCase()}for(var W=0,V=aa.length,U;W<V;W++){if((U=aa[W])){while((U=U.previousSibling)&&U.nodeType!==1){}aa[W]=Y||U&&U.nodeName===T?U||false:U===T}}if(Y){F.filter(T,aa,true)}},">":function(Z,U,aa){var X=typeof U==="string";if(X&&!/\W/.test(U)){U=aa?U:U.toUpperCase();for(var V=0,T=Z.length;V<T;V++){var Y=Z[V];if(Y){var W=Y.parentNode;Z[V]=W.nodeName===U?W:false}}}else{for(var V=0,T=Z.length;V<T;V++){var Y=Z[V];if(Y){Z[V]=X?Y.parentNode:Y.parentNode===U}}if(X){F.filter(U,Z,true)}}},"":function(W,U,Y){var V=L++,T=S;if(!U.match(/\W/)){var X=U=Y?U:U.toUpperCase();T=P}T("parentNode",U,V,W,X,Y)},"~":function(W,U,Y){var V=L++,T=S;if(typeof U==="string"&&!U.match(/\W/)){var X=U=Y?U:U.toUpperCase();T=P}T("previousSibling",U,V,W,X,Y)}},find:{ID:function(U,V,W){if(typeof V.getElementById!=="undefined"&&!W){var T=V.getElementById(U[1]);return T?[T]:[]}},NAME:function(V,Y,Z){if(typeof Y.getElementsByName!=="undefined"){var U=[],X=Y.getElementsByName(V[1]);for(var W=0,T=X.length;W<T;W++){if(X[W].getAttribute("name")===V[1]){U.push(X[W])}}return U.length===0?null:U}},TAG:function(T,U){return U.getElementsByTagName(T[1])}},preFilter:{CLASS:function(W,U,V,T,Z,aa){W=" "+W[1].replace(/\\/g,"")+" ";if(aa){return W}for(var X=0,Y;(Y=U[X])!=null;X++){if(Y){if(Z^(Y.className&&(" "+Y.className+" ").indexOf(W)>=0)){if(!V){T.push(Y)}}else{if(V){U[X]=false}}}}return false},ID:function(T){return T[1].replace(/\\/g,"")},TAG:function(U,T){for(var V=0;T[V]===false;V++){}return T[V]&&Q(T[V])?U[1]:U[1].toUpperCase()},CHILD:function(T){if(T[1]=="nth"){var U=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(T[2]=="even"&&"2n"||T[2]=="odd"&&"2n+1"||!/\D/.test(T[2])&&"0n+"+T[2]||T[2]);T[2]=(U[1]+(U[2]||1))-0;T[3]=U[3]-0}T[0]=L++;return T},ATTR:function(X,U,V,T,Y,Z){var W=X[1].replace(/\\/g,"");if(!Z&&I.attrMap[W]){X[1]=I.attrMap[W]}if(X[2]==="~="){X[4]=" "+X[4]+" "}return X},PSEUDO:function(X,U,V,T,Y){if(X[1]==="not"){if(X[3].match(R).length>1||/^\w/.test(X[3])){X[3]=F(X[3],null,null,U)}else{var W=F.filter(X[3],U,V,true^Y);if(!V){T.push.apply(T,W)}return false}}else{if(I.match.POS.test(X[0])||I.match.CHILD.test(X[0])){return true}}return X},POS:function(T){T.unshift(true);return T}},filters:{enabled:function(T){return T.disabled===false&&T.type!=="hidden"},disabled:function(T){return T.disabled===true},checked:function(T){return T.checked===true},selected:function(T){T.parentNode.selectedIndex;return T.selected===true},parent:function(T){return !!T.firstChild},empty:function(T){return !T.firstChild},has:function(V,U,T){return !!F(T[3],V).length},header:function(T){return/h\d/i.test(T.nodeName)},text:function(T){return"text"===T.type},radio:function(T){return"radio"===T.type},checkbox:function(T){return"checkbox"===T.type},file:function(T){return"file"===T.type},password:function(T){return"password"===T.type},submit:function(T){return"submit"===T.type},image:function(T){return"image"===T.type},reset:function(T){return"reset"===T.type},button:function(T){return"button"===T.type||T.nodeName.toUpperCase()==="BUTTON"},input:function(T){return/input|select|textarea|button/i.test(T.nodeName)}},setFilters:{first:function(U,T){return T===0},last:function(V,U,T,W){return U===W.length-1},even:function(U,T){return T%2===0},odd:function(U,T){return T%2===1},lt:function(V,U,T){return U<T[3]-0},gt:function(V,U,T){return U>T[3]-0},nth:function(V,U,T){return T[3]-0==U},eq:function(V,U,T){return T[3]-0==U}},filter:{PSEUDO:function(Z,V,W,aa){var U=V[1],X=I.filters[U];if(X){return X(Z,W,V,aa)}else{if(U==="contains"){return(Z.textContent||Z.innerText||"").indexOf(V[3])>=0}else{if(U==="not"){var Y=V[3];for(var W=0,T=Y.length;W<T;W++){if(Y[W]===Z){return false}}return true}}}},CHILD:function(T,W){var Z=W[1],U=T;switch(Z){case"only":case"first":while(U=U.previousSibling){if(U.nodeType===1){return false}}if(Z=="first"){return true}U=T;case"last":while(U=U.nextSibling){if(U.nodeType===1){return false}}return true;case"nth":var V=W[2],ac=W[3];if(V==1&&ac==0){return true}var Y=W[0],ab=T.parentNode;if(ab&&(ab.sizcache!==Y||!T.nodeIndex)){var X=0;for(U=ab.firstChild;U;U=U.nextSibling){if(U.nodeType===1){U.nodeIndex=++X}}ab.sizcache=Y}var aa=T.nodeIndex-ac;if(V==0){return aa==0}else{return(aa%V==0&&aa/V>=0)}}},ID:function(U,T){return U.nodeType===1&&U.getAttribute("id")===T},TAG:function(U,T){return(T==="*"&&U.nodeType===1)||U.nodeName===T},CLASS:function(U,T){return(" "+(U.className||U.getAttribute("class"))+" ").indexOf(T)>-1},ATTR:function(Y,W){var V=W[1],T=I.attrHandle[V]?I.attrHandle[V](Y):Y[V]!=null?Y[V]:Y.getAttribute(V),Z=T+"",X=W[2],U=W[4];return T==null?X==="!=":X==="="?Z===U:X==="*="?Z.indexOf(U)>=0:X==="~="?(" "+Z+" ").indexOf(U)>=0:!U?Z&&T!==false:X==="!="?Z!=U:X==="^="?Z.indexOf(U)===0:X==="$="?Z.substr(Z.length-U.length)===U:X==="|="?Z===U||Z.substr(0,U.length+1)===U+"-":false},POS:function(X,U,V,Y){var T=U[2],W=I.setFilters[T];if(W){return W(X,V,U,Y)}}}};var M=I.match.POS;for(var O in I.match){I.match[O]=RegExp(I.match[O].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var E=function(U,T){U=Array.prototype.slice.call(U);if(T){T.push.apply(T,U);return T}return U};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(N){E=function(X,W){var U=W||[];if(H.call(X)==="[object Array]"){Array.prototype.push.apply(U,X)}else{if(typeof X.length==="number"){for(var V=0,T=X.length;V<T;V++){U.push(X[V])}}else{for(var V=0;X[V];V++){U.push(X[V])}}}return U}}var G;if(document.documentElement.compareDocumentPosition){G=function(U,T){var V=U.compareDocumentPosition(T)&4?-1:U===T?0:1;if(V===0){hasDuplicate=true}return V}}else{if("sourceIndex" in document.documentElement){G=function(U,T){var V=U.sourceIndex-T.sourceIndex;if(V===0){hasDuplicate=true}return V}}else{if(document.createRange){G=function(W,U){var V=W.ownerDocument.createRange(),T=U.ownerDocument.createRange();V.selectNode(W);V.collapse(true);T.selectNode(U);T.collapse(true);var X=V.compareBoundaryPoints(Range.START_TO_END,T);if(X===0){hasDuplicate=true}return X}}}}(function(){var U=document.createElement("form"),V="script"+(new Date).getTime();U.innerHTML="<input name='"+V+"'/>";var T=document.documentElement;T.insertBefore(U,T.firstChild);if(!!document.getElementById(V)){I.find.ID=function(X,Y,Z){if(typeof Y.getElementById!=="undefined"&&!Z){var W=Y.getElementById(X[1]);return W?W.id===X[1]||typeof W.getAttributeNode!=="undefined"&&W.getAttributeNode("id").nodeValue===X[1]?[W]:g:[]}};I.filter.ID=function(Y,W){var X=typeof Y.getAttributeNode!=="undefined"&&Y.getAttributeNode("id");return Y.nodeType===1&&X&&X.nodeValue===W}}T.removeChild(U)})();(function(){var T=document.createElement("div");T.appendChild(document.createComment(""));if(T.getElementsByTagName("*").length>0){I.find.TAG=function(U,Y){var X=Y.getElementsByTagName(U[1]);if(U[1]==="*"){var W=[];for(var V=0;X[V];V++){if(X[V].nodeType===1){W.push(X[V])}}X=W}return X}}T.innerHTML="<a href='#'></a>";if(T.firstChild&&typeof T.firstChild.getAttribute!=="undefined"&&T.firstChild.getAttribute("href")!=="#"){I.attrHandle.href=function(U){return U.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var T=F,U=document.createElement("div");U.innerHTML="<p class='TEST'></p>";if(U.querySelectorAll&&U.querySelectorAll(".TEST").length===0){return}F=function(Y,X,V,W){X=X||document;if(!W&&X.nodeType===9&&!Q(X)){try{return E(X.querySelectorAll(Y),V)}catch(Z){}}return T(Y,X,V,W)};F.find=T.find;F.filter=T.filter;F.selectors=T.selectors;F.matches=T.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){(function(){var T=document.createElement("div");T.innerHTML="<div class='test e'></div><div class='test'></div>";if(T.getElementsByClassName("e").length===0){return}T.lastChild.className="e";if(T.getElementsByClassName("e").length===1){return}I.order.splice(1,0,"CLASS");I.find.CLASS=function(U,V,W){if(typeof V.getElementsByClassName!=="undefined"&&!W){return V.getElementsByClassName(U[1])}}})()}function P(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W<V;W++){var T=ad[W];if(T){if(ab&&T.nodeType===1){T.sizcache=Y;T.sizset=W}T=T[U];var X=false;while(T){if(T.sizcache===Y){X=ad[T.sizset];break}if(T.nodeType===1&&!ac){T.sizcache=Y;T.sizset=W}if(T.nodeName===Z){X=T;break}T=T[U]}ad[W]=X}}}function S(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W<V;W++){var T=ad[W];if(T){if(ab&&T.nodeType===1){T.sizcache=Y;T.sizset=W}T=T[U];var X=false;while(T){if(T.sizcache===Y){X=ad[T.sizset];break}if(T.nodeType===1){if(!ac){T.sizcache=Y;T.sizset=W}if(typeof Z!=="string"){if(T===Z){X=true;break}}else{if(F.filter(Z,[T]).length>0){X=T;break}}}T=T[U]}ad[W]=X}}}var K=document.compareDocumentPosition?function(U,T){return U.compareDocumentPosition(T)&16}:function(U,T){return U!==T&&(U.contains?U.contains(T):true)};var Q=function(T){return T.nodeType===9&&T.documentElement.nodeName!=="HTML"||!!T.ownerDocument&&Q(T.ownerDocument)};var J=function(T,aa){var W=[],X="",Y,V=aa.nodeType?[aa]:aa;while((Y=I.match.PSEUDO.exec(T))){X+=Y[0];T=T.replace(I.match.PSEUDO,"")}T=I.relative[T]?T+"*":T;for(var Z=0,U=V.length;Z<U;Z++){F(T,V[Z],W)}return F.filter(X,W)};o.find=F;o.filter=F.filter;o.expr=F.selectors;o.expr[":"]=o.expr.filters;F.selectors.filters.hidden=function(T){return T.offsetWidth===0||T.offsetHeight===0};F.selectors.filters.visible=function(T){return T.offsetWidth>0||T.offsetHeight>0};F.selectors.filters.animated=function(T){return o.grep(o.timers,function(U){return T===U.elem}).length};o.multiFilter=function(V,T,U){if(U){V=":not("+V+")"}return F.matches(V,T)};o.dir=function(V,U){var T=[],W=V[U];while(W&&W!=document){if(W.nodeType==1){T.push(W)}W=W[U]}return T};o.nth=function(X,T,V,W){T=T||1;var U=0;for(;X;X=X[V]){if(X.nodeType==1&&++U==T){break}}return X};o.sibling=function(V,U){var T=[];for(;V;V=V.nextSibling){if(V.nodeType==1&&V!=U){T.push(V)}}return T};return;l.Sizzle=F})();o.event={add:function(I,F,H,K){if(I.nodeType==3||I.nodeType==8){return}if(I.setInterval&&I!=l){I=l}if(!H.guid){H.guid=this.guid++}if(K!==g){var G=H;H=this.proxy(G);H.data=K}var E=o.data(I,"events")||o.data(I,"events",{}),J=o.data(I,"handle")||o.data(I,"handle",function(){return typeof o!=="undefined"&&!o.event.triggered?o.event.handle.apply(arguments.callee.elem,arguments):g});J.elem=I;o.each(F.split(/\s+/),function(M,N){var O=N.split(".");N=O.shift();H.type=O.slice().sort().join(".");var L=E[N];if(o.event.specialAll[N]){o.event.specialAll[N].setup.call(I,K,O)}if(!L){L=E[N]={};if(!o.event.special[N]||o.event.special[N].setup.call(I,K,O)===false){if(I.addEventListener){I.addEventListener(N,J,false)}else{if(I.attachEvent){I.attachEvent("on"+N,J)}}}}L[H.guid]=H;o.event.global[N]=true});I=null},guid:1,global:{},remove:function(K,H,J){if(K.nodeType==3||K.nodeType==8){return}var G=o.data(K,"events"),F,E;if(G){if(H===g||(typeof H==="string"&&H.charAt(0)==".")){for(var I in G){this.remove(K,I+(H||""))}}else{if(H.type){J=H.handler;H=H.type}o.each(H.split(/\s+/),function(M,O){var Q=O.split(".");O=Q.shift();var N=RegExp("(^|\\.)"+Q.slice().sort().join(".*\\.")+"(\\.|$)");if(G[O]){if(J){delete G[O][J.guid]}else{for(var P in G[O]){if(N.test(G[O][P].type)){delete G[O][P]}}}if(o.event.specialAll[O]){o.event.specialAll[O].teardown.call(K,Q)}for(F in G[O]){break}if(!F){if(!o.event.special[O]||o.event.special[O].teardown.call(K,Q)===false){if(K.removeEventListener){K.removeEventListener(O,o.data(K,"handle"),false)}else{if(K.detachEvent){K.detachEvent("on"+O,o.data(K,"handle"))}}}F=null;delete G[O]}}})}for(F in G){break}if(!F){var L=o.data(K,"handle");if(L){L.elem=null}o.removeData(K,"events");o.removeData(K,"handle")}}},trigger:function(I,K,H,E){var G=I.type||I;if(!E){I=typeof I==="object"?I[h]?I:o.extend(o.Event(G),I):o.Event(G);if(G.indexOf("!")>=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var J,E;K=arguments[0]=o.event.fix(K||l.event);K.currentTarget=this;var L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||this.guid++;return E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return new o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function k(){return false}function u(){return true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.result}},toggle:function(G){var E=arguments,F=1;while(F<E.length){o.event.proxy(G,E[F++])}return this.click(o.event.proxy(G,function(H){this.lastToggle=(this.lastToggle||0)%F;H.preventDefault();return E[this.lastToggle++].apply(this,arguments)||false}))},hover:function(E,F){return this.mouseenter(E).mouseleave(F)},ready:function(E){B();if(o.isReady){E.call(document,o)}else{o.readyList.push(E)}return this},live:function(G,F){var E=o.event.proxy(F);E.guid+=this.selector+G;o(document).bind(i(G,this.selector),this.selector,E);return this},die:function(F,E){o(document).unbind(i(F,this.selector),E?{guid:E.guid+this.selector+F}:null);return this}});function c(H){var E=RegExp("(^|\\.)"+H.type+"(\\.|$)"),G=true,F=[];o.each(o.data(this,"events").live||[],function(I,J){if(E.test(J.type)){var K=o(H.target).closest(J.data)[0];if(K){F.push({elem:K,fn:J})}}});F.sort(function(J,I){return o.data(J.elem,"closest")-o.data(I.elem,"closest")});o.each(F,function(){if(this.fn.call(this.elem,H,this.fn.data)===false){return(G=false)}});return G}function i(F,E){return["live",F,E.replace(/\./g,"`").replace(/ /g,"|")].join(".")}o.extend({isReady:false,readyList:[],ready:function(){if(!o.isReady){o.isReady=true;if(o.readyList){o.each(o.readyList,function(){this.call(document,o)});o.readyList=null}o(document).triggerHandler("ready")}}});var x=false;function B(){if(x){return}x=true;if(document.addEventListener){document.addEventListener("DOMContentLoaded",function(){document.removeEventListener("DOMContentLoaded",arguments.callee,false);o.ready()},false)}else{if(document.attachEvent){document.attachEvent("onreadystatechange",function(){if(document.readyState==="complete"){document.detachEvent("onreadystatechange",arguments.callee);o.ready()}});if(document.documentElement.doScroll&&l==l.top){(function(){if(o.isReady){return}try{document.documentElement.doScroll("left")}catch(E){setTimeout(arguments.callee,0);return}o.ready()})()}}}o.event.add(l,"load",o.ready)}o.each(("blur,focus,load,resize,scroll,unload,click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave,change,select,submit,keydown,keypress,keyup,error").split(","),function(F,E){o.fn[E]=function(G){return G?this.bind(E,G):this.trigger(E)}});o(l).bind("unload",function(){for(var E in o.cache){if(E!=1&&o.cache[E].handle){o.event.remove(o.cache[E].handle.elem)}}});(function(){o.support={};var F=document.documentElement,G=document.createElement("script"),K=document.createElement("div"),J="script"+(new Date).getTime();K.style.display="none";K.innerHTML='   <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';var H=K.getElementsByTagName("*"),E=K.getElementsByTagName("a")[0];if(!H||!H.length||!E){return}o.support={leadingWhitespace:K.firstChild.nodeType==3,tbody:!K.getElementsByTagName("tbody").length,objectAll:!!K.getElementsByTagName("object")[0].getElementsByTagName("*").length,htmlSerialize:!!K.getElementsByTagName("link").length,style:/red/.test(E.getAttribute("style")),hrefNormalized:E.getAttribute("href")==="/a",opacity:E.style.opacity==="0.5",cssFloat:!!E.style.cssFloat,scriptEval:false,noCloneEvent:true,boxModel:null};G.type="text/javascript";try{G.appendChild(document.createTextNode("window."+J+"=1;"))}catch(I){}F.insertBefore(G,F.firstChild);if(l[J]){o.support.scriptEval=true;delete l[J]}F.removeChild(G);if(K.attachEvent&&K.fireEvent){K.attachEvent("onclick",function(){o.support.noCloneEvent=false;K.detachEvent("onclick",arguments.callee)});K.cloneNode(true).fireEvent("onclick")}o(function(){var L=document.createElement("div");L.style.width=L.style.paddingLeft="1px";document.body.appendChild(L);o.boxModel=o.support.boxModel=L.offsetWidth===2;document.body.removeChild(L).style.display="none"})})();var w=o.support.cssFloat?"cssFloat":"styleFloat";o.props={"for":"htmlFor","class":"className","float":w,cssFloat:w,styleFloat:w,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",tabindex:"tabIndex"};o.fn.extend({_load:o.fn.load,load:function(G,J,K){if(typeof G!=="string"){return this._load(G)}var I=G.indexOf(" ");if(I>=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("<div/>").append(M.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(E):M.responseText)}if(K){F.each(K,[M.responseText,L,M])}}});return this},serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?o.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password|search/i.test(this.type))}).map(function(E,F){var G=o(this).val();return G==null?null:o.isArray(G)?o.map(G,function(I,H){return{name:F.name,value:I}}):{name:F.name,value:G}}).get()}});o.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(E,F){o.fn[F]=function(G){return this.bind(F,G)}});var r=e();o.extend({get:function(E,G,H,F){if(o.isFunction(G)){H=G;G=null}return o.ajax({type:"GET",url:E,data:G,success:H,dataType:F})},getScript:function(E,F){return o.get(E,null,F,"script")},getJSON:function(E,F,G){return o.get(E,F,G,"json")},post:function(E,G,H,F){if(o.isFunction(G)){H=G;G={}}return o.ajax({type:"POST",url:E,data:G,success:H,dataType:F})},ajaxSetup:function(E){o.extend(o.ajaxSettings,E)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(M){M=o.extend(true,M,o.extend(true,{},o.ajaxSettings,M));var W,F=/=\?(&|$)/g,R,V,G=M.type.toUpperCase();if(M.data&&M.processData&&typeof M.data!=="string"){M.data=o.param(M.data)}if(M.dataType=="jsonp"){if(G=="GET"){if(!M.url.match(F)){M.url+=(M.url.match(/\?/)?"&":"?")+(M.jsonp||"callback")+"=?"}}else{if(!M.data||!M.data.match(F)){M.data=(M.data?M.data+"&":"")+(M.jsonp||"callback")+"=?"}}M.dataType="json"}if(M.dataType=="json"&&(M.data&&M.data.match(F)||M.url.match(F))){W="jsonp"+r++;if(M.data){M.data=(M.data+"").replace(F,"="+W+"$1")}M.url=M.url.replace(F,"="+W+"$1");M.dataType="script";l[W]=function(X){V=X;I();L();l[W]=g;try{delete l[W]}catch(Y){}if(H){H.removeChild(T)}}}if(M.dataType=="script"&&M.cache==null){M.cache=false}if(M.cache===false&&G=="GET"){var E=e();var U=M.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+E+"$2");M.url=U+((U==M.url)?(M.url.match(/\?/)?"&":"?")+"_="+E:"")}if(M.data&&G=="GET"){M.url+=(M.url.match(/\?/)?"&":"?")+M.data;M.data=null}if(M.global&&!o.active++){o.event.trigger("ajaxStart")}var Q=/^(\w+:)?\/\/([^\/?#]+)/.exec(M.url);if(M.dataType=="script"&&G=="GET"&&Q&&(Q[1]&&Q[1]!=location.protocol||Q[2]!=location.host)){var H=document.getElementsByTagName("head")[0];var T=document.createElement("script");T.src=M.url;if(M.scriptCharset){T.charset=M.scriptCharset}if(!W){var O=false;T.onload=T.onreadystatechange=function(){if(!O&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){O=true;I();L();T.onload=T.onreadystatechange=null;H.removeChild(T)}}}H.appendChild(T);return g}var K=false;var J=M.xhr();if(M.username){J.open(G,M.url,M.async,M.username,M.password)}else{J.open(G,M.url,M.async)}try{if(M.data){J.setRequestHeader("Content-Type",M.contentType)}if(M.ifModified){J.setRequestHeader("If-Modified-Since",o.lastModified[M.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}J.setRequestHeader("X-Requested-With","XMLHttpRequest");J.setRequestHeader("Accept",M.dataType&&M.accepts[M.dataType]?M.accepts[M.dataType]+", */*":M.accepts._default)}catch(S){}if(M.beforeSend&&M.beforeSend(J,M)===false){if(M.global&&!--o.active){o.event.trigger("ajaxStop")}J.abort();return false}if(M.global){o.event.trigger("ajaxSend",[J,M])}var N=function(X){if(J.readyState==0){if(P){clearInterval(P);P=null;if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}}else{if(!K&&J&&(J.readyState==4||X=="timeout")){K=true;if(P){clearInterval(P);P=null}R=X=="timeout"?"timeout":!o.httpSuccess(J)?"error":M.ifModified&&o.httpNotModified(J,M.url)?"notmodified":"success";if(R=="success"){try{V=o.httpData(J,M.dataType,M)}catch(Z){R="parsererror"}}if(R=="success"){var Y;try{Y=J.getResponseHeader("Last-Modified")}catch(Z){}if(M.ifModified&&Y){o.lastModified[M.url]=Y}if(!W){I()}}else{o.handleError(M,J,R)}L();if(X){J.abort()}if(M.async){J=null}}}};if(M.async){var P=setInterval(N,13);if(M.timeout>0){setTimeout(function(){if(J&&!K){N("timeout")}},M.timeout)}}try{J.send(M.data)}catch(S){o.handleError(M,J,null,S)}if(!M.async){N()}function I(){if(M.success){M.success(V,R)}if(M.global){o.event.trigger("ajaxSuccess",[J,M])}}function L(){if(M.complete){M.complete(J,R)}if(M.global){o.event.trigger("ajaxComplete",[J,M])}if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}return J},handleError:function(F,H,E,G){if(F.error){F.error(H,E,G)}if(F.global){o.event.trigger("ajaxError",[H,F,G])}},active:0,httpSuccess:function(F){try{return !F.status&&location.protocol=="file:"||(F.status>=200&&F.status<300)||F.status==304||F.status==1223}catch(E){}return false},httpNotModified:function(G,E){try{var H=G.getResponseHeader("Last-Modified");return G.status==304||H==o.lastModified[E]}catch(F){}return false},httpData:function(J,H,G){var F=J.getResponseHeader("content-type"),E=H=="xml"||!H&&F&&F.indexOf("xml")>=0,I=E?J.responseXML:J.responseText;if(E&&I.documentElement.tagName=="parsererror"){throw"parsererror"}if(G&&G.dataFilter){I=G.dataFilter(I,H)}if(typeof I==="string"){if(H=="script"){o.globalEval(I)}if(H=="json"){I=l["eval"]("("+I+")")}}return I},param:function(E){var G=[];function H(I,J){G[G.length]=encodeURIComponent(I)+"="+encodeURIComponent(J)}if(o.isArray(E)||E.jquery){o.each(E,function(){H(this.name,this.value)})}else{for(var F in E){if(o.isArray(E[F])){o.each(E[F],function(){H(F,this)})}else{H(F,o.isFunction(E[F])?E[F]():E[F])}}}return G.join("&").replace(/%20/g,"+")}});var m={},n,d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function t(F,E){var G={};o.each(d.concat.apply([],d.slice(0,E)),function(){G[this]=F});return G}o.fn.extend({show:function(J,L){if(J){return this.animate(t("show",3),J,L)}else{for(var H=0,F=this.length;H<F;H++){var E=o.data(this[H],"olddisplay");this[H].style.display=E||"";if(o.css(this[H],"display")==="none"){var G=this[H].tagName,K;if(m[G]){K=m[G]}else{var I=o("<"+G+" />").appendTo("body");K=I.css("display");if(K==="none"){K="block"}I.remove();m[G]=K}o.data(this[H],"olddisplay",K)}}for(var H=0,F=this.length;H<F;H++){this[H].style.display=o.data(this[H],"olddisplay")||""}return this}},hide:function(H,I){if(H){return this.animate(t("hide",3),H,I)}else{for(var G=0,F=this.length;G<F;G++){var E=o.data(this[G],"olddisplay");if(!E&&E!=="none"){o.data(this[G],"olddisplay",o.css(this[G],"display"))}}for(var G=0,F=this.length;G<F;G++){this[G].style.display="none"}return this}},_toggle:o.fn.toggle,toggle:function(G,F){var E=typeof G==="boolean";return o.isFunction(G)&&o.isFunction(F)?this._toggle.apply(this,arguments):G==null||E?this.each(function(){var H=E?G:o(this).is(":hidden");o(this)[H?"show":"hide"]()}):this.animate(t("toggle",3),G,F)},fadeTo:function(E,G,F){return this.animate({opacity:G},E,F)},animate:function(I,F,H,G){var E=o.speed(F,H,G);return this[E.queue===false?"each":"queue"](function(){var K=o.extend({},E),M,L=this.nodeType==1&&o(this).is(":hidden"),J=this;for(M in I){if(I[M]=="hide"&&L||I[M]=="show"&&!L){return K.complete.call(this)}if((M=="height"||M=="width")&&this.style){K.display=o.css(this,"display");K.overflow=this.style.overflow}}if(K.overflow!=null){this.style.overflow="hidden"}K.curAnim=o.extend({},I);o.each(I,function(O,S){var R=new o.fx(J,K,O);if(/toggle|show|hide/.test(S)){R[S=="toggle"?L?"show":"hide":S](I)}else{var Q=S.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),T=R.cur(true)||0;if(Q){var N=parseFloat(Q[2]),P=Q[3]||"px";if(P!="px"){J.style[O]=(N||1)+P;T=((N||1)/R.cur(true))*T;J.style[O]=T+P}if(Q[1]){N=((Q[1]=="-="?-1:1)*N)+T}R.custom(T,N,P)}else{R.custom(T,S,"")}}});return true})},stop:function(F,E){var G=o.timers;if(F){this.queue([])}this.each(function(){for(var H=G.length-1;H>=0;H--){if(G[H].elem==this){if(E){G[H](true)}G.splice(H,1)}}});if(!E){this.dequeue()}return this}});o.each({slideDown:t("show",1),slideUp:t("hide",1),slideToggle:t("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(E,F){o.fn[E]=function(G,H){return this.animate(F,G,H)}});o.extend({speed:function(G,H,F){var E=typeof G==="object"?G:{complete:F||!F&&H||o.isFunction(G)&&G,duration:G,easing:F&&H||H&&!o.isFunction(H)&&H};E.duration=o.fx.off?0:typeof E.duration==="number"?E.duration:o.fx.speeds[E.duration]||o.fx.speeds._default;E.old=E.complete;E.complete=function(){if(E.queue!==false){o(this).dequeue()}if(o.isFunction(E.old)){E.old.call(this)}};return E},easing:{linear:function(G,H,E,F){return E+F*G},swing:function(G,H,E,F){return((-Math.cos(G*Math.PI)/2)+0.5)*F+E}},timers:[],fx:function(F,E,G){this.options=E;this.elem=F;this.prop=G;if(!E.orig){E.orig={}}}});o.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(o.fx.step[this.prop]||o.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(F){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var E=parseFloat(o.css(this.elem,this.prop,F));return E&&E>-10000?E:parseFloat(o.curCSS(this.elem,this.prop))||0},custom:function(I,H,G){this.startTime=e();this.start=I;this.end=H;this.unit=G||this.unit||"px";this.now=this.start;this.pos=this.state=0;var E=this;function F(J){return E.step(J)}F.elem=this.elem;if(F()&&o.timers.push(F)&&!n){n=setInterval(function(){var K=o.timers;for(var J=0;J<K.length;J++){if(!K[J]()){K.splice(J--,1)}}if(!K.length){clearInterval(n);n=g}},13)}},show:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.show=true;this.custom(this.prop=="width"||this.prop=="height"?1:0,this.cur());o(this.elem).show()},hide:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(H){var G=e();if(H||G>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var E=true;for(var F in this.options.curAnim){if(this.options.curAnim[F]!==true){E=false}}if(E){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(o.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){o(this.elem).hide()}if(this.options.hide||this.options.show){for(var I in this.options.curAnim){o.attr(this.elem.style,I,this.options.orig[I])}}this.options.complete.call(this.elem)}return false}else{var J=G-this.startTime;this.state=J/this.options.duration;this.pos=o.easing[this.options.easing||(o.easing.swing?"swing":"linear")](this.state,J,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};o.extend(o.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(E){o.attr(E.elem.style,"opacity",E.now)},_default:function(E){if(E.elem.style&&E.elem.style[E.prop]!=null){E.elem.style[E.prop]=E.now+E.unit}else{E.elem[E.prop]=E.now}}}});if(document.documentElement.getBoundingClientRect){o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}var G=this[0].getBoundingClientRect(),J=this[0].ownerDocument,F=J.body,E=J.documentElement,L=E.clientTop||F.clientTop||0,K=E.clientLeft||F.clientLeft||0,I=G.top+(self.pageYOffset||o.boxModel&&E.scrollTop||F.scrollTop)-L,H=G.left+(self.pageXOffset||o.boxModel&&E.scrollLeft||F.scrollLeft)-K;return{top:I,left:H}}}else{o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}o.offset.initialized||o.offset.initialize();var J=this[0],G=J.offsetParent,F=J,O=J.ownerDocument,M,H=O.documentElement,K=O.body,L=O.defaultView,E=L.getComputedStyle(J,null),N=J.offsetTop,I=J.offsetLeft;while((J=J.parentNode)&&J!==K&&J!==H){M=L.getComputedStyle(J,null);N-=J.scrollTop,I-=J.scrollLeft;if(J===G){N+=J.offsetTop,I+=J.offsetLeft;if(o.offset.doesNotAddBorder&&!(o.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(J.tagName))){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}F=G,G=J.offsetParent}if(o.offset.subtractsBorderForOverflowNotVisible&&M.overflow!=="visible"){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}E=M}if(E.position==="relative"||E.position==="static"){N+=K.offsetTop,I+=K.offsetLeft}if(E.position==="fixed"){N+=Math.max(H.scrollTop,K.scrollTop),I+=Math.max(H.scrollLeft,K.scrollLeft)}return{top:N,left:I}}}o.offset={initialize:function(){if(this.initialized){return}var L=document.body,F=document.createElement("div"),H,G,N,I,M,E,J=L.style.marginTop,K='<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" cellpadding="0" cellspacing="0"><tr><td></td></tr></table>';M={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(E in M){F.style[E]=M[E]}F.innerHTML=K;L.insertBefore(F,L.firstChild);H=F.firstChild,G=H.firstChild,I=H.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(G.offsetTop!==5);this.doesAddBorderForTableAndCells=(I.offsetTop===5);H.style.overflow="hidden",H.style.position="relative";this.subtractsBorderForOverflowNotVisible=(G.offsetTop===-5);L.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(L.offsetTop===0);L.style.marginTop=J;L.removeChild(F);this.initialized=true},bodyOffset:function(E){o.offset.initialized||o.offset.initialize();var G=E.offsetTop,F=E.offsetLeft;if(o.offset.doesNotIncludeMarginInBodyOffset){G+=parseInt(o.curCSS(E,"marginTop",true),10)||0,F+=parseInt(o.curCSS(E,"marginLeft",true),10)||0}return{top:G,left:F}}};o.fn.extend({position:function(){var I=0,H=0,F;if(this[0]){var G=this.offsetParent(),J=this.offset(),E=/^body|html$/i.test(G[0].tagName)?{top:0,left:0}:G.offset();J.top-=j(this,"marginTop");J.left-=j(this,"marginLeft");E.top+=j(G,"borderTopWidth");E.left+=j(G,"borderLeftWidth");F={top:J.top-E.top,left:J.left-E.left}}return F},offsetParent:function(){var E=this[0].offsetParent||document.body;while(E&&(!/^body|html$/i.test(E.tagName)&&o.css(E,"position")=="static")){E=E.offsetParent}return o(E)}});o.each(["Left","Top"],function(F,E){var G="scroll"+E;o.fn[G]=function(H){if(!this[0]){return null}return H!==g?this.each(function(){this==l||this==document?l.scrollTo(!F?H:o(l).scrollLeft(),F?H:o(l).scrollTop()):this[G]=H}):this[0]==l||this[0]==document?self[F?"pageYOffset":"pageXOffset"]||o.boxModel&&document.documentElement[G]||document.body[G]:this[0][G]}});o.each(["Height","Width"],function(I,G){var E=I?"Left":"Top",H=I?"Right":"Bottom",F=G.toLowerCase();o.fn["inner"+G]=function(){return this[0]?o.css(this[0],F,false,"padding"):null};o.fn["outer"+G]=function(K){return this[0]?o.css(this[0],F,false,K?"margin":"border"):null};var J=G.toLowerCase();o.fn[J]=function(K){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+G]||document.body["client"+G]:this[0]==document?Math.max(document.documentElement["client"+G],document.body["scroll"+G],document.documentElement["scroll"+G],document.body["offset"+G],document.documentElement["offset"+G]):K===g?(this.length?o.css(this[0],J):null):this.css(J,typeof K==="string"?K:K+"px")}})})();
\ No newline at end of file
+(function(z,v){function la(){if(!c.isReady){try{r.documentElement.doScroll("left")}catch(a){setTimeout(la,1);return}c.ready()}}function Ma(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,i){var j=a.length;if(typeof b==="object"){for(var n in b)X(a,n,b[n],f,e,d);return a}if(d!==v){f=!i&&f&&c.isFunction(d);for(n=0;n<j;n++)e(a[n],b,f?d.call(a[n],n,e(a[n],b)):d,i);return a}return j?
+e(a[0],b):null}function J(){return(new Date).getTime()}function Y(){return false}function Z(){return true}function ma(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function na(a){var b,d=[],f=[],e=arguments,i,j,n,o,m,s,x=c.extend({},c.data(this,"events").live);if(!(a.button&&a.type==="click")){for(o in x){j=x[o];if(j.live===a.type||j.altLive&&c.inArray(a.type,j.altLive)>-1){i=j.data;i.beforeFilter&&i.beforeFilter[a.type]&&!i.beforeFilter[a.type](a)||f.push(j.selector)}else delete x[o]}i=c(a.target).closest(f,
+a.currentTarget);m=0;for(s=i.length;m<s;m++)for(o in x){j=x[o];n=i[m].elem;f=null;if(i[m].selector===j.selector){if(j.live==="mouseenter"||j.live==="mouseleave")f=c(a.relatedTarget).closest(j.selector)[0];if(!f||f!==n)d.push({elem:n,fn:j})}}m=0;for(s=d.length;m<s;m++){i=d[m];a.currentTarget=i.elem;a.data=i.fn.data;if(i.fn.apply(i.elem,e)===false){b=false;break}}return b}}function oa(a,b){return"live."+(a?a+".":"")+b.replace(/\./g,"`").replace(/ /g,"&")}function pa(a){return!a||!a.parentNode||a.parentNode.nodeType===
+11}function qa(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var f=c.data(a[d++]),e=c.data(this,f);if(f=f&&f.events){delete e.handle;e.events={};for(var i in f)for(var j in f[i])c.event.add(this,i,f[i][j],f[i][j].data)}}})}function ra(a,b,d){var f,e,i;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&a[0].indexOf("<option")<0&&(c.support.checkClone||!sa.test(a[0]))){e=true;if(i=c.fragments[a[0]])if(i!==1)f=i}if(!f){b=b&&b[0]?b[0].ownerDocument||b[0]:r;f=b.createDocumentFragment();
+c.clean(a,b,f,d)}if(e)c.fragments[a[0]]=i?f:1;return{fragment:f,cacheable:e}}function K(a,b){var d={};c.each(ta.concat.apply([],ta.slice(0,b)),function(){d[this]=a});return d}function ua(a){return"scrollTo"in a&&a.document?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var c=function(a,b){return new c.fn.init(a,b)},Na=z.jQuery,Oa=z.$,r=z.document,S,Pa=/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,Qa=/^.[^:#\[\.,]*$/,Ra=/\S/,Sa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Ta=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,O=navigator.userAgent,
+va=false,P=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,Q=Array.prototype.slice,wa=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(typeof a==="string")if((d=Pa.exec(a))&&(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:r;if(a=Ta.exec(a))if(c.isPlainObject(b)){a=[r.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=ra([d[1]],
+[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}}else{if(b=r.getElementById(d[2])){if(b.id!==d[2])return S.find(a);this.length=1;this[0]=b}this.context=r;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=r;a=r.getElementsByTagName(a)}else return!b||b.jquery?(b||S).find(a):c(b).find(a);else if(c.isFunction(a))return S.ready(a);if(a.selector!==v){this.selector=a.selector;this.context=a.context}return c.isArray(a)?this.setArray(a):c.makeArray(a,
+this)},selector:"",jquery:"1.4.1",length:0,size:function(){return this.length},toArray:function(){return Q.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){a=c(a||null);a.prevObject=this;a.context=this.context;if(b==="find")a.selector=this.selector+(this.selector?" ":"")+d;else if(b)a.selector=this.selector+"."+b+"("+d+")";return a},setArray:function(a){this.length=0;ba.apply(this,a);return this},each:function(a,b){return c.each(this,
+a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(r,c);else P&&P.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(Q.apply(this,arguments),"slice",Q.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this,function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};
+c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,i,j,n;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b<d;b++)if((e=arguments[b])!=null)for(i in e){j=a[i];n=e[i];if(a!==n)if(f&&n&&(c.isPlainObject(n)||c.isArray(n))){j=j&&(c.isPlainObject(j)||c.isArray(j))?j:c.isArray(n)?[]:{};a[i]=c.extend(f,j,n)}else if(n!==v)a[i]=n}return a};c.extend({noConflict:function(a){z.$=
+Oa;if(a)z.jQuery=Na;return c},isReady:false,ready:function(){if(!c.isReady){if(!r.body)return setTimeout(c.ready,13);c.isReady=true;if(P){for(var a,b=0;a=P[b++];)a.call(r,c);P=null}c.fn.triggerHandler&&c(r).triggerHandler("ready")}},bindReady:function(){if(!va){va=true;if(r.readyState==="complete")return c.ready();if(r.addEventListener){r.addEventListener("DOMContentLoaded",L,false);z.addEventListener("load",c.ready,false)}else if(r.attachEvent){r.attachEvent("onreadystatechange",L);z.attachEvent("onload",
+c.ready);var a=false;try{a=z.frameElement==null}catch(b){}r.documentElement.doScroll&&a&&la()}}},isFunction:function(a){return $.call(a)==="[object Function]"},isArray:function(a){return $.call(a)==="[object Array]"},isPlainObject:function(a){if(!a||$.call(a)!=="[object Object]"||a.nodeType||a.setInterval)return false;if(a.constructor&&!aa.call(a,"constructor")&&!aa.call(a.constructor.prototype,"isPrototypeOf"))return false;var b;for(b in a);return b===v||aa.call(a,b)},isEmptyObject:function(a){for(var b in a)return false;
+return true},error:function(a){throw a;},parseJSON:function(a){if(typeof a!=="string"||!a)return null;if(/^[\],:{}\s]*$/.test(a.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return z.JSON&&z.JSON.parse?z.JSON.parse(a):(new Function("return "+a))();else c.error("Invalid JSON: "+a)},noop:function(){},globalEval:function(a){if(a&&Ra.test(a)){var b=r.getElementsByTagName("head")[0]||
+r.documentElement,d=r.createElement("script");d.type="text/javascript";if(c.support.scriptEval)d.appendChild(r.createTextNode(a));else d.text=a;b.insertBefore(d,b.firstChild);b.removeChild(d)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,b,d){var f,e=0,i=a.length,j=i===v||c.isFunction(a);if(d)if(j)for(f in a){if(b.apply(a[f],d)===false)break}else for(;e<i;){if(b.apply(a[e++],d)===false)break}else if(j)for(f in a){if(b.call(a[f],f,a[f])===false)break}else for(d=
+a[0];e<i&&b.call(d,e,d)!==false;d=a[++e]);return a},trim:function(a){return(a||"").replace(Sa,"")},makeArray:function(a,b){b=b||[];if(a!=null)a.length==null||typeof a==="string"||c.isFunction(a)||typeof a!=="function"&&a.setInterval?ba.call(b,a):c.merge(b,a);return b},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var d=0,f=b.length;d<f;d++)if(b[d]===a)return d;return-1},merge:function(a,b){var d=a.length,f=0;if(typeof b.length==="number")for(var e=b.length;f<e;f++)a[d++]=b[f];else for(;b[f]!==
+v;)a[d++]=b[f++];a.length=d;return a},grep:function(a,b,d){for(var f=[],e=0,i=a.length;e<i;e++)!d!==!b(a[e],e)&&f.push(a[e]);return f},map:function(a,b,d){for(var f=[],e,i=0,j=a.length;i<j;i++){e=b(a[i],i,d);if(e!=null)f[f.length]=e}return f.concat.apply([],f)},guid:1,proxy:function(a,b,d){if(arguments.length===2)if(typeof b==="string"){d=a;a=d[b];b=v}else if(b&&!c.isFunction(b)){d=b;b=v}if(!b&&a)b=function(){return a.apply(d||this,arguments)};if(a)b.guid=a.guid=a.guid||b.guid||c.guid++;return b},
+uaMatch:function(a){a=a.toLowerCase();a=/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||!/compatible/.test(a)&&/(mozilla)(?:.*? rv:([\w.]+))?/.exec(a)||[];return{browser:a[1]||"",version:a[2]||"0"}},browser:{}});O=c.uaMatch(O);if(O.browser){c.browser[O.browser]=true;c.browser.version=O.version}if(c.browser.webkit)c.browser.safari=true;if(wa)c.inArray=function(a,b){return wa.call(b,a)};S=c(r);if(r.addEventListener)L=function(){r.removeEventListener("DOMContentLoaded",
+L,false);c.ready()};else if(r.attachEvent)L=function(){if(r.readyState==="complete"){r.detachEvent("onreadystatechange",L);c.ready()}};(function(){c.support={};var a=r.documentElement,b=r.createElement("script"),d=r.createElement("div"),f="script"+J();d.style.display="none";d.innerHTML="   <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";var e=d.getElementsByTagName("*"),i=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!i)){c.support=
+{leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(i.getAttribute("style")),hrefNormalized:i.getAttribute("href")==="/a",opacity:/^0.55$/.test(i.style.opacity),cssFloat:!!i.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:r.createElement("select").appendChild(r.createElement("option")).selected,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};
+b.type="text/javascript";try{b.appendChild(r.createTextNode("window."+f+"=1;"))}catch(j){}a.insertBefore(b,a.firstChild);if(z[f]){c.support.scriptEval=true;delete z[f]}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function n(){c.support.noCloneEvent=false;d.detachEvent("onclick",n)});d.cloneNode(true).fireEvent("onclick")}d=r.createElement("div");d.innerHTML="<input type='radio' name='radiotest' checked='checked'/>";a=r.createDocumentFragment();a.appendChild(d.firstChild);
+c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var n=r.createElement("div");n.style.width=n.style.paddingLeft="1px";r.body.appendChild(n);c.boxModel=c.support.boxModel=n.offsetWidth===2;r.body.removeChild(n).style.display="none"});a=function(n){var o=r.createElement("div");n="on"+n;var m=n in o;if(!m){o.setAttribute(n,"return;");m=typeof o[n]==="function"}return m};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=i=null}})();c.props=
+{"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ua=0,xa={},Va={};c.extend({cache:{},expando:G,noData:{embed:true,object:true,applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==z?xa:a;var f=a[G],e=c.cache;if(!b&&!f)return null;f||(f=++Ua);if(typeof b==="object"){a[G]=f;e=e[f]=c.extend(true,
+{},b)}else e=e[f]?e[f]:typeof d==="undefined"?Va:(e[f]={});if(d!==v){a[G]=f;e[b]=d}return typeof b==="string"?e[b]:e}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==z?xa:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{try{delete a[G]}catch(i){a.removeAttribute&&a.removeAttribute(G)}delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,
+a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===v){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===v&&this.length)f=c.data(this[0],a);return f===v&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this,a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);
+return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===v)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||
+a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var ya=/[\n\t]/g,ca=/\s+/,Wa=/\r/g,Xa=/href|src|style/,Ya=/(button|input)/i,Za=/(button|input|object|select|textarea)/i,$a=/^(a|area)$/i,za=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(o){var m=
+c(this);m.addClass(a.call(this,o,m.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1)if(e.className)for(var i=" "+e.className+" ",j=0,n=b.length;j<n;j++){if(i.indexOf(" "+b[j]+" ")<0)e.className+=" "+b[j]}else e.className=a}return this},removeClass:function(a){if(c.isFunction(a))return this.each(function(o){var m=c(this);m.removeClass(a.call(this,o,m.attr("class")))});if(a&&typeof a==="string"||a===v)for(var b=(a||"").split(ca),
+d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1&&e.className)if(a){for(var i=(" "+e.className+" ").replace(ya," "),j=0,n=b.length;j<n;j++)i=i.replace(" "+b[j]+" "," ");e.className=i.substring(1,i.length-1)}else e.className=""}return this},toggleClass:function(a,b){var d=typeof a,f=typeof b==="boolean";if(c.isFunction(a))return this.each(function(e){var i=c(this);i.toggleClass(a.call(this,e,i.attr("class"),b),b)});return this.each(function(){if(d==="string")for(var e,i=0,j=c(this),n=b,o=
+a.split(ca);e=o[i++];){n=f?n:!j.hasClass(e);j[n?"addClass":"removeClass"](e)}else if(d==="undefined"||d==="boolean"){this.className&&c.data(this,"__className__",this.className);this.className=this.className||a===false?"":c.data(this,"__className__")||""}})},hasClass:function(a){a=" "+a+" ";for(var b=0,d=this.length;b<d;b++)if((" "+this[b].className+" ").replace(ya," ").indexOf(a)>-1)return true;return false},val:function(a){if(a===v){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||
+{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var i=b?d:0;for(d=b?d+1:e.length;i<d;i++){var j=e[i];if(j.selected){a=c(j).val();if(b)return a;f.push(a)}}return f}if(za.test(b.type)&&!c.support.checkOn)return b.getAttribute("value")===null?"on":b.value;return(b.value||"").replace(Wa,"")}return v}var n=c.isFunction(a);return this.each(function(o){var m=c(this),s=a;if(this.nodeType===1){if(n)s=a.call(this,o,m.val());
+if(typeof s==="number")s+="";if(c.isArray(s)&&za.test(this.type))this.checked=c.inArray(m.val(),s)>=0;else if(c.nodeName(this,"select")){var x=c.makeArray(s);c("option",this).each(function(){this.selected=c.inArray(c(this).val(),x)>=0});if(!x.length)this.selectedIndex=-1}else this.value=s}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return v;if(f&&b in c.attrFn)return c(a)[b](d);
+f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==v;b=f&&c.props[b]||b;if(a.nodeType===1){var i=Xa.test(b);if(b in a&&f&&!i){if(e){b==="type"&&Ya.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:Za.test(a.nodeName)||$a.test(a.nodeName)&&a.href?0:v;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=
+""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&i?a.getAttribute(b,2):a.getAttribute(b);return a===null?v:a}return c.style(a,b,d)}});var ab=function(a){return a.replace(/[^\w\s\.\|`]/g,function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==z&&!a.frameElement)a=z;if(!d.guid)d.guid=c.guid++;if(f!==v){d=c.proxy(d);d.data=f}var e=c.data(a,"events")||c.data(a,"events",{}),i=c.data(a,"handle"),j;if(!i){j=
+function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(j.elem,arguments):v};i=c.data(a,"handle",j)}if(i){i.elem=a;b=b.split(/\s+/);for(var n,o=0;n=b[o++];){var m=n.split(".");n=m.shift();if(o>1){d=c.proxy(d);if(f!==v)d.data=f}d.type=m.slice(0).sort().join(".");var s=e[n],x=this.special[n]||{};if(!s){s=e[n]={};if(!x.setup||x.setup.call(a,f,m,d)===false)if(a.addEventListener)a.addEventListener(n,i,false);else a.attachEvent&&a.attachEvent("on"+n,i)}if(x.add)if((m=x.add.call(a,
+d,f,m,s))&&c.isFunction(m)){m.guid=m.guid||d.guid;m.data=m.data||d.data;m.type=m.type||d.type;d=m}s[d.guid]=d;this.global[n]=true}a=null}}},global:{},remove:function(a,b,d){if(!(a.nodeType===3||a.nodeType===8)){var f=c.data(a,"events"),e,i,j;if(f){if(b===v||typeof b==="string"&&b.charAt(0)===".")for(i in f)this.remove(a,i+(b||""));else{if(b.type){d=b.handler;b=b.type}b=b.split(/\s+/);for(var n=0;i=b[n++];){var o=i.split(".");i=o.shift();var m=!o.length,s=c.map(o.slice(0).sort(),ab);s=new RegExp("(^|\\.)"+
+s.join("\\.(?:.*\\.)?")+"(\\.|$)");var x=this.special[i]||{};if(f[i]){if(d){j=f[i][d.guid];delete f[i][d.guid]}else for(var A in f[i])if(m||s.test(f[i][A].type))delete f[i][A];x.remove&&x.remove.call(a,o,j);for(e in f[i])break;if(!e){if(!x.teardown||x.teardown.call(a,o)===false)if(a.removeEventListener)a.removeEventListener(i,c.data(a,"handle"),false);else a.detachEvent&&a.detachEvent("on"+i,c.data(a,"handle"));e=null;delete f[i]}}}}for(e in f)break;if(!e){if(A=c.data(a,"handle"))A.elem=null;c.removeData(a,
+"events");c.removeData(a,"handle")}}}},trigger:function(a,b,d,f){var e=a.type||a;if(!f){a=typeof a==="object"?a[G]?a:c.extend(c.Event(e),a):c.Event(e);if(e.indexOf("!")>=0){a.type=e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();this.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return v;a.result=v;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,
+b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(i){}if(!a.isPropagationStopped()&&f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){d=a.target;var j;if(!(c.nodeName(d,"a")&&e==="click")&&!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()])){try{if(d[e]){if(j=d["on"+e])d["on"+e]=null;this.triggered=true;d[e]()}}catch(n){}if(j)d["on"+e]=j;this.triggered=false}}},handle:function(a){var b,
+d;a=arguments[0]=c.event.fix(a||z.event);a.currentTarget=this;d=a.type.split(".");a.type=d.shift();b=!d.length&&!a.exclusive;var f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)");d=(c.data(this,"events")||{})[a.type];for(var e in d){var i=d[e];if(b||f.test(i.type)){a.handler=i;a.data=i.data;i=i.apply(this,arguments);if(i!==v){a.result=i;if(i===false){a.preventDefault();a.stopPropagation()}}if(a.isImmediatePropagationStopped())break}}return a.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
+fix:function(a){if(a[G])return a;var b=a;a=c.Event(b);for(var d=this.props.length,f;d;){f=this.props[--d];a[f]=b[f]}if(!a.target)a.target=a.srcElement||r;if(a.target.nodeType===3)a.target=a.target.parentNode;if(!a.relatedTarget&&a.fromElement)a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement;if(a.pageX==null&&a.clientX!=null){b=r.documentElement;d=r.body;a.pageX=a.clientX+(b&&b.scrollLeft||d&&d.scrollLeft||0)-(b&&b.clientLeft||d&&d.clientLeft||0);a.pageY=a.clientY+(b&&b.scrollTop||
+d&&d.scrollTop||0)-(b&&b.clientTop||d&&d.clientTop||0)}if(!a.which&&(a.charCode||a.charCode===0?a.charCode:a.keyCode))a.which=a.charCode||a.keyCode;if(!a.metaKey&&a.ctrlKey)a.metaKey=a.ctrlKey;if(!a.which&&a.button!==v)a.which=a.button&1?1:a.button&2?3:a.button&4?2:0;return a},guid:1E8,proxy:c.proxy,special:{ready:{setup:c.bindReady,teardown:c.noop},live:{add:function(a,b){c.extend(a,b||{});a.guid+=b.selector+b.live;b.liveProxy=a;c.event.add(this,b.live,na,b)},remove:function(a){if(a.length){var b=
+0,d=new RegExp("(^|\\.)"+a[0]+"(\\.|$)");c.each(c.data(this,"events").live||{},function(){d.test(this.type)&&b++});b<1&&c.event.remove(this,a[0],na)}},special:{}},beforeunload:{setup:function(a,b,d){if(this.setInterval)this.onbeforeunload=d;return false},teardown:function(a,b){if(this.onbeforeunload===b)this.onbeforeunload=null}}}};c.Event=function(a){if(!this.preventDefault)return new c.Event(a);if(a&&a.type){this.originalEvent=a;this.type=a.type}else this.type=a;this.timeStamp=J();this[G]=true};
+c.Event.prototype={preventDefault:function(){this.isDefaultPrevented=Z;var a=this.originalEvent;if(a){a.preventDefault&&a.preventDefault();a.returnValue=false}},stopPropagation:function(){this.isPropagationStopped=Z;var a=this.originalEvent;if(a){a.stopPropagation&&a.stopPropagation();a.cancelBubble=true}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=Z;this.stopPropagation()},isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y};var Aa=function(a){for(var b=
+a.relatedTarget;b&&b!==this;)try{b=b.parentNode}catch(d){break}if(b!==this){a.type=a.data;c.event.handle.apply(this,arguments)}},Ba=function(a){a.type=a.data;c.event.handle.apply(this,arguments)};c.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){c.event.special[a]={setup:function(d){c.event.add(this,b,d&&d.selector?Ba:Aa,a)},teardown:function(d){c.event.remove(this,b,d&&d.selector?Ba:Aa)}}});if(!c.support.submitBubbles)c.event.special.submit={setup:function(a,b,d){if(this.nodeName.toLowerCase()!==
+"form"){c.event.add(this,"click.specialSubmit."+d.guid,function(f){var e=f.target,i=e.type;if((i==="submit"||i==="image")&&c(e).closest("form").length)return ma("submit",this,arguments)});c.event.add(this,"keypress.specialSubmit."+d.guid,function(f){var e=f.target,i=e.type;if((i==="text"||i==="password")&&c(e).closest("form").length&&f.keyCode===13)return ma("submit",this,arguments)})}else return false},remove:function(a,b){c.event.remove(this,"click.specialSubmit"+(b?"."+b.guid:""));c.event.remove(this,
+"keypress.specialSubmit"+(b?"."+b.guid:""))}};if(!c.support.changeBubbles){var da=/textarea|input|select/i;function Ca(a){var b=a.type,d=a.value;if(b==="radio"||b==="checkbox")d=a.checked;else if(b==="select-multiple")d=a.selectedIndex>-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d}function ea(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Ca(d);if(a.type!=="focusout"||
+d.type!=="radio")c.data(d,"_change_data",e);if(!(f===v||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}}c.event.special.change={filters:{focusout:ea,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return ea.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return ea.call(this,a)},beforeactivate:function(a){a=
+a.target;a.nodeName.toLowerCase()==="input"&&a.type==="radio"&&c.data(a,"_change_data",Ca(a))}},setup:function(a,b,d){for(var f in T)c.event.add(this,f+".specialChange."+d.guid,T[f]);return da.test(this.nodeName)},remove:function(a,b){for(var d in T)c.event.remove(this,d+".specialChange"+(b?"."+b.guid:""),T[d]);return da.test(this.nodeName)}};var T=c.event.special.change.filters}r.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,
+f)}c.event.special[b]={setup:function(){this.addEventListener(a,d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var i in d)this[b](i,f,d[i],e);return this}if(c.isFunction(f)){e=f;f=v}var j=b==="one"?c.proxy(e,function(n){c(this).unbind(n,j);return e.apply(this,arguments)}):e;return d==="unload"&&b!=="one"?this.one(d,f,e):this.each(function(){c.event.add(this,d,j,f)})}});c.fn.extend({unbind:function(a,
+b){if(typeof a==="object"&&!a.preventDefault){for(var d in a)this.unbind(d,a[d]);return this}return this.each(function(){c.event.remove(this,a,b)})},trigger:function(a,b){return this.each(function(){c.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){a=c.Event(a);a.preventDefault();a.stopPropagation();c.event.trigger(a,b,this[0]);return a.result}},toggle:function(a){for(var b=arguments,d=1;d<b.length;)c.proxy(a,b[d++]);return this.click(c.proxy(a,function(f){var e=(c.data(this,"lastToggle"+
+a.guid)||0)%d;c.data(this,"lastToggle"+a.guid,e+1);f.preventDefault();return b[e].apply(this,arguments)||false}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});c.each(["live","die"],function(a,b){c.fn[b]=function(d,f,e){var i,j=0;if(c.isFunction(f)){e=f;f=v}for(d=(d||"").split(/\s+/);(i=d[j++])!=null;){i=i==="focus"?"focusin":i==="blur"?"focusout":i==="hover"?d.push("mouseleave")&&"mouseenter":i;b==="live"?c(this.context).bind(oa(i,this.selector),{data:f,selector:this.selector,
+live:i},e):c(this.context).unbind(oa(i,this.selector),e?{guid:e.guid+this.selector+i}:null)}return this}});c.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){c.fn[b]=function(d){return d?this.bind(b,d):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});z.attachEvent&&!z.addEventListener&&z.attachEvent("onunload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}});
+(function(){function a(g){for(var h="",k,l=0;g[l];l++){k=g[l];if(k.nodeType===3||k.nodeType===4)h+=k.nodeValue;else if(k.nodeType!==8)h+=a(k.childNodes)}return h}function b(g,h,k,l,q,p){q=0;for(var u=l.length;q<u;q++){var t=l[q];if(t){t=t[g];for(var y=false;t;){if(t.sizcache===k){y=l[t.sizset];break}if(t.nodeType===1&&!p){t.sizcache=k;t.sizset=q}if(t.nodeName.toLowerCase()===h){y=t;break}t=t[g]}l[q]=y}}}function d(g,h,k,l,q,p){q=0;for(var u=l.length;q<u;q++){var t=l[q];if(t){t=t[g];for(var y=false;t;){if(t.sizcache===
+k){y=l[t.sizset];break}if(t.nodeType===1){if(!p){t.sizcache=k;t.sizset=q}if(typeof h!=="string"){if(t===h){y=true;break}}else if(o.filter(h,[t]).length>0){y=t;break}}t=t[g]}l[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,i=Object.prototype.toString,j=false,n=true;[0,0].sort(function(){n=false;return 0});var o=function(g,h,k,l){k=k||[];var q=h=h||r;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||
+typeof g!=="string")return k;for(var p=[],u,t,y,R,H=true,M=w(h),I=g;(f.exec(""),u=f.exec(I))!==null;){I=u[3];p.push(u[1]);if(u[2]){R=u[3];break}}if(p.length>1&&s.exec(g))if(p.length===2&&m.relative[p[0]])t=fa(p[0]+p[1],h);else for(t=m.relative[p[0]]?[h]:o(p.shift(),h);p.length;){g=p.shift();if(m.relative[g])g+=p.shift();t=fa(g,t)}else{if(!l&&p.length>1&&h.nodeType===9&&!M&&m.match.ID.test(p[0])&&!m.match.ID.test(p[p.length-1])){u=o.find(p.shift(),h,M);h=u.expr?o.filter(u.expr,u.set)[0]:u.set[0]}if(h){u=
+l?{expr:p.pop(),set:A(l)}:o.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=u.expr?o.filter(u.expr,u.set):u.set;if(p.length>0)y=A(t);else H=false;for(;p.length;){var D=p.pop();u=D;if(m.relative[D])u=p.pop();else D="";if(u==null)u=h;m.relative[D](y,u,M)}}else y=[]}y||(y=t);y||o.error(D||g);if(i.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))k.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&
+y[g].nodeType===1&&k.push(t[g]);else k.push.apply(k,y);else A(y,k);if(R){o(R,q,k,l);o.uniqueSort(k)}return k};o.uniqueSort=function(g){if(C){j=n;g.sort(C);if(j)for(var h=1;h<g.length;h++)g[h]===g[h-1]&&g.splice(h--,1)}return g};o.matches=function(g,h){return o(g,null,null,h)};o.find=function(g,h,k){var l,q;if(!g)return[];for(var p=0,u=m.order.length;p<u;p++){var t=m.order[p];if(q=m.leftMatch[t].exec(g)){var y=q[1];q.splice(1,1);if(y.substr(y.length-1)!=="\\"){q[1]=(q[1]||"").replace(/\\/g,"");l=m.find[t](q,
+h,k);if(l!=null){g=g.replace(m.match[t],"");break}}}}l||(l=h.getElementsByTagName("*"));return{set:l,expr:g}};o.filter=function(g,h,k,l){for(var q=g,p=[],u=h,t,y,R=h&&h[0]&&w(h[0]);g&&h.length;){for(var H in m.filter)if((t=m.leftMatch[H].exec(g))!=null&&t[2]){var M=m.filter[H],I,D;D=t[1];y=false;t.splice(1,1);if(D.substr(D.length-1)!=="\\"){if(u===p)p=[];if(m.preFilter[H])if(t=m.preFilter[H](t,u,k,p,l,R)){if(t===true)continue}else y=I=true;if(t)for(var U=0;(D=u[U])!=null;U++)if(D){I=M(D,t,U,u);var Da=
+l^!!I;if(k&&I!=null)if(Da)y=true;else u[U]=false;else if(Da){p.push(D);y=true}}if(I!==v){k||(u=p);g=g.replace(m.match[H],"");if(!y)return[];break}}}if(g===q)if(y==null)o.error(g);else break;q=g}return u};o.error=function(g){throw"Syntax error, unrecognized expression: "+g;};var m=o.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
+TAG:/^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(g){return g.getAttribute("href")}},relative:{"+":function(g,h){var k=typeof h==="string",l=k&&!/\W/.test(h);k=k&&!l;if(l)h=h.toLowerCase();l=0;for(var q=g.length,
+p;l<q;l++)if(p=g[l]){for(;(p=p.previousSibling)&&p.nodeType!==1;);g[l]=k||p&&p.nodeName.toLowerCase()===h?p||false:p===h}k&&o.filter(h,g,true)},">":function(g,h){var k=typeof h==="string";if(k&&!/\W/.test(h)){h=h.toLowerCase();for(var l=0,q=g.length;l<q;l++){var p=g[l];if(p){k=p.parentNode;g[l]=k.nodeName.toLowerCase()===h?k:false}}}else{l=0;for(q=g.length;l<q;l++)if(p=g[l])g[l]=k?p.parentNode:p.parentNode===h;k&&o.filter(h,g,true)}},"":function(g,h,k){var l=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=
+h=h.toLowerCase();q=b}q("parentNode",h,l,g,p,k)},"~":function(g,h,k){var l=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("previousSibling",h,l,g,p,k)}},find:{ID:function(g,h,k){if(typeof h.getElementById!=="undefined"&&!k)return(g=h.getElementById(g[1]))?[g]:[]},NAME:function(g,h){if(typeof h.getElementsByName!=="undefined"){var k=[];h=h.getElementsByName(g[1]);for(var l=0,q=h.length;l<q;l++)h[l].getAttribute("name")===g[1]&&k.push(h[l]);return k.length===0?null:k}},
+TAG:function(g,h){return h.getElementsByTagName(g[1])}},preFilter:{CLASS:function(g,h,k,l,q,p){g=" "+g[1].replace(/\\/g,"")+" ";if(p)return g;p=0;for(var u;(u=h[p])!=null;p++)if(u)if(q^(u.className&&(" "+u.className+" ").replace(/[\t\n]/g," ").indexOf(g)>=0))k||l.push(u);else if(k)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&
+"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,k,l,q,p){h=g[1].replace(/\\/g,"");if(!p&&m.attrMap[h])g[1]=m.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,k,l,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=o(g[3],null,null,h);else{g=o.filter(g[3],h,k,true^q);k||l.push.apply(l,g);return false}else if(m.match.POS.test(g[0])||m.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);
+return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,k){return!!o(k[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===
+g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,h){return h===0},last:function(g,h,k,l){return h===l.length-1},even:function(g,h){return h%2===
+0},odd:function(g,h){return h%2===1},lt:function(g,h,k){return h<k[3]-0},gt:function(g,h,k){return h>k[3]-0},nth:function(g,h,k){return k[3]-0===h},eq:function(g,h,k){return k[3]-0===h}},filter:{PSEUDO:function(g,h,k,l){var q=h[1],p=m.filters[q];if(p)return p(g,k,h,l);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h=h[3];k=0;for(l=h.length;k<l;k++)if(h[k]===g)return false;return true}else o.error("Syntax error, unrecognized expression: "+
+q)},CHILD:function(g,h){var k=h[1],l=g;switch(k){case "only":case "first":for(;l=l.previousSibling;)if(l.nodeType===1)return false;if(k==="first")return true;l=g;case "last":for(;l=l.nextSibling;)if(l.nodeType===1)return false;return true;case "nth":k=h[2];var q=h[3];if(k===1&&q===0)return true;h=h[0];var p=g.parentNode;if(p&&(p.sizcache!==h||!g.nodeIndex)){var u=0;for(l=p.firstChild;l;l=l.nextSibling)if(l.nodeType===1)l.nodeIndex=++u;p.sizcache=h}g=g.nodeIndex-q;return k===0?g===0:g%k===0&&g/k>=
+0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var k=h[1];g=m.attrHandle[k]?m.attrHandle[k](g):g[k]!=null?g[k]:g.getAttribute(k);k=g+"";var l=h[2];h=h[4];return g==null?l==="!=":l==="="?k===h:l==="*="?k.indexOf(h)>=0:l==="~="?(" "+k+" ").indexOf(h)>=0:!h?k&&g!==false:l==="!="?k!==h:l==="^="?
+k.indexOf(h)===0:l==="$="?k.substr(k.length-h.length)===h:l==="|="?k===h||k.substr(0,h.length+1)===h+"-":false},POS:function(g,h,k,l){var q=m.setFilters[h[2]];if(q)return q(g,k,h,l)}}},s=m.match.POS;for(var x in m.match){m.match[x]=new RegExp(m.match[x].source+/(?![^\[]*\])(?![^\(]*\))/.source);m.leftMatch[x]=new RegExp(/(^(?:.|\r|\n)*?)/.source+m.match[x].source.replace(/\\(\d+)/g,function(g,h){return"\\"+(h-0+1)}))}var A=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};
+try{Array.prototype.slice.call(r.documentElement.childNodes,0)}catch(B){A=function(g,h){h=h||[];if(i.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var k=0,l=g.length;k<l;k++)h.push(g[k]);else for(k=0;g[k];k++)h.push(g[k]);return h}}var C;if(r.documentElement.compareDocumentPosition)C=function(g,h){if(!g.compareDocumentPosition||!h.compareDocumentPosition){if(g==h)j=true;return g.compareDocumentPosition?-1:1}g=g.compareDocumentPosition(h)&4?-1:g===
+h?0:1;if(g===0)j=true;return g};else if("sourceIndex"in r.documentElement)C=function(g,h){if(!g.sourceIndex||!h.sourceIndex){if(g==h)j=true;return g.sourceIndex?-1:1}g=g.sourceIndex-h.sourceIndex;if(g===0)j=true;return g};else if(r.createRange)C=function(g,h){if(!g.ownerDocument||!h.ownerDocument){if(g==h)j=true;return g.ownerDocument?-1:1}var k=g.ownerDocument.createRange(),l=h.ownerDocument.createRange();k.setStart(g,0);k.setEnd(g,0);l.setStart(h,0);l.setEnd(h,0);g=k.compareBoundaryPoints(Range.START_TO_END,
+l);if(g===0)j=true;return g};(function(){var g=r.createElement("div"),h="script"+(new Date).getTime();g.innerHTML="<a name='"+h+"'/>";var k=r.documentElement;k.insertBefore(g,k.firstChild);if(r.getElementById(h)){m.find.ID=function(l,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(l[1]))?q.id===l[1]||typeof q.getAttributeNode!=="undefined"&&q.getAttributeNode("id").nodeValue===l[1]?[q]:v:[]};m.filter.ID=function(l,q){var p=typeof l.getAttributeNode!=="undefined"&&l.getAttributeNode("id");
+return l.nodeType===1&&p&&p.nodeValue===q}}k.removeChild(g);k=g=null})();(function(){var g=r.createElement("div");g.appendChild(r.createComment(""));if(g.getElementsByTagName("*").length>0)m.find.TAG=function(h,k){k=k.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var l=0;k[l];l++)k[l].nodeType===1&&h.push(k[l]);k=h}return k};g.innerHTML="<a href='#'></a>";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")m.attrHandle.href=function(h){return h.getAttribute("href",
+2)};g=null})();r.querySelectorAll&&function(){var g=o,h=r.createElement("div");h.innerHTML="<p class='TEST'></p>";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){o=function(l,q,p,u){q=q||r;if(!u&&q.nodeType===9&&!w(q))try{return A(q.querySelectorAll(l),p)}catch(t){}return g(l,q,p,u)};for(var k in g)o[k]=g[k];h=null}}();(function(){var g=r.createElement("div");g.innerHTML="<div class='test e'></div><div class='test'></div>";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===
+0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){m.order.splice(1,0,"CLASS");m.find.CLASS=function(h,k,l){if(typeof k.getElementsByClassName!=="undefined"&&!l)return k.getElementsByClassName(h[1])};g=null}}})();var E=r.compareDocumentPosition?function(g,h){return g.compareDocumentPosition(h)&16}:function(g,h){return g!==h&&(g.contains?g.contains(h):true)},w=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},fa=function(g,h){var k=[],
+l="",q;for(h=h.nodeType?[h]:h;q=m.match.PSEUDO.exec(g);){l+=q[0];g=g.replace(m.match.PSEUDO,"")}g=m.relative[g]?g+"*":g;q=0;for(var p=h.length;q<p;q++)o(g,h[q],k);return o.filter(l,k)};c.find=o;c.expr=o.selectors;c.expr[":"]=c.expr.filters;c.unique=o.uniqueSort;c.getText=a;c.isXMLDoc=w;c.contains=E})();var bb=/Until$/,cb=/^(?:parents|prevUntil|prevAll)/,db=/,/;Q=Array.prototype.slice;var Ea=function(a,b,d){if(c.isFunction(b))return c.grep(a,function(e,i){return!!b.call(e,i,e)===d});else if(b.nodeType)return c.grep(a,
+function(e){return e===b===d});else if(typeof b==="string"){var f=c.grep(a,function(e){return e.nodeType===1});if(Qa.test(b))return c.filter(b,f,!d);else b=c.filter(b,f)}return c.grep(a,function(e){return c.inArray(e,b)>=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f<e;f++){d=b.length;c.find(a,this[f],b);if(f>0)for(var i=d;i<b.length;i++)for(var j=0;j<d;j++)if(b[j]===b[i]){b.splice(i--,1);break}}return b},has:function(a){var b=c(a);return this.filter(function(){for(var d=
+0,f=b.length;d<f;d++)if(c.contains(this,b[d]))return true})},not:function(a){return this.pushStack(Ea(this,a,false),"not",a)},filter:function(a){return this.pushStack(Ea(this,a,true),"filter",a)},is:function(a){return!!a&&c.filter(a,this).length>0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,i={},j;if(f&&a.length){e=0;for(var n=a.length;e<n;e++){j=a[e];i[j]||(i[j]=c.expr.match.POS.test(j)?c(j,b||this.context):j)}for(;f&&f.ownerDocument&&f!==b;){for(j in i){e=i[j];if(e.jquery?e.index(f)>
+-1:c(f).is(e)){d.push({selector:j,elem:f});delete i[j]}}f=f.parentNode}}return d}var o=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(m,s){for(;s&&s.ownerDocument&&s!==b;){if(o?o.index(s)>-1:c(s).is(a))return s;s=s.parentNode}return null})},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),
+a);return this.pushStack(pa(a[0])||pa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},
+nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);bb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):
+e;if((this.length>1||db.test(f))&&cb.test(a))e=e.reverse();return this.pushStack(e,a,Q.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===v||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==
+b&&d.push(a);return d}});var Fa=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ga=/(<([\w:]+)[^>]*?)\/>/g,eb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,Ha=/<([\w:]+)/,fb=/<tbody/i,gb=/<|&\w+;/,sa=/checked\s*(?:[^=]|=\s*.checked.)/i,Ia=function(a,b,d){return eb.test(d)?a:b+"></"+d+">"},F={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],
+col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div<div>","</div>"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==v)return this.empty().append((this[0]&&this[0].ownerDocument||r).createTextNode(a));return c.getText(this)},
+wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?
+d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,
+false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&
+!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Fa,"").replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){qa(this,b);qa(this.find("*"),b.find("*"))}return b},html:function(a){if(a===v)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Fa,""):null;else if(typeof a==="string"&&!/<script/i.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(Ha.exec(a)||
+["",""])[1].toLowerCase()]){a=a.replace(Ga,Ia);try{for(var b=0,d=this.length;b<d;b++)if(this[b].nodeType===1){c.cleanData(this[b].getElementsByTagName("*"));this[b].innerHTML=a}}catch(f){this.empty().append(a)}}else c.isFunction(a)?this.each(function(e){var i=c(this),j=i.html();i.empty().append(function(){return a.call(this,e,j)})}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(c.isFunction(a))return this.each(function(b){var d=c(this),f=d.html();d.replaceWith(a.call(this,
+b,f))});else a=c(a).detach();return this.each(function(){var b=this.nextSibling,d=this.parentNode;c(this).remove();b?c(b).before(a):c(d).append(a)})}else return this.pushStack(c(c.isFunction(a)?a():a),"replaceWith",a)},detach:function(a){return this.remove(a,true)},domManip:function(a,b,d){function f(s){return c.nodeName(s,"table")?s.getElementsByTagName("tbody")[0]||s.appendChild(s.ownerDocument.createElement("tbody")):s}var e,i,j=a[0],n=[];if(!c.support.checkClone&&arguments.length===3&&typeof j===
+"string"&&sa.test(j))return this.each(function(){c(this).domManip(a,b,d,true)});if(c.isFunction(j))return this.each(function(s){var x=c(this);a[0]=j.call(this,s,b?x.html():v);x.domManip(a,b,d)});if(this[0]){e=a[0]&&a[0].parentNode&&a[0].parentNode.nodeType===11?{fragment:a[0].parentNode}:ra(a,this,n);if(i=e.fragment.firstChild){b=b&&c.nodeName(i,"tr");for(var o=0,m=this.length;o<m;o++)d.call(b?f(this[o],i):this[o],e.cacheable||this.length>1||o>0?e.fragment.cloneNode(true):e.fragment)}n&&c.each(n,
+Ma)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);for(var e=0,i=d.length;e<i;e++){var j=(e>0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),j);f=f.concat(j)}return this.pushStack(f,a,d.selector)}});c.each({remove:function(a,b){if(!a||c.filter(a,[this]).length){if(!b&&this.nodeType===1){c.cleanData(this.getElementsByTagName("*"));c.cleanData([this])}this.parentNode&&
+this.parentNode.removeChild(this)}},empty:function(){for(this.nodeType===1&&c.cleanData(this.getElementsByTagName("*"));this.firstChild;)this.removeChild(this.firstChild)}},function(a,b){c.fn[a]=function(){return this.each(b,arguments)}});c.extend({clean:function(a,b,d,f){b=b||r;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||r;var e=[];c.each(a,function(i,j){if(typeof j==="number")j+="";if(j){if(typeof j==="string"&&!gb.test(j))j=b.createTextNode(j);else if(typeof j===
+"string"){j=j.replace(Ga,Ia);var n=(Ha.exec(j)||["",""])[1].toLowerCase(),o=F[n]||F._default,m=o[0];i=b.createElement("div");for(i.innerHTML=o[1]+j+o[2];m--;)i=i.lastChild;if(!c.support.tbody){m=fb.test(j);n=n==="table"&&!m?i.firstChild&&i.firstChild.childNodes:o[1]==="<table>"&&!m?i.childNodes:[];for(o=n.length-1;o>=0;--o)c.nodeName(n[o],"tbody")&&!n[o].childNodes.length&&n[o].parentNode.removeChild(n[o])}!c.support.leadingWhitespace&&V.test(j)&&i.insertBefore(b.createTextNode(V.exec(j)[0]),i.firstChild);
+j=c.makeArray(i.childNodes)}if(j.nodeType)e.push(j);else e=c.merge(e,j)}});if(d)for(a=0;e[a];a++)if(f&&c.nodeName(e[a],"script")&&(!e[a].type||e[a].type.toLowerCase()==="text/javascript"))f.push(e[a].parentNode?e[a].parentNode.removeChild(e[a]):e[a]);else{e[a].nodeType===1&&e.splice.apply(e,[a+1,0].concat(c.makeArray(e[a].getElementsByTagName("script"))));d.appendChild(e[a])}return e},cleanData:function(a){for(var b=0,d;(d=a[b])!=null;b++){c.event.remove(d);c.removeData(d)}}});var hb=/z-?index|font-?weight|opacity|zoom|line-?height/i,
+Ja=/alpha\([^)]*\)/,Ka=/opacity=([^)]*)/,ga=/float/i,ha=/-([a-z])/ig,ib=/([A-Z])/g,jb=/^-?\d+(?:px)?$/i,kb=/^-?\d/,lb={position:"absolute",visibility:"hidden",display:"block"},mb=["Left","Right"],nb=["Top","Bottom"],ob=r.defaultView&&r.defaultView.getComputedStyle,La=c.support.cssFloat?"cssFloat":"styleFloat",ia=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===v)return c.curCSS(d,f);if(typeof e==="number"&&!hb.test(f))e+="px";c.style(d,f,e)})};
+c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return v;if((b==="width"||b==="height")&&parseFloat(d)<0)d=v;var f=a.style||a,e=d!==v;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter=Ja.test(a)?a.replace(Ja,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Ka.exec(f.filter)[1])/100+"":""}if(ga.test(b))b=La;b=b.replace(ha,ia);if(e)f[b]=d;return f[b]},css:function(a,
+b,d,f){if(b==="width"||b==="height"){var e,i=b==="width"?mb:nb;function j(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(i,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a,"border"+this+"Width",true))||0})}a.offsetWidth!==0?j():c.swap(a,lb,j);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&
+a.currentStyle){f=Ka.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ga.test(b))b=La;if(!d&&e&&e[b])f=e[b];else if(ob){if(ga.test(b))b="float";b=b.replace(ib,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f=a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ha,ia);f=a.currentStyle[b]||a.currentStyle[d];if(!jb.test(f)&&kb.test(f)){b=e.left;var i=a.runtimeStyle.left;a.runtimeStyle.left=
+a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=i}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var pb=
+J(),qb=/<script(.|\s)*?\/script>/gi,rb=/select|textarea/i,sb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ja=/\?/,tb=/(\?|&)_=.*?(&|$)/,ub=/^(\w+:)?\/\/([^\/?#]+)/,vb=/%20/g;c.fn.extend({_load:c.fn.load,load:function(a,b,d){if(typeof a!=="string")return this._load(a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=
+c.param(b,c.ajaxSettings.traditional);f="POST"}var i=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(j,n){if(n==="success"||n==="notmodified")i.html(e?c("<div />").append(j.responseText.replace(qb,"")).find(e):j.responseText);d&&i.each(d,[j.responseText,n,j])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&
+(this.checked||rb.test(this.nodeName)||sb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,
+b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:z.XMLHttpRequest&&(z.location.protocol!=="file:"||!z.ActiveXObject)?function(){return new z.XMLHttpRequest}:
+function(){try{return new z.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&&e.success.call(o,n,j,w);e.global&&f("ajaxSuccess",[w,e])}function d(){e.complete&&e.complete.call(o,w,j);e.global&&f("ajaxComplete",[w,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}
+function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),i,j,n,o=a&&a.context||e,m=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(m==="GET")N.test(e.url)||(e.url+=(ja.test(e.url)?"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||
+N.test(e.url))){i=e.jsonpCallback||"jsonp"+pb++;if(e.data)e.data=(e.data+"").replace(N,"="+i+"$1");e.url=e.url.replace(N,"="+i+"$1");e.dataType="script";z[i]=z[i]||function(q){n=q;b();d();z[i]=v;try{delete z[i]}catch(p){}A&&A.removeChild(B)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache===false&&m==="GET"){var s=J(),x=e.url.replace(tb,"$1_="+s+"$2");e.url=x+(x===e.url?(ja.test(e.url)?"&":"?")+"_="+s:"")}if(e.data&&m==="GET")e.url+=(ja.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&
+c.event.trigger("ajaxStart");s=(s=ub.exec(e.url))&&(s[1]&&s[1]!==location.protocol||s[2]!==location.host);if(e.dataType==="script"&&m==="GET"&&s){var A=r.getElementsByTagName("head")[0]||r.documentElement,B=r.createElement("script");B.src=e.url;if(e.scriptCharset)B.charset=e.scriptCharset;if(!i){var C=false;B.onload=B.onreadystatechange=function(){if(!C&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){C=true;b();d();B.onload=B.onreadystatechange=null;A&&B.parentNode&&
+A.removeChild(B)}}}A.insertBefore(B,A.firstChild);return v}var E=false,w=e.xhr();if(w){e.username?w.open(m,e.url,e.async,e.username,e.password):w.open(m,e.url,e.async);try{if(e.data||a&&a.contentType)w.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&w.setRequestHeader("If-Modified-Since",c.lastModified[e.url]);c.etag[e.url]&&w.setRequestHeader("If-None-Match",c.etag[e.url])}s||w.setRequestHeader("X-Requested-With","XMLHttpRequest");w.setRequestHeader("Accept",
+e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(fa){}if(e.beforeSend&&e.beforeSend.call(o,w,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");w.abort();return false}e.global&&f("ajaxSend",[w,e]);var g=w.onreadystatechange=function(q){if(!w||w.readyState===0||q==="abort"){E||d();E=true;if(w)w.onreadystatechange=c.noop}else if(!E&&w&&(w.readyState===4||q==="timeout")){E=true;w.onreadystatechange=c.noop;j=q==="timeout"?"timeout":!c.httpSuccess(w)?
+"error":e.ifModified&&c.httpNotModified(w,e.url)?"notmodified":"success";var p;if(j==="success")try{n=c.httpData(w,e.dataType,e)}catch(u){j="parsererror";p=u}if(j==="success"||j==="notmodified")i||b();else c.handleError(e,w,j,p);d();q==="timeout"&&w.abort();if(e.async)w=null}};try{var h=w.abort;w.abort=function(){w&&h.call(w);g("abort")}}catch(k){}e.async&&e.timeout>0&&setTimeout(function(){w&&!E&&g("timeout")},e.timeout);try{w.send(m==="POST"||m==="PUT"||m==="DELETE"?e.data:null)}catch(l){c.handleError(e,
+w,null,l);d()}e.async||g();return w}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=
+f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(j,n){if(c.isArray(n))c.each(n,
+function(o,m){b?f(j,m):d(j+"["+(typeof m==="object"||c.isArray(m)?o:"")+"]",m)});else!b&&n!=null&&typeof n==="object"?c.each(n,function(o,m){d(j+"["+o+"]",m)}):f(j,n)}function f(j,n){n=c.isFunction(n)?n():n;e[e.length]=encodeURIComponent(j)+"="+encodeURIComponent(n)}var e=[];if(b===v)b=c.ajaxSettings.traditional;if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var i in a)d(i,a[i]);return e.join("&").replace(vb,"+")}});var ka={},wb=/toggle|show|hide/,xb=/^([+-]=)?([\d+-.]+)(.*)$/,
+W,ta=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");this[a].style.display=d||"";if(c.css(this[a],"display")==="none"){d=this[a].nodeName;var f;if(ka[d])f=ka[d];else{var e=c("<"+d+" />").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();
+ka[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a<b;a++)this[a].style.display=c.data(this[a],"olddisplay")||"";return this}},hide:function(a,b){if(a||a===0)return this.animate(K("hide",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");!d&&d!=="none"&&c.data(this[a],"olddisplay",c.css(this[a],"display"))}a=0;for(b=this.length;a<b;a++)this[a].style.display="none";return this}},_toggle:c.fn.toggle,toggle:function(a,b){var d=typeof a==="boolean";if(c.isFunction(a)&&
+c.isFunction(b))this._toggle.apply(this,arguments);else a==null||d?this.each(function(){var f=d?a:c(this).is(":hidden");c(this)[f?"show":"hide"]()}):this.animate(K("toggle",3),a,b);return this},fadeTo:function(a,b,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,d)},animate:function(a,b,d,f){var e=c.speed(b,d,f);if(c.isEmptyObject(a))return this.each(e.complete);return this[e.queue===false?"each":"queue"](function(){var i=c.extend({},e),j,n=this.nodeType===1&&c(this).is(":hidden"),
+o=this;for(j in a){var m=j.replace(ha,ia);if(j!==m){a[m]=a[j];delete a[j];j=m}if(a[j]==="hide"&&n||a[j]==="show"&&!n)return i.complete.call(this);if((j==="height"||j==="width")&&this.style){i.display=c.css(this,"display");i.overflow=this.style.overflow}if(c.isArray(a[j])){(i.specialEasing=i.specialEasing||{})[j]=a[j][1];a[j]=a[j][0]}}if(i.overflow!=null)this.style.overflow="hidden";i.curAnim=c.extend({},a);c.each(a,function(s,x){var A=new c.fx(o,i,s);if(wb.test(x))A[x==="toggle"?n?"show":"hide":x](a);
+else{var B=xb.exec(x),C=A.cur(true)||0;if(B){x=parseFloat(B[2]);var E=B[3]||"px";if(E!=="px"){o.style[s]=(x||1)+E;C=(x||1)/A.cur(true)*C;o.style[s]=C+E}if(B[1])x=(B[1]==="-="?-1:1)*x+C;A.custom(C,x,E)}else A.custom(C,x,"")}});return true})},stop:function(a,b){var d=c.timers;a&&this.queue([]);this.each(function(){for(var f=d.length-1;f>=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",
+1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration==="number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,
+b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==
+null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(i){return e.step(i)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop===
+"width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=
+this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem,e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=
+c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length||c.fx.stop()},stop:function(){clearInterval(W);W=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){c.style(a.elem,"opacity",a.now)},_default:function(a){if(a.elem.style&&a.elem.style[a.prop]!=
+null)a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit;else a.elem[a.prop]=a.now}}});if(c.expr&&c.expr.filters)c.expr.filters.animated=function(a){return c.grep(c.timers,function(b){return a===b.elem}).length};c.fn.offset="getBoundingClientRect"in r.documentElement?function(a){var b=this[0];if(a)return this.each(function(e){c.offset.setOffset(this,a,e)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);var d=b.getBoundingClientRect(),
+f=b.ownerDocument;b=f.body;f=f.documentElement;return{top:d.top+(self.pageYOffset||c.support.boxModel&&f.scrollTop||b.scrollTop)-(f.clientTop||b.clientTop||0),left:d.left+(self.pageXOffset||c.support.boxModel&&f.scrollLeft||b.scrollLeft)-(f.clientLeft||b.clientLeft||0)}}:function(a){var b=this[0];if(a)return this.each(function(s){c.offset.setOffset(this,a,s)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);c.offset.initialize();var d=b.offsetParent,f=
+b,e=b.ownerDocument,i,j=e.documentElement,n=e.body;f=(e=e.defaultView)?e.getComputedStyle(b,null):b.currentStyle;for(var o=b.offsetTop,m=b.offsetLeft;(b=b.parentNode)&&b!==n&&b!==j;){if(c.offset.supportsFixedPosition&&f.position==="fixed")break;i=e?e.getComputedStyle(b,null):b.currentStyle;o-=b.scrollTop;m-=b.scrollLeft;if(b===d){o+=b.offsetTop;m+=b.offsetLeft;if(c.offset.doesNotAddBorder&&!(c.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(b.nodeName))){o+=parseFloat(i.borderTopWidth)||
+0;m+=parseFloat(i.borderLeftWidth)||0}f=d;d=b.offsetParent}if(c.offset.subtractsBorderForOverflowNotVisible&&i.overflow!=="visible"){o+=parseFloat(i.borderTopWidth)||0;m+=parseFloat(i.borderLeftWidth)||0}f=i}if(f.position==="relative"||f.position==="static"){o+=n.offsetTop;m+=n.offsetLeft}if(c.offset.supportsFixedPosition&&f.position==="fixed"){o+=Math.max(j.scrollTop,n.scrollTop);m+=Math.max(j.scrollLeft,n.scrollLeft)}return{top:o,left:m}};c.offset={initialize:function(){var a=r.body,b=r.createElement("div"),
+d,f,e,i=parseFloat(c.curCSS(a,"marginTop",true))||0;c.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"});b.innerHTML="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";a.insertBefore(b,a.firstChild);
+d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i;a.removeChild(b);c.offset.initialize=c.noop},
+bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),i=parseInt(c.curCSS(a,"top",true),10)||0,j=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a,d,e);d={top:b.top-e.top+i,left:b.left-
+e.left+j};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top-f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=
+this.offsetParent||r.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],i;if(!e)return null;if(f!==v)return this.each(function(){if(i=ua(this))i.scrollTo(!a?f:c(i).scrollLeft(),a?f:c(i).scrollTop());else this[d]=f});else return(i=ua(e))?"pageXOffset"in i?i[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&i.document.documentElement[d]||i.document.body[d]:e[d]}});
+c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(i){var j=c(this);j[d](f.call(this,i,j[d]()))});return"scrollTo"in e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||
+e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===v?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});z.jQuery=z.$=c})(window);
+
index b864867fd17d12114092c3a9bb5ea5c9b0865fb1..3623337b9f65725acbcf16f6ce33e420350de5b5 100644 (file)
@@ -143,87 +143,85 @@ var SN = { // StatusNet
             SN.U.Counter(form);
         },
 
-        FormXHR: function(f) {
-            if (jQuery.data(f[0], "ElementData") === undefined) {
-                jQuery.data(f[0], "ElementData", {Bind:'submit'});
-                f.bind('submit', function(e) {
-                    form_id = $(this)[0].id;
-                    $.ajax({
-                        type: 'POST',
-                        dataType: 'xml',
-                        url: $(this)[0].action,
-                        data: $(this).serialize() + '&ajax=1',
-                        beforeSend: function(xhr) {
-                            $('#'+form_id).addClass(SN.C.S.Processing);
-                            $('#'+form_id+' .submit').addClass(SN.C.S.Disabled);
-                            $('#'+form_id+' .submit').attr(SN.C.S.Disabled, SN.C.S.Disabled);
-                        },
-                        error: function (xhr, textStatus, errorThrown) {
-                            alert(errorThrown || textStatus);
-                        },
-                        success: function(data, textStatus) {
-                            if (typeof($('form', data)[0]) != 'undefined') {
-                                form_new = document._importNode($('form', data)[0], true);
-                                $('#'+form_id).replaceWith(form_new);
-                                $('#'+form_new.id).each(function() { SN.U.FormXHR($(this)); });
-                            }
-                            else {
-                                $('#'+form_id).replaceWith(document._importNode($('p', data)[0], true));
-                            }
-                        }
-                    });
-                    return false;
-                });
-            }
+        FormXHR: function(form) {
+            $.ajax({
+                type: 'POST',
+                dataType: 'xml',
+                url: form.attr('action'),
+                data: form.serialize() + '&ajax=1',
+                beforeSend: function(xhr) {
+                    form
+                        .addClass(SN.C.S.Processing)
+                        .find('.submit')
+                            .addClass(SN.C.S.Disabled)
+                            .attr(SN.C.S.Disabled, SN.C.S.Disabled);
+                },
+                error: function (xhr, textStatus, errorThrown) {
+                    alert(errorThrown || textStatus);
+                },
+                success: function(data, textStatus) {
+                    if (typeof($('form', data)[0]) != 'undefined') {
+                        form_new = document._importNode($('form', data)[0], true);
+                        form.replaceWith(form_new);
+                    }
+                    else {
+                        form.replaceWith(document._importNode($('p', data)[0], true));
+                    }
+                }
+            });
         },
 
         FormNoticeXHR: function(form) {
-            var NDG, NLat, NLon, NLNS, NLID;
+            SN.C.I.NoticeDataGeo = {};
             form_id = form.attr('id');
             form.append('<input type="hidden" name="ajax" value="1"/>');
             form.ajaxForm({
                 dataType: 'xml',
                 timeout: '60000',
                 beforeSend: function(formData) {
-                    if ($('#'+form_id+' #'+SN.C.S.NoticeDataText)[0].value.length === 0) {
+                    if (form.find('#'+SN.C.S.NoticeDataText)[0].value.length === 0) {
                         form.addClass(SN.C.S.Warning);
                         return false;
                     }
-                    form.addClass(SN.C.S.Processing);
-                    $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).addClass(SN.C.S.Disabled);
-                    $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).attr(SN.C.S.Disabled, SN.C.S.Disabled);
-
-                    NLat = $('#'+SN.C.S.NoticeLat).val();
-                    NLon = $('#'+SN.C.S.NoticeLon).val();
-                    NLNS = $('#'+SN.C.S.NoticeLocationNs).val();
-                    NLID = $('#'+SN.C.S.NoticeLocationId).val();
-                    NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked');
+                    form
+                        .addClass(SN.C.S.Processing)
+                        .find('#'+SN.C.S.NoticeActionSubmit)
+                            .addClass(SN.C.S.Disabled)
+                            .attr(SN.C.S.Disabled, SN.C.S.Disabled);
+
+                    SN.C.I.NoticeDataGeo.NLat = $('#'+SN.C.S.NoticeLat).val();
+                    SN.C.I.NoticeDataGeo.NLon = $('#'+SN.C.S.NoticeLon).val();
+                    SN.C.I.NoticeDataGeo.NLNS = $('#'+SN.C.S.NoticeLocationNs).val();
+                    SN.C.I.NoticeDataGeo.NLID = $('#'+SN.C.S.NoticeLocationId).val();
+                    SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked');
 
                     cookieValue = $.cookie(SN.C.S.NoticeDataGeoCookie);
 
                     if (cookieValue !== null && cookieValue != 'disabled') {
                         cookieValue = JSON.parse(cookieValue);
-                        NLat = $('#'+SN.C.S.NoticeLat).val(cookieValue.NLat).val();
-                        NLon = $('#'+SN.C.S.NoticeLon).val(cookieValue.NLon).val();
+                        SN.C.I.NoticeDataGeo.NLat = $('#'+SN.C.S.NoticeLat).val(cookieValue.NLat).val();
+                        SN.C.I.NoticeDataGeo.NLon = $('#'+SN.C.S.NoticeLon).val(cookieValue.NLon).val();
                         if ($('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS)) {
-                            NLNS = $('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS).val();
-                            NLID = $('#'+SN.C.S.NoticeLocationId).val(cookieValue.NLID).val();
+                            SN.C.I.NoticeDataGeo.NLNS = $('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS).val();
+                            SN.C.I.NoticeDataGeo.NLID = $('#'+SN.C.S.NoticeLocationId).val(cookieValue.NLID).val();
                         }
                     }
                     if (cookieValue == 'disabled') {
-                        NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', false).attr('checked');
+                        SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', false).attr('checked');
                     }
                     else {
-                        NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', true).attr('checked');
+                        SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', true).attr('checked');
                     }
 
                     return true;
                 },
                 error: function (xhr, textStatus, errorThrown) {
-                    form.removeClass(SN.C.S.Processing);
-                    $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).removeClass(SN.C.S.Disabled);
-                    $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).removeAttr(SN.C.S.Disabled, SN.C.S.Disabled);
-                    $('#'+form_id+' .form_response').remove();
+                    form
+                        .removeClass(SN.C.S.Processing)
+                        .find('#'+SN.C.S.NoticeActionSubmit)
+                            .removeClass(SN.C.S.Disabled)
+                            .removeAttr(SN.C.S.Disabled, SN.C.S.Disabled);
+                    form.find('.form_response').remove();
                     if (textStatus == 'timeout') {
                         form.append('<p class="form_response error">Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists.</p>');
                     }
@@ -233,9 +231,10 @@ var SN = { // StatusNet
                         }
                         else {
                             if (parseInt(xhr.status) === 0 || jQuery.inArray(parseInt(xhr.status), SN.C.I.HTTP20x30x) >= 0) {
-                                $('#'+form_id).resetForm();
-                                $('#'+form_id+' #'+SN.C.S.NoticeDataAttachSelected).remove();
-                                SN.U.FormNoticeEnhancements($('#'+form_id));
+                                form
+                                    .resetForm()
+                                    .find('#'+SN.C.S.NoticeDataAttachSelected).remove();
+                                SN.U.FormNoticeEnhancements(form);
                             }
                             else {
                                 form.append('<p class="form_response error">(Sorry! We had trouble sending your notice ('+xhr.status+' '+xhr.statusText+'). Please report the problem to the site administrator if this happens again.</p>');
@@ -244,7 +243,7 @@ var SN = { // StatusNet
                     }
                 },
                 success: function(data, textStatus) {
-                    $('#'+form_id+' .form_response').remove();
+                    form.find('.form_response').remove();
                     var result;
                     if ($('#'+SN.C.S.Error, data).length > 0) {
                         result = document._importNode($('p', data)[0], true);
@@ -277,11 +276,11 @@ var SN = { // StatusNet
                                     else {
                                         notices.prepend(notice);
                                     }
-                                    $('#'+notice.id).css({display:'none'});
-                                    $('#'+notice.id).fadeIn(2500);
+                                    $('#'+notice.id)
+                                        .css({display:'none'})
+                                        .fadeIn(2500);
                                     SN.U.NoticeWithAttachment($('#'+notice.id));
                                     SN.U.NoticeReplyTo($('#'+notice.id));
-                                    SN.U.FormXHR($('#'+notice.id+' .form_favor'));
                                 }
                             }
                             else {
@@ -290,24 +289,26 @@ var SN = { // StatusNet
                                 form.append('<p class="form_response success">'+result_title+'</p>');
                             }
                         }
-                        $('#'+form_id).resetForm();
-                        $('#'+form_id+' #'+SN.C.S.NoticeInReplyTo).val('');
-                        $('#'+form_id+' #'+SN.C.S.NoticeDataAttachSelected).remove();
-                        SN.U.FormNoticeEnhancements($('#'+form_id));
+                        form.resetForm();
+                        form.find('#'+SN.C.S.NoticeInReplyTo).val('');
+                        form.find('#'+SN.C.S.NoticeDataAttachSelected).remove();
+                        SN.U.FormNoticeEnhancements(form);
                     }
                 },
                 complete: function(xhr, textStatus) {
-                    form.removeClass(SN.C.S.Processing);
-                    $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).removeAttr(SN.C.S.Disabled);
-                    $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).removeClass(SN.C.S.Disabled);
-
-                    $('#'+SN.C.S.NoticeLat).val(NLat);
-                    $('#'+SN.C.S.NoticeLon).val(NLon);
+                    form
+                        .removeClass(SN.C.S.Processing)
+                        .find('#'+SN.C.S.NoticeActionSubmit)
+                            .removeAttr(SN.C.S.Disabled)
+                            .removeClass(SN.C.S.Disabled);
+
+                    $('#'+SN.C.S.NoticeLat).val(SN.C.I.NoticeDataGeo.NLat);
+                    $('#'+SN.C.S.NoticeLon).val(SN.C.I.NoticeDataGeo.NLon);
                     if ($('#'+SN.C.S.NoticeLocationNs)) {
-                        $('#'+SN.C.S.NoticeLocationNs).val(NLNS);
-                        $('#'+SN.C.S.NoticeLocationId).val(NLID);
+                        $('#'+SN.C.S.NoticeLocationNs).val(SN.C.I.NoticeDataGeo.NLNS);
+                        $('#'+SN.C.S.NoticeLocationId).val(SN.C.I.NoticeDataGeo.NLID);
                     }
-                    $('#'+SN.C.S.NoticeDataGeo).attr('checked', NDG);
+                    $('#'+SN.C.S.NoticeDataGeo).attr('checked', SN.C.I.NoticeDataGeo.NDG);
                 }
             });
         },
@@ -350,46 +351,49 @@ var SN = { // StatusNet
         },
 
         NoticeFavor: function() {
-            $('.form_favor').each(function() { SN.U.FormXHR($(this)); });
-            $('.form_disfavor').each(function() { SN.U.FormXHR($(this)); });
+            $('.form_favor').live('click', function() { SN.U.FormXHR($(this)); return false; });
+            $('.form_disfavor').live('click', function() { SN.U.FormXHR($(this)); return false; });
         },
 
         NoticeRepeat: function() {
-            $('.form_repeat').each(function() {
-                SN.U.FormXHR($(this));
+            $('.form_repeat').live('click', function(e) {
+                e.preventDefault();
+
                 SN.U.NoticeRepeatConfirmation($(this));
+                return false;
             });
         },
 
         NoticeRepeatConfirmation: function(form) {
-            function NRC() {
-                form.closest('.notice-options').addClass('opaque');
-                form.addClass('dialogbox');
+            var submit_i = form.find('.submit');
 
-                form.append('<button class="close">&#215;</button>');
-                form.find('button.close').click(function(){
-                    $(this).remove();
+            var submit = submit_i.clone();
+            submit
+                .addClass('submit_dialogbox')
+                .removeClass('submit');
+            form.append(submit);
+            submit.bind('click', function() { SN.U.FormXHR(form); return false; });
 
-                    form.closest('.notice-options').removeClass('opaque');
-                    form.removeClass('dialogbox');
-                    form.find('.submit_dialogbox').remove();
-                    form.find('.submit').show();
+            submit_i.hide();
 
-                    return false;
-                });
-            };
+            form
+                .addClass('dialogbox')
+                .append('<button class="close">&#215;</button>')
+                .closest('.notice-options')
+                    .addClass('opaque');
 
-            form.find('.submit').bind('click', function(e) {
-                e.preventDefault();
+            form.find('button.close').click(function(){
+                $(this).remove();
 
-                var submit = form.find('.submit').clone();
-                submit.addClass('submit_dialogbox');
-                submit.removeClass('submit');
-                form.append(submit);
+                form
+                    .removeClass('dialogbox')
+                    .closest('.notice-options')
+                        .removeClass('opaque');
 
-                $(this).hide();
+                form.find('.submit_dialogbox').remove();
+                form.find('.submit').show();
 
-                NRC();
+                return false;
             });
         },
 
@@ -400,12 +404,10 @@ var SN = { // StatusNet
         },
 
         NoticeWithAttachment: function(notice) {
-            if ($('.attachment', notice).length === 0) {
+            if (notice.find('.attachment').length === 0) {
                 return;
             }
 
-            var notice_id = notice.attr('id');
-
             $.fn.jOverlay.options = {
                 method : 'GET',
                 data : '',
@@ -425,35 +427,37 @@ var SN = { // StatusNet
                 css : {'max-width':'542px', 'top':'5%', 'left':'32.5%'}
             };
 
-            $('#'+notice_id+' a.attachment').click(function() {
+            notice.find('a.attachment').click(function() {
                 $().jOverlay({url: $('address .url')[0].href+'attachment/' + ($(this).attr('id').substring('attachment'.length + 1)) + '/ajax'});
                 return false;
             });
 
-            var t;
-            $("body:not(#shownotice) #"+notice_id+" a.thumbnail").hover(
-                function() {
-                    var anchor = $(this);
-                    $("a.thumbnail").children('img').hide();
-                    anchor.closest(".entry-title").addClass('ov');
-
-                    if (anchor.children('img').length === 0) {
-                        t = setTimeout(function() {
-                            $.get($('address .url')[0].href+'attachment/' + (anchor.attr('id').substring('attachment'.length + 1)) + '/thumbnail', null, function(data) {
-                                anchor.append(data);
-                            });
-                        }, 500);
-                    }
-                    else {
-                        anchor.children('img').show();
+            if ($('#shownotice').length == 0) {
+                var t;
+                notice.find('a.thumbnail').hover(
+                    function() {
+                        var anchor = $(this);
+                        $('a.thumbnail').children('img').hide();
+                        anchor.closest(".entry-title").addClass('ov');
+
+                        if (anchor.children('img').length === 0) {
+                            t = setTimeout(function() {
+                                $.get($('address .url')[0].href+'attachment/' + (anchor.attr('id').substring('attachment'.length + 1)) + '/thumbnail', null, function(data) {
+                                    anchor.append(data);
+                                });
+                            }, 500);
+                        }
+                        else {
+                            anchor.children('img').show();
+                        }
+                    },
+                    function() {
+                        clearTimeout(t);
+                        $('a.thumbnail').children('img').hide();
+                        $(this).closest('.entry-title').removeClass('ov');
                     }
-                },
-                function() {
-                    clearTimeout(t);
-                    $("a.thumbnail").children('img').hide();
-                    $(this).closest(".entry-title").removeClass('ov');
-                }
-            );
+                );
+            }
         },
 
         NoticeDataAttach: function() {
@@ -639,7 +643,7 @@ var SN = { // StatusNet
             NDM.bind('click', function() {
                 var NDMF = $('.entity_send-a-message form');
                 if (NDMF.length === 0) {
-                    $(this).addClass('processing');
+                    $(this).addClass(SN.C.S.Processing);
                     $.get(NDM.attr('href'), null, function(data) {
                         $('.entity_send-a-message').append(document._importNode($('form', data)[0], true));
                         NDMF = $('.entity_send-a-message .form_notice');
@@ -650,7 +654,7 @@ var SN = { // StatusNet
                             NDMF.hide();
                             return false;
                         });
-                        NDM.removeClass('processing');
+                        NDM.removeClass(SN.C.S.Processing);
                     });
                 }
                 else {
@@ -695,11 +699,11 @@ var SN = { // StatusNet
 
         EntityActions: function() {
             if ($('body.user_in').length > 0) {
-                $('.form_user_subscribe').each(function() { SN.U.FormXHR($(this)); });
-                $('.form_user_unsubscribe').each(function() { SN.U.FormXHR($(this)); });
-                $('.form_group_join').each(function() { SN.U.FormXHR($(this)); });
-                $('.form_group_leave').each(function() { SN.U.FormXHR($(this)); });
-                $('.form_user_nudge').each(function() { SN.U.FormXHR($(this)); });
+                $('.form_user_subscribe').live('click', function() { SN.U.FormXHR($(this)); return false; });
+                $('.form_user_unsubscribe').live('click', function() { SN.U.FormXHR($(this)); return false; });
+                $('.form_group_join').live('click', function() { SN.U.FormXHR($(this)); return false; });
+                $('.form_group_leave').live('click', function() { SN.U.FormXHR($(this)); return false; });
+                $('.form_user_nudge').live('click', function() { SN.U.FormXHR($(this)); return false; });
 
                 SN.U.NewDirectMessage();
             }
index cc4f4aad074b910399ac84133cc993621dcb5e5a..b85f353a3d14087a460e8a492d4d0504183ab87e 100644 (file)
@@ -405,6 +405,7 @@ class Action extends HTMLOutputter // lawsuit
                                             'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'),
                                             'alt' => common_config('site', 'name')));
             }
+            $this->text(' ');
             $this->element('span', array('class' => 'fn org'), common_config('site', 'name'));
             $this->elementEnd('a');
             Event::handle('EndAddressData', array($this));
@@ -822,12 +823,14 @@ class Action extends HTMLOutputter // lawsuit
                                             'alt' => common_config('license', 'title'),
                                             'width' => '80',
                                             'height' => '15'));
+                $this->text(' ');
                 //TODO: This is dirty: i18n
                 $this->text(_('All '.common_config('site', 'name').' content and data are available under the '));
                 $this->element('a', array('class' => 'license',
                                           'rel' => 'external license',
                                           'href' => common_config('license', 'url')),
                                common_config('license', 'title'));
+                $this->text(' ');
                 $this->text(_('license.'));
                 $this->elementEnd('p');
                 break;
index f8197521672264a8ed8d7ae5e06aa7e89bc1071b..22eef7436dfc8ac1f010882f699786e480ff1b8a 100644 (file)
@@ -77,6 +77,7 @@ class ApiAction extends Action
 
     function prepare($args)
     {
+        StatusNet::setApi(true); // reduce exception reports to aid in debugging
         parent::prepare($args);
 
         $this->format   = $this->arg('format');
@@ -1102,7 +1103,7 @@ class ApiAction extends Action
         }
     }
 
-    function serverError($msg, $code = 500, $content_type = 'json')
+    function serverError($msg, $code = 500, $content_type = 'xml')
     {
         $action = $this->trimmed('action');
 
@@ -1153,7 +1154,6 @@ class ApiAction extends Action
         $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
                                           'xml:lang' => 'en-US',
                                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
-        Event::handle('StartApiAtom', array($this));
     }
 
     function endTwitterAtom()
@@ -1320,4 +1320,22 @@ class ApiAction extends Action
         }
     }
 
+    function getSelfUri($action, $aargs)
+    {
+        parse_str($_SERVER['QUERY_STRING'], $params);
+        $pstring = '';
+        if (!empty($params)) {
+            unset($params['p']);
+            $pstring = http_build_query($params);
+        }
+
+        $uri = common_local_url($action, $aargs);
+
+        if (!empty($pstring)) {
+            $uri .= '?' . $pstring;
+        }
+
+        return $uri;
+    }
+
 }
diff --git a/lib/atom10entry.php b/lib/atom10entry.php
new file mode 100644 (file)
index 0000000..5710c80
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building / manipulating an Atom entry in memory
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Feed
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')
+{
+    exit(1);
+}
+
+class Atom10EntryException extends Exception
+{
+}
+
+/**
+ * Class for manipulating an Atom entry in memory. Get the entry as an XML
+ * string with Atom10Entry::getString().
+ *
+ * @category Feed
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ */
+class Atom10Entry extends XMLStringer
+{
+    private $namespaces;
+    private $categories;
+    private $content;
+    private $contributors;
+    private $id;
+    private $links;
+    private $published;
+    private $rights;
+    private $source;
+    private $summary;
+    private $title;
+
+    function __construct($indent = true) {
+        parent::__construct($indent);
+        $this->namespaces = array();
+    }
+
+    function addNamespace($namespace, $uri)
+    {
+        $ns = array($namespace => $uri);
+        $this->namespaces = array_merge($this->namespaces, $ns);
+    }
+
+    function initEntry()
+    {
+
+    }
+
+    function endEntry()
+    {
+
+    }
+
+    /**
+     * Check that all required elements have been set, etc.
+     * Throws an Atom10EntryException if something's missing.
+     *
+     * @return void
+     */
+    function validate
+    {
+
+    }
+
+    function getString()
+    {
+        $this->validate();
+
+        $this->initEntry();
+        $this->renderEntries();
+        $this->endEntry();
+
+        return $this->xw->outputMemory();
+    }
+
+}
\ No newline at end of file
diff --git a/lib/atom10feed.php b/lib/atom10feed.php
new file mode 100644 (file)
index 0000000..14a3beb
--- /dev/null
@@ -0,0 +1,298 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an Atom feed in memory
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Feed
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET'))
+{
+    exit(1);
+}
+
+class Atom10FeedException extends Exception
+{
+}
+
+/**
+ * Class for building an Atom feed in memory.  Get the finished doc
+ * as a string with Atom10Feed::getString().
+ *
+ * @category Feed
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ */
+class Atom10Feed extends XMLStringer
+{
+    public  $xw;
+    private $namespaces;
+    private $authors;
+    private $subject;
+    private $categories;
+    private $contributors;
+    private $generator;
+    private $icon;
+    private $links;
+    private $logo;
+    private $rights;
+    private $subtitle;
+    private $title;
+    private $published;
+    private $updated;
+    private $entries;
+
+    /**
+     * Constructor
+     *
+     * @param boolean $indent  flag to turn indenting on or off
+     *
+     * @return void
+     */
+    function __construct($indent = true) {
+        parent::__construct($indent);
+        $this->namespaces = array();
+        $this->authors    = array();
+        $this->links      = array();
+        $this->entries    = array();
+        $this->addNamespace('xmlns', 'http://www.w3.org/2005/Atom');
+    }
+
+    /**
+     * Add another namespace to the feed
+     *
+     * @param string $namespace the namespace
+     * @param string $uri       namspace uri
+     *
+     * @return void
+     */
+    function addNamespace($namespace, $uri)
+    {
+        $ns = array($namespace => $uri);
+        $this->namespaces = array_merge($this->namespaces, $ns);
+    }
+
+    function addAuthor($name, $uri = null, $email = null)
+    {
+        $xs = new XMLStringer(true);
+
+        $xs->elementStart('author');
+
+        if (!empty($name)) {
+            $xs->element('name', null, $name);
+        } else {
+            throw new Atom10FeedException(
+                'author element must contain a name element.'
+            );
+        }
+
+        if (!is_null($uri)) {
+            $xs->element('uri', null, $uri);
+        }
+
+        if (!is_null(email)) {
+            $xs->element('email', null, $email);
+        }
+
+        $xs->elementEnd('author');
+
+        array_push($this->authors, $xs->getString());
+    }
+
+    /**
+     * Add an Author to the feed via raw XML string
+     *
+     * @param string $xmlAuthor An XML string representation author
+     *
+     * @return void
+     */
+    function addAuthorRaw($xmlAuthor)
+    {
+        array_push($this->authors, $xmlAuthor);
+    }
+
+    function renderAuthors()
+    {
+        foreach ($this->authors as $author) {
+            $this->raw($author);
+        }
+    }
+
+    /**
+     * Add a activity feed subject via raw XML string
+     *
+     * @param string $xmlSubject An XML string representation of the subject
+     *
+     * @return void
+     */
+    function setActivitySubject($xmlSubject)
+    {
+        $this->subject = $xmlSubject;
+    }
+
+    function getNamespaces()
+    {
+        return $this->namespaces;
+    }
+
+    function initFeed()
+    {
+        $this->xw->startDocument('1.0', 'UTF-8');
+        $commonAttrs = array('xml:lang' => 'en-US');
+        $commonAttrs = array_merge($commonAttrs, $this->namespaces);
+        $this->elementStart('feed', $commonAttrs);
+
+        $this->element('id', null, $this->id);
+        $this->element('title', null, $this->title);
+        $this->element('subtitle', null, $this->subtitle);
+
+        if (!empty($this->logo)) {
+            $this->element('logo', null, $this->logo);
+        }
+
+        $this->element('updated', null, $this->updated);
+
+        $this->renderAuthors();
+
+        $this->renderLinks();
+    }
+
+    /**
+     * Check that all required elements have been set, etc.
+     * Throws an Atom10FeedException if something's missing.
+     *
+     * @return void
+     */
+    function validate()
+    {
+    }
+
+    function renderLinks()
+    {
+        foreach ($this->links as $attrs)
+        {
+            $this->element('link', $attrs, null);
+        }
+    }
+
+    function addEntryRaw($xmlEntry)
+    {
+        array_push($this->entries, $xmlEntry);
+    }
+
+    function addEntry($entry)
+    {
+        array_push($this->entries, $entry->getString());
+    }
+
+    function renderEntries()
+    {
+        foreach ($this->entries as $entry) {
+            $this->raw($entry);
+        }
+    }
+
+    function endFeed()
+    {
+        $this->elementEnd('feed');
+        $this->xw->endDocument();
+    }
+
+    function getString()
+    {
+        if (Event::handle('StartApiAtom', array($this))) {
+
+            $this->validate();
+            $this->initFeed();
+
+            if (!empty($this->subject)) {
+                $this->raw($this->subject);
+            }
+
+            $this->renderEntries();
+            $this->endFeed();
+
+            Event::handle('EndApiAtom', array($this));
+        }
+
+        return $this->xw->outputMemory();
+    }
+
+    function setId($id)
+    {
+        $this->id = $id;
+    }
+
+    function setTitle($title)
+    {
+        $this->title = $title;
+    }
+
+    function setSubtitle($subtitle)
+    {
+        $this->subtitle = $subtitle;
+    }
+
+    function setLogo($logo)
+    {
+        $this->logo = $logo;
+    }
+
+    function setUpdated($dt)
+    {
+        $this->updated = common_date_iso8601($dt);
+    }
+
+    function setPublished($dt)
+    {
+        $this->published = common_date_iso8601($dt);
+    }
+
+    /**
+     * Adds a link element into the Atom document
+     *
+     * Assumes you want rel="alternate" and type="text/html" unless
+     * you send in $otherAttrs.
+     *
+     * @param string $uri            the uri the href needs to point to
+     * @param array  $otherAttrs     other attributes to stick in
+     *
+     * @return void
+     */
+    function addLink($uri, $otherAttrs = null) {
+        $attrs = array('href' => $uri);
+
+        if (is_null($otherAttrs)) {
+            $attrs['rel']  = 'alternate';
+            $attrs['type'] = 'text/html';
+        } else {
+            $attrs = array_merge($attrs, $otherAttrs);
+        }
+
+        array_push($this->links, $attrs);
+    }
+
+}
diff --git a/lib/atomgroupnoticefeed.php b/lib/atomgroupnoticefeed.php
new file mode 100644 (file)
index 0000000..52ee4c7
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an in-memory Atom feed for a particular group's
+ * timeline.
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Feed
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET'))
+{
+    exit(1);
+}
+
+/**
+ * Class for group notice feeds.  May contains a reference to the group.
+ *
+ * @category Feed
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ */
+class AtomGroupNoticeFeed extends AtomNoticeFeed
+{
+    private $group;
+
+    /**
+     * Constructor
+     *
+     * @param Group   $group   the group for the feed (optional)
+     * @param boolean $indent  flag to turn indenting on or off
+     *
+     * @return void
+     */
+    function __construct($group = null, $indent = true) {
+        parent::__construct($indent);
+        $this->group = $group;
+    }
+
+    function getGroup()
+    {
+        return $this->group;
+    }
+
+}
diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php
new file mode 100644 (file)
index 0000000..b7a60bd
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an Atom feed from a collection of notices
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Feed
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET'))
+{
+    exit(1);
+}
+
+/**
+ * Class for creating a feed that represents a collection of notices. Builds the
+ * feed in memory. Get the feed as a string with AtomNoticeFeed::getString().
+ *
+ * @category Feed
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ */
+class AtomNoticeFeed extends Atom10Feed
+{
+    function __construct($indent = true) {
+        parent::__construct($indent);
+
+        // Feeds containing notice info use these namespaces
+
+        $this->addNamespace(
+            'xmlns:thr',
+            'http://purl.org/syndication/thread/1.0'
+        );
+
+        $this->addNamespace(
+            'xmlns:georss',
+            'http://www.georss.org/georss'
+        );
+
+        $this->addNamespace(
+            'xmlns:activity',
+            'http://activitystrea.ms/spec/1.0/'
+        );
+
+        // XXX: What should the uri be?
+        $this->addNamespace(
+            'xmlns:ostatus',
+            'http://ostatus.org/schema/1.0'
+        );
+    }
+
+    /**
+     * Add more than one Notice to the feed
+     *
+     * @param mixed $notices an array of Notice objects or handle
+     *
+     */
+    function addEntryFromNotices($notices)
+    {
+        if (is_array($notices)) {
+            foreach ($notices as $notice) {
+                $this->addEntryFromNotice($notice);
+            }
+        } else {
+            while ($notices->fetch()) {
+                $this->addEntryFromNotice($notices);
+            }
+        }
+    }
+
+    /**
+     * Add a single Notice to the feed
+     *
+     * @param Notice $notice a Notice to add
+     */
+    function addEntryFromNotice($notice)
+    {
+        $this->addEntryRaw($notice->asAtomEntry());
+    }
+
+}
+
+
diff --git a/lib/atomusernoticefeed.php b/lib/atomusernoticefeed.php
new file mode 100644 (file)
index 0000000..9f22432
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an in-memory Atom feed for a particular user's
+ * timeline.
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Feed
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET'))
+{
+    exit(1);
+}
+
+/**
+ * Class for user notice feeds.  May contain a reference to the user.
+ *
+ * @category Feed
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ */
+class AtomUserNoticeFeed extends AtomNoticeFeed
+{
+    private $user;
+
+    /**
+     * Constructor
+     *
+     * @param User    $user    the user for the feed (optional)
+     * @param boolean $indent  flag to turn indenting on or off
+     *
+     * @return void
+     */
+    function __construct($user = null, $indent = true) {
+        parent::__construct($indent);
+        $this->user = $user;
+    }
+
+    function getUser()
+    {
+        return $this->user;
+    }
+}
index b3ec7534f5f0c92bf1d01a24ead2ead603b4ebed..c09a1dd9f27c75676eb24f634f6c0378547c798d 100644 (file)
@@ -47,6 +47,8 @@ class Cache
     var $_items   = array();
     static $_inst = null;
 
+    const COMPRESSED = 1;
+
     /**
      * Singleton constructor
      *
@@ -133,7 +135,7 @@ class Cache
      *
      * @param string  $key    The key to use for lookups
      * @param string  $value  The value to store
-     * @param integer $flag   Flags to use, mostly ignored
+     * @param integer $flag   Flags to use, may include Cache::COMPRESSED
      * @param integer $expiry Expiry value, mostly ignored
      *
      * @return boolean success flag
index a74cccae12fb29fa76053a84bbff58be440d31ea..3f53edf1457e8c7d9d90ad51b70ccf4885a566a6 100644 (file)
@@ -117,11 +117,13 @@ $default =
         'avatar' =>
         array('server' => null,
               'dir' => INSTALLDIR . '/avatar/',
-              'path' => $_path . '/avatar/'),
+              'path' => $_path . '/avatar/',
+              'ssl' => null),
         'background' =>
         array('server' => null,
               'dir' => INSTALLDIR . '/background/',
-              'path' => $_path . '/background/'),
+              'path' => $_path . '/background/',
+              'ssl' => null),
         'public' =>
         array('localonly' => true,
               'blacklist' => array(),
@@ -129,10 +131,12 @@ $default =
         'theme' =>
         array('server' => null,
               'dir' => null,
-              'path'=> null),
+              'path'=> null,
+              'ssl' => null),
         'javascript' =>
         array('server' => null,
-              'path'=> null),
+              'path'=> null,
+              'ssl' => null),
         'throttle' =>
         array('enabled' => false, // whether to throttle edits; false by default
               'count' => 20, // number of allowed messages in timespan
@@ -190,6 +194,7 @@ $default =
         array('server' => null,
               'dir' => INSTALLDIR . '/file/',
               'path' => $_path . '/file/',
+              'ssl' => null,
               'supported' => array('image/png',
                                    'image/jpeg',
                                    'image/gif',
index 87a4d913b41b1287f7af821926c5a8dea82a8439..a6a29119f7f612046f2f2c0075167c5d03acef84 100644 (file)
@@ -56,6 +56,7 @@ class ErrorAction extends Action
 
         $this->code = $code;
         $this->message = $message;
+        $this->minimal = StatusNet::isApi();
 
         // XXX: hack alert: usually we aren't going to
         // call this page directly, but because it's
@@ -102,7 +103,14 @@ class ErrorAction extends Action
 
     function showPage()
     {
-        parent::showPage();
+        if ($this->minimal) {
+            // Even more minimal -- we're in a machine API
+            // and don't want to flood the output.
+            $this->extraHeaders();
+            $this->showContent();
+        } else {
+            parent::showPage();
+        }
 
         // We don't want to have any more output after this
         exit();
index 99bff9cdc031769b31e74960703111388148fcfe..854bc34e2c3a5ed8f9e86b9ff50cba1e0d6c65d9 100644 (file)
@@ -105,6 +105,7 @@ class GroupList extends Widget
                                          'alt' =>
                                          ($this->group->fullname) ? $this->group->fullname :
                                          $this->group->nickname));
+        $this->out->text(' ');
         $hasFN = ($this->group->fullname) ? 'nickname' : 'fn org nickname';
         $this->out->elementStart('span', $hasFN);
         $this->out->raw($this->highlight($this->group->nickname));
@@ -112,16 +113,19 @@ class GroupList extends Widget
         $this->out->elementEnd('a');
 
         if ($this->group->fullname) {
+            $this->out->text(' ');
             $this->out->elementStart('span', 'fn org');
             $this->out->raw($this->highlight($this->group->fullname));
             $this->out->elementEnd('span');
         }
         if ($this->group->location) {
+            $this->out->text(' ');
             $this->out->elementStart('span', 'label');
             $this->out->raw($this->highlight($this->group->location));
             $this->out->elementEnd('span');
         }
         if ($this->group->homepage) {
+            $this->out->text(' ');
             $this->out->elementStart('a', array('href' => $this->group->homepage,
                                                 'class' => 'url'));
             $this->out->raw($this->highlight($this->group->homepage));
index 7327f9e1a06610b34dba5b87a8fca55e03be685e..3b0b3029dd19a87b7e6aead2427d2ae87cce0b71 100644 (file)
@@ -85,9 +85,9 @@ class GroupSection extends Section
                                             'href' => $group->homeUrl(),
                                             'rel' => 'contact group',
                                             'class' => 'url'));
+        $this->out->text(' ');
         $logo = ($group->stream_logo) ?
           $group->stream_logo : User_group::defaultLogo(AVATAR_STREAM_SIZE);
-
         $this->out->element('img', array('src' => $logo,
                                          'width' => AVATAR_MINI_SIZE,
                                          'height' => AVATAR_MINI_SIZE,
@@ -95,6 +95,7 @@ class GroupSection extends Section
                                          'alt' =>  ($group->fullname) ?
                                          $group->fullname :
                                          $group->nickname));
+        $this->out->text(' ');
         $this->out->element('span', 'fn org nickname', $group->nickname);
         $this->out->elementEnd('a');
         $this->out->elementEnd('span');
index 4a88337bc509b66429efe6ae4181bcc9953bedef..7315fe2ad44d34c43c107af91da9bbaa6e6c595e 100644 (file)
@@ -376,9 +376,20 @@ class HTMLOutputter extends XMLOutputter
                     $server = common_config('site', 'server');
                 }
 
-                // XXX: protocol
+                $ssl = common_config('javascript', 'ssl');
+
+                if (is_null($ssl)) { // null -> guess
+                    if (common_config('site', 'ssl') == 'always' &&
+                        !common_config('javascript', 'server')) {
+                        $ssl = true;
+                    } else {
+                        $ssl = false;
+                    }
+                }
+
+                $protocol = ($ssl) ? 'https' : 'http';
 
-                $src = 'http://'.$server.$path.$src . '?version=' . STATUSNET_VERSION;
+                $src = $protocol.'://'.$server.$path.$src . '?version=' . STATUSNET_VERSION;
             }
 
             $this->element('script', array('type' => $type,
index 3f82620761c6eef8d953cc11fa9611addad5ba96..4c3af8d7dd950fd1b23a4988e3710f8148f93ef8 100644 (file)
@@ -81,12 +81,13 @@ class HTTPResponse extends HTTP_Request2_Response
     }
 
     /**
-     * Check if the response is OK, generally a 200 status code.
+     * Check if the response is OK, generally a 200 or other 2xx status code.
      * @return bool
      */
     function isOk()
     {
-        return ($this->getStatus() == 200);
+        $status = $this->getStatus();
+        return ($status >= 200 && $status < 300);
     }
 }
 
diff --git a/lib/mysqlschema.php b/lib/mysqlschema.php
new file mode 100644 (file)
index 0000000..485096a
--- /dev/null
@@ -0,0 +1,538 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Database schema utilities
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Database
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Class representing the database schema
+ *
+ * A class representing the database schema. Can be used to
+ * manipulate the schema -- especially for plugins and upgrade
+ * utilities.
+ *
+ * @category Database
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ */
+
+class MysqlSchema extends Schema
+{
+    static $_single = null;
+    protected $conn = null;
+
+    /**
+     * Constructor. Only run once for singleton object.
+     */
+
+    protected function __construct()
+    {
+        // XXX: there should be an easier way to do this.
+        $user = new User();
+
+        $this->conn = $user->getDatabaseConnection();
+
+        $user->free();
+
+        unset($user);
+    }
+
+    /**
+     * Main public entry point. Use this to get
+     * the singleton object.
+     *
+     * @return Schema the (single) Schema object
+     */
+
+    static function get()
+    {
+        if (empty(self::$_single)) {
+            self::$_single = new Schema();
+        }
+        return self::$_single;
+    }
+
+    /**
+     * Returns a TableDef object for the table
+     * in the schema with the given name.
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $name Name of the table to get
+     *
+     * @return TableDef tabledef for that table.
+     */
+
+    public function getTableDef($name)
+    {
+        $res = $this->conn->query('DESCRIBE ' . $name);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        $td = new TableDef();
+
+        $td->name    = $name;
+        $td->columns = array();
+
+        $row = array();
+
+        while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
+
+            $cd = new ColumnDef();
+
+            $cd->name = $row['Field'];
+
+            $packed = $row['Type'];
+
+            if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
+                $cd->type = $match[1];
+                $cd->size = $match[2];
+            } else {
+                $cd->type = $packed;
+            }
+
+            $cd->nullable = ($row['Null'] == 'YES') ? true : false;
+            $cd->key      = $row['Key'];
+            $cd->default  = $row['Default'];
+            $cd->extra    = $row['Extra'];
+
+            $td->columns[] = $cd;
+        }
+
+        return $td;
+    }
+
+    /**
+     * Gets a ColumnDef object for a single column.
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $table  name of the table
+     * @param string $column name of the column
+     *
+     * @return ColumnDef definition of the column or null
+     *                   if not found.
+     */
+
+    public function getColumnDef($table, $column)
+    {
+        $td = $this->getTableDef($table);
+
+        foreach ($td->columns as $cd) {
+            if ($cd->name == $column) {
+                return $cd;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Creates a table with the given names and columns.
+     *
+     * @param string $name    Name of the table
+     * @param array  $columns Array of ColumnDef objects
+     *                        for new table.
+     *
+     * @return boolean success flag
+     */
+
+    public function createTable($name, $columns)
+    {
+        $uniques = array();
+        $primary = array();
+        $indices = array();
+
+        $sql = "CREATE TABLE $name (\n";
+
+        for ($i = 0; $i < count($columns); $i++) {
+
+            $cd =& $columns[$i];
+
+            if ($i > 0) {
+                $sql .= ",\n";
+            }
+
+            $sql .= $this->_columnSql($cd);
+
+            switch ($cd->key) {
+            case 'UNI':
+                $uniques[] = $cd->name;
+                break;
+            case 'PRI':
+                $primary[] = $cd->name;
+                break;
+            case 'MUL':
+                $indices[] = $cd->name;
+                break;
+            }
+        }
+
+        if (count($primary) > 0) { // it really should be...
+            $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")";
+        }
+
+        foreach ($uniques as $u) {
+            $sql .= ",\nunique index {$name}_{$u}_idx ($u)";
+        }
+
+        foreach ($indices as $i) {
+            $sql .= ",\nindex {$name}_{$i}_idx ($i)";
+        }
+
+        $sql .= "); ";
+
+        common_log(LOG_INFO, $sql);
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a table from the schema
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $name Name of the table to drop
+     *
+     * @return boolean success flag
+     */
+
+    public function dropTable($name)
+    {
+        $res = $this->conn->query("DROP TABLE $name");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Adds an index to a table.
+     *
+     * If no name is provided, a name will be made up based
+     * on the table name and column names.
+     *
+     * Throws an exception on database error, esp. if the table
+     * does not exist.
+     *
+     * @param string $table       Name of the table
+     * @param array  $columnNames Name of columns to index
+     * @param string $name        (Optional) name of the index
+     *
+     * @return boolean success flag
+     */
+
+    public function createIndex($table, $columnNames, $name=null)
+    {
+        if (!is_array($columnNames)) {
+            $columnNames = array($columnNames);
+        }
+
+        if (empty($name)) {
+            $name = "$table_".implode("_", $columnNames)."_idx";
+        }
+
+        $res = $this->conn->query("ALTER TABLE $table ".
+                                   "ADD INDEX $name (".
+                                   implode(",", $columnNames).")");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a named index from a table.
+     *
+     * @param string $table name of the table the index is on.
+     * @param string $name  name of the index
+     *
+     * @return boolean success flag
+     */
+
+    public function dropIndex($table, $name)
+    {
+        $res = $this->conn->query("ALTER TABLE $table DROP INDEX $name");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Adds a column to a table
+     *
+     * @param string    $table     name of the table
+     * @param ColumnDef $columndef Definition of the new
+     *                             column.
+     *
+     * @return boolean success flag
+     */
+
+    public function addColumn($table, $columndef)
+    {
+        $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Modifies a column in the schema.
+     *
+     * The name must match an existing column and table.
+     *
+     * @param string    $table     name of the table
+     * @param ColumnDef $columndef new definition of the column.
+     *
+     * @return boolean success flag
+     */
+
+    public function modifyColumn($table, $columndef)
+    {
+        $sql = "ALTER TABLE $table MODIFY COLUMN " .
+          $this->_columnSql($columndef);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a column from a table
+     *
+     * The name must match an existing column.
+     *
+     * @param string $table      name of the table
+     * @param string $columnName name of the column to drop
+     *
+     * @return boolean success flag
+     */
+
+    public function dropColumn($table, $columnName)
+    {
+        $sql = "ALTER TABLE $table DROP COLUMN $columnName";
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Ensures that a table exists with the given
+     * name and the given column definitions.
+     *
+     * If the table does not yet exist, it will
+     * create the table. If it does exist, it will
+     * alter the table to match the column definitions.
+     *
+     * @param string $tableName name of the table
+     * @param array  $columns   array of ColumnDef
+     *                          objects for the table
+     *
+     * @return boolean success flag
+     */
+
+    public function ensureTable($tableName, $columns)
+    {
+        // XXX: DB engine portability -> toilet
+
+        try {
+            $td = $this->getTableDef($tableName);
+        } catch (Exception $e) {
+            if (preg_match('/no such table/', $e->getMessage())) {
+                return $this->createTable($tableName, $columns);
+            } else {
+                throw $e;
+            }
+        }
+
+        $cur = $this->_names($td->columns);
+        $new = $this->_names($columns);
+
+        $toadd  = array_diff($new, $cur);
+        $todrop = array_diff($cur, $new);
+        $same   = array_intersect($new, $cur);
+        $tomod  = array();
+
+        foreach ($same as $m) {
+            $curCol = $this->_byName($td->columns, $m);
+            $newCol = $this->_byName($columns, $m);
+
+            if (!$newCol->equals($curCol)) {
+                $tomod[] = $newCol->name;
+            }
+        }
+
+        if (count($toadd) + count($todrop) + count($tomod) == 0) {
+            // nothing to do
+            return true;
+        }
+
+        // For efficiency, we want this all in one
+        // query, instead of using our methods.
+
+        $phrase = array();
+
+        foreach ($toadd as $columnName) {
+            $cd = $this->_byName($columns, $columnName);
+
+            $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
+        }
+
+        foreach ($todrop as $columnName) {
+            $phrase[] = 'DROP COLUMN ' . $columnName;
+        }
+
+        foreach ($tomod as $columnName) {
+            $cd = $this->_byName($columns, $columnName);
+
+            $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
+        }
+
+        $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the array of names from an array of
+     * ColumnDef objects.
+     *
+     * @param array $cds array of ColumnDef objects
+     *
+     * @return array strings for name values
+     */
+
+    private function _names($cds)
+    {
+        $names = array();
+
+        foreach ($cds as $cd) {
+            $names[] = $cd->name;
+        }
+
+        return $names;
+    }
+
+    /**
+     * Get a ColumnDef from an array matching
+     * name.
+     *
+     * @param array  $cds  Array of ColumnDef objects
+     * @param string $name Name of the column
+     *
+     * @return ColumnDef matching item or null if no match.
+     */
+
+    private function _byName($cds, $name)
+    {
+        foreach ($cds as $cd) {
+            if ($cd->name == $name) {
+                return $cd;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Return the proper SQL for creating or
+     * altering a column.
+     *
+     * Appropriate for use in CREATE TABLE or
+     * ALTER TABLE statements.
+     *
+     * @param ColumnDef $cd column to create
+     *
+     * @return string correct SQL for that column
+     */
+
+    private function _columnSql($cd)
+    {
+        $sql = "{$cd->name} ";
+
+        if (!empty($cd->size)) {
+            $sql .= "{$cd->type}({$cd->size}) ";
+        } else {
+            $sql .= "{$cd->type} ";
+        }
+
+        if (!empty($cd->default)) {
+            $sql .= "default {$cd->default} ";
+        } else {
+            $sql .= ($cd->nullable) ? "null " : "not null ";
+        }
+        
+        if (!empty($cd->auto_increment)) {
+            $sql .= " auto_increment ";
+        }
+
+        if (!empty($cd->extra)) {
+            $sql .= "{$cd->extra} ";
+        }
+
+        return $sql;
+    }
+}
index a4a0f2651a190084f67e811fc540f914cecc0050..837cb90faa13bb29a9fdfe0f5734c569995c0650 100644 (file)
@@ -294,6 +294,7 @@ class NoticeListItem extends Widget
         }
         $this->out->elementStart('a', $attrs);
         $this->showAvatar();
+        $this->out->text(' ');
         $this->showNickname();
         $this->out->elementEnd('a');
         $this->out->elementEnd('span');
@@ -432,8 +433,10 @@ class NoticeListItem extends Widget
 
         $url  = $location->getUrl();
 
+        $this->out->text(' ');
         $this->out->elementStart('span', array('class' => 'location'));
         $this->out->text(_('at'));
+        $this->out->text(' ');
         if (empty($url)) {
             $this->out->element('span', array('class' => 'geo',
                                               'title' => $latlon),
@@ -473,9 +476,11 @@ class NoticeListItem extends Widget
     function showNoticeSource()
     {
         if ($this->notice->source) {
+            $this->out->text(' ');
             $this->out->elementStart('span', 'source');
             $this->out->text(_('from'));
             $source_name = _($this->notice->source);
+            $this->out->text(' ');
             switch ($this->notice->source) {
              case 'web':
              case 'xmpp':
@@ -487,30 +492,34 @@ class NoticeListItem extends Widget
                 break;
              default:
 
-                $name = null;
+                $name = $source_name;
                 $url  = null;
 
-                $ns = Notice_source::staticGet($this->notice->source);
-
-                if ($ns) {
-                    $name = $ns->name;
-                    $url  = $ns->url;
-                } else {
-                    $app = Oauth_application::staticGet('name', $this->notice->source);
-                    if ($app) {
-                        $name = $app->name;
-                        $url  = $app->source_url;
+                if (Event::handle('StartNoticeSourceLink', array($this->notice, &$name, &$url, &$title))) {
+                    $ns = Notice_source::staticGet($this->notice->source);
+
+                    if ($ns) {
+                        $name = $ns->name;
+                        $url  = $ns->url;
+                    } else {
+                        $app = Oauth_application::staticGet('name', $this->notice->source);
+                        if ($app) {
+                            $name = $app->name;
+                            $url  = $app->source_url;
+                        }
                     }
                 }
+                Event::handle('EndNoticeSourceLink', array($this->notice, &$name, &$url, &$title));
 
                 if (!empty($name) && !empty($url)) {
                     $this->out->elementStart('span', 'device');
                     $this->out->element('a', array('href' => $url,
-                                                   'rel' => 'external'),
+                                                   'rel' => 'external',
+                                                   'title' => $title),
                                         $name);
                     $this->out->elementEnd('span');
                 } else {
-                    $this->out->element('span', 'device', $source_name);
+                    $this->out->element('span', 'device', $name);
                 }
                 break;
             }
@@ -540,6 +549,7 @@ class NoticeListItem extends Widget
             }
         }
         if ($hasConversation){
+            $this->out->text(' ');
             $convurl = common_local_url('conversation',
                                          array('id' => $this->notice->conversation));
             $this->out->element('a', array('href' => $convurl.'#notice-'.$this->notice->id,
@@ -591,12 +601,14 @@ class NoticeListItem extends Widget
     function showReplyLink()
     {
         if (common_logged_in()) {
+            $this->out->text(' ');
             $reply_url = common_local_url('newnotice',
                                           array('replyto' => $this->profile->nickname, 'inreplyto' => $this->notice->id));
             $this->out->elementStart('a', array('href' => $reply_url,
                                                 'class' => 'notice_reply',
                                                 'title' => _('Reply to this notice')));
             $this->out->text(_('Reply'));
+            $this->out->text(' ');
             $this->out->element('span', 'notice_id', $this->notice->id);
             $this->out->elementEnd('a');
         }
@@ -616,7 +628,7 @@ class NoticeListItem extends Widget
 
         if (!empty($user) &&
             ($todel->profile_id == $user->id || $user->hasRight(Right::DELETEOTHERSNOTICE))) {
-
+            $this->out->text(' ');
             $deleteurl = common_local_url('deletenotice',
                                           array('notice' => $todel->id));
             $this->out->element('a', array('href' => $deleteurl,
@@ -635,6 +647,7 @@ class NoticeListItem extends Widget
     {
         $user = common_current_user();
         if ($user && $user->id != $this->notice->profile_id) {
+            $this->out->text(' ');
             $profile = $user->getProfile();
             if ($profile->hasRepeated($this->notice->id)) {
                 $this->out->element('span', array('class' => 'repeated',
index 24465f8baf1f835ff87f46ce0c19667213e61c51..7157feafc565bd81b30f1cba4d0cee5c4dc18442 100644 (file)
@@ -90,6 +90,7 @@ class NoticeSection extends Section
                                          'alt' =>  ($profile->fullname) ?
                                          $profile->fullname :
                                          $profile->nickname));
+        $this->out->text(' ');
         $this->out->element('span', 'fn nickname', $profile->nickname);
         $this->out->elementEnd('a');
         $this->out->elementEnd('span');
index b22fd789740c95b93fc0c5623202ceb920447b3e..bc7587183b2c4ca8dde3878f4ccd9d5507beeded 100644 (file)
@@ -90,20 +90,47 @@ class OAuthClient
     /**
      * Gets a request token from the given url
      *
-     * @param string $url OAuth endpoint for grabbing request tokens
+     * @param string $url      OAuth endpoint for grabbing request tokens
+     * @param string $callback authorized request token callback
      *
      * @return OAuthToken $token the request token
      */
-    function getRequestToken($url)
+    function getRequestToken($url, $callback = null)
     {
-        $response = $this->oAuthGet($url);
+        $params = null;
+
+        if (!is_null($callback)) {
+            $params['oauth_callback'] = $callback;
+        }
+
+        $response = $this->oAuthGet($url, $params);
+
         $arr = array();
         parse_str($response, $arr);
-        if (isset($arr['oauth_token']) && isset($arr['oauth_token_secret'])) {
-            $token = new OAuthToken($arr['oauth_token'], @$arr['oauth_token_secret']);
+
+        $token   = $arr['oauth_token'];
+        $secret  = $arr['oauth_token_secret'];
+        $confirm = $arr['oauth_callback_confirmed'];
+
+        if (isset($token) && isset($secret)) {
+
+            $token = new OAuthToken($token, $secret);
+
+            if (isset($confirm)) {
+                if ($confirm == 'true') {
+                    common_debug('Twitter bridge - callback confirmed.');
+                    return $token;
+                } else {
+                    throw new OAuthClientException(
+                        'Callback was not confirmed by Twitter.'
+                    );
+                }
+            }
             return $token;
         } else {
-            throw new OAuthClientException();
+            throw new OAuthClientException(
+                'Could not get a request token from Twitter.'
+            );
         }
     }
 
@@ -113,49 +140,64 @@ class OAuthClient
      *
      * @param string     $url            endpoint for authorizing request tokens
      * @param OAuthToken $request_token  the request token to be authorized
-     * @param string     $oauth_callback optional callback url
      *
      * @return string $authorize_url the url to redirect to
      */
-    function getAuthorizeLink($url, $request_token, $oauth_callback = null)
+    function getAuthorizeLink($url, $request_token)
     {
         $authorize_url = $url . '?oauth_token=' .
             $request_token->key;
 
-        if (isset($oauth_callback)) {
-            $authorize_url .= '&oauth_callback=' . urlencode($oauth_callback);
-        }
-
         return $authorize_url;
     }
 
     /**
      * Fetches an access token
      *
-     * @param string $url OAuth endpoint for exchanging authorized request tokens
-     *                     for access tokens
+     * @param string $url      OAuth endpoint for exchanging authorized request tokens
+     *                         for access tokens
+     * @param string $verifier 1.0a verifier
      *
      * @return OAuthToken $token the access token
      */
-    function getAccessToken($url)
+    function getAccessToken($url, $verifier = null)
     {
-        $response = $this->oAuthPost($url);
-        parse_str($response);
-        $token = new OAuthToken($oauth_token, $oauth_token_secret);
-        return $token;
+        $params = array();
+
+        if (!is_null($verifier)) {
+            $params['oauth_verifier'] = $verifier;
+        }
+
+        $response = $this->oAuthPost($url, $params);
+
+        $arr = array();
+        parse_str($response, $arr);
+
+        $token  = $arr['oauth_token'];
+        $secret = $arr['oauth_token_secret'];
+
+        if (isset($token) && isset($secret)) {
+            $token = new OAuthToken($token, $secret);
+            return $token;
+        } else {
+            throw new OAuthClientException(
+                'Could not get a access token from Twitter.'
+            );
+        }
     }
 
     /**
-     * Use HTTP GET to make a signed OAuth request
+     * Use HTTP GET to make a signed OAuth requesta
      *
-     * @param string $url OAuth endpoint
+     * @param string $url    OAuth request token endpoint
+     * @param array  $params additional parameters
      *
      * @return mixed the request
      */
-    function oAuthGet($url)
+    function oAuthGet($url, $params = null)
     {
         $request = OAuthRequest::from_consumer_and_token($this->consumer,
-            $this->token, 'GET', $url, null);
+            $this->token, 'GET', $url, $params);
         $request->sign_request($this->sha1_method,
             $this->consumer, $this->token);
 
diff --git a/lib/pgsqlschema.php b/lib/pgsqlschema.php
new file mode 100644 (file)
index 0000000..91bc096
--- /dev/null
@@ -0,0 +1,503 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Database schema utilities
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Database
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Class representing the database schema
+ *
+ * A class representing the database schema. Can be used to
+ * manipulate the schema -- especially for plugins and upgrade
+ * utilities.
+ *
+ * @category Database
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ */
+
+class PgsqlSchema extends Schema
+{
+
+    /**
+     * Returns a TableDef object for the table
+     * in the schema with the given name.
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $name Name of the table to get
+     *
+     * @return TableDef tabledef for that table.
+     */
+
+    public function getTableDef($name)
+    {
+        $res = $this->conn->query("select *, column_default as default, is_nullable as Null, udt_name as Type, column_name AS Field from INFORMATION_SCHEMA.COLUMNS where table_name = '$name'");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        $td = new TableDef();
+
+        $td->name    = $name;
+        $td->columns = array();
+
+        $row = array();
+
+        while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
+//             var_dump($row);
+            $cd = new ColumnDef();
+
+            $cd->name = $row['field'];
+
+            $packed = $row['type'];
+
+            if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
+                $cd->type = $match[1];
+                $cd->size = $match[2];
+            } else {
+                $cd->type = $packed;
+            }
+
+            $cd->nullable = ($row['null'] == 'YES') ? true : false;
+            $cd->key      = $row['Key'];
+            $cd->default  = $row['default'];
+            $cd->extra    = $row['Extra'];
+
+            $td->columns[] = $cd;
+        }
+        return $td;
+    }
+
+    /**
+     * Gets a ColumnDef object for a single column.
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $table  name of the table
+     * @param string $column name of the column
+     *
+     * @return ColumnDef definition of the column or null
+     *                   if not found.
+     */
+
+    public function getColumnDef($table, $column)
+    {
+        $td = $this->getTableDef($table);
+
+        foreach ($td->columns as $cd) {
+            if ($cd->name == $column) {
+                return $cd;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Creates a table with the given names and columns.
+     *
+     * @param string $name    Name of the table
+     * @param array  $columns Array of ColumnDef objects
+     *                        for new table.
+     *
+     * @return boolean success flag
+     */
+
+    public function createTable($name, $columns)
+    {
+        $uniques = array();
+        $primary = array();
+        $indices = array();
+
+        $sql = "CREATE TABLE $name (\n";
+
+        for ($i = 0; $i < count($columns); $i++) {
+
+            $cd =& $columns[$i];
+
+            if ($i > 0) {
+                $sql .= ",\n";
+            }
+
+            $sql .= $this->_columnSql($cd);
+
+            switch ($cd->key) {
+            case 'UNI':
+                $uniques[] = $cd->name;
+                break;
+            case 'PRI':
+                $primary[] = $cd->name;
+                break;
+            case 'MUL':
+                $indices[] = $cd->name;
+                break;
+            }
+        }
+
+        if (count($primary) > 0) { // it really should be...
+            $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")";
+        }
+
+        foreach ($uniques as $u) {
+            $sql .= ",\nunique index {$name}_{$u}_idx ($u)";
+        }
+
+        foreach ($indices as $i) {
+            $sql .= ",\nindex {$name}_{$i}_idx ($i)";
+        }
+
+        $sql .= "); ";
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a table from the schema
+     *
+     * Throws an exception if the table is not found.
+     *
+     * @param string $name Name of the table to drop
+     *
+     * @return boolean success flag
+     */
+
+    public function dropTable($name)
+    {
+        $res = $this->conn->query("DROP TABLE $name");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Adds an index to a table.
+     *
+     * If no name is provided, a name will be made up based
+     * on the table name and column names.
+     *
+     * Throws an exception on database error, esp. if the table
+     * does not exist.
+     *
+     * @param string $table       Name of the table
+     * @param array  $columnNames Name of columns to index
+     * @param string $name        (Optional) name of the index
+     *
+     * @return boolean success flag
+     */
+
+    public function createIndex($table, $columnNames, $name=null)
+    {
+        if (!is_array($columnNames)) {
+            $columnNames = array($columnNames);
+        }
+
+        if (empty($name)) {
+            $name = "$table_".implode("_", $columnNames)."_idx";
+        }
+
+        $res = $this->conn->query("ALTER TABLE $table ".
+                                   "ADD INDEX $name (".
+                                   implode(",", $columnNames).")");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a named index from a table.
+     *
+     * @param string $table name of the table the index is on.
+     * @param string $name  name of the index
+     *
+     * @return boolean success flag
+     */
+
+    public function dropIndex($table, $name)
+    {
+        $res = $this->conn->query("ALTER TABLE $table DROP INDEX $name");
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Adds a column to a table
+     *
+     * @param string    $table     name of the table
+     * @param ColumnDef $columndef Definition of the new
+     *                             column.
+     *
+     * @return boolean success flag
+     */
+
+    public function addColumn($table, $columndef)
+    {
+        $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Modifies a column in the schema.
+     *
+     * The name must match an existing column and table.
+     *
+     * @param string    $table     name of the table
+     * @param ColumnDef $columndef new definition of the column.
+     *
+     * @return boolean success flag
+     */
+
+    public function modifyColumn($table, $columndef)
+    {
+        $sql = "ALTER TABLE $table MODIFY COLUMN " .
+          $this->_columnSql($columndef);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Drops a column from a table
+     *
+     * The name must match an existing column.
+     *
+     * @param string $table      name of the table
+     * @param string $columnName name of the column to drop
+     *
+     * @return boolean success flag
+     */
+
+    public function dropColumn($table, $columnName)
+    {
+        $sql = "ALTER TABLE $table DROP COLUMN $columnName";
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Ensures that a table exists with the given
+     * name and the given column definitions.
+     *
+     * If the table does not yet exist, it will
+     * create the table. If it does exist, it will
+     * alter the table to match the column definitions.
+     *
+     * @param string $tableName name of the table
+     * @param array  $columns   array of ColumnDef
+     *                          objects for the table
+     *
+     * @return boolean success flag
+     */
+
+    public function ensureTable($tableName, $columns)
+    {
+        // XXX: DB engine portability -> toilet
+
+        try {
+            $td = $this->getTableDef($tableName);
+        } catch (Exception $e) {
+            if (preg_match('/no such table/', $e->getMessage())) {
+                return $this->createTable($tableName, $columns);
+            } else {
+                throw $e;
+            }
+        }
+
+        $cur = $this->_names($td->columns);
+        $new = $this->_names($columns);
+
+        $toadd  = array_diff($new, $cur);
+        $todrop = array_diff($cur, $new);
+        $same   = array_intersect($new, $cur);
+        $tomod  = array();
+
+        foreach ($same as $m) {
+            $curCol = $this->_byName($td->columns, $m);
+            $newCol = $this->_byName($columns, $m);
+
+            if (!$newCol->equals($curCol)) {
+                $tomod[] = $newCol->name;
+            }
+        }
+
+        if (count($toadd) + count($todrop) + count($tomod) == 0) {
+            // nothing to do
+            return true;
+        }
+
+        // For efficiency, we want this all in one
+        // query, instead of using our methods.
+
+        $phrase = array();
+
+        foreach ($toadd as $columnName) {
+            $cd = $this->_byName($columns, $columnName);
+
+            $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
+        }
+
+        foreach ($todrop as $columnName) {
+            $phrase[] = 'DROP COLUMN ' . $columnName;
+        }
+
+        foreach ($tomod as $columnName) {
+            $cd = $this->_byName($columns, $columnName);
+
+            $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
+        }
+
+        $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
+
+        $res = $this->conn->query($sql);
+
+        if (PEAR::isError($res)) {
+            throw new Exception($res->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the array of names from an array of
+     * ColumnDef objects.
+     *
+     * @param array $cds array of ColumnDef objects
+     *
+     * @return array strings for name values
+     */
+
+    private function _names($cds)
+    {
+        $names = array();
+
+        foreach ($cds as $cd) {
+            $names[] = $cd->name;
+        }
+
+        return $names;
+    }
+
+    /**
+     * Get a ColumnDef from an array matching
+     * name.
+     *
+     * @param array  $cds  Array of ColumnDef objects
+     * @param string $name Name of the column
+     *
+     * @return ColumnDef matching item or null if no match.
+     */
+
+    private function _byName($cds, $name)
+    {
+        foreach ($cds as $cd) {
+            if ($cd->name == $name) {
+                return $cd;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Return the proper SQL for creating or
+     * altering a column.
+     *
+     * Appropriate for use in CREATE TABLE or
+     * ALTER TABLE statements.
+     *
+     * @param ColumnDef $cd column to create
+     *
+     * @return string correct SQL for that column
+     */
+
+    private function _columnSql($cd)
+    {
+        $sql = "{$cd->name} ";
+
+        if (!empty($cd->size)) {
+            $sql .= "{$cd->type}({$cd->size}) ";
+        } else {
+            $sql .= "{$cd->type} ";
+        }
+
+        if (!empty($cd->default)) {
+            $sql .= "default {$cd->default} ";
+        } else {
+            $sql .= ($cd->nullable) ? "null " : "not null ";
+        }
+        
+        if (!empty($cd->auto_increment)) {
+            $sql .= " auto_increment ";
+        }
+
+        if (!empty($cd->extra)) {
+            $sql .= "{$cd->extra} ";
+        }
+
+        return $sql;
+    }
+}
index 3412d41d1c94b794b1a9251c2fd9e75d5eb326cc..693cd64492b26fde8c0a6e0a639ea7db43137a50 100644 (file)
@@ -191,6 +191,7 @@ class ProfileListItem extends Widget
                                          'alt' =>
                                          ($this->profile->fullname) ? $this->profile->fullname :
                                          $this->profile->nickname));
+        $this->out->text(' ');
         $hasFN = (!empty($this->profile->fullname)) ? 'nickname' : 'fn nickname';
         $this->out->elementStart('span', $hasFN);
         $this->out->raw($this->highlight($this->profile->nickname));
@@ -201,6 +202,7 @@ class ProfileListItem extends Widget
     function showFullName()
     {
         if (!empty($this->profile->fullname)) {
+            $this->out->text(' ');
             $this->out->elementStart('span', 'fn');
             $this->out->raw($this->highlight($this->profile->fullname));
             $this->out->elementEnd('span');
@@ -210,6 +212,7 @@ class ProfileListItem extends Widget
     function showLocation()
     {
         if (!empty($this->profile->location)) {
+            $this->out->text(' ');
             $this->out->elementStart('span', 'location');
             $this->out->raw($this->highlight($this->profile->location));
             $this->out->elementEnd('span');
@@ -219,6 +222,7 @@ class ProfileListItem extends Widget
     function showHomepage()
     {
         if (!empty($this->profile->homepage)) {
+            $this->out->text(' ');
             $this->out->elementStart('a', array('href' => $this->profile->homepage,
                                                 'class' => 'url'));
             $this->out->raw($this->highlight($this->profile->homepage));
index 504b1b7f75c64ea12e4b04580a607ddb8d04d1fa..a9482cd634cff9783d199d0fc9957e87808de590 100644 (file)
@@ -85,6 +85,7 @@ class ProfileSection extends Section
                                        'href' => $profile->profileurl,
                                        'rel' => 'contact member',
                                        'class' => 'url'));
+        $this->out->text(' ');
         $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
         $this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() :  Avatar::defaultImage(AVATAR_MINI_SIZE)),
                                     'width' => AVATAR_MINI_SIZE,
@@ -93,6 +94,7 @@ class ProfileSection extends Section
                                     'alt' =>  ($profile->fullname) ?
                                     $profile->fullname :
                                     $profile->nickname));
+        $this->out->text(' ');
         $this->out->element('span', 'fn nickname', $profile->nickname);
         $this->out->elementEnd('a');
         $this->out->elementEnd('span');
index 5e66eae0ed3da13e43d638eb6e33f9534ebb9043..4e9c5a918dbe02b2e2795c63167dd9a99d7db8e7 100644 (file)
@@ -57,5 +57,6 @@ class Right
     const EMAILONREPLY       = 'emailonreply';
     const EMAILONSUBSCRIBE   = 'emailonsubscribe';
     const EMAILONFAVE        = 'emailonfave';
+    const MAKEGROUPADMIN     = 'makegroupadmin';
 }
 
index 5981ef5d7a2af756952a90944a6937aa10c642c7..987d0152e462e266a8ddc40d169e66882a990c79 100644 (file)
@@ -712,6 +712,10 @@ class Router
                                   'nickname' => $nickname),
                             array('tag' => '[a-zA-Z0-9]+'));
 
+                $m->connect('rsd.xml',
+                            array('action' => 'rsd',
+                                  'nickname' => $nickname));
+
                 $m->connect('',
                             array('action' => 'showstream',
                                   'nickname' => $nickname));
@@ -726,6 +730,7 @@ class Router
                 $m->connect('featured', array('action' => 'featured'));
                 $m->connect('favorited/', array('action' => 'favorited'));
                 $m->connect('favorited', array('action' => 'favorited'));
+                $m->connect('rsd.xml', array('action' => 'rsd'));
 
                 foreach (array('subscriptions', 'subscribers',
                                'nudge', 'all', 'foaf', 'xrds',
@@ -773,6 +778,10 @@ class Router
                             array('nickname' => '[a-zA-Z0-9]{1,64}'),
                             array('tag' => '[a-zA-Z0-9]+'));
 
+                $m->connect(':nickname/rsd.xml',
+                            array('action' => 'rsd'),
+                            array('nickname' => '[a-zA-Z0-9]{1,64}'));
+
                 $m->connect(':nickname',
                             array('action' => 'showstream'),
                             array('nickname' => '[a-zA-Z0-9]{1,64}'));
index a7f64ebed10cfce99fb780756a6fffa78831df83..137b814e0269ed727978e11e55ce25cca763a199 100644 (file)
@@ -75,64 +75,14 @@ class Schema
 
     static function get()
     {
+        $type = common_config('db', 'type');
         if (empty(self::$_single)) {
-            self::$_single = new Schema();
+            $schemaClass = ucfirst($type).'Schema';
+            self::$_single = new $schemaClass();
         }
         return self::$_single;
     }
 
-    /**
-     * Returns a TableDef object for the table
-     * in the schema with the given name.
-     *
-     * Throws an exception if the table is not found.
-     *
-     * @param string $name Name of the table to get
-     *
-     * @return TableDef tabledef for that table.
-     */
-
-    public function getTableDef($name)
-    {
-        $res = $this->conn->query('DESCRIBE ' . $name);
-
-        if (PEAR::isError($res)) {
-            throw new Exception($res->getMessage());
-        }
-
-        $td = new TableDef();
-
-        $td->name    = $name;
-        $td->columns = array();
-
-        $row = array();
-
-        while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
-
-            $cd = new ColumnDef();
-
-            $cd->name = $row['Field'];
-
-            $packed = $row['Type'];
-
-            if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
-                $cd->type = $match[1];
-                $cd->size = $match[2];
-            } else {
-                $cd->type = $packed;
-            }
-
-            $cd->nullable = ($row['Null'] == 'YES') ? true : false;
-            $cd->key      = $row['Key'];
-            $cd->default  = $row['Default'];
-            $cd->extra    = $row['Extra'];
-
-            $td->columns[] = $cd;
-        }
-
-        return $td;
-    }
-
     /**
      * Gets a ColumnDef object for a single column.
      *
@@ -523,7 +473,7 @@ class Schema
         } else {
             $sql .= ($cd->nullable) ? "null " : "not null ";
         }
-        
+
         if (!empty($cd->auto_increment)) {
             $sql .= " auto_increment ";
         }
index 257bd861da61ff881af67e781049178b7d299cba..7c4df84b4a7692ed59c5b770d5a044dbcdcfcc55 100644 (file)
@@ -30,6 +30,7 @@ global $config, $_server, $_path;
 class StatusNet
 {
     protected static $have_config;
+    protected static $is_api;
 
     /**
      * Configure and instantiate a plugin into the current configuration.
@@ -201,6 +202,16 @@ class StatusNet
         return self::$have_config;
     }
 
+    public function isApi()
+    {
+        return self::$is_api;
+    }
+    
+    public function setApi($mode)
+    {
+        self::$is_api = $mode;
+    }
+
     /**
      * Build default configuration array
      * @return array
index 020ce1ac40cc77d7a57cd5e486ed3224973b1e5e..0be8c3b9dfaa8ba2631bb67e08cbfb49b36ceb60 100644 (file)
@@ -110,9 +110,20 @@ class Theme
                 $server = common_config('site', 'server');
             }
 
-            // XXX: protocol
+            $ssl = common_config('theme', 'ssl');
+
+            if (is_null($ssl)) { // null -> guess
+                if (common_config('site', 'ssl') == 'always' &&
+                    !common_config('theme', 'server')) {
+                    $ssl = true;
+                } else {
+                    $ssl = false;
+                }
+            }
+
+            $protocol = ($ssl) ? 'https' : 'http';
 
-            $this->path = 'http://'.$server.$path.$name;
+            $this->path = $protocol . '://'.$server.$path.$name;
         }
     }
 
index 07e5750852eaa77a8e86eafd73e939296dd71eb0..43dfd05be5988e0d912af9f28124b5fa8b56da70 100644 (file)
@@ -238,9 +238,12 @@ class UserProfile extends Widget
 
             if (Event::handle('StartProfilePageActionsElements', array(&$this->out, $this->profile))) {
                 if (empty($cur)) { // not logged in
-                    $this->out->elementStart('li', 'entity_subscribe');
-                    $this->showRemoteSubscribeLink();
-                    $this->out->elementEnd('li');
+                    if (Event::handle('StartProfileRemoteSubscribe', array(&$this->out, $this->profile))) {
+                        $this->out->elementStart('li', 'entity_subscribe');
+                        $this->showRemoteSubscribeLink();
+                        $this->out->elementEnd('li');
+                        Event::handle('EndProfileRemoteSubscribe', array(&$this->out, $this->profile));
+                    }
                 } else {
                     if ($cur->id == $this->profile->id) { // your own page
                         $this->out->elementStart('li', 'entity_edit');
index dd8189a5827b011bdaed1df5d27b31c3c943f4f1..ae812e8cf4d0aacdd6a7166461c761fed0eeb7e1 100644 (file)
@@ -367,7 +367,8 @@ function common_current_user()
 
     if ($_cur === false) {
 
-        if (isset($_REQUEST[session_name()]) || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
+        if (isset($_COOKIE[session_name()]) || isset($_GET[session_name()])
+            || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
             common_ensure_session();
             $id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false;
             if ($id) {
@@ -665,6 +666,9 @@ function common_valid_profile_tag($str)
 function common_at_link($sender_id, $nickname)
 {
     $sender = Profile::staticGet($sender_id);
+    if (!$sender) {
+        return $nickname;
+    }
     $recipient = common_relative_profile($sender, common_canonical_nickname($nickname));
     if ($recipient) {
         $user = User::staticGet('id', $recipient->id);
@@ -694,7 +698,7 @@ function common_group_link($sender_id, $nickname)
 {
     $sender = Profile::staticGet($sender_id);
     $group = User_group::getForNickname($nickname);
-    if ($group && $sender->isMember($group)) {
+    if ($sender && $group && $sender->isMember($group)) {
         $attrs = array('href' => $group->permalink(),
                        'class' => 'url');
         if (!empty($group->fullname)) {
@@ -996,9 +1000,13 @@ function common_enqueue_notice($notice)
     static $localTransports = array('omb',
                                     'ping');
 
-    static $allTransports = array('sms', 'plugin');
-
-    $transports = $allTransports;
+    $transports = array();
+    if (common_config('sms', 'enabled')) {
+        $transports[] = 'sms';
+    }
+    if (Event::hasHandler('HandleQueuedNotice')) {
+        $transports[] = 'plugin';
+    }
 
     $xmpp = common_config('xmpp', 'enabled');
 
@@ -1006,6 +1014,7 @@ function common_enqueue_notice($notice)
         $transports[] = 'jabber';
     }
 
+    // @fixme move these checks into QueueManager and/or individual handlers
     if ($notice->is_local == Notice::LOCAL_PUBLIC ||
         $notice->is_local == Notice::LOCAL_NONPUBLIC) {
         $transports = array_merge($transports, $localTransports);
@@ -1564,3 +1573,56 @@ function common_client_ip()
 
     return array($proxy, $ip);
 }
+
+function common_url_to_nickname($url)
+{
+    static $bad = array('query', 'user', 'password', 'port', 'fragment');
+
+    $parts = parse_url($url);
+
+    # If any of these parts exist, this won't work
+
+    foreach ($bad as $badpart) {
+        if (array_key_exists($badpart, $parts)) {
+            return null;
+        }
+    }
+
+    # We just have host and/or path
+
+    # If it's just a host...
+    if (array_key_exists('host', $parts) &&
+        (!array_key_exists('path', $parts) || strcmp($parts['path'], '/') == 0))
+    {
+        $hostparts = explode('.', $parts['host']);
+
+        # Try to catch common idiom of nickname.service.tld
+
+        if ((count($hostparts) > 2) &&
+            (strlen($hostparts[count($hostparts) - 2]) > 3) && # try to skip .co.uk, .com.au
+            (strcmp($hostparts[0], 'www') != 0))
+        {
+            return common_nicknamize($hostparts[0]);
+        } else {
+            # Do the whole hostname
+            return common_nicknamize($parts['host']);
+        }
+    } else {
+        if (array_key_exists('path', $parts)) {
+            # Strip starting, ending slashes
+            $path = preg_replace('@/$@', '', $parts['path']);
+            $path = preg_replace('@^/@', '', $path);
+            if (strpos($path, '/') === false) {
+                return common_nicknamize($path);
+            }
+        }
+    }
+
+    return null;
+}
+
+function common_nicknamize($str)
+{
+    $str = preg_replace('/\W/', '', $str);
+    return strtolower($str);
+}
diff --git a/plugins/FeedSub/FeedSubPlugin.php b/plugins/FeedSub/FeedSubPlugin.php
deleted file mode 100644 (file)
index e49e2a6..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-<?php
-/*
-StatusNet Plugin: 0.9
-Plugin Name: FeedSub
-Plugin URI: http://status.net/wiki/Feed_subscription
-Description: FeedSub allows subscribing to real-time updates from external feeds supporting PubHubSubbub protocol.
-Version: 0.1
-Author: Brion Vibber <brion@status.net>
-Author URI: http://status.net/
-*/
-
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package FeedSubPlugin
- * @maintainer Brion Vibber <brion@status.net>
- */
-
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-
-define('FEEDSUB_SERVICE', 100); // fixme -- avoid hardcoding these?
-
-// We bundle the XML_Parse_Feed library...
-set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib');
-
-class FeedSubException extends Exception
-{
-}
-
-class FeedSubPlugin extends Plugin
-{
-    /**
-     * Hook for RouterInitialized event.
-     *
-     * @param Net_URL_Mapper $m path-to-action mapper
-     * @return boolean hook return
-     */
-    function onRouterInitialized($m)
-    {
-        $m->connect('feedsub/callback/:feed',
-                    array('action' => 'feedsubcallback'),
-                    array('feed' => '[0-9]+'));
-        $m->connect('settings/feedsub',
-                    array('action' => 'feedsubsettings'));
-        return true;
-    }
-
-    /**
-     * Add the feed settings page to the Connect Settings menu
-     *
-     * @param Action &$action The calling page
-     *
-     * @return boolean hook return
-     */
-    function onEndConnectSettingsNav(&$action)
-    {
-        $action_name = $action->trimmed('action');
-
-        $action->menuItem(common_local_url('feedsubsettings'),
-                          _m('Feeds'),
-                          _m('Feed subscription options'),
-                          $action_name === 'feedsubsettings');
-
-        return true;
-    }
-
-    /**
-     * Automatically load the actions and libraries used by the plugin
-     *
-     * @param Class $cls the class
-     *
-     * @return boolean hook return
-     *
-     */
-    function onAutoload($cls)
-    {
-        $base = dirname(__FILE__);
-        $lower = strtolower($cls);
-        $files = array("$base/$lower.php");
-        if (substr($lower, -6) == 'action') {
-            $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php";
-        }
-        foreach ($files as $file) {
-            if (file_exists($file)) {
-                include_once $file;
-                return false;
-            }
-        }
-        return true;
-    }
-
-    function onCheckSchema() {
-        // warning: the autoincrement doesn't seem to set.
-        // alter table feedinfo change column id id int(11) not null  auto_increment;
-        $schema = Schema::get();
-        $schema->ensureTable('feedinfo', Feedinfo::schemaDef());
-        return true;
-    }
-}
diff --git a/plugins/FeedSub/README b/plugins/FeedSub/README
deleted file mode 100644 (file)
index cbf3adb..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-Plugin to support importing updates from external RSS and Atom feeds into your timeline.
-
-Uses PubSubHubbub for push feed updates; currently non-PuSH feeds cannot be subscribed.
-
-Todo:
-* set feed icon avatar for actual profiles as well as for preview
-* use channel image and/or favicon for avatar?
-* garbage-collect subscriptions that are no longer being used
-* administrative way to kill feeds?
-* functional l10n
-* clean up subscription form look and workflow
-* use ajax for test/preview in subscription form
-* rssCloud support? (Does anything use it that doesn't support PuSH as well?)
-* possibly a polling daemon to support non-PuSH feeds?
-* likely problems with multiple feeds from the same site, such as category feeds on a blog
-  (currently each feed would publish a separate notice on a separate profile, but pointing to the same post URI.)
-  (could use the local URI I guess, but that's so icky!)
-* problems with Atom feeds that list <link rel="alternate" href="..."/> but don't have the type
-  (such as http://atomgen.appspot.com/feed/5 demo feed); currently it's not recognized and we end up with the feed's master URI
-* make it easier to see what you're subscribed to and unsub from things
-* saner treatment of fullname/nickname?
-* make use of tags/categories from feeds
-* update feed profile data when it changes
-* XML_Feed_Parser has major problems with category and link tags; consider replacing?
diff --git a/plugins/FeedSub/actions/feedsubcallback.php b/plugins/FeedSub/actions/feedsubcallback.php
deleted file mode 100644 (file)
index 0c4280c..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package FeedSubPlugin
- * @maintainer Brion Vibber <brion@status.net>
- */
-
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-
-
-class FeedSubCallbackAction extends Action
-{
-    function handle()
-    {
-        parent::handle();
-        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
-            $this->handlePost();
-        } else {
-            $this->handleGet();
-        }
-    }
-    
-    /**
-     * Handler for POST content updates from the hub
-     */
-    function handlePost()
-    {
-        $feedid = $this->arg('feed');
-        common_log(LOG_INFO, "POST for feed id $feedid");
-        if (!$feedid) {
-            throw new ServerException('Empty or invalid feed id', 400);
-        }
-
-        $feedinfo = Feedinfo::staticGet('id', $feedid);
-        if (!$feedinfo) {
-            throw new ServerException('Unknown feed id ' . $feedid, 400);
-        }
-        
-        $post = file_get_contents('php://input');
-        $feedinfo->postUpdates($post);
-    }
-    
-    /**
-     * Handler for GET verification requests from the hub
-     */
-    function handleGet()
-    {
-        $mode = $this->arg('hub_mode');
-        $topic = $this->arg('hub_topic');
-        $challenge = $this->arg('hub_challenge');
-        $lease_seconds = $this->arg('hub_lease_seconds');
-        $verify_token = $this->arg('hub_verify_token');
-        
-        if ($mode != 'subscribe' && $mode != 'unsubscribe') {
-            common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\"");
-            throw new ServerException("Bogus hub callback: bad mode", 404);
-        }
-        
-        $feedinfo = Feedinfo::staticGet('feeduri', $topic);
-        if (!$feedinfo) {
-            common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic");
-            throw new ServerException("Bogus hub callback: unknown feed", 404);
-        }
-
-        # Can't currently set the token in our sub api
-        #if ($feedinfo->verify_token !== $verify_token) {
-        #    common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
-        #    throw new ServerError("Bogus hub callback: bad token", 404);
-        #}
-        
-        // OK!
-        common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
-        $feedinfo->sub_start = common_sql_date(time());
-        if ($lease_seconds > 0) {
-            $feedinfo->sub_end = common_sql_date(time() + $lease_seconds);
-        } else {
-            $feedinfo->sub_end = null;
-        }
-        $feedinfo->update();
-        
-        print $challenge;
-    }
-}
diff --git a/plugins/FeedSub/actions/feedsubsettings.php b/plugins/FeedSub/actions/feedsubsettings.php
deleted file mode 100644 (file)
index 0fba20a..0000000
+++ /dev/null
@@ -1,255 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package FeedSubPlugin
- * @maintainer Brion Vibber <brion@status.net>
- */
-
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-
-class FeedSubSettingsAction extends ConnectSettingsAction
-{
-    protected $feedurl;
-    protected $preview;
-    protected $munger;
-
-    /**
-     * Title of the page
-     *
-     * @return string Title of the page
-     */
-
-    function title()
-    {
-        return _m('Feed subscriptions');
-    }
-
-    /**
-     * Instructions for use
-     *
-     * @return instructions for use
-     */
-
-    function getInstructions()
-    {
-        return _m('You can subscribe to feeds from other sites; ' .
-                  'updates will appear in your personal timeline.');
-    }
-
-    /**
-     * Content area of the page
-     *
-     * Shows a form for associating a Twitter account with this
-     * StatusNet account. Also lets the user set preferences.
-     *
-     * @return void
-     */
-
-    function showContent()
-    {
-        $user = common_current_user();
-
-        $profile = $user->getProfile();
-
-        $fuser = null;
-
-        $flink = Foreign_link::getByUserID($user->id, FEEDSUB_SERVICE);
-
-        if (!empty($flink)) {
-            $fuser = $flink->getForeignUser();
-        }
-
-        $this->elementStart('form', array('method' => 'post',
-                                          'id' => 'form_settings_feedsub',
-                                          'class' => 'form_settings',
-                                          'action' =>
-                                          common_local_url('feedsubsettings')));
-
-        $this->hidden('token', common_session_token());
-
-        $this->elementStart('fieldset', array('id' => 'settings_feeds'));
-
-        $this->elementStart('ul', 'form_data');
-        $this->elementStart('li', array('id' => 'settings_twitter_login_button'));
-        $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
-        $this->elementEnd('li');
-        $this->elementEnd('ul');
-
-        if ($this->preview) {
-            $this->submit('subscribe', _m('Subscribe'));
-        } else {
-            $this->submit('validate', _m('Continue'));
-        }
-
-        $this->elementEnd('fieldset');
-
-        $this->elementEnd('form');
-
-        if ($this->preview) {
-            $this->previewFeed();
-        }
-    }
-
-    /**
-     * Handle posts to this form
-     *
-     * Based on the button that was pressed, muxes out to other functions
-     * to do the actual task requested.
-     *
-     * All sub-functions reload the form with a message -- success or failure.
-     *
-     * @return void
-     */
-
-    function handlePost()
-    {
-        // CSRF protection
-        $token = $this->trimmed('token');
-        if (!$token || $token != common_session_token()) {
-            $this->showForm(_('There was a problem with your session token. '.
-                              'Try again, please.'));
-            return;
-        }
-
-        if ($this->arg('validate')) {
-            $this->validateAndPreview();
-        } else if ($this->arg('subscribe')) {
-            $this->saveFeed();
-        } else {
-            $this->showForm(_('Unexpected form submission.'));
-        }
-    }
-
-    /**
-     * Set up and add a feed
-     *
-     * @return boolean true if feed successfully read
-     * Sends you back to input form if not.
-     */
-    function validateFeed()
-    {
-        $feedurl = trim($this->arg('feedurl'));
-        
-        if ($feedurl == '') {
-            $this->showForm(_m('Empty feed URL!'));
-            return;
-        }
-        $this->feedurl = $feedurl;
-        
-        // Get the canonical feed URI and check it
-        try {
-            $discover = new FeedDiscovery();
-            $uri = $discover->discoverFromURL($feedurl);
-        } catch (FeedSubBadURLException $e) {
-            $this->showForm(_m('Invalid URL or could not reach server.'));
-            return false;
-        } catch (FeedSubBadResponseException $e) {
-            $this->showForm(_m('Cannot read feed; server returned error.'));
-            return false;
-        } catch (FeedSubEmptyException $e) {
-            $this->showForm(_m('Cannot read feed; server returned an empty page.'));
-            return false;
-        } catch (FeedSubBadHTMLException $e) {
-            $this->showForm(_m('Bad HTML, could not find feed link.'));
-            return false;
-        } catch (FeedSubNoFeedException $e) {
-            $this->showForm(_m('Could not find a feed linked from this URL.'));
-            return false;
-        } catch (FeedSubUnrecognizedTypeException $e) {
-            $this->showForm(_m('Not a recognized feed type.'));
-            return false;
-        } catch (FeedSubException $e) {
-            // Any new ones we forgot about
-            $this->showForm(_m('Bad feed URL.'));
-            return false;
-        }
-        
-        $this->munger = $discover->feedMunger();
-        $this->feedinfo = $this->munger->feedInfo();
-
-        if ($this->feedinfo->huburi == '') {
-            $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
-            return false;
-        }
-        
-        return true;
-    }
-
-    function saveFeed()
-    {
-        if ($this->validateFeed()) {
-            $this->preview = true;
-            $this->feedinfo = Feedinfo::ensureProfile($this->munger);
-
-            // If not already in use, subscribe to updates via the hub
-            if ($this->feedinfo->sub_start) {
-                common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}");
-            } else {
-                $ok = $this->feedinfo->subscribe();
-                common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
-                if (!$ok) {
-                    $this->showForm(_m('Feed subscription failed! Bad response from hub.'));
-                    return;
-                }
-            }
-            
-            // And subscribe the current user to the local profile
-            $user = common_current_user();
-            $profile = $this->feedinfo->getProfile();
-            
-            if ($user->isSubscribed($profile)) {
-                $this->showForm(_m('Already subscribed!'));
-            } elseif ($user->subscribeTo($profile)) {
-                $this->showForm(_m('Feed subscribed!'));
-            } else {
-                $this->showForm(_m('Feed subscription failed!'));
-            }
-        }
-    }
-
-    function validateAndPreview()
-    {
-        if ($this->validateFeed()) {
-            $this->preview = true;
-            $this->showForm(_m('Previewing feed:'));
-        }
-    }
-
-    function previewFeed()
-    {
-        $feedinfo = $this->munger->feedinfo();
-        $notice = $this->munger->notice(0, true); // preview
-
-        if ($notice) {
-            $this->element('b', null, 'Preview of latest post from this feed:');
-
-            $item = new NoticeList($notice, $this);
-            $item->show();
-        } else {
-            $this->element('b', null, 'No posts in this feed yet.');
-        }
-    }
-
-    function showScripts()
-    {
-        parent::showScripts();
-        $this->autofocus('feedurl');
-    }
-}
diff --git a/plugins/FeedSub/extlib/README b/plugins/FeedSub/extlib/README
deleted file mode 100644 (file)
index 799b40c..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-XML_Feed_Parser 1.0.3 is not currently actively maintained, and has
-a nasty bug which breaks getting the feed target link from WordPress
-feeds and possibly others that are RSS2-formatted but include an
-<atom:link> self-link element as well.
-
-Patch from this bug report is included:
-http://pear.php.net/bugs/bug.php?id=16416
-
-If upgrading, be sure that fix is included with the future upgrade!
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser.php b/plugins/FeedSub/extlib/XML/Feed/Parser.php
deleted file mode 100755 (executable)
index ffe8220..0000000
+++ /dev/null
@@ -1,351 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Key gateway class for XML_Feed_Parser package
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL
- * @version    CVS: $Id: Parser.php,v 1.24 2006/08/15 13:04:00 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * XML_Feed_Parser_Type is an abstract class required by all of our
- * feed types. It makes sense to load it here to keep the other files
- * clean.
- */
-require_once 'XML/Feed/Parser/Type.php';
-
-/**
- * We will throw exceptions when errors occur.
- */
-require_once 'XML/Feed/Parser/Exception.php';
-
-/**
- * This is the core of the XML_Feed_Parser package. It identifies feed types 
- * and abstracts access to them. It is an iterator, allowing for easy access 
- * to the entire feed.
- *
- * @author  James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser implements Iterator
-{
-    /**
-     * This is where we hold the feed object 
-     * @var Object
-     */
-    private $feed;
-
-    /**
-     * To allow for extensions, we make a public reference to the feed model 
-     * @var DOMDocument
-     */
-    public $model;
-    
-    /**
-     * A map between entry ID and offset
-     * @var array
-     */
-    protected $idMappings = array();
-
-    /**
-     * A storage space for Namespace URIs.
-     * @var array
-     */
-    private $feedNamespaces = array(
-        'rss2' => array(
-            'http://backend.userland.com/rss',
-            'http://backend.userland.com/rss2',
-            'http://blogs.law.harvard.edu/tech/rss'));
-    /**
-     * Detects feed types and instantiate appropriate objects.
-     *
-     * Our constructor takes care of detecting feed types and instantiating
-     * appropriate classes. For now we're going to treat Atom 0.3 as Atom 1.0
-     * but raise a warning. I do not intend to introduce full support for 
-     * Atom 0.3 as it has been deprecated, but others are welcome to.
-     *
-     * @param    string    $feed    XML serialization of the feed
-     * @param    bool    $strict    Whether or not to validate the feed
-     * @param    bool    $suppressWarnings Trigger errors for deprecated feed types?
-     * @param    bool    $tidy    Whether or not to try and use the tidy library on input
-     */
-    function __construct($feed, $strict = false, $suppressWarnings = false, $tidy = false)
-    {
-        $this->model = new DOMDocument;
-        if (! $this->model->loadXML($feed)) {
-            if (extension_loaded('tidy') && $tidy) {
-                $tidy = new tidy;
-                $tidy->parseString($feed, 
-                    array('input-xml' => true, 'output-xml' => true));
-                $tidy->cleanRepair();
-                if (! $this->model->loadXML((string) $tidy)) {
-                    throw new XML_Feed_Parser_Exception('Invalid input: this is not ' .
-                        'valid XML');
-                }
-            } else {
-                throw new XML_Feed_Parser_Exception('Invalid input: this is not valid XML');
-            }
-
-        }
-
-        /* detect feed type */
-        $doc_element = $this->model->documentElement;
-        $error = false;
-
-        switch (true) {
-            case ($doc_element->namespaceURI == 'http://www.w3.org/2005/Atom'):
-                require_once 'XML/Feed/Parser/Atom.php';
-                require_once 'XML/Feed/Parser/AtomElement.php';
-                $class = 'XML_Feed_Parser_Atom';
-                break;
-            case ($doc_element->namespaceURI == 'http://purl.org/atom/ns#'):
-                require_once 'XML/Feed/Parser/Atom.php';
-                require_once 'XML/Feed/Parser/AtomElement.php';
-                $class = 'XML_Feed_Parser_Atom';
-                $error = 'Atom 0.3 deprecated, using 1.0 parser which won\'t provide ' .
-                    'all options';
-                break;
-            case ($doc_element->namespaceURI == 'http://purl.org/rss/1.0/' || 
-                ($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1 
-                && $doc_element->childNodes->item(1)->namespaceURI == 
-                'http://purl.org/rss/1.0/')):
-                require_once 'XML/Feed/Parser/RSS1.php';
-                require_once 'XML/Feed/Parser/RSS1Element.php';
-                $class = 'XML_Feed_Parser_RSS1';
-                break;
-            case ($doc_element->namespaceURI == 'http://purl.org/rss/1.1/' || 
-                ($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1 
-                && $doc_element->childNodes->item(1)->namespaceURI == 
-                'http://purl.org/rss/1.1/')):
-                require_once 'XML/Feed/Parser/RSS11.php';
-                require_once 'XML/Feed/Parser/RSS11Element.php';
-                $class = 'XML_Feed_Parser_RSS11';
-                break;
-            case (($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1
-                && $doc_element->childNodes->item(1)->namespaceURI == 
-                'http://my.netscape.com/rdf/simple/0.9/') || 
-                $doc_element->namespaceURI == 'http://my.netscape.com/rdf/simple/0.9/'):
-                require_once 'XML/Feed/Parser/RSS09.php';
-                require_once 'XML/Feed/Parser/RSS09Element.php';
-                $class = 'XML_Feed_Parser_RSS09';
-                break;
-            case ($doc_element->tagName == 'rss' and
-                $doc_element->hasAttribute('version') && 
-                $doc_element->getAttribute('version') == 0.91):
-                $error = 'RSS 0.91 has been superceded by RSS2.0. Using RSS2.0 parser.';
-                require_once 'XML/Feed/Parser/RSS2.php';
-                require_once 'XML/Feed/Parser/RSS2Element.php';
-                $class = 'XML_Feed_Parser_RSS2';
-                break;
-            case ($doc_element->tagName == 'rss' and
-                $doc_element->hasAttribute('version') && 
-                $doc_element->getAttribute('version') == 0.92):
-                $error = 'RSS 0.92 has been superceded by RSS2.0. Using RSS2.0 parser.';
-                require_once 'XML/Feed/Parser/RSS2.php';
-                require_once 'XML/Feed/Parser/RSS2Element.php';
-                $class = 'XML_Feed_Parser_RSS2';
-                break;
-            case (in_array($doc_element->namespaceURI, $this->feedNamespaces['rss2'])
-                || $doc_element->tagName == 'rss'):
-                if (! $doc_element->hasAttribute('version') || 
-                    $doc_element->getAttribute('version') != 2) {
-                    $error = 'RSS version not specified. Parsing as RSS2.0';
-                }
-                require_once 'XML/Feed/Parser/RSS2.php';
-                require_once 'XML/Feed/Parser/RSS2Element.php';
-                $class = 'XML_Feed_Parser_RSS2';
-                break;
-            default:
-                throw new XML_Feed_Parser_Exception('Feed type unknown');
-                break;
-        }
-
-        if (! $suppressWarnings && ! empty($error)) {
-            trigger_error($error, E_USER_WARNING);
-        }
-
-        /* Instantiate feed object */
-        $this->feed = new $class($this->model, $strict);
-    }
-
-    /**
-     * Proxy to allow feed element names to be used as method names
-     *
-     * For top-level feed elements we will provide access using methods or 
-     * attributes. This function simply passes on a request to the appropriate 
-     * feed type object.
-     *
-     * @param   string  $call - the method being called
-     * @param   array   $attributes
-     */
-    function __call($call, $attributes)
-    {
-        $attributes = array_pad($attributes, 5, false);
-        list($a, $b, $c, $d, $e) = $attributes;
-        return $this->feed->$call($a, $b, $c, $d, $e);
-    }
-
-    /**
-     * Proxy to allow feed element names to be used as attribute names
-     *
-     * To allow variable-like access to feed-level data we use this
-     * method. It simply passes along to __call() which in turn passes
-     * along to the relevant object.
-     *
-     * @param   string  $val - the name of the variable required
-     */
-    function __get($val)
-    {
-        return $this->feed->$val;
-    }
-
-    /**
-     * Provides iteration functionality.
-     *
-     * Of course we must be able to iterate... This function simply increases
-     * our internal counter.
-     */
-    function next()
-    {
-        if (isset($this->current_item) && 
-            $this->current_item <= $this->feed->numberEntries - 1) {
-            ++$this->current_item;
-        } else if (! isset($this->current_item)) {
-            $this->current_item = 0;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Return XML_Feed_Type object for current element
-     *
-     * @return    XML_Feed_Parser_Type Object
-     */
-    function current()
-    {
-        return $this->getEntryByOffset($this->current_item);
-    }
-
-    /**
-     * For iteration -- returns the key for the current stage in the array.
-     *
-     * @return    int
-     */    
-    function key()
-    {
-        return $this->current_item;
-    }
-
-    /**
-     * For iteration -- tells whether we have reached the 
-     * end.
-     *
-     * @return    bool
-     */
-    function valid()
-    {
-        return $this->current_item < $this->feed->numberEntries;
-    }
-
-    /**
-     * For iteration -- resets the internal counter to the beginning.
-     */
-    function rewind()
-    {
-        $this->current_item = 0;
-    }
-
-    /**
-     * Provides access to entries by ID if one is specified in the source feed.
-     *
-     * As well as allowing the items to be iterated over we want to allow
-     * users to be able to access a specific entry. This is one of two ways of
-     * doing that, the other being by offset. This method can be quite slow
-     * if dealing with a large feed that hasn't yet been processed as it
-     * instantiates objects for every entry until it finds the one needed.
-     *
-     * @param    string    $id  Valid ID for the given feed format
-     * @return    XML_Feed_Parser_Type|false
-     */            
-    function getEntryById($id)
-    {
-        if (isset($this->idMappings[$id])) {
-            return $this->getEntryByOffset($this->idMappings[$id]);
-        }
-
-        /* 
-         * Since we have not yet encountered that ID, let's go through all the
-         * remaining entries in order till we find it.
-         * This is a fairly slow implementation, but it should work.
-         */
-        return $this->feed->getEntryById($id);
-    }
-
-    /**
-     * Retrieve entry by numeric offset, starting from zero.
-     *
-     * As well as allowing the items to be iterated over we want to allow
-     * users to be able to access a specific entry. This is one of two ways of
-     * doing that, the other being by ID.
-     *
-     * @param    int    $offset The position of the entry within the feed, starting from 0
-     * @return    XML_Feed_Parser_Type|false
-     */
-    function getEntryByOffset($offset)
-    {
-        if ($offset < $this->feed->numberEntries) {
-            if (isset($this->feed->entries[$offset])) {
-                return $this->feed->entries[$offset];
-            } else {
-                try {
-                    $this->feed->getEntryByOffset($offset);
-                } catch (Exception $e) {
-                    return false;
-                }
-                $id = $this->feed->entries[$offset]->getID();
-                $this->idMappings[$id] = $offset;
-                return $this->feed->entries[$offset];
-            }
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Retrieve version details from feed type class.
-     *
-     * @return void
-     * @author James Stewart
-     */
-    function version()
-    {
-        return $this->feed->version;
-    }
-    
-    /**
-     * Returns a string representation of the feed.
-     * 
-     * @return String
-     **/
-    function __toString()
-    {
-        return $this->feed->__toString();
-    }
-}
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php
deleted file mode 100644 (file)
index c7e218a..0000000
+++ /dev/null
@@ -1,365 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Atom feed class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
- * @version    CVS: $Id: Atom.php,v 1.29 2008/03/30 22:00:36 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
-*/
-
-/**
- * This is the class that determines how we manage Atom 1.0 feeds
- * 
- * How we deal with constructs:
- *  date - return as unix datetime for use with the 'date' function unless specified otherwise
- *  text - return as is. optional parameter will give access to attributes
- *  person - defaults to name, but parameter based access
- *
- * @author    James Stewart <james@jystewart.net>
- * @version    Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_Atom extends XML_Feed_Parser_Type
-{
-    /**
-     * The URI of the RelaxNG schema used to (optionally) validate the feed 
-     * @var string
-     */
-    private $relax = 'atom.rnc';
-
-    /**
-     * We're likely to use XPath, so let's keep it global 
-     * @var DOMXPath
-     */
-    public $xpath;
-
-    /**
-     * When performing XPath queries we will use this prefix 
-     * @var string
-     */
-    private $xpathPrefix = '//';
-
-    /**
-     * The feed type we are parsing 
-     * @var string
-     */
-    public $version = 'Atom 1.0';
-
-    /** 
-     * The class used to represent individual items 
-     * @var string
-     */
-    protected $itemClass = 'XML_Feed_Parser_AtomElement';
-    
-    /** 
-     * The element containing entries 
-     * @var string
-     */
-    protected $itemElement = 'entry';
-
-    /**
-     * Here we map those elements we're not going to handle individually
-     * to the constructs they are. The optional second parameter in the array
-     * tells the parser whether to 'fall back' (not apt. at the feed level) or
-     * fail if the element is missing. If the parameter is not set, the function
-     * will simply return false and leave it to the client to decide what to do.
-     * @var array
-     */
-    protected $map = array(
-        'author' => array('Person'),
-        'contributor' => array('Person'),
-        'icon' => array('Text'),
-        'logo' => array('Text'),
-        'id' => array('Text', 'fail'),
-        'rights' => array('Text'),
-        'subtitle' => array('Text'),
-        'title' => array('Text', 'fail'),
-        'updated' => array('Date', 'fail'),
-        'link' => array('Link'),
-        'generator' => array('Text'),
-        'category' => array('Category'));
-
-    /**
-     * Here we provide a few mappings for those very special circumstances in
-     * which it makes sense to map back to the RSS2 spec. Key is RSS2 version
-     * value is an array consisting of the equivalent in atom and any attributes
-     * needed to make the mapping.
-     * @var array
-     */
-    protected $compatMap = array(
-        'guid' => array('id'),
-        'links' => array('link'),
-        'tags' => array('category'),
-        'contributors' => array('contributor'));
-
-    /**
-     * Our constructor does nothing more than its parent.
-     * 
-     * @param    DOMDocument    $xml    A DOM object representing the feed
-     * @param    bool (optional) $string    Whether or not to validate this feed
-     */
-    function __construct(DOMDocument $model, $strict = false)
-    {
-        $this->model = $model;
-
-        if ($strict) {
-            if (! $this->model->relaxNGValidateSource($this->relax)) {
-                throw new XML_Feed_Parser_Exception('Failed required validation');
-            }
-        }
-
-        $this->xpath = new DOMXPath($this->model);
-        $this->xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
-        $this->numberEntries = $this->count('entry');
-    }
-
-    /**
-     * Implement retrieval of an entry based on its ID for atom feeds.
-     *
-     * This function uses XPath to get the entry based on its ID. If DOMXPath::evaluate
-     * is available, we also use that to store a reference to the entry in the array
-     * used by getEntryByOffset so that method does not have to seek out the entry
-     * if it's requested that way.
-     * 
-     * @param    string    $id    any valid Atom ID.
-     * @return    XML_Feed_Parser_AtomElement
-     */
-    function getEntryById($id)
-    {
-        if (isset($this->idMappings[$id])) {
-            return $this->entries[$this->idMappings[$id]];
-        }
-
-        $entries = $this->xpath->query("//atom:entry[atom:id='$id']");
-
-        if ($entries->length > 0) {
-            $xmlBase = $entries->item(0)->baseURI;
-            $entry = new $this->itemClass($entries->item(0), $this, $xmlBase);
-            
-            if (in_array('evaluate', get_class_methods($this->xpath))) {
-                $offset = $this->xpath->evaluate("count(preceding-sibling::atom:entry)", $entries->item(0));
-                $this->entries[$offset] = $entry;
-            }
-
-            $this->idMappings[$id] = $entry;
-
-            return $entry;
-        }
-        
-    }
-
-    /**
-     * Retrieves data from a person construct.
-     *
-     * Get a person construct. We default to the 'name' element but allow
-     * access to any of the elements.
-     * 
-     * @param    string    $method    The name of the person construct we want
-     * @param    array     $arguments    An array which we hope gives a 'param'
-     * @return    string|false
-     */
-    protected function getPerson($method, $arguments)
-    {
-        $offset = empty($arguments[0]) ? 0 : $arguments[0];
-        $parameter = empty($arguments[1]['param']) ? 'name' : $arguments[1]['param'];
-        $section = $this->model->getElementsByTagName($method);
-        
-        if ($parameter == 'url') {
-            $parameter = 'uri';
-        }
-
-        if ($section->length <= $offset) {
-            return false;
-        }
-
-        $param = $section->item($offset)->getElementsByTagName($parameter);
-        if ($param->length == 0) {
-            return false;
-        }
-        return $param->item(0)->nodeValue;
-    }
-
-    /**
-     * Retrieves an element's content where that content is a text construct.
-     *
-     * Get a text construct. When calling this method, the two arguments
-     * allowed are 'offset' and 'attribute', so $parser->subtitle() would
-     * return the content of the element, while $parser->subtitle(false, 'type')
-     * would return the value of the type attribute.
-     *
-     * @todo    Clarify overlap with getContent()
-     * @param    string    $method    The name of the text construct we want
-     * @param    array     $arguments    An array which we hope gives a 'param'
-     * @return    string
-     */
-    protected function getText($method, $arguments)
-    {
-        $offset = empty($arguments[0]) ? 0: $arguments[0];
-        $attribute = empty($arguments[1]) ? false : $arguments[1];
-        $tags = $this->model->getElementsByTagName($method);
-
-        if ($tags->length <= $offset) {
-            return false;
-        }
-
-        $content = $tags->item($offset);
-
-        if (! $content->hasAttribute('type')) {
-            $content->setAttribute('type', 'text');
-        }
-        $type = $content->getAttribute('type');
-
-        if (! empty($attribute) and 
-            ! ($method == 'generator' and $attribute == 'name')) {
-            if ($content->hasAttribute($attribute)) {
-                return $content->getAttribute($attribute);
-            } else if ($attribute == 'href' and $content->hasAttribute('uri')) {
-                return $content->getAttribute('uri');
-            }
-            return false;
-        }
-
-        return $this->parseTextConstruct($content);
-    }
-    
-    /**
-     * Extract content appropriately from atom text constructs
-     *
-     * Because of different rules applied to the content element and other text
-     * constructs, they are deployed as separate functions, but they share quite
-     * a bit of processing. This method performs the core common process, which is
-     * to apply the rules for different mime types in order to extract the content.
-     *
-     * @param   DOMNode $content    the text construct node to be parsed
-     * @return String
-     * @author James Stewart
-     **/
-    protected function parseTextConstruct(DOMNode $content)
-    {
-        if ($content->hasAttribute('type')) {
-            $type = $content->getAttribute('type');
-        } else {
-            $type = 'text';
-        }
-
-        if (strpos($type, 'text/') === 0) {
-            $type = 'text';
-        }
-
-        switch ($type) {
-            case 'text':
-            case 'html':
-                return $content->textContent;
-                break;
-            case 'xhtml':
-                $container = $content->getElementsByTagName('div');
-                if ($container->length == 0) {
-                    return false;
-                }
-                $contents = $container->item(0);
-                if ($contents->hasChildNodes()) {
-                    /* Iterate through, applying xml:base and store the result */
-                    $result = '';
-                    foreach ($contents->childNodes as $node) {
-                        $result .= $this->traverseNode($node);
-                    }
-                    return $result;
-                }
-                break;
-            case preg_match('@^[a-zA-Z]+/[a-zA-Z+]*xml@i', $type) > 0:
-                return $content;
-                break;
-            case 'application/octet-stream':
-            default:
-                return base64_decode(trim($content->nodeValue));
-                break;
-        }
-        return false;
-    }
-    /**
-     * Get a category from the entry.
-     *
-     * A feed or entry can have any number of categories. A category can have the
-     * attributes term, scheme and label.
-     * 
-     * @param    string    $method    The name of the text construct we want
-     * @param    array     $arguments    An array which we hope gives a 'param'
-     * @return    string
-     */
-    function getCategory($method, $arguments)
-    {
-        $offset = empty($arguments[0]) ? 0: $arguments[0];
-        $attribute = empty($arguments[1]) ? 'term' : $arguments[1];
-        $categories = $this->model->getElementsByTagName('category');
-        if ($categories->length <= $offset) {
-            $category = $categories->item($offset);
-            if ($category->hasAttribute($attribute)) {
-                return $category->getAttribute($attribute);
-            }
-        }
-        return false;
-    }
-
-    /**
-     * This element must be present at least once with rel="feed". This element may be 
-     * present any number of further times so long as there is no clash. If no 'rel' is 
-     * present and we're asked for one, we follow the example of the Universal Feed
-     * Parser and presume 'alternate'.
-     *
-     * @param    int    $offset    the position of the link within the container
-     * @param    string    $attribute    the attribute name required
-     * @param    array     an array of attributes to search by
-     * @return    string    the value of the attribute
-     */
-    function getLink($offset = 0, $attribute = 'href', $params = false)
-    {
-        if (is_array($params) and !empty($params)) {
-            $terms = array();
-            $alt_predicate = '';
-            $other_predicate = '';
-
-            foreach ($params as $key => $value) {
-                if ($key == 'rel' && $value == 'alternate') {
-                    $alt_predicate = '[not(@rel) or @rel="alternate"]';
-                } else {
-                    $terms[] = "@$key='$value'";
-                }
-            }
-            if (!empty($terms)) {
-                $other_predicate = '[' . join(' and ', $terms) . ']';
-            }
-            $query =  $this->xpathPrefix . 'atom:link' . $alt_predicate . $other_predicate;
-            $links = $this->xpath->query($query);
-        } else {
-            $links = $this->model->getElementsByTagName('link');
-        }
-        if ($links->length > $offset) {
-            if ($links->item($offset)->hasAttribute($attribute)) {
-                $value = $links->item($offset)->getAttribute($attribute);
-                if ($attribute == 'href') {
-                    $value = $this->addBase($value, $links->item($offset));
-                }
-                return $value;
-            } else if ($attribute == 'rel') {
-                return 'alternate';
-            }
-        }
-        return false;
-    }
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php b/plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php
deleted file mode 100755 (executable)
index 063ecb6..0000000
+++ /dev/null
@@ -1,261 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * AtomElement class for XML_Feed_Parser package
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
- * @version    CVS: $Id: AtomElement.php,v 1.19 2007/03/26 12:43:11 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class provides support for atom entries. It will usually be called by
- * XML_Feed_Parser_Atom with which it shares many methods.
- *
- * @author    James Stewart <james@jystewart.net>
- * @version    Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_AtomElement extends XML_Feed_Parser_Atom
-{
-    /**
-     * This will be a reference to the parent object for when we want
-     * to use a 'fallback' rule 
-     * @var XML_Feed_Parser_Atom
-     */
-    protected $parent;
-
-    /**
-     * When performing XPath queries we will use this prefix 
-     * @var string
-     */
-    private $xpathPrefix = '';
-    
-    /**
-     * xml:base values inherited by the element 
-     * @var string
-     */
-    protected $xmlBase;
-
-    /**
-     * Here we provide a few mappings for those very special circumstances in
-     * which it makes sense to map back to the RSS2 spec or to manage other
-     * compatibilities (eg. with the Univeral Feed Parser). Key is the other version's
-     * name for the command, value is an array consisting of the equivalent in our atom 
-     * api and any attributes needed to make the mapping.
-     * @var array
-     */
-    protected $compatMap = array(
-        'guid' => array('id'),
-        'links' => array('link'),
-        'tags' => array('category'),
-        'contributors' => array('contributor'));
-        
-    /**
-     * Our specific element map 
-     * @var array
-     */
-    protected $map = array(
-        'author' => array('Person', 'fallback'),
-        'contributor' => array('Person'),
-        'id' => array('Text', 'fail'),
-        'published' => array('Date'),
-        'updated' => array('Date', 'fail'),
-        'title' => array('Text', 'fail'),
-        'rights' => array('Text', 'fallback'),
-        'summary' => array('Text'),
-        'content' => array('Content'),
-        'link' => array('Link'),
-        'enclosure' => array('Enclosure'),
-        'category' => array('Category'));
-
-    /**
-     * Store useful information for later.
-     *
-     * @param   DOMElement  $element - this item as a DOM element
-     * @param   XML_Feed_Parser_Atom    $parent - the feed of which this is a member
-     */
-    function __construct(DOMElement $element, $parent, $xmlBase = '')
-    {
-        $this->model = $element;
-        $this->parent = $parent;
-        $this->xmlBase = $xmlBase;
-        $this->xpathPrefix = "//atom:entry[atom:id='" . $this->id . "']/";
-        $this->xpath = $this->parent->xpath;
-    }
-
-    /**
-     * Provides access to specific aspects of the author data for an atom entry
-     *
-     * Author data at the entry level is more complex than at the feed level.
-     * If atom:author is not present for the entry we need to look for it in
-     * an atom:source child of the atom:entry. If it's not there either, then
-     * we look to the parent for data.
-     *
-     * @param   array
-     * @return  string
-     */
-    function getAuthor($arguments)
-    {
-        /* Find out which part of the author data we're looking for */
-        if (isset($arguments['param'])) {
-            $parameter = $arguments['param'];
-        } else {
-            $parameter = 'name';
-        }
-        
-        $test = $this->model->getElementsByTagName('author');
-        if ($test->length > 0) {
-            $item = $test->item(0);
-            return $item->getElementsByTagName($parameter)->item(0)->nodeValue;
-        }
-        
-        $source = $this->model->getElementsByTagName('source');
-        if ($source->length > 0) {
-            $test = $this->model->getElementsByTagName('author');
-            if ($test->length > 0) {
-                $item = $test->item(0);
-                return $item->getElementsByTagName($parameter)->item(0)->nodeValue;
-            }
-        }
-        return $this->parent->getAuthor($arguments);
-    }
-
-    /**
-     * Returns the content of the content element or info on a specific attribute
-     *
-     * This element may or may not be present. It cannot be present more than
-     * once. It may have a 'src' attribute, in which case there's no content
-     * If not present, then the entry must have link with rel="alternate".
-     * If there is content we return it, if not and there's a 'src' attribute
-     * we return the value of that instead. The method can take an 'attribute'
-     * argument, in which case we return the value of that attribute if present.
-     * eg. $item->content("type") will return the type of the content. It is
-     * recommended that all users check the type before getting the content to
-     * ensure that their script is capable of handling the type of returned data.
-     * (data carried in the content element can be either 'text', 'html', 'xhtml', 
-     * or any standard MIME type).
-     *
-     * @return  string|false
-     */
-    protected function getContent($method, $arguments = array())
-    {
-        $attribute = empty($arguments[0]) ? false : $arguments[0];
-        $tags = $this->model->getElementsByTagName('content');
-
-        if ($tags->length == 0) {
-            return false;
-        }
-
-        $content = $tags->item(0);
-
-        if (! $content->hasAttribute('type')) {
-            $content->setAttribute('type', 'text');
-        }
-        if (! empty($attribute)) {
-            return $content->getAttribute($attribute);
-        }
-
-        $type = $content->getAttribute('type');
-
-        if (! empty($attribute)) {
-            if ($content->hasAttribute($attribute))
-            {
-                return $content->getAttribute($attribute);
-            }
-            return false;
-        }
-
-        if ($content->hasAttribute('src')) {
-            return $content->getAttribute('src');
-        }
-
-        return $this->parseTextConstruct($content);
-     }
-
-    /**
-     * For compatibility, this method provides a mapping to access enclosures.
-     *
-     * The Atom spec doesn't provide for an enclosure element, but it is
-     * generally supported using the link element with rel='enclosure'.
-     *
-     * @param   string  $method - for compatibility with our __call usage
-     * @param   array   $arguments - for compatibility with our __call usage
-     * @return  array|false
-     */
-    function getEnclosure($method, $arguments = array())
-    {
-        $offset = isset($arguments[0]) ? $arguments[0] : 0;
-        $query = "//atom:entry[atom:id='" . $this->getText('id', false) . 
-            "']/atom:link[@rel='enclosure']";
-
-        $encs = $this->parent->xpath->query($query);
-        if ($encs->length > $offset) {
-            try {
-                if (! $encs->item($offset)->hasAttribute('href')) {
-                    return false;
-                }
-                $attrs = $encs->item($offset)->attributes;
-                $length = $encs->item($offset)->hasAttribute('length') ? 
-                    $encs->item($offset)->getAttribute('length') : false;
-                return array(
-                    'url' => $attrs->getNamedItem('href')->value,
-                    'type' => $attrs->getNamedItem('type')->value,
-                    'length' => $length);
-            } catch (Exception $e) {
-                return false;
-            }
-        }
-        return false;
-    }
-    
-    /**
-     * Get details of this entry's source, if available/relevant
-     *
-     * Where an atom:entry is taken from another feed then the aggregator
-     * is supposed to include an atom:source element which replicates at least
-     * the atom:id, atom:title, and atom:updated metadata from the original
-     * feed. Atom:source therefore has a very similar structure to atom:feed
-     * and if we find it we will return it as an XML_Feed_Parser_Atom object.
-     *
-     * @return  XML_Feed_Parser_Atom|false
-     */
-    function getSource()
-    {
-        $test = $this->model->getElementsByTagName('source');
-        if ($test->length == 0) {
-            return false;
-        }
-        $source = new XML_Feed_Parser_Atom($test->item(0));
-    }
-
-    /**
-     * Get the entry as an XML string
-     *
-     * Return an XML serialization of the feed, should it be required. Most 
-     * users however, will already have a serialization that they used when 
-     * instantiating the object.
-     *
-     * @return    string    XML serialization of element
-     */    
-    function __toString()
-    {
-        $simple = simplexml_import_dom($this->model);
-        return $simple->asXML();
-    }
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php
deleted file mode 100755 (executable)
index 1e76e3f..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Keeps the exception class for XML_Feed_Parser.
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL
- * @version    CVS: $Id: Exception.php,v 1.3 2005/11/07 01:52:35 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-/**
- * We are extending PEAR_Exception
- */
-require_once 'PEAR/Exception.php';
-
-/**
- * XML_Feed_Parser_Exception is a simple extension of PEAR_Exception, existing
- * to help with identification of the source of exceptions.
- *
- * @author  James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- */ 
-class XML_Feed_Parser_Exception extends PEAR_Exception
-{
-
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php
deleted file mode 100755 (executable)
index 07f38f9..0000000
+++ /dev/null
@@ -1,214 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS0.9 class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
- * @version    CVS: $Id: RSS09.php,v 1.5 2006/07/26 21:18:46 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class handles RSS0.9 feeds.
- * 
- * @author    James Stewart <james@jystewart.net>
- * @version    Release: 1.0.3
- * @package XML_Feed_Parser
- * @todo    Find a Relax NG URI we can use
- */
-class XML_Feed_Parser_RSS09 extends XML_Feed_Parser_Type
-{
-    /**
-     * The URI of the RelaxNG schema used to (optionally) validate the feed 
-     * @var string
-     */
-    private $relax = '';
-
-    /**
-     * We're likely to use XPath, so let's keep it global
-     * @var DOMXPath
-     */
-    protected $xpath;
-
-    /**
-     * The feed type we are parsing 
-     * @var string
-     */
-    public $version = 'RSS 0.9';
-
-    /**
-     * The class used to represent individual items 
-     * @var string
-     */
-    protected $itemClass = 'XML_Feed_Parser_RSS09Element';
-    
-    /**
-     * The element containing entries 
-     * @var string
-     */
-    protected $itemElement = 'item';
-
-    /**
-     * Here we map those elements we're not going to handle individually
-     * to the constructs they are. The optional second parameter in the array
-     * tells the parser whether to 'fall back' (not apt. at the feed level) or
-     * fail if the element is missing. If the parameter is not set, the function
-     * will simply return false and leave it to the client to decide what to do.
-     * @var array
-     */
-    protected $map = array(
-        'title' => array('Text'),
-        'link' => array('Text'),
-        'description' => array('Text'),
-        'image' => array('Image'),
-        'textinput' => array('TextInput'));
-
-    /**
-     * Here we map some elements to their atom equivalents. This is going to be
-     * quite tricky to pull off effectively (and some users' methods may vary)
-     * but is worth trying. The key is the atom version, the value is RSS2.
-     * @var array
-     */
-    protected $compatMap = array(
-        'title' => array('title'),
-        'link' => array('link'),
-        'subtitle' => array('description'));
-
-    /**
-     * We will be working with multiple namespaces and it is useful to 
-     * keep them together 
-     * @var array
-     */
-    protected $namespaces = array(
-        'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
-
-    /**
-     * Our constructor does nothing more than its parent.
-     * 
-     * @todo    RelaxNG validation
-     * @param    DOMDocument    $xml    A DOM object representing the feed
-     * @param    bool (optional) $string    Whether or not to validate this feed
-     */
-    function __construct(DOMDocument $model, $strict = false)
-    {
-        $this->model = $model;
-
-        $this->xpath = new DOMXPath($model);
-        foreach ($this->namespaces as $key => $value) {
-            $this->xpath->registerNamespace($key, $value);
-        }            
-        $this->numberEntries = $this->count('item');
-    }
-
-    /**
-     * Included for compatibility -- will not work with RSS 0.9
-     *
-     * This is not something that will work with RSS0.9 as it does not have
-     * clear restrictions on the global uniqueness of IDs.
-     *
-     * @param    string    $id    any valid ID.
-     * @return    false
-     */
-    function getEntryById($id)
-    {
-        return false;        
-    }
-
-    /**
-     * Get details of the image associated with the feed.
-     *
-     * @return  array|false an array simply containing the child elements
-     */
-    protected function getImage()
-    {
-        $images = $this->model->getElementsByTagName('image');
-        if ($images->length > 0) {
-            $image = $images->item(0);
-            $details = array();
-            if ($image->hasChildNodes()) {
-                $details = array(
-                    'title' => $image->getElementsByTagName('title')->item(0)->value,
-                    'link' => $image->getElementsByTagName('link')->item(0)->value,
-                    'url' => $image->getElementsByTagName('url')->item(0)->value);
-            } else {
-                $details = array('title' => false,
-                    'link' => false,
-                    'url' => $image->attributes->getNamedItem('resource')->nodeValue);
-            }
-            $details = array_merge($details, 
-                array('description' => false, 'height' => false, 'width' => false));
-            if (! empty($details)) {
-                return $details;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * The textinput element is little used, but in the interests of
-     * completeness we will support it.
-     *
-     * @return  array|false
-     */
-    protected function getTextInput()
-    {
-        $inputs = $this->model->getElementsByTagName('textinput');
-        if ($inputs->length > 0) {
-            $input = $inputs->item(0);
-            $results = array();
-            $results['title'] = isset(
-                $input->getElementsByTagName('title')->item(0)->value) ? 
-                $input->getElementsByTagName('title')->item(0)->value : null;
-            $results['description'] = isset(
-                $input->getElementsByTagName('description')->item(0)->value) ? 
-                $input->getElementsByTagName('description')->item(0)->value : null;
-            $results['name'] = isset(
-                $input->getElementsByTagName('name')->item(0)->value) ? 
-                $input->getElementsByTagName('name')->item(0)->value : null;
-            $results['link'] = isset(
-                   $input->getElementsByTagName('link')->item(0)->value) ? 
-                   $input->getElementsByTagName('link')->item(0)->value : null;
-            if (empty($results['link']) && 
-                $input->attributes->getNamedItem('resource')) {
-                $results['link'] = $input->attributes->getNamedItem('resource')->nodeValue;
-            }
-            if (! empty($results)) {
-                return $results;
-            }
-        }
-        return false;
-    }
-    
-    /**
-     * Get details of a link from the feed.
-     *
-     * In RSS1 a link is a text element but in order to ensure that we resolve
-     * URLs properly we have a special function for them.
-     *
-     * @return  string
-     */
-    function getLink($offset = 0, $attribute = 'href', $params = false)
-    {
-        $links = $this->model->getElementsByTagName('link');
-        if ($links->length <= $offset) {
-            return false;
-        }
-        $link = $links->item($offset);
-        return $this->addBase($link->nodeValue, $link);
-    }
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php
deleted file mode 100755 (executable)
index d41f36e..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS0.9 Element class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
- * @version    CVS: $Id: RSS09Element.php,v 1.4 2006/06/30 17:41:56 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/*
- * This class provides support for RSS 0.9 entries. It will usually be called by
- * XML_Feed_Parser_RSS09 with which it shares many methods.
- *
- * @author    James Stewart <james@jystewart.net>
- * @version    Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_RSS09Element extends XML_Feed_Parser_RSS09
-{
-    /**
-     * This will be a reference to the parent object for when we want
-     * to use a 'fallback' rule 
-     * @var XML_Feed_Parser_RSS09
-     */
-    protected $parent;
-
-    /**
-     * Our specific element map 
-     * @var array
-     */
-    protected $map = array(
-        'title' => array('Text'),
-        'link' => array('Link'));
-
-    /**
-     * Store useful information for later.
-     *
-     * @param   DOMElement  $element - this item as a DOM element
-     * @param   XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
-     */
-    function __construct(DOMElement $element, $parent, $xmlBase = '')
-    {
-        $this->model = $element;
-        $this->parent = $parent;
-    }
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php
deleted file mode 100755 (executable)
index 60c9938..0000000
+++ /dev/null
@@ -1,277 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS1 class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
- * @version    CVS: $Id: RSS1.php,v 1.10 2006/07/27 13:52:05 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class handles RSS1.0 feeds.
- * 
- * @author    James Stewart <james@jystewart.net>
- * @version    Release: 1.0.3
- * @package XML_Feed_Parser
- * @todo    Find a Relax NG URI we can use
- */
-class XML_Feed_Parser_RSS1 extends XML_Feed_Parser_Type
-{
-    /**
-     * The URI of the RelaxNG schema used to (optionally) validate the feed 
-     * @var string
-     */
-    private $relax = 'rss10.rnc';
-
-    /**
-     * We're likely to use XPath, so let's keep it global
-     * @var DOMXPath
-     */
-    protected $xpath;
-
-    /**
-     * The feed type we are parsing 
-     * @var string
-     */
-    public $version = 'RSS 1.0';
-
-    /**
-     * The class used to represent individual items 
-     * @var string
-     */
-    protected $itemClass = 'XML_Feed_Parser_RSS1Element';
-    
-    /**
-     * The element containing entries 
-     * @var string
-     */
-    protected $itemElement = 'item';
-
-    /**
-     * Here we map those elements we're not going to handle individually
-     * to the constructs they are. The optional second parameter in the array
-     * tells the parser whether to 'fall back' (not apt. at the feed level) or
-     * fail if the element is missing. If the parameter is not set, the function
-     * will simply return false and leave it to the client to decide what to do.
-     * @var array
-     */
-    protected $map = array(
-        'title' => array('Text'),
-        'link' => array('Text'),
-        'description' => array('Text'),
-        'image' => array('Image'),
-        'textinput' => array('TextInput'),
-        'updatePeriod' => array('Text'),
-        'updateFrequency' => array('Text'),
-        'updateBase' => array('Date'),
-        'rights' => array('Text'), # dc:rights
-        'description' => array('Text'), # dc:description
-        'creator' => array('Text'), # dc:creator
-        'publisher' => array('Text'), # dc:publisher
-        'contributor' => array('Text'), # dc:contributor
-        'date' => array('Date') # dc:contributor
-        );
-
-    /**
-     * Here we map some elements to their atom equivalents. This is going to be
-     * quite tricky to pull off effectively (and some users' methods may vary)
-     * but is worth trying. The key is the atom version, the value is RSS2.
-     * @var array
-     */
-    protected $compatMap = array(
-        'title' => array('title'),
-        'link' => array('link'),
-        'subtitle' => array('description'),
-        'author' => array('creator'),
-        'updated' => array('date'));
-
-    /**
-     * We will be working with multiple namespaces and it is useful to 
-     * keep them together 
-     * @var array
-     */
-    protected $namespaces = array(
-        'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
-        'rss' => 'http://purl.org/rss/1.0/',
-        'dc' => 'http://purl.org/rss/1.0/modules/dc/',
-        'content' => 'http://purl.org/rss/1.0/modules/content/',
-        'sy' => 'http://web.resource.org/rss/1.0/modules/syndication/');
-
-    /**
-     * Our constructor does nothing more than its parent.
-     * 
-     * @param    DOMDocument    $xml    A DOM object representing the feed
-     * @param    bool (optional) $string    Whether or not to validate this feed
-     */
-    function __construct(DOMDocument $model, $strict = false)
-    {
-        $this->model = $model;
-        if ($strict) {
-            $validate = $this->model->relaxNGValidate(self::getSchemaDir . 
-                DIRECTORY_SEPARATOR . $this->relax);
-            if (! $validate) {
-                throw new XML_Feed_Parser_Exception('Failed required validation');
-            }
-        }
-
-        $this->xpath = new DOMXPath($model);
-        foreach ($this->namespaces as $key => $value) {
-            $this->xpath->registerNamespace($key, $value);
-        }
-        $this->numberEntries = $this->count('item');
-    }
-
-    /**
-     * Allows retrieval of an entry by ID where the rdf:about attribute is used
-     *
-     * This is not really something that will work with RSS1 as it does not have
-     * clear restrictions on the global uniqueness of IDs. We will employ the
-     * _very_ hit and miss method of selecting entries based on the rdf:about
-     * attribute. If DOMXPath::evaluate is available, we also use that to store 
-     * a reference to the entry in the array used by getEntryByOffset so that 
-     * method does not have to seek out the entry if it's requested that way.
-     *
-     * @param    string    $id    any valid ID.
-     * @return    XML_Feed_Parser_RSS1Element
-     */
-    function getEntryById($id)
-    {
-        if (isset($this->idMappings[$id])) {
-            return $this->entries[$this->idMappings[$id]];
-        }
-
-        $entries = $this->xpath->query("//rss:item[@rdf:about='$id']");
-        if ($entries->length > 0) {
-            $classname = $this->itemClass;
-            $entry = new $classname($entries->item(0), $this);
-            if (in_array('evaluate', get_class_methods($this->xpath))) {
-                $offset = $this->xpath->evaluate("count(preceding-sibling::rss:item)", $entries->item(0));
-                $this->entries[$offset] = $entry;
-            }
-            $this->idMappings[$id] = $entry;
-            return $entry;
-        }
-        return false;
-    }
-
-    /**
-     * Get details of the image associated with the feed.
-     *
-     * @return  array|false an array simply containing the child elements
-     */
-    protected function getImage()
-    {
-        $images = $this->model->getElementsByTagName('image');
-        if ($images->length > 0) {
-            $image = $images->item(0);
-            $details = array();
-            if ($image->hasChildNodes()) {
-                $details = array(
-                    'title' => $image->getElementsByTagName('title')->item(0)->value,
-                    'link' => $image->getElementsByTagName('link')->item(0)->value,
-                    'url' => $image->getElementsByTagName('url')->item(0)->value);
-            } else {
-                $details = array('title' => false,
-                    'link' => false,
-                    'url' => $image->attributes->getNamedItem('resource')->nodeValue);
-            }
-            $details = array_merge($details, array('description' => false, 'height' => false, 'width' => false));
-            if (! empty($details)) {
-                return $details;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * The textinput element is little used, but in the interests of
-     * completeness we will support it.
-     *
-     * @return  array|false
-     */
-    protected function getTextInput()
-    {
-        $inputs = $this->model->getElementsByTagName('textinput');
-        if ($inputs->length > 0) {
-            $input = $inputs->item(0);
-            $results = array();
-            $results['title'] = isset(
-                $input->getElementsByTagName('title')->item(0)->value) ? 
-                $input->getElementsByTagName('title')->item(0)->value : null;
-            $results['description'] = isset(
-                $input->getElementsByTagName('description')->item(0)->value) ? 
-                $input->getElementsByTagName('description')->item(0)->value : null;
-            $results['name'] = isset(
-                $input->getElementsByTagName('name')->item(0)->value) ? 
-                $input->getElementsByTagName('name')->item(0)->value : null;
-            $results['link'] = isset(
-                   $input->getElementsByTagName('link')->item(0)->value) ? 
-                   $input->getElementsByTagName('link')->item(0)->value : null;
-            if (empty($results['link']) and 
-                $input->attributes->getNamedItem('resource')) {
-                $results['link'] = 
-                    $input->attributes->getNamedItem('resource')->nodeValue;
-            }
-            if (! empty($results)) {
-                return $results;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Employs various techniques to identify the author
-     *
-     * Dublin Core provides the dc:creator, dc:contributor, and dc:publisher
-     * elements for defining authorship in RSS1. We will try each of those in
-     * turn in order to simulate the atom author element and will return it
-     * as text.
-     *
-     * @return  array|false
-     */
-    function getAuthor()
-    {
-        $options = array('creator', 'contributor', 'publisher');
-        foreach ($options as $element) {
-            $test = $this->model->getElementsByTagName($element);
-            if ($test->length > 0) {
-                return $test->item(0)->value;
-            }
-        }
-        return false;
-    }
-    
-    /**
-     * Retrieve a link
-     * 
-     * In RSS1 a link is a text element but in order to ensure that we resolve
-     * URLs properly we have a special function for them.
-     *
-     * @return  string
-     */
-    function getLink($offset = 0, $attribute = 'href', $params = false)
-    {
-        $links = $this->model->getElementsByTagName('link');
-        if ($links->length <= $offset) {
-            return false;
-        }
-        $link = $links->item($offset);
-        return $this->addBase($link->nodeValue, $link);
-    }
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php
deleted file mode 100755 (executable)
index 3cd1ef1..0000000
+++ /dev/null
@@ -1,276 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS1.1 class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
- * @version    CVS: $Id: RSS11.php,v 1.6 2006/07/27 13:52:05 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class handles RSS1.1 feeds. RSS1.1 is documented at:
- * http://inamidst.com/rss1.1/
- * 
- * @author    James Stewart <james@jystewart.net>
- * @version    Release: 1.0.3
- * @package XML_Feed_Parser
- * @todo    Support for RDF:List
- * @todo    Ensure xml:lang is accessible to users
- */
-class XML_Feed_Parser_RSS11 extends XML_Feed_Parser_Type
-{
-    /**
-     * The URI of the RelaxNG schema used to (optionally) validate the feed 
-     * @var string
-     */
-    private $relax = 'rss11.rnc';
-
-    /**
-     * We're likely to use XPath, so let's keep it global
-     * @var DOMXPath
-     */
-    protected $xpath;
-
-    /**
-     * The feed type we are parsing 
-     * @var string
-     */
-    public $version = 'RSS 1.0';
-
-    /**
-     * The class used to represent individual items 
-     * @var string
-     */
-    protected $itemClass = 'XML_Feed_Parser_RSS1Element';
-    
-    /**
-     * The element containing entries 
-     * @var string
-     */
-    protected $itemElement = 'item';
-
-    /**
-     * Here we map those elements we're not going to handle individually
-     * to the constructs they are. The optional second parameter in the array
-     * tells the parser whether to 'fall back' (not apt. at the feed level) or
-     * fail if the element is missing. If the parameter is not set, the function
-     * will simply return false and leave it to the client to decide what to do.
-     * @var array
-     */
-    protected $map = array(
-        'title' => array('Text'),
-        'link' => array('Text'),
-        'description' => array('Text'),
-        'image' => array('Image'),
-        'updatePeriod' => array('Text'),
-        'updateFrequency' => array('Text'),
-        'updateBase' => array('Date'),
-        'rights' => array('Text'), # dc:rights
-        'description' => array('Text'), # dc:description
-        'creator' => array('Text'), # dc:creator
-        'publisher' => array('Text'), # dc:publisher
-        'contributor' => array('Text'), # dc:contributor
-        'date' => array('Date') # dc:contributor
-        );
-
-    /**
-     * Here we map some elements to their atom equivalents. This is going to be
-     * quite tricky to pull off effectively (and some users' methods may vary)
-     * but is worth trying. The key is the atom version, the value is RSS2.
-     * @var array
-     */
-    protected $compatMap = array(
-        'title' => array('title'),
-        'link' => array('link'),
-        'subtitle' => array('description'),
-        'author' => array('creator'),
-        'updated' => array('date'));
-
-    /**
-     * We will be working with multiple namespaces and it is useful to 
-     * keep them together. We will retain support for some common RSS1.0 modules
-     * @var array
-     */
-    protected $namespaces = array(
-        'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
-        'rss' => 'http://purl.org/net/rss1.1#',
-        'dc' => 'http://purl.org/rss/1.0/modules/dc/',
-        'content' => 'http://purl.org/rss/1.0/modules/content/',
-        'sy' => 'http://web.resource.org/rss/1.0/modules/syndication/');
-
-    /**
-     * Our constructor does nothing more than its parent.
-     * 
-     * @param    DOMDocument    $xml    A DOM object representing the feed
-     * @param    bool (optional) $string    Whether or not to validate this feed
-     */
-    function __construct(DOMDocument $model, $strict = false)
-    {
-        $this->model = $model;
-
-        if ($strict) {
-            $validate = $this->model->relaxNGValidate(self::getSchemaDir . 
-                DIRECTORY_SEPARATOR . $this->relax);
-            if (! $validate) {
-                throw new XML_Feed_Parser_Exception('Failed required validation');
-            }
-        }
-
-        $this->xpath = new DOMXPath($model);
-        foreach ($this->namespaces as $key => $value) {
-            $this->xpath->registerNamespace($key, $value);
-        }            
-        $this->numberEntries = $this->count('item');
-    }
-
-    /**
-     * Attempts to identify an element by ID given by the rdf:about attribute
-     *
-     * This is not really something that will work with RSS1.1 as it does not have
-     * clear restrictions on the global uniqueness of IDs. We will employ the
-     * _very_ hit and miss method of selecting entries based on the rdf:about
-     * attribute. Please note that this is even more hit and miss with RSS1.1 than
-     * with RSS1.0 since RSS1.1 does not require the rdf:about attribute for items.
-     *
-     * @param    string    $id    any valid ID.
-     * @return    XML_Feed_Parser_RSS1Element
-     */
-    function getEntryById($id)
-    {
-        if (isset($this->idMappings[$id])) {
-            return $this->entries[$this->idMappings[$id]];
-        }
-
-        $entries = $this->xpath->query("//rss:item[@rdf:about='$id']");
-        if ($entries->length > 0) {
-            $classname = $this->itemClass;
-            $entry = new $classname($entries->item(0), $this);
-            return $entry;
-        }
-        return false;
-    }
-
-    /**
-     * Get details of the image associated with the feed.
-     *
-     * @return  array|false an array simply containing the child elements
-     */
-    protected function getImage()
-    {
-        $images = $this->model->getElementsByTagName('image');
-        if ($images->length > 0) {
-            $image = $images->item(0);
-            $details = array();
-            if ($image->hasChildNodes()) {
-                $details = array(
-                    'title' => $image->getElementsByTagName('title')->item(0)->value,
-                    'url' => $image->getElementsByTagName('url')->item(0)->value);
-                if ($image->getElementsByTagName('link')->length > 0) {
-                    $details['link'] = 
-                        $image->getElementsByTagName('link')->item(0)->value;
-                }
-            } else {
-                $details = array('title' => false,
-                    'link' => false,
-                    'url' => $image->attributes->getNamedItem('resource')->nodeValue);
-            }
-            $details = array_merge($details, 
-                array('description' => false, 'height' => false, 'width' => false));
-            if (! empty($details)) {
-                return $details;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * The textinput element is little used, but in the interests of
-     * completeness we will support it.
-     *
-     * @return  array|false
-     */
-    protected function getTextInput()
-    {
-        $inputs = $this->model->getElementsByTagName('textinput');
-        if ($inputs->length > 0) {
-            $input = $inputs->item(0);
-            $results = array();
-            $results['title'] = isset(
-                $input->getElementsByTagName('title')->item(0)->value) ? 
-                $input->getElementsByTagName('title')->item(0)->value : null;
-            $results['description'] = isset(
-                $input->getElementsByTagName('description')->item(0)->value) ? 
-                $input->getElementsByTagName('description')->item(0)->value : null;
-            $results['name'] = isset(
-                $input->getElementsByTagName('name')->item(0)->value) ? 
-                $input->getElementsByTagName('name')->item(0)->value : null;
-            $results['link'] = isset(
-                   $input->getElementsByTagName('link')->item(0)->value) ? 
-                   $input->getElementsByTagName('link')->item(0)->value : null;
-            if (empty($results['link']) and 
-                $input->attributes->getNamedItem('resource')) {
-                $results['link'] = $input->attributes->getNamedItem('resource')->nodeValue;
-            }
-            if (! empty($results)) {
-                return $results;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Attempts to discern authorship
-     *
-     * Dublin Core provides the dc:creator, dc:contributor, and dc:publisher
-     * elements for defining authorship in RSS1. We will try each of those in
-     * turn in order to simulate the atom author element and will return it
-     * as text.
-     *
-     * @return  array|false
-     */
-    function getAuthor()
-    {
-        $options = array('creator', 'contributor', 'publisher');
-        foreach ($options as $element) {
-            $test = $this->model->getElementsByTagName($element);
-            if ($test->length > 0) {
-                return $test->item(0)->value;
-            }
-        }
-        return false;
-    }
-    
-    /**
-     * Retrieve a link
-     *
-     * In RSS1 a link is a text element but in order to ensure that we resolve
-     * URLs properly we have a special function for them.
-     *
-     * @return  string
-     */
-    function getLink($offset = 0, $attribute = 'href', $params = false)
-    {
-        $links = $this->model->getElementsByTagName('link');
-        if ($links->length <= $offset) {
-            return false;
-        }
-        $link = $links->item($offset);
-        return $this->addBase($link->nodeValue, $link);
-    }
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php
deleted file mode 100755 (executable)
index 75918be..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS1 Element class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
- * @version    CVS: $Id: RSS11Element.php,v 1.4 2006/06/30 17:41:56 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/*
- * This class provides support for RSS 1.1 entries. It will usually be called by
- * XML_Feed_Parser_RSS11 with which it shares many methods.
- *
- * @author    James Stewart <james@jystewart.net>
- * @version    Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_RSS11Element extends XML_Feed_Parser_RSS11
-{
-    /**
-     * This will be a reference to the parent object for when we want
-     * to use a 'fallback' rule 
-     * @var XML_Feed_Parser_RSS1
-     */
-    protected $parent;
-
-    /**
-     * Our specific element map 
-     * @var array
-     */
-    protected $map = array(
-        'id' => array('Id'),
-        'title' => array('Text'),
-        'link' => array('Link'),
-        'description' => array('Text'), # or dc:description
-        'category' => array('Category'),
-        'rights' => array('Text'), # dc:rights
-        'creator' => array('Text'), # dc:creator
-        'publisher' => array('Text'), # dc:publisher
-        'contributor' => array('Text'), # dc:contributor
-        'date' => array('Date'), # dc:date
-        'content' => array('Content')
-        );
-
-    /**
-     * Here we map some elements to their atom equivalents. This is going to be
-     * quite tricky to pull off effectively (and some users' methods may vary)
-     * but is worth trying. The key is the atom version, the value is RSS1.
-     * @var array
-     */
-    protected $compatMap = array(
-        'content' => array('content'),
-        'updated' => array('lastBuildDate'),
-        'published' => array('pubdate'),
-        'subtitle' => array('description'),
-        'updated' => array('date'),
-        'author' => array('creator'),
-        'contributor' => array('contributor')
-    );
-
-    /**
-     * Store useful information for later.
-     *
-     * @param   DOMElement  $element - this item as a DOM element
-     * @param   XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
-     */
-    function __construct(DOMElement $element, $parent, $xmlBase = '')
-    {
-        $this->model = $element;
-        $this->parent = $parent;
-    }
-
-    /**
-     * If an rdf:about attribute is specified, return that as an ID
-     *
-     * There is no established way of showing an ID for an RSS1 entry. We will 
-     * simulate it using the rdf:about attribute of the entry element. This cannot
-     * be relied upon for unique IDs but may prove useful.
-     *
-     * @return  string|false
-     */
-    function getId()
-    {
-        if ($this->model->attributes->getNamedItem('about')) {
-            return $this->model->attributes->getNamedItem('about')->nodeValue;
-        }
-        return false;
-    }
-
-    /**
-     * Return the entry's content
-     *
-     * The official way to include full content in an RSS1 entry is to use
-     * the content module's element 'encoded'. Often, however, the 'description'
-     * element is used instead. We will offer that as a fallback.
-     *
-     * @return  string|false
-     */
-    function getContent()
-    {
-        $options = array('encoded', 'description');
-        foreach ($options as $element) {
-            $test = $this->model->getElementsByTagName($element);
-            if ($test->length == 0) {
-                continue;
-            }
-            if ($test->item(0)->hasChildNodes()) {
-                $value = '';
-                foreach ($test->item(0)->childNodes as $child) {
-                    if ($child instanceof DOMText) {
-                        $value .= $child->nodeValue;
-                    } else {
-                        $simple = simplexml_import_dom($child);
-                        $value .= $simple->asXML();
-                    }
-                }
-                return $value;
-            } else if ($test->length > 0) {
-                return $test->item(0)->nodeValue;
-            }
-        }
-        return false;
-    }
-    
-    /**
-     * How RSS1.1 should support for enclosures is not clear. For now we will return
-     * false.
-     *
-     * @return  false
-     */
-    function getEnclosure()
-    {
-        return false;
-    }
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php
deleted file mode 100755 (executable)
index 8e36d5a..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS1 Element class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
- * @version    CVS: $Id: RSS1Element.php,v 1.6 2006/06/30 17:41:56 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/*
- * This class provides support for RSS 1.0 entries. It will usually be called by
- * XML_Feed_Parser_RSS1 with which it shares many methods.
- *
- * @author    James Stewart <james@jystewart.net>
- * @version    Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_RSS1Element extends XML_Feed_Parser_RSS1
-{
-    /**
-     * This will be a reference to the parent object for when we want
-     * to use a 'fallback' rule 
-     * @var XML_Feed_Parser_RSS1
-     */
-    protected $parent;
-
-    /**
-     * Our specific element map 
-     * @var array
-     */
-    protected $map = array(
-        'id' => array('Id'),
-        'title' => array('Text'),
-        'link' => array('Link'),
-        'description' => array('Text'), # or dc:description
-        'category' => array('Category'),
-        'rights' => array('Text'), # dc:rights
-        'creator' => array('Text'), # dc:creator
-        'publisher' => array('Text'), # dc:publisher
-        'contributor' => array('Text'), # dc:contributor
-        'date' => array('Date'), # dc:date
-        'content' => array('Content')
-        );
-
-    /**
-     * Here we map some elements to their atom equivalents. This is going to be
-     * quite tricky to pull off effectively (and some users' methods may vary)
-     * but is worth trying. The key is the atom version, the value is RSS1.
-     * @var array
-     */
-    protected $compatMap = array(
-        'content' => array('content'),
-        'updated' => array('lastBuildDate'),
-        'published' => array('pubdate'),
-        'subtitle' => array('description'),
-        'updated' => array('date'),
-        'author' => array('creator'),
-        'contributor' => array('contributor')
-    );
-
-    /**
-     * Store useful information for later.
-     *
-     * @param   DOMElement  $element - this item as a DOM element
-     * @param   XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
-     */
-    function __construct(DOMElement $element, $parent, $xmlBase = '')
-    {
-        $this->model = $element;
-        $this->parent = $parent;
-    }
-
-    /**
-     * If an rdf:about attribute is specified, return it as an ID
-     *
-     * There is no established way of showing an ID for an RSS1 entry. We will 
-     * simulate it using the rdf:about attribute of the entry element. This cannot
-     * be relied upon for unique IDs but may prove useful.
-     *
-     * @return  string|false
-     */
-    function getId()
-    {
-        if ($this->model->attributes->getNamedItem('about')) {
-            return $this->model->attributes->getNamedItem('about')->nodeValue;
-        }
-        return false;
-    }
-
-    /**
-     * How RSS1 should support for enclosures is not clear. For now we will return
-     * false.
-     *
-     * @return  false
-     */
-    function getEnclosure()
-    {
-        return false;
-    }
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php
deleted file mode 100644 (file)
index 0936bd2..0000000
+++ /dev/null
@@ -1,335 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Class representing feed-level data for an RSS2 feed
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
- * @version    CVS: $Id: RSS2.php,v 1.12 2008/03/08 18:16:45 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class handles RSS2 feeds.
- * 
- * @author    James Stewart <james@jystewart.net>
- * @version    Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_RSS2 extends XML_Feed_Parser_Type
-{
-    /**
-     * The URI of the RelaxNG schema used to (optionally) validate the feed
-     * @var string
-     */
-    private $relax = 'rss20.rnc';
-
-    /**
-     * We're likely to use XPath, so let's keep it global
-     * @var DOMXPath
-     */
-    protected $xpath;
-
-    /**
-     * The feed type we are parsing
-     * @var string
-     */
-    public $version = 'RSS 2.0';
-
-    /**
-     * The class used to represent individual items
-     * @var string
-     */     
-    protected $itemClass = 'XML_Feed_Parser_RSS2Element';
-    
-    /**
-     * The element containing entries 
-     * @var string
-     */
-    protected $itemElement = 'item';
-
-    /**
-     * Here we map those elements we're not going to handle individually
-     * to the constructs they are. The optional second parameter in the array
-     * tells the parser whether to 'fall back' (not apt. at the feed level) or
-     * fail if the element is missing. If the parameter is not set, the function
-     * will simply return false and leave it to the client to decide what to do.
-     * @var array
-     */
-    protected $map = array(
-        'ttl' => array('Text'),
-        'pubDate' => array('Date'),
-        'lastBuildDate' => array('Date'),
-        'title' => array('Text'),
-        'link' => array('Link'),
-        'description' => array('Text'),
-        'language' => array('Text'),
-        'copyright' => array('Text'),
-        'managingEditor' => array('Text'),
-        'webMaster' => array('Text'),
-        'category' => array('Text'),
-        'generator' => array('Text'),
-        'docs' => array('Text'),
-        'ttl' => array('Text'),
-        'image' => array('Image'),
-        'skipDays' => array('skipDays'),
-        'skipHours' => array('skipHours'));
-
-    /**
-     * Here we map some elements to their atom equivalents. This is going to be
-     * quite tricky to pull off effectively (and some users' methods may vary)
-     * but is worth trying. The key is the atom version, the value is RSS2.
-     * @var array
-     */
-    protected $compatMap = array(
-        'title' => array('title'),
-        'rights' => array('copyright'),
-        'updated' => array('lastBuildDate'),
-        'subtitle' => array('description'),
-        'date' => array('pubDate'),
-        'author' => array('managingEditor'));
-
-    protected $namespaces = array(
-        'dc' => 'http://purl.org/rss/1.0/modules/dc/',
-        'content' => 'http://purl.org/rss/1.0/modules/content/');
-
-    /**
-     * Our constructor does nothing more than its parent.
-     * 
-     * @param    DOMDocument    $xml    A DOM object representing the feed
-     * @param    bool (optional) $string    Whether or not to validate this feed
-     */
-    function __construct(DOMDocument $model, $strict = false)
-    {
-        $this->model = $model;
-
-        if ($strict) {
-            if (! $this->model->relaxNGValidate($this->relax)) {
-                throw new XML_Feed_Parser_Exception('Failed required validation');
-            }
-        }
-
-        $this->xpath = new DOMXPath($this->model);
-        foreach ($this->namespaces as $key => $value) {
-            $this->xpath->registerNamespace($key, $value);
-        }
-        $this->numberEntries = $this->count('item');
-    }
-
-    /**
-     * Retrieves an entry by ID, if the ID is specified with the guid element
-     *
-     * This is not really something that will work with RSS2 as it does not have
-     * clear restrictions on the global uniqueness of IDs. But we can emulate
-     * it by allowing access based on the 'guid' element. If DOMXPath::evaluate
-     * is available, we also use that to store a reference to the entry in the array
-     * used by getEntryByOffset so that method does not have to seek out the entry
-     * if it's requested that way.
-     *
-     * @param    string    $id    any valid ID.
-     * @return    XML_Feed_Parser_RSS2Element
-     */
-    function getEntryById($id)
-    {
-        if (isset($this->idMappings[$id])) {
-            return $this->entries[$this->idMappings[$id]];
-        }
-
-        $entries = $this->xpath->query("//item[guid='$id']");
-        if ($entries->length > 0) {
-            $entry = new $this->itemElement($entries->item(0), $this);
-            if (in_array('evaluate', get_class_methods($this->xpath))) {
-                $offset = $this->xpath->evaluate("count(preceding-sibling::item)", $entries->item(0));
-                $this->entries[$offset] = $entry;
-            }
-            $this->idMappings[$id] = $entry;
-            return $entry;
-        }        
-    }
-
-    /**
-     * Get a category from the element
-     *
-     * The category element is a simple text construct which can occur any number
-     * of times. We allow access by offset or access to an array of results.
-     *
-     * @param    string    $call    for compatibility with our overloading
-     * @param   array $arguments - arg 0 is the offset, arg 1 is whether to return as array
-     * @return  string|array|false
-     */
-    function getCategory($call, $arguments = array())
-    {
-        $categories = $this->model->getElementsByTagName('category');
-        $offset = empty($arguments[0]) ? 0 : $arguments[0];
-        $array = empty($arguments[1]) ? false : true;
-        if ($categories->length <= $offset) {
-            return false;
-        }
-        if ($array) {
-            $list = array();
-            foreach ($categories as $category) {
-                array_push($list, $category->nodeValue);
-            }
-            return $list;
-        }
-        return $categories->item($offset)->nodeValue;
-    }
-
-    /**
-     * Get details of the image associated with the feed.
-     *
-     * @return  array|false an array simply containing the child elements
-     */
-    protected function getImage()
-    {
-        $images = $this->xpath->query("//image");
-        if ($images->length > 0) {
-            $image = $images->item(0);
-            $desc = $image->getElementsByTagName('description');
-            $description = $desc->length ? $desc->item(0)->nodeValue : false;
-            $heigh = $image->getElementsByTagName('height'); 
-            $height = $heigh->length ? $heigh->item(0)->nodeValue : false;
-            $widt = $image->getElementsByTagName('width'); 
-            $width = $widt->length ? $widt->item(0)->nodeValue : false;
-            return array(
-                'title' => $image->getElementsByTagName('title')->item(0)->nodeValue,
-                'link' => $image->getElementsByTagName('link')->item(0)->nodeValue,
-                'url' => $image->getElementsByTagName('url')->item(0)->nodeValue,
-                'description' => $description,
-                'height' => $height,
-                'width' => $width);
-        }
-        return false;
-    }
-
-    /**
-     * The textinput element is little used, but in the interests of
-     * completeness...
-     *
-     * @return  array|false
-     */
-    function getTextInput()
-    {
-        $inputs = $this->model->getElementsByTagName('input');
-        if ($inputs->length > 0) {
-            $input = $inputs->item(0);
-            return array(
-                'title' => $input->getElementsByTagName('title')->item(0)->value,
-                'description' => 
-                    $input->getElementsByTagName('description')->item(0)->value,
-                'name' => $input->getElementsByTagName('name')->item(0)->value,
-                'link' => $input->getElementsByTagName('link')->item(0)->value);
-        }
-        return false;
-    }
-
-    /**
-     * Utility function for getSkipDays and getSkipHours
-     *
-     * This is a general function used by both getSkipDays and getSkipHours. It simply
-     * returns an array of the values of the children of the appropriate tag.
-     *
-     * @param   string      $tagName    The tag name (getSkipDays or getSkipHours)
-     * @return  array|false
-     */
-    protected function getSkips($tagName)
-    {
-        $hours = $this->model->getElementsByTagName($tagName);
-        if ($hours->length == 0) {
-            return false;
-        }
-        $skipHours = array();
-        foreach($hours->item(0)->childNodes as $hour) {
-            if ($hour instanceof DOMElement) {
-                array_push($skipHours, $hour->nodeValue);
-            }
-        }
-        return $skipHours;
-    }
-
-    /**
-     * Retrieve skipHours data
-     *
-     * The skiphours element provides a list of hours on which this feed should
-     * not be checked. We return an array of those hours (integers, 24 hour clock)
-     *
-     * @return  array
-     */    
-    function getSkipHours()
-    {
-        return $this->getSkips('skipHours');
-    }
-
-    /**
-     * Retrieve skipDays data
-     *
-     * The skipdays element provides a list of days on which this feed should
-     * not be checked. We return an array of those days.
-     *
-     * @return  array
-     */
-    function getSkipDays()
-    {
-        return $this->getSkips('skipDays');
-    }
-
-    /**
-     * Return content of the little-used 'cloud' element
-     *
-     * The cloud element is rarely used. It is designed to provide some details
-     * of a location to update the feed.
-     *
-     * @return  array   an array of the attributes of the element
-     */
-    function getCloud()
-    {
-        $cloud = $this->model->getElementsByTagName('cloud');
-        if ($cloud->length == 0) {
-            return false;
-        }
-        $cloudData = array();
-        foreach ($cloud->item(0)->attributes as $attribute) {
-            $cloudData[$attribute->name] = $attribute->value;
-        }
-        return $cloudData;
-    }
-    
-    /**
-     * Get link URL
-     *
-     * In RSS2 a link is a text element but in order to ensure that we resolve
-     * URLs properly we have a special function for them. We maintain the 
-     * parameter used by the atom getLink method, though we only use the offset
-     * parameter.
-     *
-     * @param   int     $offset The position of the link within the feed. Starts from 0
-     * @param   string  $attribute  The attribute of the link element required
-     * @param   array   $params An array of other parameters. Not used.
-     * @return  string
-     */
-    function getLink($offset, $attribute = 'href', $params = array())
-    {
-        $xPath = new DOMXPath($this->model);
-        $links = $xPath->query('//link');
-
-        if ($links->length <= $offset) {
-            return false;
-        }
-        $link = $links->item($offset);
-        return $this->addBase($link->nodeValue, $link);
-    }
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php
deleted file mode 100755 (executable)
index 6edf910..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Class representing entries in an RSS2 feed.
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
- * @version    CVS: $Id: RSS2Element.php,v 1.11 2006/07/26 21:18:47 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class provides support for RSS 2.0 entries. It will usually be 
- * called by XML_Feed_Parser_RSS2 with which it shares many methods.
- *
- * @author    James Stewart <james@jystewart.net>
- * @version    Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_RSS2Element extends XML_Feed_Parser_RSS2
-{
-    /**
-     * This will be a reference to the parent object for when we want
-     * to use a 'fallback' rule
-     * @var XML_Feed_Parser_RSS2
-     */
-    protected $parent;
-
-    /**
-     * Our specific element map 
-     * @var array
-     */
-    protected $map = array(
-        'title' => array('Text'),
-        'guid' => array('Guid'),
-        'description' => array('Text'),
-        'author' => array('Text'),
-        'comments' => array('Text'),
-        'enclosure' => array('Enclosure'),
-        'pubDate' => array('Date'),
-        'source' => array('Source'),
-        'link' => array('Text'),
-        'content' => array('Content'));
-
-    /**
-     * Here we map some elements to their atom equivalents. This is going to be
-     * quite tricky to pull off effectively (and some users' methods may vary)
-     * but is worth trying. The key is the atom version, the value is RSS2.
-     * @var array
-     */
-    protected $compatMap = array(
-        'id' => array('guid'),
-        'updated' => array('lastBuildDate'),
-        'published' => array('pubdate'),
-        'guidislink' => array('guid', 'ispermalink'),
-        'summary' => array('description'));
-
-    /**
-     * Store useful information for later.
-     *
-     * @param   DOMElement  $element - this item as a DOM element
-     * @param   XML_Feed_Parser_RSS2    $parent - the feed of which this is a member
-     */
-    function __construct(DOMElement $element, $parent, $xmlBase = '')
-    {
-        $this->model = $element;
-        $this->parent = $parent;
-    }
-
-    /**
-     * Get the value of the guid element, if specified
-     *
-     * guid is the closest RSS2 has to atom's ID. It is usually but not always a
-     * URI. The one attribute that RSS2 can posess is 'ispermalink' which specifies
-     * whether the guid is itself dereferencable. Use of guid is not obligatory,
-     * but is advisable. To get the guid you would call $item->id() (for atom
-     * compatibility) or $item->guid(). To check if this guid is a permalink call
-     * $item->guid("ispermalink").
-     *
-     * @param   string  $method - the method name being called
-     * @param   array   $params - parameters required
-     * @return  string  the guid or value of ispermalink
-     */
-    protected function getGuid($method, $params)
-    {
-        $attribute = (isset($params[0]) and $params[0] == 'ispermalink') ? 
-            true : false;
-        $tag = $this->model->getElementsByTagName('guid');
-        if ($tag->length > 0) {
-            if ($attribute) {
-                if ($tag->hasAttribute("ispermalink")) {
-                    return $tag->getAttribute("ispermalink");
-                }
-            }
-            return $tag->item(0)->nodeValue;
-        }
-        return false;
-    }
-
-    /**
-     * Access details of file enclosures
-     *
-     * The RSS2 spec is ambiguous as to whether an enclosure element must be
-     * unique in a given entry. For now we will assume it needn't, and allow
-     * for an offset.
-     *
-     * @param   string $method - the method being called
-     * @param   array   $parameters - we expect the first of these to be our offset
-     * @return  array|false
-     */
-    protected function getEnclosure($method, $parameters)
-    {
-        $encs = $this->model->getElementsByTagName('enclosure');
-        $offset = isset($parameters[0]) ? $parameters[0] : 0;
-        if ($encs->length > $offset) {
-            try {
-                if (! $encs->item($offset)->hasAttribute('url')) {
-                    return false;
-                }
-                $attrs = $encs->item($offset)->attributes;
-                return array(
-                    'url' => $attrs->getNamedItem('url')->value,
-                    'length' => $attrs->getNamedItem('length')->value,
-                    'type' => $attrs->getNamedItem('type')->value);
-            } catch (Exception $e) {
-                return false;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Get the entry source if specified
-     *
-     * source is an optional sub-element of item. Like atom:source it tells
-     * us about where the entry came from (eg. if it's been copied from another
-     * feed). It is not a rich source of metadata in the same way as atom:source
-     * and while it would be good to maintain compatibility by returning an
-     * XML_Feed_Parser_RSS2 element, it makes a lot more sense to return an array.
-     *
-     * @return array|false
-     */
-    protected function getSource()
-    {
-        $get = $this->model->getElementsByTagName('source');
-        if ($get->length) {
-            $source = $get->item(0);
-            $array = array(
-                'content' => $source->nodeValue);
-            foreach ($source->attributes as $attribute) {
-                $array[$attribute->name] = $attribute->value;
-            }
-            return $array;
-        }
-        return false;
-    }
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/Type.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Type.php
deleted file mode 100644 (file)
index 7505261..0000000
+++ /dev/null
@@ -1,467 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Abstract class providing common methods for XML_Feed_Parser feeds.
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category   XML
- * @package    XML_Feed_Parser
- * @author     James Stewart <james@jystewart.net>
- * @copyright  2005 James Stewart <james@jystewart.net>
- * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
- * @version    CVS: $Id: Type.php,v 1.25 2008/03/08 18:39:09 jystewart Exp $
- * @link       http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This abstract class provides some general methods that are likely to be
- * implemented exactly the same way for all feed types.
- *
- * @package XML_Feed_Parser
- * @author  James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- */
-abstract class XML_Feed_Parser_Type
-{
-    /**
-     * Where we store our DOM object for this feed 
-     * @var DOMDocument
-     */
-    public $model;
-
-    /**
-     * For iteration we'll want a count of the number of entries 
-     * @var int
-     */
-    public $numberEntries;
-
-    /**
-     * Where we store our entry objects once instantiated 
-     * @var array
-     */
-    public $entries = array();
-
-    /**
-     * Store mappings between entry IDs and their position in the feed
-     */
-    public $idMappings = array();
-
-    /**
-     * Proxy to allow use of element names as method names
-     *
-     * We are not going to provide methods for every entry type so this
-     * function will allow for a lot of mapping. We rely pretty heavily
-     * on this to handle our mappings between other feed types and atom.
-     *
-     * @param   string  $call - the method attempted
-     * @param   array   $arguments - arguments to that method
-     * @return  mixed
-     */
-    function __call($call, $arguments = array())
-    {
-        if (! is_array($arguments)) {
-            $arguments = array();
-        }
-
-        if (isset($this->compatMap[$call])) {
-            $tempMap = $this->compatMap;
-            $tempcall = array_pop($tempMap[$call]);
-            if (! empty($tempMap)) {
-                $arguments = array_merge($arguments, $tempMap[$call]);
-            }
-            $call = $tempcall;
-        }
-
-        /* To be helpful, we allow a case-insensitive search for this method */
-        if (! isset($this->map[$call])) {
-            foreach (array_keys($this->map) as $key) {
-                if (strtoupper($key) == strtoupper($call)) {
-                    $call = $key;
-                    break;
-                }
-            }
-        }
-
-        if (empty($this->map[$call])) {
-            return false;
-        }
-
-        $method = 'get' . $this->map[$call][0];
-        if ($method == 'getLink') {
-            $offset = empty($arguments[0]) ? 0 : $arguments[0];
-            $attribute = empty($arguments[1]) ? 'href' : $arguments[1];
-            $params = isset($arguments[2]) ? $arguments[2] : array();
-            return $this->getLink($offset, $attribute, $params);
-        }
-        if (method_exists($this, $method)) {
-            return $this->$method($call, $arguments);
-        }
-
-        return false;
-    }
-
-    /**
-     * Proxy to allow use of element names as attribute names
-     *
-     * For many elements variable-style access will be desirable. This function
-     * provides for that.
-     *
-     * @param   string  $value - the variable required
-     * @return  mixed
-     */
-    function __get($value)
-    {
-        return $this->__call($value, array());
-    }
-
-    /**
-     * Utility function to help us resolve xml:base values
-     *
-     * We have other methods which will traverse the DOM and work out the different
-     * xml:base declarations we need to be aware of. We then need to combine them.
-     * If a declaration starts with a protocol then we restart the string. If it 
-     * starts with a / then we add on to the domain name. Otherwise we simply tag 
-     * it on to the end.
-     *
-     * @param   string  $base - the base to add the link to
-     * @param   string  $link
-     */
-    function combineBases($base, $link)
-    {
-        if (preg_match('/^[A-Za-z]+:\/\//', $link)) {
-            return $link;
-        } else if (preg_match('/^\//', $link)) {
-            /* Extract domain and suffix link to that */
-            preg_match('/^([A-Za-z]+:\/\/.*)?\/*/', $base, $results);
-            $firstLayer = $results[0];
-            return $firstLayer . "/" . $link;
-        } else if (preg_match('/^\.\.\//', $base)) {
-            /* Step up link to find place to be */
-            preg_match('/^((\.\.\/)+)(.*)$/', $link, $bases);
-            $suffix = $bases[3];
-            $count = preg_match_all('/\.\.\//', $bases[1], $steps);
-            $url = explode("/", $base);
-            for ($i = 0; $i <= $count; $i++) {
-                array_pop($url);
-            }
-            return implode("/", $url) . "/" . $suffix;
-        } else if (preg_match('/^(?!\/$)/', $base)) {
-            $base = preg_replace('/(.*\/).*$/', '$1', $base)  ;
-            return $base . $link;
-        } else {
-            /* Just stick it on the end */
-            return $base . $link;
-        }
-    }
-
-    /**
-     * Determine whether we need to apply our xml:base rules
-     *
-     * Gets us the xml:base data and then processes that with regard
-     * to our current link.
-     *
-     * @param   string
-     * @param   DOMElement
-     * @return  string
-     */
-    function addBase($link, $element)
-    {
-        if (preg_match('/^[A-Za-z]+:\/\//', $link)) {
-            return $link;
-        }
-
-        return $this->combineBases($element->baseURI, $link);
-    }
-
-    /**
-     * Get an entry by its position in the feed, starting from zero
-     *
-     * As well as allowing the items to be iterated over we want to allow
-     * users to be able to access a specific entry. This is one of two ways of
-     * doing that, the other being by ID.
-     * 
-     * @param   int $offset
-     * @return  XML_Feed_Parser_RSS1Element
-     */
-    function getEntryByOffset($offset)
-    {
-        if (! isset($this->entries[$offset])) {
-            $entries = $this->model->getElementsByTagName($this->itemElement);
-            if ($entries->length > $offset) {
-                $xmlBase = $entries->item($offset)->baseURI;
-                $this->entries[$offset] = new $this->itemClass(
-                    $entries->item($offset), $this, $xmlBase);
-                if ($id = $this->entries[$offset]->id) {
-                    $this->idMappings[$id] = $this->entries[$offset];
-                }
-            } else {
-                throw new XML_Feed_Parser_Exception('No entries found');
-            }
-        }
-
-        return $this->entries[$offset];
-    }
-
-    /**
-     * Return a date in seconds since epoch.
-     *
-     * Get a date construct. We use PHP's strtotime to return it as a unix datetime, which
-     * is the number of seconds since 1970-01-01 00:00:00.
-     * 
-     * @link    http://php.net/strtotime
-     * @param    string    $method        The name of the date construct we want
-     * @param    array     $arguments    Included for compatibility with our __call usage
-     * @return    int|false datetime
-     */
-    protected function getDate($method, $arguments)
-    {
-        $time = $this->model->getElementsByTagName($method);
-        if ($time->length == 0 || empty($time->item(0)->nodeValue)) {
-            return false;
-        }
-        return strtotime($time->item(0)->nodeValue);
-    }
-
-    /**
-     * Get a text construct. 
-     *
-     * @param    string    $method    The name of the text construct we want
-     * @param    array     $arguments    Included for compatibility with our __call usage
-     * @return    string
-     */
-    protected function getText($method, $arguments = array())
-    {
-        $tags = $this->model->getElementsByTagName($method);
-        if ($tags->length > 0) {
-            $value = $tags->item(0)->nodeValue;
-            return $value;
-        }
-        return false;
-    }
-
-    /**
-     * Apply various rules to retrieve category data.
-     *
-     * There is no single way of declaring a category in RSS1/1.1 as there is in RSS2 
-     * and  Atom. Instead the usual approach is to use the dublin core namespace to 
-     * declare  categories. For example delicious use both: 
-     * <dc:subject>PEAR</dc:subject> and: <taxo:topics><rdf:Bag>
-     * <rdf:li resource="http://del.icio.us/tag/PEAR" /></rdf:Bag></taxo:topics>
-     * to declare a categorisation of 'PEAR'.
-     *
-     * We need to be sensitive to this where possible.
-     *
-     * @param    string    $call    for compatibility with our overloading
-     * @param   array $arguments - arg 0 is the offset, arg 1 is whether to return as array
-     * @return  string|array|false
-     */
-    protected function getCategory($call, $arguments)
-    {
-        $categories = $this->model->getElementsByTagName('subject');
-        $offset = empty($arguments[0]) ? 0 : $arguments[0];
-        $array = empty($arguments[1]) ? false : true;
-        if ($categories->length <= $offset) {
-            return false;
-        }
-        if ($array) {
-            $list = array();
-            foreach ($categories as $category) {
-                array_push($list, $category->nodeValue);
-            }
-            return $list;
-        }
-        return $categories->item($offset)->nodeValue;
-    }
-
-    /**
-     * Count occurrences of an element
-     *
-     * This function will tell us how many times the element $type
-     * appears at this level of the feed.
-     * 
-     * @param    string    $type    the element we want to get a count of
-     * @return    int
-     */
-    protected function count($type)
-    {
-        if ($tags = $this->model->getElementsByTagName($type)) {
-            return $tags->length;
-        }
-        return 0;
-    }
-
-    /**
-     * Part of our xml:base processing code
-     *
-     * We need a couple of methods to access XHTML content stored in feeds. 
-     * This is because we dereference all xml:base references before returning
-     * the element. This method handles the attributes.
-     *
-     * @param   DOMElement $node    The DOM node we are iterating over
-     * @return  string
-     */
-    function processXHTMLAttributes($node) {
-        $return = '';
-        foreach ($node->attributes as $attribute) {
-            if ($attribute->name == 'src' or $attribute->name == 'href') {
-                $attribute->value = $this->addBase(htmlentities($attribute->value, NULL, 'utf-8'), $attribute);
-            }
-            if ($attribute->name == 'base') {
-                continue;
-            }
-            $return .= $attribute->name . '="' . htmlentities($attribute->value, NULL, 'utf-8') .'" ';
-        }
-        if (! empty($return)) {
-            return ' ' . trim($return);
-        }
-        return '';
-    }
-
-    /**
-     * Convert HTML entities based on the current character set.
-     * 
-     * @param String
-     * @return String
-     */
-    function processEntitiesForNodeValue($node) 
-    {
-        if (function_exists('iconv')) {
-          $current_encoding = $node->ownerDocument->encoding;
-          $value = iconv($current_encoding, 'UTF-8', $node->nodeValue);
-        } else if ($current_encoding == 'iso-8859-1') {
-          $value = utf8_encode($node->nodeValue);
-        } else {
-          $value = $node->nodeValue;
-        }
-
-        $decoded = html_entity_decode($value, NULL, 'UTF-8');
-        return htmlentities($decoded, NULL, 'UTF-8');
-    }
-
-    /**
-     * Part of our xml:base processing code
-     *
-     * We need a couple of methods to access XHTML content stored in feeds. 
-     * This is because we dereference all xml:base references before returning
-     * the element. This method recurs through the tree descending from the node
-     * and builds our string.
-     *
-     * @param   DOMElement $node    The DOM node we are processing
-     * @return   string
-     */
-    function traverseNode($node)
-    {
-        $content = '';
-
-        /* Add the opening of this node to the content */
-        if ($node instanceof DOMElement) {
-            $content .= '<' . $node->tagName . 
-                $this->processXHTMLAttributes($node) . '>';
-        }
-
-        /* Process children */
-        if ($node->hasChildNodes()) {
-            foreach ($node->childNodes as $child) {
-                $content .= $this->traverseNode($child);
-            }
-        }
-
-        if ($node instanceof DOMText) {
-            $content .= $this->processEntitiesForNodeValue($node);
-        }
-
-        /* Add the closing of this node to the content */
-        if ($node instanceof DOMElement) {
-            $content .= '</' . $node->tagName . '>';
-        }
-
-        return $content;
-    }
-
-    /**
-     * Get content from RSS feeds (atom has its own implementation)
-     *
-     * The official way to include full content in an RSS1 entry is to use
-     * the content module's element 'encoded', and RSS2 feeds often duplicate that.
-     * Often, however, the 'description' element is used instead. We will offer that 
-     * as a fallback. Atom uses its own approach and overrides this method.
-     *
-     * @return  string|false
-     */
-    protected function getContent()
-    {
-        $options = array('encoded', 'description');
-        foreach ($options as $element) {
-            $test = $this->model->getElementsByTagName($element);
-            if ($test->length == 0) {
-                continue;
-            }
-            if ($test->item(0)->hasChildNodes()) {
-                $value = '';
-                foreach ($test->item(0)->childNodes as $child) {
-                    if ($child instanceof DOMText) {
-                        $value .= $child->nodeValue;
-                    } else {
-                        $simple = simplexml_import_dom($child);
-                        $value .= $simple->asXML();
-                    }
-                }
-                return $value;
-            } else if ($test->length > 0) {
-                return $test->item(0)->nodeValue;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Checks if this element has a particular child element.
-     *
-     * @param   String
-     * @param   Integer
-     * @return  bool
-     **/
-    function hasKey($name, $offset = 0)
-    {
-        $search = $this->model->getElementsByTagName($name);
-        return $search->length > $offset;
-    }
-
-    /**
-     * Return an XML serialization of the feed, should it be required. Most 
-     * users however, will already have a serialization that they used when 
-     * instantiating the object.
-     *
-     * @return    string    XML serialization of element
-     */    
-    function __toString()
-    {
-        $simple = simplexml_import_dom($this->model);
-        return $simple->asXML();
-    }
-    
-    /**
-     * Get directory holding RNG schemas. Method is based on that 
-     * found in Contact_AddressBook.
-     *
-     * @return string PEAR data directory.
-     * @access public
-     * @static
-     */
-    static function getSchemaDir()
-    {
-        require_once 'PEAR/Config.php';
-        $config = new PEAR_Config;
-        return $config->get('data_dir') . '/XML_Feed_Parser/schemas';
-    }
-}
-
-?>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml
deleted file mode 100755 (executable)
index 02e1c58..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<entry xmlns="http://www.w3.org/2005/Atom">
-    <title>Atom draft-07 snapshot</title>
-    <link rel="alternate" type="text/html" 
-     href="http://example.org/2005/04/02/atom"/>
-    <link rel='enclosure' type="audio/mpeg" length="1337"
-     href="http://example.org/audio/ph34r_my_podcast.mp3"/>
-    <id>tag:example.org,2003:3.2397</id>
-    <updated>2005-07-10T12:29:29Z</updated>
-    <published>2003-12-13T08:29:29-04:00</published>
-    <author>
-      <name>Mark Pilgrim</name>
-      <uri>http://example.org/</uri>
-      <email>f8dy@example.com</email>
-    </author>
-    <contributor>
-      <name>Sam Ruby</name>
-    </contributor>
-    <contributor>
-      <name>Joe Gregorio</name>
-    </contributor>
-    <content type="xhtml" xml:lang="en" 
-     xml:base="http://diveintomark.org/">
-      <div xmlns="http://www.w3.org/1999/xhtml">
-        <p><i>[Update: The Atom draft is finished.]</i></p>
-      </div>
-    </content>
-  </entry>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml
deleted file mode 100755 (executable)
index d181d2b..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<feed xmlns="http://www.w3.org/2005/Atom">
-
- <title>Example Feed</title>
- <link href="http://example.org/"/>
- <updated>2003-12-13T18:30:02Z</updated>
- <author>
-   <name>John Doe</name>
- </author>
- <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
-
- <entry>
-   <title>Atom-Powered Robots Run Amok</title>
-   <link href="http://example.org/2003/12/13/atom03"/>
-   <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
-   <updated>2003-12-13T18:30:02Z</updated>
-   <summary>Some text.</summary>
- </entry>
-
-</feed>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml
deleted file mode 100755 (executable)
index 98abf9d..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-   <feed xmlns="http://www.w3.org/2005/Atom">
-     <title type="text">dive into mark</title>
-     <subtitle type="html">
-       A &lt;em&gt;lot&lt;/em&gt; of effort
-       went into making this effortless
-     </subtitle>
-     <updated>2005-07-31T12:29:29Z</updated>
-     <id>tag:example.org,2003:3</id>
-     <link rel="alternate" type="text/html"
-      hreflang="en" href="http://example.org/"/>
-     <link rel="self" type="application/atom+xml"
-      href="http://example.org/feed.atom"/>
-     <rights>Copyright (c) 2003, Mark Pilgrim</rights>
-     <generator uri="http://www.example.com/" version="1.0">
-       Example Toolkit
-     </generator>
-     <entry>
-       <title>Atom draft-07 snapshot</title>
-       <link rel="alternate" type="text/html"
-        href="http://example.org/2005/04/02/atom"/>
-       <link rel='enclosure' type="audio/mpeg" length="1337"
-        href="http://example.org/audio/ph34r_my_podcast.mp3"/>
-       <id>tag:example.org,2003:3.2397</id>
-       <updated>2005-07-31T12:29:29Z</updated>
-       <published>2003-12-13T08:29:29-04:00</published>
-       <author>
-         <name>Mark Pilgrim</name>
-         <uri>http://example.org/</uri>
-         <email>f8dy@example.com</email>
-       </author>
-       <contributor>
-         <name>Sam Ruby</name>
-       </contributor>
-       <contributor>
-         <name>Joe Gregorio</name>
-       </contributor>
-       <content type="xhtml" xml:lang="en"
-        xml:base="http://diveintomark.org/">
-         <div xmlns="http://www.w3.org/1999/xhtml">
-           <p><i>[Update: The Atom draft is finished.]</i></p>
-         </div>
-       </content>
-     </entry>
-   </feed>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed b/plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed
deleted file mode 100755 (executable)
index 32f9fa4..0000000
+++ /dev/null
@@ -1,177 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<rdf:RDF
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns="http://purl.org/rss/1.0/"
- xmlns:cc="http://web.resource.org/cc/"
- xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
- xmlns:admin="http://webns.net/mvcb/"
->
-<channel rdf:about="http://del.icio.us/tag/greenbelt">
-<title>del.icio.us/tag/greenbelt</title>
-<link>http://del.icio.us/tag/greenbelt</link>
-<description>Text</description>
-<items>
- <rdf:Seq>
-  <rdf:li rdf:resource="http://www.greenbelt.org.uk/" />
-  <rdf:li rdf:resource="http://www.greenbelt.org.uk/" />
-  <rdf:li rdf:resource="http://www.natuerlichwien.at/rundumadum/dergruenguertel/" />
-  <rdf:li rdf:resource="http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars" />
-  <rdf:li rdf:resource="http://www.greenbelt.ca/home.htm" />
-  <rdf:li rdf:resource="http://pipwilsonbhp.blogspot.com/" />
-  <rdf:li rdf:resource="http://maggidawn.typepad.com/maggidawn/" />
-  <rdf:li rdf:resource="http://www.johndavies.org/" />
-  <rdf:li rdf:resource="http://jonnybaker.blogs.com/" />
- </rdf:Seq>
-</items>
-</channel>
-
-<item rdf:about="http://www.greenbelt.org.uk/">
-<dc:title>Greenbelt - Homepage Section</dc:title>
-<link>http://www.greenbelt.org.uk/</link>
-<dc:creator>jonnybaker</dc:creator>
-<dc:date>2005-05-16T16:30:38Z</dc:date>
-<dc:subject>greenbelt</dc:subject>
-<taxo:topics>
-  <rdf:Bag>
-    <rdf:li resource="http://del.icio.us/tag/greenbelt" />
-  </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://www.greenbelt.org.uk/">
-<title>Greenbelt festival (uk)</title>
-<link>http://www.greenbelt.org.uk/</link>
-<dc:creator>sssshhhh</dc:creator>
-<dc:date>2005-05-14T18:19:40Z</dc:date>
-<dc:subject>audiology festival gigs greenbelt</dc:subject>
-<taxo:topics>
-  <rdf:Bag>
-    <rdf:li resource="http://del.icio.us/tag/gigs" />
-    <rdf:li resource="http://del.icio.us/tag/audiology" />
-    <rdf:li resource="http://del.icio.us/tag/festival" />
-    <rdf:li resource="http://del.icio.us/tag/greenbelt" />
-  </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://www.natuerlichwien.at/rundumadum/dergruenguertel/">
-<title>Natuerlichwien.at - Rundumadum</title>
-<link>http://www.natuerlichwien.at/rundumadum/dergruenguertel/</link>
-<dc:creator>egmilman47</dc:creator>
-<dc:date>2005-05-06T21:33:41Z</dc:date>
-<dc:subject>Austria Vienna Wien greenbelt nature walking</dc:subject>
-<taxo:topics>
-  <rdf:Bag>
-    <rdf:li resource="http://del.icio.us/tag/Vienna" />
-    <rdf:li resource="http://del.icio.us/tag/Wien" />
-    <rdf:li resource="http://del.icio.us/tag/Austria" />
-    <rdf:li resource="http://del.icio.us/tag/walking" />
-    <rdf:li resource="http://del.icio.us/tag/nature" />
-    <rdf:li resource="http://del.icio.us/tag/greenbelt" />
-  </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars">
-<title>Tank - GBMediaWiki</title>
-<link>http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars</link>
-<dc:creator>jystewart</dc:creator>
-<dc:date>2005-03-21T22:44:11Z</dc:date>
-<dc:subject>greenbelt</dc:subject>
-<taxo:topics>
-  <rdf:Bag>
-    <rdf:li resource="http://del.icio.us/tag/greenbelt" />
-  </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://www.greenbelt.ca/home.htm">
-<title>Greenbelt homepage</title>
-<link>http://www.greenbelt.ca/home.htm</link>
-<dc:creator>Gooberoo</dc:creator>
-<dc:date>2005-03-01T22:43:17Z</dc:date>
-<dc:subject>greenbelt ontario</dc:subject>
-<taxo:topics>
-  <rdf:Bag>
-    <rdf:li resource="http://del.icio.us/tag/ontario" />
-    <rdf:li resource="http://del.icio.us/tag/greenbelt" />
-  </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://pipwilsonbhp.blogspot.com/">
-<title>Pip Wilson bhp ...... blog</title>
-<link>http://pipwilsonbhp.blogspot.com/</link>
-<dc:creator>sssshhhh</dc:creator>
-<dc:date>2004-12-27T11:20:51Z</dc:date>
-<dc:subject>Greenbelt friend ideas links thinking weblog</dc:subject>
-<taxo:topics>
-  <rdf:Bag>
-    <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
-    <rdf:li resource="http://del.icio.us/tag/thinking" />
-    <rdf:li resource="http://del.icio.us/tag/ideas" />
-    <rdf:li resource="http://del.icio.us/tag/links" />
-    <rdf:li resource="http://del.icio.us/tag/friend" />
-    <rdf:li resource="http://del.icio.us/tag/weblog" />
-  </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://maggidawn.typepad.com/maggidawn/">
-<title>maggi dawn</title>
-<link>http://maggidawn.typepad.com/maggidawn/</link>
-<dc:creator>sssshhhh</dc:creator>
-<dc:date>2004-12-27T11:20:11Z</dc:date>
-<dc:subject>Greenbelt ideas links thinking weblog</dc:subject>
-<taxo:topics>
-  <rdf:Bag>
-    <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
-    <rdf:li resource="http://del.icio.us/tag/thinking" />
-    <rdf:li resource="http://del.icio.us/tag/ideas" />
-    <rdf:li resource="http://del.icio.us/tag/links" />
-    <rdf:li resource="http://del.icio.us/tag/weblog" />
-  </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://www.johndavies.org/">
-<title>John Davies</title>
-<link>http://www.johndavies.org/</link>
-<dc:creator>sssshhhh</dc:creator>
-<dc:date>2004-12-27T11:18:37Z</dc:date>
-<dc:subject>Greenbelt ideas links thinking weblog</dc:subject>
-<taxo:topics>
-  <rdf:Bag>
-    <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
-    <rdf:li resource="http://del.icio.us/tag/thinking" />
-    <rdf:li resource="http://del.icio.us/tag/ideas" />
-    <rdf:li resource="http://del.icio.us/tag/links" />
-    <rdf:li resource="http://del.icio.us/tag/weblog" />
-  </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://jonnybaker.blogs.com/">
-<title>jonnybaker</title>
-<link>http://jonnybaker.blogs.com/</link>
-<dc:creator>sssshhhh</dc:creator>
-<dc:date>2004-12-27T11:18:17Z</dc:date>
-<dc:subject>Greenbelt event ideas links resources thinking weblog youth</dc:subject>
-<taxo:topics>
-  <rdf:Bag>
-    <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
-    <rdf:li resource="http://del.icio.us/tag/thinking" />
-    <rdf:li resource="http://del.icio.us/tag/ideas" />
-    <rdf:li resource="http://del.icio.us/tag/links" />
-    <rdf:li resource="http://del.icio.us/tag/weblog" />
-    <rdf:li resource="http://del.icio.us/tag/youth" />
-    <rdf:li resource="http://del.icio.us/tag/event" />
-    <rdf:li resource="http://del.icio.us/tag/resources" />
-  </rdf:Bag>
-</taxo:topics>
-</item>
-
-</rdf:RDF>
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed b/plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed
deleted file mode 100755 (executable)
index 57e83af..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r
-<feed version="0.3" xmlns="http://purl.org/atom/ns#" \r
-    xmlns:dc="http://purl.org/dc/elements/1.1/">\r
-\r
-       <title>jamesstewart - Everyone's Tagged Photos</title>\r
-       <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/tags/jamesstewart/"/>\r
-       <link rel="icon" type="image/jpeg" href="http://www.flickr.com/images/buddyicon.jpg"/>\r
-       <info type="text/html" mode="escaped">A feed of jamesstewart - Everyone's Tagged Photos</info>\r
-       <modified>2005-08-01T18:50:26Z</modified>\r
-       <generator url="http://www.flickr.com/">Flickr</generator>\r
-\r
-       <entry>\r
-               <title>Oma and James</title>\r
-               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/30484029@N00/30367516/"/>\r
-               <link rel='enclosure' type="application/xml" href="http://james.anthropiccollective.org" />\r
-               <id>tag:flickr.com,2004:/photo/30367516</id>\r
-               <issued>2005-08-01T18:50:26Z</issued>\r
-               <modified>2005-08-01T18:50:26Z</modified>\r
-               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/30484029@N00/&quot;&gt;kstewart&lt;/a&gt; posted a photo:&lt;/p&gt;\r
-\r
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/30484029@N00/30367516/&quot; title=&quot;Oma and James&quot;&gt;&lt;img src=&quot;http://photos23.flickr.com/30367516_1f685a16e8_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;Oma and James&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
-\r
-&lt;p&gt;I have a beautiful Oma and a gorgeous husband.&lt;/p&gt;</content>\r
-               <author>\r
-                       <name>kstewart</name>\r
-                       <url>http://www.flickr.com/people/30484029@N00/</url>\r
-               </author>\r
-                               <dc:subject>jamesstewart oma stoelfamily</dc:subject>\r
-       </entry>\r
-       <entry>\r
-               <title></title>\r
-               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21376174/"/>\r
-               <id>tag:flickr.com,2004:/photo/21376174</id>\r
-               <issued>2005-06-25T02:00:35Z</issued>\r
-               <modified>2005-06-25T02:00:35Z</modified>\r
-               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/buddscreek/&quot;&gt;Lan Rover&lt;/a&gt; posted a photo:&lt;/p&gt;\r
-\r
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/buddscreek/21376174/&quot; title=&quot;&quot;&gt;&lt;img src=&quot;http://photos17.flickr.com/21376174_4314fd8d5c_m.jpg&quot; width=&quot;240&quot; height=&quot;160&quot; alt=&quot;&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
-\r
-&lt;p&gt;AMA Motocross Championship 2005, Budds Creek, Maryland&lt;/p&gt;</content>\r
-               <author>\r
-                       <name>Lan Rover</name>\r
-                       <url>http://www.flickr.com/people/buddscreek/</url>\r
-               </author>\r
-                               <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational rickycarmichael 259 jamesstewart 4</dc:subject>\r
-       </entry>\r
-       <entry>\r
-               <title></title>\r
-               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21375650/"/>\r
-               <id>tag:flickr.com,2004:/photo/21375650</id>\r
-               <issued>2005-06-25T01:56:24Z</issued>\r
-               <modified>2005-06-25T01:56:24Z</modified>\r
-               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/buddscreek/&quot;&gt;Lan Rover&lt;/a&gt; posted a photo:&lt;/p&gt;\r
-\r
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/buddscreek/21375650/&quot; title=&quot;&quot;&gt;&lt;img src=&quot;http://photos16.flickr.com/21375650_5c60e0dab1_m.jpg&quot; width=&quot;240&quot; height=&quot;160&quot; alt=&quot;&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
-\r
-</content>\r
-               <author>\r
-                       <name>Lan Rover</name>\r
-                       <url>http://www.flickr.com/people/buddscreek/</url>\r
-               </author>\r
-                               <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart</dc:subject>\r
-       </entry>\r
-       <entry>\r
-               <title></title>\r
-               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21375345/"/>\r
-               <id>tag:flickr.com,2004:/photo/21375345</id>\r
-               <issued>2005-06-25T01:54:11Z</issued>\r
-               <modified>2005-06-25T01:54:11Z</modified>\r
-               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/buddscreek/&quot;&gt;Lan Rover&lt;/a&gt; posted a photo:&lt;/p&gt;\r
-\r
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/buddscreek/21375345/&quot; title=&quot;&quot;&gt;&lt;img src=&quot;http://photos15.flickr.com/21375345_4205fdd22b_m.jpg&quot; width=&quot;160&quot; height=&quot;240&quot; alt=&quot;&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
-\r
-</content>\r
-               <author>\r
-                       <name>Lan Rover</name>\r
-                       <url>http://www.flickr.com/people/buddscreek/</url>\r
-               </author>\r
-                               <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart</dc:subject>\r
-       </entry>\r
-       <entry>\r
-               <title>Lunch with Kari &amp; James, café in the crypt of St Martin in the fields</title>\r
-               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/16516618/"/>\r
-               <id>tag:flickr.com,2004:/photo/16516618</id>\r
-               <issued>2005-05-30T21:56:39Z</issued>\r
-               <modified>2005-05-30T21:56:39Z</modified>\r
-               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;\r
-\r
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/16516618/&quot; title=&quot;Lunch with Kari &amp;amp; James, café in the crypt of St Martin in the fields&quot;&gt;&lt;img src=&quot;http://photos14.flickr.com/16516618_afaa4a395e_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;Lunch with Kari &amp;amp; James, café in the crypt of St Martin in the fields&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
-\r
-</content>\r
-               <author>\r
-                       <name>fidothe</name>\r
-                       <url>http://www.flickr.com/people/fidothe/</url>\r
-               </author>\r
-                               <dc:subject>nokia7610 london stmartininthefields clarepatterson jamesstewart parvinstewart jimstewart susanstewart</dc:subject>\r
-       </entry>\r
-       <entry>\r
-               <title>Stewart keeping it low over the obstacle.</title>\r
-               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/pqbon/10224728/"/>\r
-               <id>tag:flickr.com,2004:/photo/10224728</id>\r
-               <issued>2005-04-21T07:30:29Z</issued>\r
-               <modified>2005-04-21T07:30:29Z</modified>\r
-               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/pqbon/&quot;&gt;pqbon&lt;/a&gt; posted a photo:&lt;/p&gt;\r
-\r
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/pqbon/10224728/&quot; title=&quot;Stewart keeping it low over the obstacle.&quot;&gt;&lt;img src=&quot;http://photos7.flickr.com/10224728_b756341957_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;Stewart keeping it low over the obstacle.&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
-\r
-</content>\r
-               <author>\r
-                       <name>pqbon</name>\r
-                       <url>http://www.flickr.com/people/pqbon/</url>\r
-               </author>\r
-                               <dc:subject>ama hangtown motocross jamesstewart bubba</dc:subject>\r
-       </entry>\r
-       <entry>\r
-               <title>king james stewart</title>\r
-               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/jjlook/7152910/"/>\r
-               <id>tag:flickr.com,2004:/photo/7152910</id>\r
-               <issued>2005-03-22T21:53:37Z</issued>\r
-               <modified>2005-03-22T21:53:37Z</modified>\r
-               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/jjlook/&quot;&gt;jj look&lt;/a&gt; posted a photo:&lt;/p&gt;\r
-\r
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/jjlook/7152910/&quot; title=&quot;king james stewart&quot;&gt;&lt;img src=&quot;http://photos7.flickr.com/7152910_a02ab5a750_m.jpg&quot; width=&quot;180&quot; height=&quot;240&quot; alt=&quot;king james stewart&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
-\r
-&lt;p&gt;11th&lt;/p&gt;</content>\r
-               <author>\r
-                       <name>jj look</name>\r
-                       <url>http://www.flickr.com/people/jjlook/</url>\r
-               </author>\r
-                               <dc:subject>dilomar05 eastside austin texas 78702 kingjames stewart jamesstewart borrowed</dc:subject>\r
-       </entry>\r
-       <entry>\r
-               <title>It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)</title>\r
-               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586562/"/>\r
-               <id>tag:flickr.com,2004:/photo/1586562</id>\r
-               <issued>2004-11-20T09:34:28Z</issued>\r
-               <modified>2004-11-20T09:34:28Z</modified>\r
-               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;\r
-\r
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/1586562/&quot; title=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot;&gt;&lt;img src=&quot;http://photos2.flickr.com/1586562_0bc5313a3e_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
-\r
-</content>\r
-               <author>\r
-                       <name>fidothe</name>\r
-                       <url>http://www.flickr.com/people/fidothe/</url>\r
-               </author>\r
-                               <dc:subject>holiday grandrapids jamesstewart</dc:subject>\r
-       </entry>\r
-       <entry>\r
-               <title>It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)</title>\r
-               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586539/"/>\r
-               <id>tag:flickr.com,2004:/photo/1586539</id>\r
-               <issued>2004-11-20T09:28:16Z</issued>\r
-               <modified>2004-11-20T09:28:16Z</modified>\r
-               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;\r
-\r
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/1586539/&quot; title=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot;&gt;&lt;img src=&quot;http://photos2.flickr.com/1586539_c51e5f2e7a_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
-\r
-</content>\r
-               <author>\r
-                       <name>fidothe</name>\r
-                       <url>http://www.flickr.com/people/fidothe/</url>\r
-               </author>\r
-                               <dc:subject>holiday grandrapids jamesstewart</dc:subject>\r
-       </entry>\r
-       <entry>\r
-               <title>It's a Grind, James and Jim can't decide)</title>\r
-               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586514/"/>\r
-               <id>tag:flickr.com,2004:/photo/1586514</id>\r
-               <issued>2004-11-20T09:25:05Z</issued>\r
-               <modified>2004-11-20T09:25:05Z</modified>\r
-               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;\r
-\r
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/1586514/&quot; title=&quot;It's a Grind, James and Jim can't decide)&quot;&gt;&lt;img src=&quot;http://photos2.flickr.com/1586514_733c2dfa3e_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;It's a Grind, James and Jim can't decide)&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
-\r
-</content>\r
-               <author>\r
-                       <name>fidothe</name>\r
-                       <url>http://www.flickr.com/people/fidothe/</url>\r
-               </author>\r
-                               <dc:subject>holiday grandrapids jamesstewart johnkentish</dc:subject>\r
-       </entry>\r
-\r
-</feed>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml b/plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml
deleted file mode 100755 (executable)
index c351d3c..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="iso-8859-1"?>\r<feed xmlns="http://www.w3.org/2005/Atom"\r xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en">\r<title>Updates to Grand Rapids WiFi hotspot details</title>\r<link rel="alternate" type="text/html" href="http://grwifi.net/"/>\r<link rel="self" type="application/atom+xml" href="http://grwifi.net/atom/locations"/>\r<updated>2005-09-01T15:43:01-05:00</updated>\r<subtitle>WiFi Hotspots in Grand Rapids, MI</subtitle>\r<id>http://grwifi.net/atom/locations</id>\r<rights>Creative Commons Attribution-NonCommercial-ShareAlike 2.0 http://creativecommons.org/licenses/by-nc-sa/2.0/ </rights>\r\r\r<entry>\r    <title>Hotspot Details Updated: Sweetwaters</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/sweetwaters"/>\r    <id>http://grwifi.net/location/sweetwaters</id>\r    <updated>2005-09-01T15:43:01-05:00</updated>\r\r        <summary type="html">\r          The details of the WiFi hotspot at: Sweetwaters have been updated. Find out more at:
-http://grwifi.net/location/sweetwaters\r        </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r\r<entry>\r    <title>Hotspot Details Updated: Common Ground Coffee Shop</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/common-ground"/>\r    <id>http://grwifi.net/location/common-ground</id>\r    <updated>2005-09-01T15:42:39-05:00</updated>\r\r     <summary type="html">\r          The details of the WiFi hotspot at: Common Ground Coffee Shop have been updated. Find out more at:
-http://grwifi.net/location/common-ground\r      </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r\r<entry>\r    <title>Hotspot Details Updated: Grand Rapids Public Library, Main Branch</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/grpl-main-branch"/>\r    <id>http://grwifi.net/location/grpl-main-branch</id>\r    <updated>2005-09-01T15:42:20-05:00</updated>\r\r        <summary type="html">\r          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\r   </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r\r<entry>\r    <title>Hotspot Details Updated: Four Friends Coffee House</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/four-friends"/>\r    <id>http://grwifi.net/location/four-friends</id>\r    <updated>2005-09-01T15:41:35-05:00</updated>\r\r       <summary type="html">\r          The details of the WiFi hotspot at: Four Friends Coffee House have been updated. Find out more at:
-http://grwifi.net/location/four-friends\r       </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r\r<entry>\r    <title>Hotspot Details Updated: Barnes and Noble, Rivertown Crossings</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/barnes-noble-rivertown"/>\r    <id>http://grwifi.net/location/barnes-noble-rivertown</id>\r    <updated>2005-09-01T15:40:41-05:00</updated>\r\r       <summary type="html">\r          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\r     </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r\r<entry>\r    <title>Hotspot Details Updated: The Boss Sports Bar &amp; Grille</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/boss-sports-bar"/>\r    <id>http://grwifi.net/location/boss-sports-bar</id>\r    <updated>2005-09-01T15:40:19-05:00</updated>\r\r  <summary type="html">\r          The details of the WiFi hotspot at: The Boss Sports Bar &amp; Grille have been updated. Find out more at:
-http://grwifi.net/location/boss-sports-bar\r    </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r</feed>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml b/plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml
deleted file mode 100755 (executable)
index 0994635..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<rss version="2.0" 
-  xmlns:dc="http://purl.org/dc/elements/1.1/"
-  xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
-  xmlns:admin="http://webns.net/mvcb/"
-  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
-
-<channel>
-<title>Editor: Myself (Persian)</title>
-<link>http://editormyself.info</link>
-<description>This is a Persian (Farsi) weblog, written by Hossein Derakhshan (aka, Hoder), an Iranian Multimedia designer and a journalist who lives in Toronto since Dec 2000. He also keeps an English weblog with the same name.</description>
-<dc:language>en-us</dc:language>
-<dc:creator>hoder@hotmail.com</dc:creator>
-<dc:date>2005-10-12T19:45:32-05:00</dc:date>
-<admin:generatorAgent rdf:resource="http://www.movabletype.org/?v=3.15" />
-<sy:updatePeriod>hourly</sy:updatePeriod>
-<sy:updateFrequency>1</sy:updateFrequency>
-<sy:updateBase>2000-01-01T12:00+00:00</sy:updateBase>
-
-
-<item>
-<title>لينکدونی‌ | جلسه‌ی امریکن انترپرایز برای تقسیم قومی ایران</title>
-<link>http://www.aei.org/events/type.upcoming,eventID.1166,filter.all/event_detail.asp</link>
-<description>چطور بعضی‌ها فکر می‌کنند دست راستی‌های آمریکا از خامنه‌ای ملی‌گراترند</description>
-<guid isPermaLink="false">14645@http://i.hoder.com/</guid>
-<dc:subject>iran</dc:subject>
-<dc:date>2005-10-12T19:45:32-05:00</dc:date>
-</item>
-
-<item>
-<title>لينکدونی‌ | به صبحانه آگهی بدهید</title>
-<link>http://www.adbrite.com/mb/commerce/purchase_form.php?opid=24346&amp;afsid=1</link>
-<description>خیلی ارزان و راحت است</description>
-<guid isPermaLink="false">14644@http://i.hoder.com/</guid>
-<dc:subject>media/journalism</dc:subject>
-<dc:date>2005-10-12T17:23:15-05:00</dc:date>
-</item>
-
-<item>
-<title>لينکدونی‌ | نیروی انتظامی چگونه تابوهای هم‌جنس‌گرایانه را می‌شکند؛ فرنگوپولیس</title>
-<link>http://farangeopolis.blogspot.com/2005/10/blog-post_08.html</link>
-<description>از پس و پیش و حاشیه‌ی این ماجرا می‌توان یک مستند بی‌نظیر ساخت</description>
-<guid isPermaLink="false">14643@http://i.hoder.com/</guid>
-<dc:subject>soc_popculture</dc:subject>
-<dc:date>2005-10-12T17:06:40-05:00</dc:date>
-</item>
-
-<item>
-<title>لينکدونی‌ | بازتاب توقیف شد</title>
-<link>http://www.baztab.com/news/30201.php</link>
-<description>اگر گفتید یک وب‌سایت را چطور توقیف می‌کنند؟ لابد ماوس‌شان را قایم می‌کنند.</description>
-<guid isPermaLink="false">14642@http://i.hoder.com/</guid>
-<dc:subject>media/journalism</dc:subject>
-<dc:date>2005-10-12T14:41:57-05:00</dc:date>
-</item>
-
-<item>
-<title>لينکدونی‌ | رشد وب در سال 2005 از همیشه بیشتر بوده است&quot; بی.بی.سی</title>
-<link>http://news.bbc.co.uk/2/hi/technology/4325918.stm</link>
-<description></description>
-<guid isPermaLink="false">14640@http://i.hoder.com/</guid>
-<dc:subject>tech</dc:subject>
-<dc:date>2005-10-12T13:04:46-05:00</dc:date>
-</item>
-
-
-
-<item>
-<title>==قرعه کشی گرین کارد به زودی شروع می‌شود==</title>
-<link>http://nice.newsxphotos.biz/05/09/2007_dv_lottery_registration_to_begin_oct_5_14589.php</link>
-<description></description>
-<guid isPermaLink="false">14613@http://vagrantly.com</guid>
-<dc:subject>ads03</dc:subject>
-<dc:date>2005-09-27T04:49:22-05:00</dc:date>
-</item>
-
-
-
-
-
-
-<item>
-<title>پروژه‌ی هاروارد، قدم دوم</title>
-<link>http://editormyself.info/archives/2005/10/051012_014641.shtml</link>
-<description><![CDATA[<p>اگر یادتان باشد <a href="/archives/2005/09/050906_014504.shtml">چند وقت پیش نوشتم</a> که دانشگاه هاروارد پروژه‌ای دارد با نام آواهای جهانی که در آن به وبلاگ‌های غیر انگلیسی‌زبان می‌پردازد. خواشتم که اگر کسی علاقه دارد ایمیل بزند. تعداد زیادی جواب دادند و ابراز علاقه کردند. حالا وقت قدم دوم است.</p>
-
-<p>قدم دوم این است که برای اینکه مسوولین پروژه بتوانند تصمیم بگیرند که با چه کسی کار کنند، می‌خواهند نمونه‌ی کارهای علاقمندان مشارکت در این پرزو‌ه را ببینند.</p>
-
-<p>برای همین از همه‌ی علاقماندان، حتی کسانی که قبلا اعلام آمادگی نکرده بودند، می‌‌خواهم که یک موضوع رایج این روزهای وبلاگستان فارسی را انتخاب کنند و در هفتصد کلمه، به انگلیسی، بنویسند که وبلاگ‌دارهای درباره‌اش چه می‌گویند. لینک به پنج، شش وبلاگ و بازنویسی آنچه آنها از جنبه‌های گوناگون درباره‌ی آن موضوع نوشته‌اند با نقل قول مستقیم از آنها (البته ترجمه شده از فارسی) کافی است. دو سه جمله هم اول کار توضیح دهید که چرا این موضوع مهم است.</p>
-
-<p>متن نمونه را به آدرس ایمیل من hoder@hoder.com و نیز برای افراد زیر تا روز دوشنبه بفرستید:<br />
-ربکا : rmackinnon@cyber.law.harvard.edu<br />
-هیثم: haitham.sabbah@gmail.com</p>]]></description>
-<guid isPermaLink="false">14641@http://editormyself.info</guid>
-<dc:subject>weblog</dc:subject>
-<dc:date>2005-10-12T14:04:23-05:00</dc:date>
-</item>
-
-
-
-</channel>
-</rss>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml b/plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml
deleted file mode 100755 (executable)
index 6121868..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<!--\r
-Description: entry author name\r
-Expect:      bozo and entries[0]['author_detail']['name'] == u'Example author'\r
--->\r
-<feed xmlns="http://www.w3.org/2005/Atom">\r
-<entry>\r
-  <author>\r
-    <name>Example author</name>\r
-    <email>me@example.com</email>\r
-    <uri>http://example.com/</uri>\r
-  </author>\r
-</entry>\r
-</feed
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml
deleted file mode 100755 (executable)
index b0a1fee..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd">
-<rss version="0.91">
-<channel>
-<copyright>Copyright 1997-1999 UserLand Software, Inc.</copyright>
-<pubDate>Thu, 08 Jul 1999 07:00:00 GMT</pubDate>
-<lastBuildDate>Thu, 08 Jul 1999 16:20:26 GMT</lastBuildDate>
-<docs>http://my.userland.com/stories/storyReader$11</docs>
-<description>News and commentary from the cross-platform scripting community.</description>
-<link>http://www.scripting.com/</link>
-<title>Scripting News</title>
-<image>
-<link>http://www.scripting.com/</link>
-<title>Scripting News</title>
-<url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
-<height>40</height>
-<width>78</width>
-<description>What is this used for?</description>
-</image>
-<managingEditor>dave@userland.com (Dave Winer)</managingEditor>
-<webMaster>dave@userland.com (Dave Winer)</webMaster>
-<language>en-us</language>
-<skipHours>
-<hour>6</hour>
-<hour>7</hour>
-<hour>8</hour>
-<hour>9</hour>
-<hour>10</hour>
-<hour>11</hour>
-</skipHours>
-<skipDays>
-<day>Sunday</day>
-</skipDays>
-<rating>(PICS-1.1 "http://www.rsac.org/ratingsv01.html" l gen true comment "RSACi North America Server" for "http://www.rsac.org" on "1996.04.16T08:15-0500" r (n 0 s 0 v 0 l 0))</rating>
-<item>
-<title>stuff</title>
-<link>http://bar</link>
-<description>This is an article about some stuff</description>
-</item>
-<textinput>
-<title>Search Now!</title>
-<description>Enter your search &lt;terms&gt;</description>
-<name>find</name>
-<link>http://my.site.com/search.cgi</link>
-</textinput>
-</channel>
-</rss>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml
deleted file mode 100755 (executable)
index cfe9169..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="EuC-JP"?>  
-<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd">
-<rss version="0.91">
-<channel>
-<title>膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ</title>
-<link>http://www.mozilla.org</link>
-<description>膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ</description>
-<language>ja</language>  <!-- tagged as Japanese content -->
-<item>
-<title>NYÒ™Á¢¸»ÌêÛì15285.25´ƒ´‘Á£´Û´—´ÀÁ¹´ê´Ì´éÒ™Ûì¡êçÒÕ‰ÌêÁ£</title>
-<link>http://www.mozilla.org/status/</link>
-<description>This is an item description...</description>
-</item>
-<item>
-<title>‚§±Çç¡ËßÛÂÒ\8féøÓ¸Á£Ë²®Ÿè†Ûèå\8d±ÇÌ’¡Íæ—éøë‡Á£</title>
-<link>http://www.mozilla.org/status/</link>
-<description>This is an item description...</description>
-</item>
-<item>
-<title>ËÜË”\81ïÌëÈšÁ¢È†Ë§æàÀ豎ˉۂÁ¢Ë‚åܼšÛ˜íËüËÁ£</title>
-<link>http://www.mozilla.org/status/</link>
-<description>This is an item description...</description>
-</item>
-<item>
-<title>2000‚øíŠå\90Á¢«‘¦éÛë¹\8fÛ\90çéÛ§ÛÂè†ÒæÓ¸Á£Ì¾«…æ—ÕÝéøƒ¸Á£</title>
-<link>http://www.mozilla.org/status/</link>
-<description>This is an item description...</description>
-</item>
-</channel>
-</rss>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml
deleted file mode 100755 (executable)
index f0964a2..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd">
-<rss version="0.91">
-<channel>
-<language>en</language>
-<description>News and commentary from the cross-platform scripting community.</description>
-<link>http://www.scripting.com/</link>
-<title>Scripting News</title>
-<image>
-<link>http://www.scripting.com/</link>
-<title>Scripting News</title>
-<url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
-</image>
-</channel>
-</rss>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml
deleted file mode 100755 (executable)
index 5d75c35..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-<?xml version="1.0"?>
-<!-- RSS generation done by 'Radio UserLand' on Fri, 13 Apr 2001 19:23:02 GMT -->
-<rss version="0.92">
-       <channel>
-               <title>Dave Winer: Grateful Dead</title>
-               <link>http://www.scripting.com/blog/categories/gratefulDead.html</link>
-               <description>A high-fidelity Grateful Dead song every day. This is where we&apos;re experimenting with enclosures on RSS news items that download when you&apos;re not using your computer. If it works (it will) it will be the end of the Click-And-Wait multimedia experience on the Internet. </description>
-               <lastBuildDate>Fri, 13 Apr 2001 19:23:02 GMT</lastBuildDate>
-               <docs>http://backend.userland.com/rss092</docs>
-               <managingEditor>dave@userland.com (Dave Winer)</managingEditor>
-               <webMaster>dave@userland.com (Dave Winer)</webMaster>
-               <cloud domain="data.ourfavoritesongs.com" port="80" path="/RPC2" registerProcedure="ourFavoriteSongs.rssPleaseNotify" protocol="xml-rpc"/>
-               <item>
-                       <description>It&apos;s been a few days since I added a song to the Grateful Dead channel. Now that there are all these new Radio users, many of whom are tuned into this channel (it&apos;s #16 on the hotlist of upstreaming Radio users, there&apos;s no way of knowing how many non-upstreaming users are subscribing, have to do something about this..). Anyway, tonight&apos;s song is a live version of Weather Report Suite from Dick&apos;s Picks Volume 7. It&apos;s wistful music. Of course a beautiful song, oft-quoted here on Scripting News. &lt;i&gt;A little change, the wind and rain.&lt;/i&gt;
-</description>
-                       <enclosure url="http://www.scripting.com/mp3s/weatherReportDicksPicsVol7.mp3" length="6182912" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>Kevin Drennan started a &lt;a href=&quot;http://deadend.editthispage.com/&quot;&gt;Grateful Dead Weblog&lt;/a&gt;. Hey it&apos;s cool, he even has a &lt;a href=&quot;http://deadend.editthispage.com/directory/61&quot;&gt;directory&lt;/a&gt;. &lt;i&gt;A Frontier 7 feature.&lt;/i&gt;</description>
-                       <source url="http://scriptingnews.userland.com/xml/scriptingNews2.xml">Scripting News</source>
-                       </item>
-               <item>
-                       <description>&lt;a href=&quot;http://arts.ucsc.edu/GDead/AGDL/other1.html&quot;&gt;The Other One&lt;/a&gt;, live instrumental, One From The Vault. Very rhythmic very spacy, you can listen to it many times, and enjoy something new every time.</description>
-                       <enclosure url="http://www.scripting.com/mp3s/theOtherOne.mp3" length="6666097" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>This is a test of a change I just made. Still diggin..</description>
-                       </item>
-               <item>
-                       <description>The HTML rendering almost &lt;a href=&quot;http://validator.w3.org/check/referer&quot;&gt;validates&lt;/a&gt;. Close. Hey I wonder if anyone has ever published a style guide for ALT attributes on images? What are you supposed to say in the ALT attribute? I sure don&apos;t know. If you&apos;re blind send me an email if u cn rd ths. </description>
-                       </item>
-               <item>
-                       <description>&lt;a href=&quot;http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Franklin&apos;s_Tower.txt&quot;&gt;Franklin&apos;s Tower&lt;/a&gt;, a live version from One From The Vault.</description>
-                       <enclosure url="http://www.scripting.com/mp3s/franklinsTower.mp3" length="6701402" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>Moshe Weitzman says Shakedown Street is what I&apos;m lookin for for tonight. I&apos;m listening right now. It&apos;s one of my favorites. &quot;Don&apos;t tell me this town ain&apos;t got no heart.&quot; Too bright. I like the jazziness of Weather Report Suite. Dreamy and soft. How about The Other One? &quot;Spanish lady come to me..&quot;</description>
-                       <source url="http://scriptingnews.userland.com/xml/scriptingNews2.xml">Scripting News</source>
-                       </item>
-               <item>
-                       <description>&lt;a href=&quot;http://www.scripting.com/mp3s/youWinAgain.mp3&quot;&gt;The news is out&lt;/a&gt;, all over town..&lt;p&gt;
-You&apos;ve been seen, out runnin round. &lt;p&gt;
-The lyrics are &lt;a href=&quot;http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/You_Win_Again.txt&quot;&gt;here&lt;/a&gt;, short and sweet. &lt;p&gt;
-&lt;i&gt;You win again!&lt;/i&gt;
-</description>
-                       <enclosure url="http://www.scripting.com/mp3s/youWinAgain.mp3" length="3874816" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>&lt;a href=&quot;http://www.getlyrics.com/lyrics/grateful-dead/wake-of-the-flood/07.htm&quot;&gt;Weather Report Suite&lt;/a&gt;: &quot;Winter rain, now tell me why, summers fade, and roses die? The answer came. The wind and rain. Golden hills, now veiled in grey, summer leaves have blown away. Now what remains? The wind and rain.&quot;</description>
-                       <enclosure url="http://www.scripting.com/mp3s/weatherReportSuite.mp3" length="12216320" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>&lt;a href=&quot;http://arts.ucsc.edu/gdead/agdl/darkstar.html&quot;&gt;Dark Star&lt;/a&gt; crashes, pouring its light into ashes.</description>
-                       <enclosure url="http://www.scripting.com/mp3s/darkStar.mp3" length="10889216" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>DaveNet: &lt;a href=&quot;http://davenet.userland.com/2001/01/21/theUsBlues&quot;&gt;The U.S. Blues&lt;/a&gt;.</description>
-                       </item>
-               <item>
-                       <description>Still listening to the US Blues. &lt;i&gt;&quot;Wave that flag, wave it wide and high..&quot;&lt;/i&gt; Mistake made in the 60s. We gave our country to the assholes. Ah ah. Let&apos;s take it back. Hey I&apos;m still a hippie. &lt;i&gt;&quot;You could call this song The United States Blues.&quot;&lt;/i&gt;</description>
-                       </item>
-               <item>
-                       <description>&lt;a href=&quot;http://www.sixties.com/html/garcia_stack_0.html&quot;&gt;&lt;img src=&quot;http://www.scripting.com/images/captainTripsSmall.gif&quot; height=&quot;51&quot; width=&quot;42&quot; border=&quot;0&quot; hspace=&quot;10&quot; vspace=&quot;10&quot; align=&quot;right&quot;&gt;&lt;/a&gt;In celebration of today&apos;s inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the &lt;a href=&quot;http://searchlyrics2.homestead.com/gd_usblues.html&quot;&gt;lyrics&lt;/a&gt;. Click on the audio icon to the left to give it a listen. &quot;Red and white, blue suede shoes, I&apos;m Uncle Sam, how do you do?&quot; It&apos;s a different kind of patriotic music, but man I love my country and I love Jerry and the band. &lt;i&gt;I truly do!&lt;/i&gt;</description>
-                       <enclosure url="http://www.scripting.com/mp3s/usBlues.mp3" length="5272510" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>Grateful Dead: &quot;Tennessee, Tennessee, ain&apos;t no place I&apos;d rather be.&quot;</description>
-                       <enclosure url="http://www.scripting.com/mp3s/tennesseeJed.mp3" length="3442648" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>Ed Cone: &quot;Had a nice Deadhead experience with my wife, who never was one but gets the vibe and knows and likes a lot of the music. Somehow she made it to the age of 40 without ever hearing Wharf Rat. We drove to Jersey and back over Christmas with the live album commonly known as Skull and Roses in the CD player much of the way, and it was cool to see her discover one the band&apos;s finest moments. That song is unique and underappreciated. Fun to hear that disc again after a few years off -- you get Jerry as blues-guitar hero on Big Railroad Blues and a nice version of Bertha.&quot;</description>
-                       <enclosure url="http://www.scripting.com/mp3s/darkStarWharfRat.mp3" length="27503386" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>&lt;a href=&quot;http://arts.ucsc.edu/GDead/AGDL/fotd.html&quot;&gt;Tonight&apos;s Song&lt;/a&gt;: &quot;If I get home before daylight I just might get some sleep tonight.&quot; </description>
-                       <enclosure url="http://www.scripting.com/mp3s/friendOfTheDevil.mp3" length="3219742" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>&lt;a href=&quot;http://arts.ucsc.edu/GDead/AGDL/uncle.html&quot;&gt;Tonight&apos;s song&lt;/a&gt;: &quot;Come hear Uncle John&apos;s Band by the river side. Got some things to talk about here beside the rising tide.&quot;</description>
-                       <enclosure url="http://www.scripting.com/mp3s/uncleJohnsBand.mp3" length="4587102" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>&lt;a href=&quot;http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Me_and_My_Uncle.txt&quot;&gt;Me and My Uncle&lt;/a&gt;: &quot;I loved my uncle, God rest his soul, taught me good, Lord, taught me all I know. Taught me so well, I grabbed that gold and I left his dead ass there by the side of the road.&quot;
-</description>
-                       <enclosure url="http://www.scripting.com/mp3s/meAndMyUncle.mp3" length="2949248" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>Truckin, like the doo-dah man, once told me gotta play your hand. Sometimes the cards ain&apos;t worth a dime, if you don&apos;t lay em down.</description>
-                       <enclosure url="http://www.scripting.com/mp3s/truckin.mp3" length="4847908" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>Two-Way-Web: &lt;a href=&quot;http://www.thetwowayweb.com/payloadsForRss&quot;&gt;Payloads for RSS&lt;/a&gt;. &quot;When I started talking with Adam late last year, he wanted me to think about high quality video on the Internet, and I totally didn&apos;t want to hear about it.&quot;</description>
-                       </item>
-               <item>
-                       <description>A touch of gray, kinda suits you anyway..</description>
-                       <enclosure url="http://www.scripting.com/mp3s/touchOfGrey.mp3" length="5588242" type="audio/mpeg"/>
-                       </item>
-               <item>
-                       <description>&lt;a href=&quot;http://www.sixties.com/html/garcia_stack_0.html&quot;&gt;&lt;img src=&quot;http://www.scripting.com/images/captainTripsSmall.gif&quot; height=&quot;51&quot; width=&quot;42&quot; border=&quot;0&quot; hspace=&quot;10&quot; vspace=&quot;10&quot; align=&quot;right&quot;&gt;&lt;/a&gt;In celebration of today&apos;s inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the &lt;a href=&quot;http://searchlyrics2.homestead.com/gd_usblues.html&quot;&gt;lyrics&lt;/a&gt;. Click on the audio icon to the left to give it a listen. &quot;Red and white, blue suede shoes, I&apos;m Uncle Sam, how do you do?&quot; It&apos;s a different kind of patriotic music, but man I love my country and I love Jerry and the band. &lt;i&gt;I truly do!&lt;/i&gt;</description>
-                       <enclosure url="http://www.scripting.com/mp3s/usBlues.mp3" length="5272510" type="audio/mpeg"/>
-                       </item>
-               </channel>
-       </rss>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml
deleted file mode 100755 (executable)
index 0edecf5..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-<?xml version="1.0"?>
-
-<rdf:RDF 
-  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-  xmlns="http://purl.org/rss/1.0/"
->
-
-  <channel rdf:about="http://www.xml.com/xml/news.rss">
-    <title>XML.com</title>
-    <link>http://xml.com/pub</link>
-    <description>
-      XML.com features a rich mix of information and services 
-      for the XML community.
-    </description>
-
-    <image rdf:resource="http://xml.com/universal/images/xml_tiny.gif" />
-
-    <items>
-      <rdf:Seq>
-        <rdf:li resource="http://xml.com/pub/2000/08/09/xslt/xslt.html" />
-        <rdf:li resource="http://xml.com/pub/2000/08/09/rdfdb/index.html" />
-      </rdf:Seq>
-    </items>
-
-    <textinput rdf:resource="http://search.xml.com" />
-
-  </channel>
-  
-  <image rdf:about="http://xml.com/universal/images/xml_tiny.gif">
-    <title>XML.com</title>
-    <link>http://www.xml.com</link>
-    <url>http://xml.com/universal/images/xml_tiny.gif</url>
-  </image>
-  
-  <item rdf:about="http://xml.com/pub/2000/08/09/xslt/xslt.html">
-    <title>Processing Inclusions with XSLT</title>
-    <link>http://xml.com/pub/2000/08/09/xslt/xslt.html</link>
-    <description>
-     Processing document inclusions with general XML tools can be 
-     problematic. This article proposes a way of preserving inclusion 
-     information through SAX-based processing.
-    </description>
-  </item>
-  
-  <item rdf:about="http://xml.com/pub/2000/08/09/rdfdb/index.html">
-    <title>Putting RDF to Work</title>
-    <link>http://xml.com/pub/2000/08/09/rdfdb/index.html</link>
-    <description>
-     Tool and API support for the Resource Description Framework 
-     is slowly coming of age. Edd Dumbill takes a look at RDFDB, 
-     one of the most exciting new RDF toolkits.
-    </description>
-  </item>
-
-  <textinput rdf:about="http://search.xml.com">
-    <title>Search XML.com</title>
-    <description>Search XML.com's XML collection</description>
-    <name>s</name>
-    <link>http://search.xml.com</link>
-  </textinput>
-
-</rdf:RDF>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml
deleted file mode 100755 (executable)
index 26235f7..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?> 
-
-<rdf:RDF 
-  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 
-  xmlns:dc="http://purl.org/dc/elements/1.1/"
-  xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
-  xmlns:co="http://purl.org/rss/1.0/modules/company/"
-  xmlns:ti="http://purl.org/rss/1.0/modules/textinput/"
-  xmlns="http://purl.org/rss/1.0/"
-> 
-
-  <channel rdf:about="http://meerkat.oreillynet.com/?_fl=rss1.0">
-    <title>Meerkat</title>
-    <link>http://meerkat.oreillynet.com</link>
-    <description>Meerkat: An Open Wire Service</description>
-    <dc:publisher>The O'Reilly Network</dc:publisher>
-    <dc:creator>Rael Dornfest (mailto:rael@oreilly.com)</dc:creator>
-    <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
-    <dc:date>2000-01-01T12:00+00:00</dc:date>
-    <sy:updatePeriod>hourly</sy:updatePeriod>
-    <sy:updateFrequency>2</sy:updateFrequency>
-    <sy:updateBase>2000-01-01T12:00+00:00</sy:updateBase>
-
-    <image rdf:resource="http://meerkat.oreillynet.com/icons/meerkat-powered.jpg" />
-
-    <items>
-      <rdf:Seq>
-        <rdf:li resource="http://c.moreover.com/click/here.pl?r123" />
-      </rdf:Seq>
-    </items>
-
-    <textinput rdf:resource="http://meerkat.oreillynet.com" />
-
-  </channel>
-
-  <image rdf:about="http://meerkat.oreillynet.com/icons/meerkat-powered.jpg">
-    <title>Meerkat Powered!</title>
-    <url>http://meerkat.oreillynet.com/icons/meerkat-powered.jpg</url>
-    <link>http://meerkat.oreillynet.com</link>
-  </image>
-
-  <item rdf:about="http://c.moreover.com/click/here.pl?r123">
-    <title>XML: A Disruptive Technology</title> 
-    <link>http://c.moreover.com/click/here.pl?r123</link>
-    <dc:description>
-      XML is placing increasingly heavy loads on the existing technical
-      infrastructure of the Internet.
-    </dc:description>
-    <dc:publisher>The O'Reilly Network</dc:publisher>
-    <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
-    <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
-    <dc:subject>XML</dc:subject>
-    <co:name>XML.com</co:name>
-    <co:market>NASDAQ</co:market>
-    <co:symbol>XML</co:symbol>
-  </item> 
-
-  <textinput rdf:about="http://meerkat.oreillynet.com">
-    <title>Search Meerkat</title>
-    <description>Search Meerkat's RSS Database...</description>
-    <name>s</name>
-    <link>http://meerkat.oreillynet.com/</link>
-    <ti:function>search</ti:function>
-    <ti:inputType>regex</ti:inputType>
-  </textinput>
-
-</rdf:RDF>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml
deleted file mode 100755 (executable)
index 53483cc..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0"?>\r
-<rss version="2.0" xmlns:content="http://purl.org/rss/1.0.modules/content/">\r
-   <channel>\r
-      <title>Liftoff News</title>\r
-      <link>http://liftoff.msfc.nasa.gov/</link>\r
-      <description>Liftoff to Space Exploration.</description>\r
-      <language>en-us</language>\r
-      <pubDate>Tue, 10 Jun 2003 04:00:00 GMT</pubDate>\r
-      <lastBuildDate>Tue, 10 Jun 2003 09:41:01 GMT</lastBuildDate>\r
-      <docs>http://blogs.law.harvard.edu/tech/rss</docs>\r
-      <generator>Weblog Editor 2.0</generator>\r
-      <managingEditor>editor@example.com</managingEditor>\r
-      <webMaster>webmaster@example.com</webMaster>\r
-      <item>\r
-         <title>Star City</title>\r
-         <link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>\r
-         <description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's &lt;a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm"&gt;Star City&lt;/a&gt;.</description>\r
-         <pubDate>Tue, 03 Jun 2003 09:39:21 GMT</pubDate>\r
-         <guid>http://liftoff.msfc.nasa.gov/2003/06/03.html#item573</guid>\r
-      </item>\r
-      <item>\r
-         <description>Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a &lt;a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm"&gt;partial eclipse of the Sun&lt;/a&gt; on Saturday, May 31st.</description>\r
-         <pubDate>Fri, 30 May 2003 11:06:42 GMT</pubDate>\r
-         <guid>http://liftoff.msfc.nasa.gov/2003/05/30.html#item572</guid>\r
-      </item>\r
-      <item>\r
-         <title>The Engine That Does More</title>\r
-         <link>http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp</link>\r
-         <description>Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly.  The proposed VASIMR engine would do that.</description>\r
-         <pubDate>Tue, 27 May 2003 08:37:32 GMT</pubDate>\r
-         <guid>http://liftoff.msfc.nasa.gov/2003/05/27.html#item571</guid>\r
-                <content:encoded><![CDATA[<p>Test content</p>]]></content:encoded>\r
-      </item>\r
-      <item>\r
-         <title>Astronauts' Dirty Laundry</title>\r
-         <link>http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp</link>\r
-         <description>Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them.  Instead, astronauts have other options.</description>\r
-         <pubDate>Tue, 20 May 2003 08:56:02 GMT</pubDate>\r
-         <guid>http://liftoff.msfc.nasa.gov/2003/05/20.html#item570</guid>\r
-      </item>\r
-   </channel>\r
-</rss>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml b/plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml
deleted file mode 100755 (executable)
index f8a04bb..0000000
+++ /dev/null
@@ -1,226 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<rss version="2.0">
-<channel>
-<title>Six Apart - News</title>
-<link>http://www.sixapart.jp/</link>
-<description></description>
-<language>ja</language>
-<copyright>Copyright 2005</copyright>
-<lastBuildDate>Fri, 07 Oct 2005 19:09:34 +0900</lastBuildDate>
-<generator>http://www.movabletype.org/?v=3.2-ja</generator>
-<docs>http://blogs.law.harvard.edu/tech/rss</docs> 
-
-<item>
-<title>ファイブ・ディーが、Movable Typeでブログプロモーションをスタート</title>
-<description><![CDATA[<p><img alt="MIYAZAWAblog_banner.jpg" src="http://www.sixapart.jp/MIYAZAWAblog_banner.jpg" width="200" height="88" align="right" /><br />
-ファイブ・ディーは、Movable Typeで構築したプロモーション ブログ『宮沢和史 中南米ツアーblog Latin America 2005』を開設しました。</p>
-
-<p>9月21日に開設されたこのブログは、ブラジル、ホンジュラス、ニカラグア、メキシコ、キューバの5か国を巡る「Latin America 2005」ツアーに合わせ、そのツアーの模様を同行マネージャーがレポートしていきます。<br />
-さらに今月2日からは宮沢和史自身が日々録音した声をPodcastingするという点でも、ブログを使ったユニークなプロモーションとなっています。</p>
-
-<p><a href="http://www.five-d.co.jp/miyazawa/jp/blog/la2005/">「宮沢和史 中南米ツアーblog Latin America 2005」</a></p>
-
-<p>※シックス・アパートではこうしたブログを使ったプロモーションに最適な製品をご用意しております。<br />
-<ul><li><a href="/movabletype/">Movable Type</a><br />
-<li><a href="/typepad/typepad_promotion.html">TypePad Promotion</a><br />
-</ul></p>]]></description>
-<link>http://www.sixapart.jp/news/2005/10/07-1909.html</link>
-<guid>http://www.sixapart.jp/news/2005/10/07-1909.html</guid>
-<category>news</category>
-<pubDate>Fri, 07 Oct 2005 19:09:34 +0900</pubDate>
-</item>
-<item>
-<title>Movable Type 3.2日本語版の提供を開始</title>
-<description><![CDATA[<p><img alt="Movable Type Logo" src="/images/mt3-logo-small.gif" width="151" height="37"/></p>
-<p>シックス・アパートは、Movable Type 3.2日本語版の提供を開始いたしました。<br />
-ベータテストにご協力いただいた多くの皆様に、スタッフ一同、心から感謝いたします。</p>
-<p>製品概要など、詳しくは<a href="http://www.sixapart.jp/press_releases/2005/09/29-1529.html" title="Six Apart - News: シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始">プレスリリース</a>をご参照下さい。</p>
-<p>ご購入のご検討は、<a href="http://www.sixapart.jp/movabletype/purchase-mt.html">Movable Typeのご購入</a>からどうぞ。</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/29-1530.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/29-1530.html</guid>
-<category>news</category>
-<pubDate>Thu, 29 Sep 2005 15:30:00 +0900</pubDate>
-</item>
-<item>
-<title>シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始</title>
-<description><![CDATA[<p><プレスリリース資料></p>
-<ul>
-  <li><a href="http://www.sixapart.jp/sixapart20050929.pdf">印刷用(PDF版)</a></li>
-</ul>
-<p><strong>シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始 ~ スパムの自動判別機能や新ユーザー・インターフェースで、運用管理の機能を強化 ~</strong></p>
-<p>2005年9月29日<br />
-シックス・アパート株式会社</p>
-<p>ブログ・ソフトウェア大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、「Movable Type(ムーバブル・タイプ) 3.2 日本語版」(URL:<a href="http://www.sixapart.jp/movabletype/">http://www.sixapart.jp/movabletype/</a>)を9月29日より提供開始いたします。</p>]]></description>
-<link>http://www.sixapart.jp/press_releases/2005/09/29-1529.html</link>
-<guid>http://www.sixapart.jp/press_releases/2005/09/29-1529.html</guid>
-<category>Press Releases</category>
-<pubDate>Thu, 29 Sep 2005 15:29:00 +0900</pubDate>
-</item>
-<item>
-<title>スタッフを募集しています</title>
-<description><![CDATA[<p>シックス・アパートはMovable TypeやTypePadの開発エンジニアなど、スタッフを広く募集しています。具体的な募集職種は次の通りです。</p>
-
-<ul>
-<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0007.html">Movable Type開発エンジニア</a></li>
-<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0004.html">TypePad開発エンジニア</a></li>
-<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0003.html">カスタマーサポート・ディレクター</a></li>
-<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0002.html">マーケティング・広報アシスタント</a></li>
-<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0001.html">開発アシスタント</a></li>
-</ul>
-
-<p>拡大を続ける、日本のブログ市場を積極的にリードする人材を、シックス・アパートは募集しています。上記以外の職種につきましても、お気軽にお問い合わせください。詳しい募集要項や応募方法については、<a href="/jobs/">求人情報のページ</a>をご覧ください。<br />
-</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/27-0906.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/27-0906.html</guid>
-<category>news</category>
-<pubDate>Tue, 27 Sep 2005 09:06:10 +0900</pubDate>
-</item>
-<item>
-<title>サイト接続不具合に関するお詫びと復旧のお知らせ</title>
-<description><![CDATA[<p>9月24日(土)の14:45ごろから、同日18:30ごろまで、シックス・アパート社のウェブサイトが不安定になっており、断続的に接続できない不具合が発生しておりました。このため、この期間中にウェブサイトの閲覧や製品のダウンロードができませんでした。</p>
-
-<p>なお現在は不具合は解消しております。みなさまにご迷惑をおかけしたことをお詫びいたします。</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/26-1000.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/26-1000.html</guid>
-<category>news</category>
-<pubDate>Mon, 26 Sep 2005 10:00:56 +0900</pubDate>
-</item>
-<item>
-<title>企業ブログ向けパッケージ「TypePad Promotion」を新発売</title>
-<description><![CDATA[<p>シックス・アパートは、ウェブログ・サービスTypePadの企業ブログ向けパッケージ「TypePad Promotion」(タイプパッド・プロモーションの発売を10月下旬から開始いたします。</p>
-
-<p>詳しくは、<a href="http://www.sixapart.jp/press_releases/2005/09/20-1500.html" title="プレスリリース: 「TypePad Promotion」新発売">プレスリリース</a>をご参照下さい。</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/20-1500.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/20-1500.html</guid>
-<category>news</category>
-<pubDate>Tue, 20 Sep 2005 15:00:01 +0900</pubDate>
-</item>
-<item>
-<title>シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売</title>
-<description><![CDATA[<p><プレスリリース資料><br />
-<a href="http://www.sixapart.jp/sixapart20050920.pdf">印刷用(PDF版)</a></p>
-
-<p><br />
-<strong>シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売<br />
-~PR/IRサイトやキャンペーンサイトなど企業のプロモーションニーズに特化~<br />
-</strong><br />
-2005年9月20日<br />
-シックス・アパート株式会社</p>
-
-<p>ブログ・サービス大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、法人向けプロモーションブログ・パッケージ「TypePad Promotion(タイプパッド・プロモーション)」(URL:<a href="http://www.sixapart.jp/typepad/typepad_promotion.html">http://www.sixapart.jp/typepad/typepad_promotion.html</a>)を10月下旬より販売開始いたします。</p>]]></description>
-<link>http://www.sixapart.jp/press_releases/2005/09/20-1500.html</link>
-<guid>http://www.sixapart.jp/press_releases/2005/09/20-1500.html</guid>
-<category>Press Releases</category>
-<pubDate>Tue, 20 Sep 2005 15:00:00 +0900</pubDate>
-</item>
-<item>
-<title>Six [days] Apart Week</title>
-<description><![CDATA[<p>本日、9月16日はSix Apartの創業者ミナ・トロットの誕生日です。<br />
-私たちの会社は、創業者のトロット夫妻(ベンとミナ)の誕生日が、6日離れていることからSix  [days] Apart →Six Apartという風に名付けられています。本日から22日までの6日間を社名の由来となる Six [days] Apart Weekとして、私たちのプロダクトをご紹介させていただきます。</p>
-
-<p>今日は、ブログ・サービスのTypePad(タイプパッド)をご紹介します。<br />
-<img alt="tp-logo.gif" src="http://www.sixapart.jp/tp-logo.gif" width="227" height="52" /></p>
-
-<p>TypePadは、米国PC MAGAZINE誌の2003年EDITOR'S CHOICE とBEST OF THE YEARに選ばれております。<br />
-<img alt="pcmag-ad.gif" src="http://www.sixapart.jp/pcmag-ad.gif" width="297" height="100" /><br />
-</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/16-1941.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/16-1941.html</guid>
-<category>news</category>
-<pubDate>Fri, 16 Sep 2005 19:41:47 +0900</pubDate>
-</item>
-<item>
-<title>ハイパーワークスが商用フォントを利用できるMovable Typeホスティングサービスを開始</title>
-<description><![CDATA[<p>ソフト開発会社の<a href="http://www.hyperwrx.co.jp/">有限会社ハイパーワークス</a>は、商用フォントなど多彩なフォントをブログ上で利用できるブログ・サービス「<a href="http://glyph-on.jp/">Glyph-On!(グリフォン) Movable Type ホスティング サービス</a>」の提供を開始しました。<br />
-</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/14-1700.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/14-1700.html</guid>
-<category>news</category>
-<pubDate>Wed, 14 Sep 2005 17:00:00 +0900</pubDate>
-</item>
-<item>
-<title>Movable Type開発エンジニアの募集</title>
-<description><![CDATA[<p>
-勤務形態: フルタイム<br />
-勤務地: 東京 (赤坂)<br />
-職種: ソフトウェア・エンジニア<br />
-職務内容: Movable Typeの開発業務全般<br />
-募集人数: 若干名
-</p>]]></description>
-<link>http://www.sixapart.jp/jobs/2005/09/13-0007.html</link>
-<guid>http://www.sixapart.jp/jobs/2005/09/13-0007.html</guid>
-<category>Jobs</category>
-<pubDate>Tue, 13 Sep 2005 00:07:00 +0900</pubDate>
-</item>
-<item>
-<title>TypePad開発エンジニアの募集</title>
-<description><![CDATA[<p>
-勤務形態: フルタイム<br />
-勤務地: 東京 (赤坂)<br />
-職種: アプリケーション・エンジニア<br />
-職務内容: TypePadのカスタマイズ、周辺開発<br />
-募集人数: 若干名
-</p>]]></description>
-<link>http://www.sixapart.jp/jobs/2005/09/13-0004.html</link>
-<guid>http://www.sixapart.jp/jobs/2005/09/13-0004.html</guid>
-<category>Jobs</category>
-<pubDate>Tue, 13 Sep 2005 00:04:00 +0900</pubDate>
-</item>
-<item>
-<title>カスタマーサポート・ディレクターの募集</title>
-<description><![CDATA[<p>勤務形態: フルタイム<br />
-勤務地: 東京(赤坂)<br />
-職種: カスタマーサポート・ディレクター<br />
-職務内容: TypePadやMovable Typeのカスタマーサポート業務の統括<br />
-募集人数: 若干名
-</p>
-]]></description>
-<link>http://www.sixapart.jp/jobs/2005/09/13-0003.html</link>
-<guid>http://www.sixapart.jp/jobs/2005/09/13-0003.html</guid>
-<category>Jobs</category>
-<pubDate>Tue, 13 Sep 2005 00:03:30 +0900</pubDate>
-</item>
-<item>
-<title>アルバイト(マーケティング・広報アシスタント)の募集</title>
-<description><![CDATA[<p>勤務形態: アルバイト<br />
-勤務地: 東京(港区)<br />
-職種:マーケティング・PRのアシスタント業務<br />
-募集人数: 若干名<br />
-時給:1000円~(但し、試用期間終了後に応相談)。交通費支給<br />
-時間:平日10時30分~18時30分まで。週3日以上(応相談)<br />
-</p>]]></description>
-<link>http://www.sixapart.jp/jobs/2005/09/13-0002.html</link>
-<guid>http://www.sixapart.jp/jobs/2005/09/13-0002.html</guid>
-<category>Jobs</category>
-<pubDate>Tue, 13 Sep 2005 00:02:00 +0900</pubDate>
-</item>
-<item>
-<title>アルバイト(開発アシスタント)の募集</title>
-<description><![CDATA[<p>勤務形態: アルバイト<br />
-勤務地: 東京(港区)<br />
-職種: アプリケーション開発のアシスタント業務<br />
-募集人数: 若干名<br />
-時給:1000円~(但し、試用期間終了後に応相談)。交通費支給<br />
-時間:平日10時30分~18時30分まで。週3日以上(応相談)
-</p>]]></description>
-<link>http://www.sixapart.jp/jobs/2005/09/13-0001.html</link>
-<guid>http://www.sixapart.jp/jobs/2005/09/13-0001.html</guid>
-<category>Jobs</category>
-<pubDate>Tue, 13 Sep 2005 00:01:00 +0900</pubDate>
-</item>
-<item>
-<title>TypePad Japan がバージョンアップしました。</title>
-<description><![CDATA[<p><a href="http://www.sixapart.jp/typepad/">「TypePad Japan(タイプパッドジャパン)」</a>において、本日、「TypePad 1.6 日本語版」へのバージョンアップを行いました。最新版となる「TypePad 1.6 日本語版」では、ブログデザインの機能強化、ポッドキャスティング対応、モブログ対応に加え、今回新たに大幅な容量アップが行われております。皆様、新しくなった<a href="http://www.sixapart.jp/typepad/">TypePad Japan</a>にどうぞご期待ください。</p>
-
-<p>なお、TypePadの携帯対応強化に関しましては、本日よりTypePad Japanのお客様を対象にオープン・ベータを開始しております。</p>
-
-<p>2005年9月5日発表のTypePad日本語版 1.6プレスリリースは<a href="http://www.sixapart.jp/press_releases/2005/09/05-1420.html">こちら</a>をご覧下さい。</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/12-1953.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/12-1953.html</guid>
-<category>news</category>
-<pubDate>Mon, 12 Sep 2005 19:53:07 +0900</pubDate>
-</item>
-
-
-</channel>
-</rss>
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed b/plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed
deleted file mode 100755 (executable)
index 6274a32..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<rss version="2.0"
-    xmlns:tapi="http://api.technorati.com/dtd/tapi-002.xml">
-    <channel>
-        <title>[Technorati] Tag results for greenbelt</title>
-        <link>http://www.technorati.com/tag/greenbelt</link>
-        <description>Posts tagged with "greenbelt" on Technorati.</description>
-        <pubDate>Mon, 08 Aug 2005 15:15:08 GMT</pubDate>
-        <category domain="http://www.technorati.com/tag">greenbelt</category>
-        <tapi:inboundblogs>2</tapi:inboundblogs>
-        <tapi:inboundlinks>2</tapi:inboundlinks>
-        <cloud domain="rpc.sys.com" port="80" path="/RPC2" registerProcedure="myCloud.rssPleaseNotify" protocol="xml-rpc" />
-        <generator>Technorati v1.0</generator>
-        <image>
-            <url>http://static.technorati.com/pix/logos/logo_reverse_sm.gif</url>
-            <title>Technorati logo</title>
-            <link>http://www.technorati.com</link>
-        </image>
-        <skipHours>
-            <hour>1</hour>
-            <hour>7</hour>
-            <hour>9</hour>
-        </skipHours>
-        <webMaster>support@technorati.com (Technorati Support)</webMaster>
-        <docs>http://blogs.law.harvad.edu/tech/rss</docs>
-        <ttl>60</ttl>
-        <item>
-            <title>Greenbelt</title>
-            <link>http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html</link>
-            <description>So if the plan goes according to plan (!)... I'll be speaking at Greenbelt at these times: Slot 1...</description>
-            <guid isPermaLink="true">http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html</guid>
-            <pubDate>Mon, 18 Jul 2005 02:11:42 GMT</pubDate>
-            <category>James</category>
-            <tapi:linkcreated>2005-07-11 02:08:12</tapi:linkcreated>
-            <comments>http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fmaggidawn.typepad.com%2Fmaggidawn%2F2005%2F07%2Fgreenbelt.html</comments>
-            <tapi:inboundblogs>190</tapi:inboundblogs>
-            <tapi:inboundlinks>237</tapi:inboundlinks>
-            <source url="http://maggidawn.typepad.com/maggidawn/index.rdf">maggi dawn</source>
-        </item>
-
-        <item>
-            <title>Walking along the Greenbelt</title>
-            <link>http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html</link>
-            <description>[IMG] Photo of homeless man walking near the greenbelt in Boise, Idaho Tags: photo homeless greenbelt Boise Idaho picture</description>
-            <guid isPermaLink="true">http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html</guid>
-            <pubDate>Tue, 28 Jun 2005 01:41:24 GMT</pubDate>
-            <tapi:linkcreated>2005-06-26 17:24:03</tapi:linkcreated>
-            <comments>http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fpictureshomeless.blogspot.com%2F2005%2F06%2Fwalking-along-greenbelt.html</comments>
-            <tapi:inboundblogs>2</tapi:inboundblogs>
-            <tapi:inboundlinks>2</tapi:inboundlinks>
-        </item>
-
-    </channel>
-</rss>
diff --git a/plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc b/plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc
deleted file mode 100755 (executable)
index e662d26..0000000
+++ /dev/null
@@ -1,338 +0,0 @@
-# -*- rnc -*-
-# RELAX NG Compact Syntax Grammar for the
-# Atom Format Specification Version 11
-
-namespace atom = "http://www.w3.org/2005/Atom"
-namespace xhtml = "http://www.w3.org/1999/xhtml"
-namespace s = "http://www.ascc.net/xml/schematron"
-namespace local = ""
-
-start = atomFeed | atomEntry
-
-# Common attributes
-
-atomCommonAttributes =
-   attribute xml:base { atomUri }?,
-   attribute xml:lang { atomLanguageTag }?,
-   undefinedAttribute*
-
-# Text Constructs
-
-atomPlainTextConstruct =
-   atomCommonAttributes,
-   attribute type { "text" | "html" }?,
-   text
-
-atomXHTMLTextConstruct =
-   atomCommonAttributes,
-   attribute type { "xhtml" },
-   xhtmlDiv
-
-atomTextConstruct = atomPlainTextConstruct | atomXHTMLTextConstruct
-
-# Person Construct
-
-atomPersonConstruct =
-   atomCommonAttributes,
-   (element atom:name { text }
-    & element atom:uri { atomUri }?
-    & element atom:email { atomEmailAddress }?
-    & extensionElement*)
-
-# Date Construct
-
-atomDateConstruct =
-   atomCommonAttributes,
-   xsd:dateTime
-
-# atom:feed
-
-atomFeed =
-   [
-      s:rule [
-         context = "atom:feed"
-         s:assert [
-            test = "atom:author or not(atom:entry[not(atom:author)])"
-            "An atom:feed must have an atom:author unless all "
-            ~ "of its atom:entry children have an atom:author."
-         ]
-      ]
-   ]
-   element atom:feed {
-      atomCommonAttributes,
-      (atomAuthor*
-       & atomCategory*
-       & atomContributor*
-       & atomGenerator?
-       & atomIcon?
-       & atomId
-       & atomLink*
-       & atomLogo?
-       & atomRights?
-       & atomSubtitle?
-       & atomTitle
-       & atomUpdated
-       & extensionElement*),
-      atomEntry*
-   }
-
-# atom:entry
-
-atomEntry =
-   [
-      s:rule [
-         context = "atom:entry"
-         s:assert [
-            test = "atom:link[@rel='alternate'] "
-            ~ "or atom:link[not(@rel)] "
-            ~ "or atom:content"
-            "An atom:entry must have at least one atom:link element "
-            ~ "with a rel attribute of 'alternate' "
-            ~ "or an atom:content."
-         ]
-      ]
-      s:rule [
-         context = "atom:entry"
-         s:assert [
-            test = "atom:author or "
-            ~ "../atom:author or atom:source/atom:author"
-            "An atom:entry must have an atom:author "
-            ~ "if its feed does not."
-         ]
-      ]
-   ]
-   element atom:entry {
-      atomCommonAttributes,
-      (atomAuthor*
-       & atomCategory*
-       & atomContent?
-       & atomContributor*
-       & atomId
-       & atomLink*
-       & atomPublished?
-       & atomRights?
-       & atomSource?
-       & atomSummary?
-       & atomTitle
-       & atomUpdated
-       & extensionElement*)
-   }
-
-# atom:content
-
-atomInlineTextContent =
-   element atom:content {
-      atomCommonAttributes,
-      attribute type { "text" | "html" }?,
-      (text)*
-   }
-
-atomInlineXHTMLContent =
-   element atom:content {
-      atomCommonAttributes,
-      attribute type { "xhtml" },
-      xhtmlDiv
-   }
-
-atomInlineOtherContent =
-   element atom:content {
-      atomCommonAttributes,
-      attribute type { atomMediaType }?,
-      (text|anyElement)*
-   }
-
-atomOutOfLineContent =
-   element atom:content {
-      atomCommonAttributes,
-      attribute type { atomMediaType }?,
-      attribute src { atomUri },
-      empty
-   }
-
-atomContent = atomInlineTextContent
- | atomInlineXHTMLContent
- | atomInlineOtherContent
- | atomOutOfLineContent
-
-# atom:author
-
-atomAuthor = element atom:author { atomPersonConstruct }
-
-# atom:category
-
-atomCategory =
-   element atom:category {
-      atomCommonAttributes,
-      attribute term { text },
-      attribute scheme { atomUri }?,
-      attribute label { text }?,
-      undefinedContent
-   }
-
-# atom:contributor
-
-atomContributor = element atom:contributor { atomPersonConstruct }
-
-# atom:generator
-
-atomGenerator = element atom:generator {
-   atomCommonAttributes,
-   attribute uri { atomUri }?,
-   attribute version { text }?,
-   text
-}
-
-# atom:icon
-
-atomIcon = element atom:icon {
-   atomCommonAttributes,
-   (atomUri)
-}
-
-# atom:id
-
-atomId = element atom:id {
-   atomCommonAttributes,
-   (atomUri)
-}
-
-# atom:logo
-
-atomLogo = element atom:logo {
-   atomCommonAttributes,
-   (atomUri)
-}
-
-# atom:link
-
-atomLink =
-   element atom:link {
-      atomCommonAttributes,
-      attribute href { atomUri },
-      attribute rel { atomNCName | atomUri }?,
-      attribute type { atomMediaType }?,
-      attribute hreflang { atomLanguageTag }?,
-      attribute title { text }?,
-      attribute length { text }?,
-      undefinedContent
-   }
-
-# atom:published
-
-atomPublished = element atom:published { atomDateConstruct }
-
-# atom:rights
-
-atomRights = element atom:rights { atomTextConstruct }
-
-# atom:source
-
-atomSource =
-   element atom:source {
-      atomCommonAttributes,
-      (atomAuthor*
-       & atomCategory*
-       & atomContributor*
-       & atomGenerator?
-       & atomIcon?
-       & atomId?
-       & atomLink*
-       & atomLogo?
-       & atomRights?
-       & atomSubtitle?
-       & atomTitle?
-       & atomUpdated?
-       & extensionElement*)
-   }
-
-# atom:subtitle
-
-atomSubtitle = element atom:subtitle { atomTextConstruct }
-
-# atom:summary
-
-atomSummary = element atom:summary { atomTextConstruct }
-
-# atom:title
-
-atomTitle = element atom:title { atomTextConstruct }
-
-# atom:updated
-
-atomUpdated = element atom:updated { atomDateConstruct }
-
-# Low-level simple types
-
-atomNCName = xsd:string { minLength = "1" pattern = "[^:]*" }
-
-# Whatever a media type is, it contains at least one slash
-atomMediaType = xsd:string { pattern = ".+/.+" }
-
-# As defined in RFC 3066
-atomLanguageTag = xsd:string {
-   pattern = "[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*"
-}
-
-# Unconstrained; it's not entirely clear how IRI fit into
-# xsd:anyURI so let's not try to constrain it here
-atomUri = text
-
-# Whatever an email address is, it contains at least one @
-atomEmailAddress = xsd:string { pattern = ".+@.+" }
-
-# Simple Extension
-
-simpleExtensionElement =
-   element * - atom:* {
-      text
-   }
-
-# Structured Extension
-
-structuredExtensionElement =
-   element * - atom:* {
-      (attribute * { text }+,
-         (text|anyElement)*)
-    | (attribute * { text }*,
-       (text?, anyElement+, (text|anyElement)*))
-   }
-
-# Other Extensibility
-
-extensionElement =
-   simpleExtensionElement | structuredExtensionElement
-
-undefinedAttribute =
-  attribute * - (xml:base | xml:lang | local:*) { text }
-
-undefinedContent = (text|anyForeignElement)*
-
-anyElement =
-   element * {
-      (attribute * { text }
-       | text
-       | anyElement)*
-   }
-
-anyForeignElement =
-   element * - atom:* {
-      (attribute * { text }
-       | text
-       | anyElement)*
-   }
-
-# XHTML
-
-anyXHTML = element xhtml:* {
-   (attribute * { text }
-    | text
-    | anyXHTML)*
-}
-
-xhtmlDiv = element xhtml:div {
-   (attribute * { text }
-    | text
-    | anyXHTML)*
-}
-
-# EOF
\ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc b/plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc
deleted file mode 100755 (executable)
index 7250947..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-<?xml version='1.0' encoding='UTF-8'?>
-<!-- http://www.xml.com/lpt/a/2002/01/23/relaxng.html -->
-<!-- http://www.oasis-open.org/committees/relax-ng/tutorial-20011203.html -->
-<!-- http://www.zvon.org/xxl/XMLSchemaTutorial/Output/ser_wildcards_st8.html -->
-
-<grammar xmlns='http://relaxng.org/ns/structure/1.0'
-        xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
-        xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
-        ns='http://purl.org/rss/1.0/'
-        datatypeLibrary='http://www.w3.org/2001/XMLSchema-datatypes'>
-
-    <start>
-        <element name='RDF' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
-            <ref name='RDFContent'/>
-        </element>
-    </start>   
-
-    <define name='RDFContent' ns='http://purl.org/rss/1.0/'>
-        <interleave>
-            <element name='channel'>
-                <ref name='channelContent'/>
-            </element>
-            <optional>
-                <element name='image'><ref name='imageContent'/></element>
-            </optional>
-            <oneOrMore>
-                <element name='item'><ref name='itemContent'/></element>
-            </oneOrMore>
-        </interleave>
-    </define>
-
-     <define name='channelContent' combine="interleave">
-        <interleave>
-            <element name='title'><data type='string'/></element>
-            <element name='link'><data type='anyURI'/></element>
-            <element name='description'><data type='string'/></element>
-            <element name='image'>
-                <attribute name='resource' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
-                    <data type='anyURI'/>
-                </attribute>
-            </element>
-            <element name='items'>
-                    <ref name='itemsContent'/>
-            </element>
-            <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
-                <data type='anyURI'/>
-            </attribute>
-        </interleave>
-    </define>
-    
-        <define name="itemsContent">
-            <element name="Seq" ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
-                <oneOrMore>
-                    <element name="li" ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
-                        <choice>
-                            <attribute name='resource'>    <!-- Why doesn't RDF/RSS1.0 ns qualify this attribute? -->
-                                <data type='anyURI'/>
-                            </attribute>
-                            <attribute name='resource' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
-                                <data type='anyURI'/>
-                            </attribute>
-                        </choice>
-                    </element>
-                </oneOrMore>
-            </element>
-        </define>
-        
-    <define name='imageContent'>
-        <interleave>
-            <element name='title'><data type='string'/></element>
-            <element name='link'><data type='anyURI'/></element>
-            <element name='url'><data type='anyURI'/></element>
-            <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
-                <data type='anyURI'/>
-            </attribute>
-        </interleave>
-    </define>
-
-    <define name='itemContent'>
-        <interleave>
-            <element name='title'><data type='string'/></element>
-            <element name='link'><data type='anyURI'/></element>
-            <optional><element name='description'><data type='string'/></element></optional>
-            <ref name="anyThing"/>
-            <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
-                <data type='anyURI'/>
-            </attribute>
-        </interleave>
-    </define>            
-            
-
-        <define name='anyThing'>
-            <zeroOrMore>
-                <choice>
-                    <text/>
-                    <element>
-                        <anyName>
-                            <except>
-                                <nsName/>
-                            </except>
-                        </anyName>
-                        <ref name='anyThing'/>
-                        <zeroOrMore>
-                            <attribute>
-                              <anyName/>
-                            </attribute>
-                        </zeroOrMore>
-                    </element>
-                </choice>
-            </zeroOrMore>
-            </define>
-            
-</grammar>
diff --git a/plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc b/plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc
deleted file mode 100755 (executable)
index c863376..0000000
+++ /dev/null
@@ -1,218 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  RELAX NG Compact Schema for RSS 1.1
-  Sean B. Palmer, inamidst.com
-  Christopher Schmidt, crschmidt.net
-  License: This schema is in the public domain
--->
-<grammar xmlns:rss="http://purl.org/net/rss1.1#" xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ns="http://purl.org/net/rss1.1#" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
-  <start>
-    <ref name="Channel"/>
-  </start>
-  <define name="Channel">
-    <a:documentation>http://purl.org/net/rss1.1#Channel</a:documentation>
-    <element name="Channel">
-      <ref name="Channel.content"/>
-
-    </element>
-  </define>
-  <define name="Channel.content">
-    <optional>
-      <ref name="AttrXMLLang"/>
-    </optional>
-    <optional>
-      <ref name="AttrXMLBase"/>
-    </optional>
-
-    <ref name="AttrRDFAbout"/>
-    <interleave>
-      <ref name="title"/>
-      <ref name="link"/>
-      <ref name="description"/>
-      <optional>
-        <ref name="image"/>
-      </optional>
-      <zeroOrMore>
-
-        <ref name="Any"/>
-      </zeroOrMore>
-      <ref name="items"/>
-    </interleave>
-  </define>
-  <define name="title">
-    <a:documentation>http://purl.org/net/rss1.1#title</a:documentation>
-    <element name="title">
-
-      <ref name="title.content"/>
-    </element>
-  </define>
-  <define name="title.content">
-    <optional>
-      <ref name="AttrXMLLang"/>
-    </optional>
-    <text/>
-  </define>
-
-  <define name="link">
-    <a:documentation>http://purl.org/net/rss1.1#link</a:documentation>
-    <element name="link">
-      <ref name="link.content"/>
-    </element>
-  </define>
-  <define name="link.content">
-    <data type="anyURI"/>
-
-  </define>
-  <define name="description">
-    <a:documentation>http://purl.org/net/rss1.1#description</a:documentation>
-    <element name="description">
-      <ref name="description.content"/>
-    </element>
-  </define>
-  <define name="description.content">
-
-    <optional>
-      <ref name="AttrXMLLang"/>
-    </optional>
-    <text/>
-  </define>
-  <define name="image">
-    <a:documentation>http://purl.org/net/rss1.1#image</a:documentation>
-    <element name="image">
-
-      <ref name="image.content"/>
-    </element>
-  </define>
-  <define name="image.content">
-    <optional>
-      <ref name="AttrXMLLang"/>
-    </optional>
-    <ref name="AttrRDFResource"/>
-    <interleave>
-
-      <ref name="title"/>
-      <optional>
-        <ref name="link"/>
-      </optional>
-      <ref name="url"/>
-      <zeroOrMore>
-        <ref name="Any"/>
-      </zeroOrMore>
-    </interleave>
-
-  </define>
-  <define name="url">
-    <a:documentation>http://purl.org/net/rss1.1#url</a:documentation>
-    <element name="url">
-      <ref name="url.content"/>
-    </element>
-  </define>
-  <define name="url.content">
-
-    <data type="anyURI"/>
-  </define>
-  <define name="items">
-    <a:documentation>http://purl.org/net/rss1.1#items</a:documentation>
-    <element name="items">
-      <ref name="items.content"/>
-    </element>
-  </define>
-
-  <define name="items.content">
-    <optional>
-      <ref name="AttrXMLLang"/>
-    </optional>
-    <ref name="AttrRDFCollection"/>
-    <zeroOrMore>
-      <ref name="item"/>
-    </zeroOrMore>
-  </define>
-
-  <define name="item">
-    <a:documentation>http://purl.org/net/rss1.1#item</a:documentation>
-    <element name="item">
-      <ref name="item.content"/>
-    </element>
-  </define>
-  <define name="item.content">
-    <optional>
-
-      <ref name="AttrXMLLang"/>
-    </optional>
-    <ref name="AttrRDFAbout"/>
-    <interleave>
-      <ref name="title"/>
-      <ref name="link"/>
-      <optional>
-        <ref name="description"/>
-      </optional>
-
-      <optional>
-        <ref name="image"/>
-      </optional>
-      <zeroOrMore>
-        <ref name="Any"/>
-      </zeroOrMore>
-    </interleave>
-  </define>
-  <define name="Any">
-
-    <a:documentation>http://purl.org/net/rss1.1#Any</a:documentation>
-    <element>
-      <anyName>
-        <except>
-          <nsName/>
-        </except>
-      </anyName>
-      <ref name="Any.content"/>
-
-    </element>
-  </define>
-  <define name="Any.content">
-    <zeroOrMore>
-      <attribute>
-        <anyName>
-          <except>
-            <nsName/>
-            <nsName ns=""/>
-
-          </except>
-        </anyName>
-      </attribute>
-    </zeroOrMore>
-    <mixed>
-      <zeroOrMore>
-        <ref name="Any"/>
-      </zeroOrMore>
-    </mixed>
-
-  </define>
-  <define name="AttrXMLLang">
-    <attribute name="xml:lang">
-      <data type="language"/>
-    </attribute>
-  </define>
-  <define name="AttrXMLBase">
-    <attribute name="xml:base">
-      <data type="anyURI"/>
-
-    </attribute>
-  </define>
-  <define name="AttrRDFAbout">
-    <attribute name="rdf:about">
-      <data type="anyURI"/>
-    </attribute>
-  </define>
-  <define name="AttrRDFResource">
-    <attribute name="rdf:parseType">
-
-      <value>Resource</value>
-    </attribute>
-  </define>
-  <define name="AttrRDFCollection">
-    <attribute name="rdf:parseType">
-      <value>Collection</value>
-    </attribute>
-  </define>
-
-</grammar>
diff --git a/plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch b/plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch
deleted file mode 100644 (file)
index c53bd97..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-diff --git a/htdocs/lib/pear/XML/Feed/Parser/RSS2.php b/htdocs/lib/pear/XML/Feed/Parser/RSS2.php
-index c5d79d1..308a4ab 100644
---- a/htdocs/lib/pear/XML/Feed/Parser/RSS2.php
-+++ b/htdocs/lib/pear/XML/Feed/Parser/RSS2.php
-@@ -321,7 +321,8 @@ class XML_Feed_Parser_RSS2 extends XML_Feed_Parser_Type
-      */
-     function getLink($offset, $attribute = 'href', $params = array())
-     {
--        $links = $this->model->getElementsByTagName('link');
-+        $xPath = new DOMXPath($this->model);
-+        $links = $xPath->query('//link');
-         if ($links->length <= $offset) {
-             return false;
diff --git a/plugins/FeedSub/feeddiscovery.php b/plugins/FeedSub/feeddiscovery.php
deleted file mode 100644 (file)
index 35edaca..0000000
+++ /dev/null
@@ -1,209 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package FeedSubPlugin
- * @maintainer Brion Vibber <brion@status.net>
- */
-
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-
-class FeedSubBadURLException extends FeedSubException
-{
-}
-
-class FeedSubBadResponseException extends FeedSubException
-{
-}
-
-class FeedSubEmptyException extends FeedSubException
-{
-}
-
-class FeedSubBadHTMLException extends FeedSubException
-{
-}
-
-class FeedSubUnrecognizedTypeException extends FeedSubException
-{
-}
-
-class FeedSubNoFeedException extends FeedSubException
-{
-}
-
-class FeedDiscovery
-{
-    public $uri;
-    public $type;
-    public $body;
-
-
-    public function feedMunger()
-    {
-        require_once 'XML/Feed/Parser.php';
-        $feed = new XML_Feed_Parser($this->body, false, false, true); // @fixme
-        return new FeedMunger($feed, $this->uri);
-    }
-
-    /**
-     * @param string $url
-     * @param bool $htmlOk
-     * @return string with validated URL
-     * @throws FeedSubBadURLException
-     * @throws FeedSubBadHtmlException
-     * @throws FeedSubNoFeedException
-     * @throws FeedSubEmptyException
-     * @throws FeedSubUnrecognizedTypeException
-     */
-    function discoverFromURL($url, $htmlOk=true)
-    {
-        try {
-            $client = new HTTPClient();
-            $response = $client->get($url);
-        } catch (HTTP_Request2_Exception $e) {
-            throw new FeedSubBadURLException($e);
-        }
-
-        if ($htmlOk) {
-            $type = $response->getHeader('Content-Type');
-            $isHtml = preg_match('!^(text/html|application/xhtml\+xml)!i', $type);
-            if ($isHtml) {
-                $target = $this->discoverFromHTML($response->getUrl(), $response->getBody());
-                if (!$target) {
-                    throw new FeedSubNoFeedException($url);
-                }
-                return $this->discoverFromURL($target, false);
-            }
-        }
-        
-        return $this->initFromResponse($response);
-    }
-    
-    function initFromResponse($response)
-    {
-        if (!$response->isOk()) {
-            throw new FeedSubBadResponseException($response->getCode());
-        }
-
-        $sourceurl = $response->getUrl();
-        $body = $response->getBody();
-        if (!$body) {
-            throw new FeedSubEmptyException($sourceurl);
-        }
-
-        $type = $response->getHeader('Content-Type');
-        if (preg_match('!^(text/xml|application/xml|application/(rss|atom)\+xml)!i', $type)) {
-            $this->uri = $sourceurl;
-            $this->type = $type;
-            $this->body = $body;
-            return true;
-        } else {
-            common_log(LOG_WARNING, "Unrecognized feed type $type for $sourceurl");
-            throw new FeedSubUnrecognizedTypeException($type);
-        }
-    }
-
-    /**
-     * @param string $url source URL, used to resolve relative links
-     * @param string $body HTML body text
-     * @return mixed string with URL or false if no target found
-     */
-    function discoverFromHTML($url, $body)
-    {
-        // DOMDocument::loadHTML may throw warnings on unrecognized elements.
-        $old = error_reporting(error_reporting() & ~E_WARNING);
-        $dom = new DOMDocument();
-        $ok = $dom->loadHTML($body);
-        error_reporting($old);
-
-        if (!$ok) {
-            throw new FeedSubBadHtmlException();
-        }
-
-        // Autodiscovery links may be relative to the page's URL or <base href>
-        $base = false;
-        $nodes = $dom->getElementsByTagName('base');
-        for ($i = 0; $i < $nodes->length; $i++) {
-            $node = $nodes->item($i);
-            if ($node->hasAttributes()) {
-                $href = $node->attributes->getNamedItem('href');
-                if ($href) {
-                    $base = trim($href->value);
-                }
-            }
-        }
-        if ($base) {
-            $base = $this->resolveURI($base, $url);
-        } else {
-            $base = $url;
-        }
-
-        // Ok... now on to the links!
-        // @fixme merge with the munger link checks
-        $nodes = $dom->getElementsByTagName('link');
-        for ($i = 0; $i < $nodes->length; $i++) {
-            $node = $nodes->item($i);
-            if ($node->hasAttributes()) {
-                $rel = $node->attributes->getNamedItem('rel');
-                $type = $node->attributes->getNamedItem('type');
-                $href = $node->attributes->getNamedItem('href');
-                if ($rel && $type && $href) {
-                    $rel = trim($rel->value);
-                    $type = trim($type->value);
-                    $href = trim($href->value);
-
-                    $feedTypes = array(
-                        'application/rss+xml',
-                        'application/atom+xml',
-                    );
-                    if (trim($rel) == 'alternate' && in_array($type, $feedTypes)) {
-                        return $this->resolveURI($href, $base);
-                    }
-                }
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Resolve a possibly relative URL against some absolute base URL
-     * @param string $rel relative or absolute URL
-     * @param string $base absolute URL
-     * @return string absolute URL, or original URL if could not be resolved.
-     */
-    function resolveURI($rel, $base)
-    {
-        require_once "Net/URL2.php";
-        try {
-            $relUrl = new Net_URL2($rel);
-            if ($relUrl->isAbsolute()) {
-                return $rel;
-            }
-            $baseUrl = new Net_URL2($base);
-            $absUrl = $baseUrl->resolve($relUrl);
-            return $absUrl->getURL();
-        } catch (Exception $e) {
-            common_log(LOG_WARNING, 'Unable to resolve relative link "' .
-                $rel . '" against base "' . $base . '": ' . $e->getMessage());
-            return $rel;
-        }
-    }
-}
diff --git a/plugins/FeedSub/feedinfo.php b/plugins/FeedSub/feedinfo.php
deleted file mode 100644 (file)
index b166bd6..0000000
+++ /dev/null
@@ -1,268 +0,0 @@
-<?php
-
-/*
-
-Subscription flow:
-
-    $feedinfo->subscribe()
-        generate random verification token
-            save to verify_token
-        sends a sub request to the hub...
-    
-    feedsub/callback
-        hub sends confirmation back to us via GET
-        We verify the request, then echo back the challenge.
-        On our end, we save the time we subscribed and the lease expiration
-    
-    feedsub/callback
-        hub sends us updates via POST
-        ?
-    
-*/
-
-class FeedDBException extends FeedSubException
-{
-    public $obj;
-
-    function __construct($obj)
-    {
-        parent::__construct('Database insert failure');
-        $this->obj = $obj;
-    }
-}
-
-class Feedinfo extends Memcached_DataObject
-{
-    public $__table = 'feedinfo';
-
-    public $id;
-    public $profile_id;
-
-    public $feeduri;
-    public $homeuri;
-    public $huburi;
-
-    // PuSH subscription data
-    public $verify_token;
-    public $sub_start;
-    public $sub_end;
-
-    public $created;
-    public $lastupdate;
-
-
-    public /*static*/ function staticGet($k, $v=null)
-    {
-        return parent::staticGet(__CLASS__, $k, $v);
-    }
-
-    /**
-     * return table definition for DB_DataObject
-     *
-     * DB_DataObject needs to know something about the table to manipulate
-     * instances. This method provides all the DB_DataObject needs to know.
-     *
-     * @return array array of column definitions
-     */
-
-    function table()
-    {
-        return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
-                     'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
-                     'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
-                     'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
-                     'huburi' =>  DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
-                     'verify_token' => DB_DATAOBJECT_STR,
-                     'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
-                     'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
-                     'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
-                     'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
-    }
-    
-    static function schemaDef()
-    {
-        return array(new ColumnDef('id', 'integer',
-                                   /*size*/ null,
-                                   /*nullable*/ false,
-                                   /*key*/ 'PRI',
-                                   /*default*/ '0',
-                                   /*extra*/ null,
-                                   /*auto_increment*/ true),
-                     new ColumnDef('profile_id', 'integer',
-                                   null, false),
-                     new ColumnDef('feeduri', 'varchar',
-                                   255, false, 'UNI'),
-                     new ColumnDef('homeuri', 'varchar',
-                                   255, false),
-                     new ColumnDef('huburi', 'varchar',
-                                   255, false),
-                     new ColumnDef('verify_token', 'varchar',
-                                   32, true),
-                     new ColumnDef('sub_start', 'datetime',
-                                   null, true),
-                     new ColumnDef('sub_end', 'datetime',
-                                   null, true),
-                     new ColumnDef('created', 'datetime',
-                                   null, false),
-                     new ColumnDef('lastupdate', 'datetime',
-                                   null, false));
-    }
-
-    /**
-     * return key definitions for DB_DataObject
-     *
-     * DB_DataObject needs to know about keys that the table has; this function
-     * defines them.
-     *
-     * @return array key definitions
-     */
-
-    function keys()
-    {
-        return array('id' => 'P'); //?
-    }
-
-    /**
-     * return key definitions for Memcached_DataObject
-     *
-     * Our caching system uses the same key definitions, but uses a different
-     * method to get them.
-     *
-     * @return array key definitions
-     */
-
-    function keyTypes()
-    {
-        return $this->keys();
-    }
-
-    /**
-     * Fetch the StatusNet-side profile for this feed
-     * @return Profile
-     */
-    public function getProfile()
-    {
-        return Profile::staticGet('id', $this->profile_id);
-    }
-
-    /**
-     * @param FeedMunger $munger
-     * @return Feedinfo
-     */
-    public static function ensureProfile($munger)
-    {
-        $feedinfo = $munger->feedinfo();
-
-        $current = self::staticGet('feeduri', $feedinfo->feeduri);
-        if ($current) {
-            // @fixme we should probably update info as necessary
-            return $current;
-        }
-
-        $feedinfo->query('BEGIN');
-
-        try {
-            $profile = $munger->profile();
-            $result = $profile->insert();
-            if (empty($result)) {
-                throw new FeedDBException($profile);
-            }
-
-            $feedinfo->profile_id = $profile->id;
-            $result = $feedinfo->insert();
-            if (empty($result)) {
-                throw new FeedDBException($feedinfo);
-            }
-
-            $feedinfo->query('COMMIT');
-        } catch (FeedDBException $e) {
-            common_log_db_error($e->obj, 'INSERT', __FILE__);
-            $feedinfo->query('ROLLBACK');
-            return false;
-        }
-        return $feedinfo;
-    }
-
-    /**
-     * Send a subscription request to the hub for this feed.
-     * The hub will later send us a confirmation POST to /feedsub/callback.
-     *
-     * @return bool true on success, false on failure
-     */
-    public function subscribe()
-    {
-        // @fixme use the verification token
-        #$token = md5(mt_rand() . ':' . $this->feeduri);
-        #$this->verify_token = $token;
-        #$this->update(); // @fixme
-        
-        try {
-            $callback = common_local_url('feedsubcallback', array('feed' => $this->id));
-            $headers = array('Content-Type: application/x-www-form-urlencoded');
-            $post = array('hub.mode' => 'subscribe',
-                          'hub.callback' => $callback,
-                          'hub.verify' => 'async',
-                          //'hub.verify_token' => $token,
-                          //'hub.lease_seconds' => 0,
-                          'hub.topic' => $this->feeduri);
-            $client = new HTTPClient();
-            $response = $client->post($this->huburi, $headers, $post);
-            if ($response->getStatus() >= 200 && $response->getStatus() < 300) {
-                common_log(LOG_INFO, __METHOD__ . ': sub req ok');
-                return true;
-            } else {
-                common_log(LOG_INFO, __METHOD__ . ': sub req failed');
-                return false;
-            }
-        } catch (Exception $e) {
-            // wtf!
-            common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
-            return false;
-        }
-    }
-
-    /**
-     * Read and post notices for updates from the feed.
-     * Currently assumes that all items in the feed are new,
-     * coming from a PuSH hub.
-     *
-     * @param string $xml source of Atom or RSS feed
-     */
-    public function postUpdates($xml)
-    {
-        common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $xml");
-        require_once "XML/Feed/Parser.php";
-        $feed = new XML_Feed_Parser($xml, false, false, true);
-        $munger = new FeedMunger($feed);
-        
-        $hits = 0;
-        foreach ($feed as $index => $entry) {
-            // @fixme this might sort in wrong order if we get multiple updates
-            
-            $notice = $munger->notice($index);
-            $notice->profile_id = $this->profile_id;
-            
-            // Double-check for oldies
-            // @fixme this could explode horribly for multiple feeds on a blog. sigh
-            $dupe = new Notice();
-            $dupe->uri = $notice->uri;
-            $dupe->find();
-            if ($dupe->fetch()) {
-                common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
-                continue;
-            }
-            
-            if (Event::handle('StartNoticeSave', array(&$notice))) {
-                $id = $notice->insert();
-                Event::handle('EndNoticeSave', array($notice));
-            }
-            $notice->addToInboxes();
-
-            common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\"");
-            $hits++;
-        }
-        if ($hits == 0) {
-            common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
-        }
-    }
-}
diff --git a/plugins/FeedSub/feedinfo.sql b/plugins/FeedSub/feedinfo.sql
deleted file mode 100644 (file)
index e9b53d2..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-CREATE TABLE `feedinfo` (
-  `id` int(11) NOT NULL auto_increment,
-  `profile_id` int(11) NOT NULL,
-  `feeduri` varchar(255) NOT NULL,
-  `homeuri` varchar(255) NOT NULL,
-  `huburi` varchar(255) NOT NULL,
-  `verify_token` varchar(32) default NULL,
-  `sub_start` datetime default NULL,
-  `sub_end` datetime default NULL,
-  `created` datetime NOT NULL,
-  `lastupdate` datetime NOT NULL,
-  PRIMARY KEY  (`id`),
-  UNIQUE KEY `feedinfo_feeduri_idx` (`feeduri`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
diff --git a/plugins/FeedSub/feedmunger.php b/plugins/FeedSub/feedmunger.php
deleted file mode 100644 (file)
index f3618b8..0000000
+++ /dev/null
@@ -1,238 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package FeedSubPlugin
- * @maintainer Brion Vibber <brion@status.net>
- */
-
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-
-class FeedSubPreviewNotice extends Notice
-{
-    protected $fetched = true;
-
-    function __construct($profile)
-    {
-        //parent::__construct(); // uhhh?
-        $this->profile = $profile;
-    }
-    
-    function getProfile()
-    {
-        return $this->profile;
-    }
-    
-    function find()
-    {
-        return true;
-    }
-    
-    function fetch()
-    {
-        $got = $this->fetched;
-        $this->fetched = false;
-        return $got;
-    }
-}
-
-class FeedSubPreviewProfile extends Profile
-{
-    function getAvatar($width, $height=null)
-    {
-        return new FeedSubPreviewAvatar($width, $height);
-    }
-}
-
-class FeedSubPreviewAvatar extends Avatar
-{
-    function displayUrl() {
-        return common_path('plugins/FeedSub/images/48px-Feed-icon.svg.png');
-    }
-}
-
-class FeedMunger
-{
-    /**
-     * @param XML_Feed_Parser $feed
-     */
-    function __construct($feed, $url=null)
-    {
-        $this->feed = $feed;
-        $this->url = $url;
-    }
-    
-    function feedinfo()
-    {
-        $feedinfo = new Feedinfo();
-        $feedinfo->feeduri = $this->url;
-        $feedinfo->homeuri = $this->feed->link;
-        $feedinfo->huburi = $this->getHubLink();
-        return $feedinfo;
-    }
-
-    function getAtomLink($item, $attribs=array())
-    {
-        // XML_Feed_Parser gets confused by multiple <link> elements.
-        $dom = $item->model;
-
-        // Note that RSS feeds would embed an <atom:link> so this should work for both.
-        /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds
-        // <link rel='hub' href='http://pubsubhubbub.appspot.com/'/>
-        $links = $dom->getElementsByTagNameNS('http://www.w3.org/2005/Atom', 'link');
-        for ($i = 0; $i < $links->length; $i++) {
-            $node = $links->item($i);
-            if ($node->hasAttributes()) {
-                $href = $node->attributes->getNamedItem('href');
-                if ($href) {
-                    $matches = 0;
-                    foreach ($attribs as $name => $val) {
-                        $attrib = $node->attributes->getNamedItem($name);
-                        if ($attrib && $attrib->value == $val) {
-                            $matches++;
-                        }
-                    }
-                    if ($matches == count($attribs)) {
-                        return $href->value;
-                    }
-                }
-            }
-        }
-        return false;
-    }
-
-    function getRssLink($item)
-    {
-        // XML_Feed_Parser gets confused by multiple <link> elements.
-        $dom = $item->model;
-
-        // Note that RSS feeds would embed an <atom:link> so this should work for both.
-        /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds
-        // <link rel='hub' href='http://pubsubhubbub.appspot.com/'/>
-        $links = $dom->getElementsByTagName('link');
-        for ($i = 0; $i < $links->length; $i++) {
-            $node = $links->item($i);
-            if (!$node->hasAttributes()) {
-                return $node->textContent;
-            }
-        }
-        return false;
-    }
-
-    function getAltLink($item)
-    {
-        // Check for an atom link...
-        $link = $this->getAtomLink($item, array('rel' => 'alternate', 'type' => 'text/html'));
-        if (!$link) {
-            $link = $this->getRssLink($item);
-        }
-        return $link;
-    }
-
-    function getHubLink()
-    {
-        return $this->getAtomLink($this->feed, array('rel' => 'hub'));
-    }
-
-    function profile($preview=false)
-    {
-        if ($preview) {
-            $profile = new FeedSubPreviewProfile();
-        } else {
-            $profile = new Profile();
-        }
-        
-        // @todo validate/normalize nick?
-        $profile->nickname   = $this->feed->title;
-        $profile->fullname   = $this->feed->title;
-        $profile->homepage   = $this->getAltLink($this->feed);
-        $profile->bio        = $this->feed->description;
-        $profile->profileurl = $this->getAltLink($this->feed);
-        
-        // @todo tags from categories
-        // @todo lat/lon/location?
-
-        return $profile;
-    }
-
-    function notice($index=1, $preview=false)
-    {
-        $entry = $this->feed->getEntryByOffset($index);
-        if (!$entry) {
-            return null;
-        }
-        
-        if ($preview) {
-            $notice = new FeedSubPreviewNotice($this->profile(true));
-            $notice->id = -1;
-        } else {
-            $notice = new Notice();
-        }
-
-        $link = $this->getAltLink($entry);
-        $notice->uri = $link;
-        $notice->url = $link;
-        $notice->content = $this->noticeFromEntry($entry);
-        $notice->rendered = common_render_content($notice->content, $notice);
-        $notice->created = common_sql_date($entry->updated); // @fixme
-        $notice->is_local = Notice::GATEWAY;
-        $notice->source = 'feed';
-        
-        return $notice;
-    }
-
-    /**
-     * @param XML_Feed_Type $entry
-     * @return string notice text, within post size limit
-     */
-    function noticeFromEntry($entry)
-    {
-        $title = $entry->title;
-        $link = $entry->link;
-        
-        // @todo We can get <category> entries like this:
-        // $cats = $entry->getCategory('category', array(0, true));
-        // but it feels like an awful hack. If it's accessible cleanly,
-        // try adding #hashtags from the categories/tags on a post.
-        
-        // @todo Should we force a language here?
-        $format = _m('New post: "%1$s" %2$s');
-        $title = $entry->title;
-        $link = $this->getAltLink($entry);
-        $out = sprintf($format, $title, $link);
-        
-        // Trim link if needed...
-        $max = Notice::maxContent();
-        if (mb_strlen($out) > $max) {
-            $link = common_shorten_url($link);
-            $out = sprintf($format, $title, $link);
-        }
-
-        // Trim title if needed...
-        if (mb_strlen($out) > $max) {
-            $ellipsis = "\xe2\x80\xa6"; // U+2026 HORIZONTAL ELLIPSIS
-            $used = mb_strlen($out) - mb_strlen($title);
-            $available = $max - $used - mb_strlen($ellipsis);
-            $title = mb_substr($title, 0, $available) . $ellipsis;
-            $out = sprintf($format, $title, $link);
-        }
-        
-        return $out;
-    }
-}
diff --git a/plugins/FeedSub/images/24px-Feed-icon.svg.png b/plugins/FeedSub/images/24px-Feed-icon.svg.png
deleted file mode 100644 (file)
index 3172258..0000000
Binary files a/plugins/FeedSub/images/24px-Feed-icon.svg.png and /dev/null differ
diff --git a/plugins/FeedSub/images/48px-Feed-icon.svg.png b/plugins/FeedSub/images/48px-Feed-icon.svg.png
deleted file mode 100644 (file)
index bd1da4f..0000000
Binary files a/plugins/FeedSub/images/48px-Feed-icon.svg.png and /dev/null differ
diff --git a/plugins/FeedSub/images/96px-Feed-icon.svg.png b/plugins/FeedSub/images/96px-Feed-icon.svg.png
deleted file mode 100644 (file)
index bf16571..0000000
Binary files a/plugins/FeedSub/images/96px-Feed-icon.svg.png and /dev/null differ
diff --git a/plugins/FeedSub/images/README b/plugins/FeedSub/images/README
deleted file mode 100644 (file)
index d9379c2..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-Feed icon rendered from http://commons.wikimedia.org/wiki/File:Feed-icon.svg
-
-Originally distributed by the Mozilla Foundation under a MPL/GPL/LGPL tri-license:
-
-http://www.mozilla.org/MPL/boilerplate-1.1/mpl-tri-license-html
diff --git a/plugins/FeedSub/locale/FeedSub.po b/plugins/FeedSub/locale/FeedSub.po
deleted file mode 100644 (file)
index dedc018..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-12-07 20:38-0800\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=CHARSET\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#: tests/gettext-speedtest.php:57 FeedSubPlugin.php:76
-msgid "Feeds"
-msgstr ""
-
-#: FeedSubPlugin.php:77
-msgid "Feed subscription options"
-msgstr ""
-
-#: feedmunger.php:215
-#, php-format
-msgid "New post: \"%1$s\" %2$s"
-msgstr ""
-
-#: actions/feedsubsettings.php:41
-msgid "Feed subscriptions"
-msgstr ""
-
-#: actions/feedsubsettings.php:52
-msgid ""
-"You can subscribe to feeds from other sites; updates will appear in your "
-"personal timeline."
-msgstr ""
-
-#: actions/feedsubsettings.php:96
-msgid "Subscribe"
-msgstr ""
-
-#: actions/feedsubsettings.php:98
-msgid "Continue"
-msgstr ""
-
-#: actions/feedsubsettings.php:151
-msgid "Empty feed URL!"
-msgstr ""
-
-#: actions/feedsubsettings.php:161
-msgid "Invalid URL or could not reach server."
-msgstr ""
-
-#: actions/feedsubsettings.php:164
-msgid "Cannot read feed; server returned error."
-msgstr ""
-
-#: actions/feedsubsettings.php:167
-msgid "Cannot read feed; server returned an empty page."
-msgstr ""
-
-#: actions/feedsubsettings.php:170
-msgid "Bad HTML, could not find feed link."
-msgstr ""
-
-#: actions/feedsubsettings.php:173
-msgid "Could not find a feed linked from this URL."
-msgstr ""
-
-#: actions/feedsubsettings.php:176
-msgid "Not a recognized feed type."
-msgstr ""
-
-#: actions/feedsubsettings.php:180
-msgid "Bad feed URL."
-msgstr ""
-
-#: actions/feedsubsettings.php:188
-msgid "Feed is not PuSH-enabled; cannot subscribe."
-msgstr ""
-
-#: actions/feedsubsettings.php:208
-msgid "Feed subscription failed! Bad response from hub."
-msgstr ""
-
-#: actions/feedsubsettings.php:218
-msgid "Already subscribed!"
-msgstr ""
-
-#: actions/feedsubsettings.php:220
-msgid "Feed subscribed!"
-msgstr ""
-
-#: actions/feedsubsettings.php:222
-msgid "Feed subscription failed!"
-msgstr ""
-
-#: actions/feedsubsettings.php:231
-msgid "Previewing feed:"
-msgstr ""
diff --git a/plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po b/plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po
deleted file mode 100644 (file)
index f17dfa5..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-12-07 14:14-0800\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#: FeedSubPlugin.php:77
-msgid "Feeds"
-msgstr "Flux"
-
-#: FeedSubPlugin.php:78
-msgid "Feed subscription options"
-msgstr "Préférences pour abonnement flux"
-
-#: feedmunger.php:215
-#, php-format
-msgid "New post: \"%1$s\" %2$s"
-msgstr "Nouveau: \"%1$s\" %2$s"
-
-#: actions/feedsubsettings.php:41
-msgid "Feed subscriptions"
-msgstr "Abonnements aux fluxes"
-
-#: actions/feedsubsettings.php:52
-msgid ""
-"You can subscribe to feeds from other sites; updates will appear in your "
-"personal timeline."
-msgstr ""
-"Abonner aux fluxes RSS ou Atom des autres sites web; les temps se trouverair"
-"en votre flux personnel."
-
-#: actions/feedsubsettings.php:96
-msgid "Subscribe"
-msgstr "Abonner"
-
-#: actions/feedsubsettings.php:98
-msgid "Continue"
-msgstr "Prochaine"
-
-#: actions/feedsubsettings.php:151
-msgid "Empty feed URL!"
-msgstr ""
-
-#: actions/feedsubsettings.php:161
-msgid "Invalid URL or could not reach server."
-msgstr ""
-
-#: actions/feedsubsettings.php:164
-msgid "Cannot read feed; server returned error."
-msgstr ""
-
-#: actions/feedsubsettings.php:167
-msgid "Cannot read feed; server returned an empty page."
-msgstr ""
-
-#: actions/feedsubsettings.php:170
-msgid "Bad HTML, could not find feed link."
-msgstr ""
-
-#: actions/feedsubsettings.php:173
-msgid "Could not find a feed linked from this URL."
-msgstr ""
-
-#: actions/feedsubsettings.php:176
-msgid "Not a recognized feed type."
-msgstr ""
-
-#: actions/feedsubsettings.php:180
-msgid "Bad feed URL."
-msgstr ""
-
-#: actions/feedsubsettings.php:188
-msgid "Feed is not PuSH-enabled; cannot subscribe."
-msgstr ""
-
-#: actions/feedsubsettings.php:208
-msgid "Feed subscription failed! Bad response from hub."
-msgstr ""
-
-#: actions/feedsubsettings.php:218
-msgid "Already subscribed!"
-msgstr ""
-
-#: actions/feedsubsettings.php:220
-msgid "Feed subscribed!"
-msgstr ""
-
-#: actions/feedsubsettings.php:222
-msgid "Feed subscription failed!"
-msgstr ""
-
-#: actions/feedsubsettings.php:231
-msgid "Previewing feed:"
-msgstr ""
diff --git a/plugins/FeedSub/tests/FeedDiscoveryTest.php b/plugins/FeedSub/tests/FeedDiscoveryTest.php
deleted file mode 100644 (file)
index 1c52497..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-<?php
-
-if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
-    print "This script must be run from the command line\n";
-    exit();
-}
-
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
-define('STATUSNET', true);
-define('LACONICA', true);
-
-require_once INSTALLDIR . '/lib/common.php';
-require_once INSTALLDIR . '/plugins/FeedSub/feedsub.php';
-
-class FeedDiscoveryTest extends PHPUnit_Framework_TestCase
-{
-    /**
-     * @dataProvider provider
-     *
-     */
-    public function testProduction($url, $html, $expected)
-    {
-        $sub = new FeedDiscovery();
-        $url = $sub->discoverFromHTML($url, $html);
-        $this->assertEquals($expected, $url);
-    }
-
-    static public function provider()
-    {
-        $sampleHeader = <<<END
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
-
-<head profile="http://gmpg.org/xfn/11">
-<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
-
-<title>leŭksman  </title>
-
-<meta name="generator" content="WordPress 2.8.6" /> <!-- leave this for stats -->
-
-<link rel="stylesheet" href="http://leuksman.com/log/wp-content/themes/leuksman/style.css" type="text/css" media="screen" />
-<link rel="alternate" type="application/rss+xml" title="leŭksman RSS Feed" href="http://leuksman.com/log/feed/" />
-<link rel="pingback" href="http://leuksman.com/log/xmlrpc.php" />
-
-<meta name="viewport" content="width = 640" />
-
-<xmeta name="viewport" content="initial-scale=2.3, user-scalable=no" />
-
-<style type="text/css" media="screen">
-
-       #page { background: url("http://leuksman.com/log/wp-content/themes/leuksman/images/kubrickbg.jpg") repeat-y top; border: none; }
-
-</style>
-
-<link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://leuksman.com/log/xmlrpc.php?rsd" />
-<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="http://leuksman.com/log/wp-includes/wlwmanifest.xml" /> 
-<link rel='index' title='leŭksman' href='http://leuksman.com/log' />
-<meta name="generator" content="WordPress 2.8.6" />
-</head>
-<body>
-</body>
-</html>
-END;
-        return array(
-                     array('http://example.com/',
-                           '<html><link rel="alternate" href="http://example.com/feed/rss" type="application/rss+xml">',
-                           'http://example.com/feed/rss'),
-                     array('http://example.com/atom',
-                           '<html><link rel="alternate" href="http://example.com/feed/atom" type="application/atom+xml">',
-                           'http://example.com/feed/atom'),
-                     array('http://example.com/empty',
-                           '<html><link rel="alternate" href="http://example.com/index.pdf" type="application/pdf">',
-                           false),
-                     array('http://example.com/tagsoup',
-                           '<body><pre><LINK rel=alternate hRef=http://example.com/feed/rss type=application/rss+xml><fnork',
-                           'http://example.com/feed/rss'),
-                     // 'rel' attribute must be lowercase, alone per http://www.rssboard.org/rss-autodiscovery
-                     array('http://example.com/tagsoup2',
-                           '<body><pre><LINK rel=" feeders    alternate 467" hRef=http://example.com/feed/rss type=application/rss+xml><fnork',
-                           false),
-                     array('http://example.com/tagsoup3',
-                           '<body><pre><LINK rel=ALTERNATE hRef=http://example.com/feed/rss type=application/rss+xml><fnork',
-                           false),
-                     array('http://example.com/relative/link1',
-                           '<html><link rel="alternate" href="/feed/rss" type="application/rss+xml">',
-                           'http://example.com/feed/rss'),
-                     array('http://example.com/relative/link2',
-                           '<html><link rel="alternate" href="../feed/rss" type="application/rss+xml">',
-                           'http://example.com/feed/rss'),
-                     array('http://example.com/relative/link3',
-                           '<html><link rel="alternate" href="http:/feed/rss" type="application/rss+xml">',
-                           'http://example.com/feed/rss'),
-                     array('http://example.com/base/link1',
-                           '<html><link rel="alternate" href="/feed/rss" type="application/rss+xml"><base href="http://target.example.com/">',
-                           'http://target.example.com/feed/rss'),
-                     array('http://example.com/base/link2',
-                           '<html><link rel="alternate" href="feed/rss" type="application/rss+xml"><base href="http://target.example.com/">',
-                           'http://target.example.com/feed/rss'),
-                     array('http://example.com/base/link3',
-                           '<html><link rel="alternate" href="http:/feed/rss" type="application/rss+xml"><base href="http://target.example.com/">',
-                           'http://target.example.com/feed/rss'),
-                     // Trick question! There's a <base> but no href on it
-                     array('http://example.com/relative/fauxbase',
-                           '<html><link rel="alternate" href="../feed/rss" type="application/rss+xml"><base target="top">',
-                           'http://example.com/feed/rss'),
-                     // Actual WordPress blog header example
-                     array('http://leuksman.com/log/',
-                           $sampleHeader,
-                           'http://leuksman.com/log/feed/'));
-    }
-}
diff --git a/plugins/FeedSub/tests/FeedMungerTest.php b/plugins/FeedSub/tests/FeedMungerTest.php
deleted file mode 100644 (file)
index 0ce24c9..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-<?php
-
-if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
-    print "This script must be run from the command line\n";
-    exit();
-}
-
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
-define('STATUSNET', true);
-define('LACONICA', true);
-
-require_once INSTALLDIR . '/lib/common.php';
-require_once INSTALLDIR . '/plugins/FeedSub/feedsub.php';
-
-require_once 'XML/Feed/Parser.php';
-
-class FeedMungerTest extends PHPUnit_Framework_TestCase
-{
-    /**
-     * @dataProvider profileProvider
-     *
-     */
-    public function testProfiles($xml, $expected)
-    {
-        $feed = new XML_Feed_Parser($xml, false, false, true);
-        
-        $munger = new FeedMunger($feed);
-        $profile = $munger->profile();
-
-        foreach ($expected as $field => $val) {
-            $this->assertEquals($expected[$field], $profile->$field, "profile->$field");
-        }
-    }
-
-    static public function profileProvider()
-    {
-        return array(
-                     array(self::samplefeed(),
-                           array('nickname' => 'leŭksman', // @todo does this need to be asciified?
-                                 'fullname' => 'leŭksman',
-                                 'bio' => 'reticula, electronica, & oddities',
-                                 'homepage' => 'http://leuksman.com/log')));
-    }
-
-    /**
-     * @dataProvider noticeProvider
-     *
-     */
-    public function testNotices($xml, $entryIndex, $expected)
-    {
-        $feed = new XML_Feed_Parser($xml, false, false, true);
-        $entry = $feed->getEntryByOffset($entryIndex);
-
-        $munger = new FeedMunger($feed);
-        $notice = $munger->noticeFromEntry($entry);
-
-        $this->assertTrue(mb_strlen($notice) <= Notice::maxContent());
-        $this->assertEquals($expected, $notice);
-    }
-
-    static public function noticeProvider()
-    {
-        return array(
-                     array('<rss version="2.0"><channel><item><title>A fairly short title</title><link>http://example.com/short/link</link></item></channel></rss>', 0,
-                           'New post: "A fairly short title" http://example.com/short/link'),
-                     // Requires URL shortening ...
-                     array('<rss version="2.0"><channel><item><title>A fairly short title</title><link>http://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh</link></item></channel></rss>', 0,
-                           'New post: "A fairly short title" http://ur1.ca/g2o1'),
-                     array('<rss version="2.0"><channel><item><title>A fairly long title in this case, which will have to get cut down at some point alongside its very long link. Really who even makes titles this long? It\'s just ridiculous imo...</title><link>http://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh</link></item></channel></rss>', 0,
-                           'New post: "A fairly long title in this case, which will have to get cut down at some point alongside its very long li…" http://ur1.ca/g2o1'),
-                     // Some real sample feeds
-                     array(self::samplefeed(), 0,
-                           'New post: "Compiling PHP on Snow Leopard" http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/'),
-                     array(self::samplefeedBlogspot(), 0,
-                           'New post: "I love posting" http://briontest.blogspot.com/2009/11/i-love-posting.html'),
-                     array(self::samplefeedBlogspot(), 1,
-                           'New post: "Hey dude" http://briontest.blogspot.com/2009/11/hey-dude.html'),
-        );
-    }
-
-    static protected function samplefeed()
-    {
-        $xml = '<' . '?xml version="1.0" encoding="UTF-8"?' . ">\n";
-        $samplefeed = $xml . <<<END
-<rss version="2.0"
-       xmlns:content="http://purl.org/rss/1.0/modules/content/"
-       xmlns:wfw="http://wellformedweb.org/CommentAPI/"
-       xmlns:dc="http://purl.org/dc/elements/1.1/"
-       xmlns:atom="http://www.w3.org/2005/Atom"
-       xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
-       xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
-       >
-
-<channel>
-       <title>leŭksman</title>
-       <atom:link href="http://leuksman.com/log/feed/" rel="self" type="application/rss+xml" />
-       <link>http://leuksman.com/log</link>
-       <description>reticula, electronica, &#38; oddities</description>
-
-       <lastBuildDate>Thu, 12 Nov 2009 17:44:42 +0000</lastBuildDate>
-       <generator>http://wordpress.org/?v=2.8.6</generator>
-       <language>en</language>
-       <sy:updatePeriod>hourly</sy:updatePeriod>
-       <sy:updateFrequency>1</sy:updateFrequency>
-                       <item>
-
-               <title>Compiling PHP on Snow Leopard</title>
-               <link>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/</link>
-               <comments>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/#comments</comments>
-               <pubDate>Thu, 12 Nov 2009 17:44:42 +0000</pubDate>
-               <dc:creator>brion</dc:creator>
-                               <category><![CDATA[apple]]></category>
-
-               <category><![CDATA[devel]]></category>
-
-               <guid isPermaLink="false">http://leuksman.com/log/?p=649</guid>
-               <description><![CDATA[If you&#8217;ve been having trouble compiling your own PHP installations on Mac OS X 10.6, here&#8217;s the secret to making it not suck! After running the configure script, edit the generated Makefile and make these fixes:
-
-Find the EXTRA_LIBS definition and add -lresolv to the end
-Find the EXE_EXT definition and remove .dSYM
-
-Standard make and make install [...]]]></description>
-                       <content:encoded><![CDATA[<p>If you&#8217;ve been having trouble compiling your own PHP installations on Mac OS X 10.6, here&#8217;s the secret to making it not suck! After running the configure script, edit the generated Makefile and make these fixes:</p>
-<ul>
-<li>Find the <strong>EXTRA_LIBS</strong> definition and add <strong>-lresolv</strong> to the end</li>
-<li>Find the <strong>EXE_EXT</strong> definition and remove <strong>.dSYM</strong></li>
-</ul>
-<p>Standard make and make install should work from here&#8230;</p>
-<p>For reference, here&#8217;s the whole configure line I currently use; MySQL is installed from the downloadable installer; other deps from MacPorts:</p>
-<p>&#8216;./configure&#8217; &#8216;&#8211;prefix=/opt/php52&#8242; &#8216;&#8211;with-mysql=/usr/local/mysql&#8217; &#8216;&#8211;with-zlib&#8217; &#8216;&#8211;with-bz2&#8242; &#8216;&#8211;enable-mbstring&#8217; &#8216;&#8211;enable-exif&#8217; &#8216;&#8211;enable-fastcgi&#8217; &#8216;&#8211;with-xmlrpc&#8217; &#8216;&#8211;with-xsl&#8217; &#8216;&#8211;with-readline=/opt/local&#8217; &#8211;without-iconv &#8211;with-gd &#8211;with-png-dir=/opt/local &#8211;with-jpeg-dir=/opt/local &#8211;with-curl &#8211;with-gettext=/opt/local &#8211;with-mysqli=/usr/local/mysql/bin/mysql_config &#8211;with-tidy=/opt/local &#8211;enable-pcntl &#8211;with-openssl</p>
-]]></content:encoded>
-                       <wfw:commentRss>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/feed/</wfw:commentRss>
-               <slash:comments>0</slash:comments>
-               </item>
-</channel>
-</rss>
-END;
-        return $samplefeed;
-    }
-    
-    static protected function samplefeedBlogspot()
-    {
-        return <<<END
-<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss'><id>tag:blogger.com,1999:blog-7780083508531697167</id><updated>2009-11-19T12:56:11.233-08:00</updated><title type='text'>Brion's Cool Test Blog</title><subtitle type='html'></subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>2</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-7780083508531697167.post-8456671879000290677</id><published>2009-11-19T12:55:00.000-08:00</published><updated>2009-11-19T12:56:11.241-08:00</updated><title type='text'>I love posting</title><content type='html'>It's pretty awesome, if you like that sort of thing.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7780083508531697167-8456671879000290677?l=briontest.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/8456671879000290677/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://briontest.blogspot.com/2009/11/i-love-posting.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8456671879000290677'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8456671879000290677'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/2009/11/i-love-posting.html' title='I love posting'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='05912464053145602436'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7780083508531697167.post-8202296917897346633</id><published>2009-11-18T13:52:00.001-08:00</published><updated>2009-11-18T13:52:48.444-08:00</updated><title type='text'>Hey dude</title><content type='html'>testingggggggggg&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7780083508531697167-8202296917897346633?l=briontest.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/8202296917897346633/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://briontest.blogspot.com/2009/11/hey-dude.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8202296917897346633'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8202296917897346633'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/2009/11/hey-dude.html' title='Hey dude'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='05912464053145602436'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry></feed>
-END;
-    }
-}
diff --git a/plugins/FeedSub/tests/gettext-speedtest.php b/plugins/FeedSub/tests/gettext-speedtest.php
deleted file mode 100644 (file)
index 8bbdf5e..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-
-if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
-    print "This script must be run from the command line\n";
-    exit();
-}
-
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
-define('STATUSNET', true);
-define('LACONICA', true);
-
-require_once INSTALLDIR . '/scripts/commandline.inc';
-require_once INSTALLDIR . '/extlib/php-gettext/gettext.inc';
-
-common_init_locale("en_US");
-common_init_locale('fr');
-
-
-putenv("LANG=fr");
-putenv("LANGUAGE=fr");
-setlocale('fr.utf8');
-_setlocale('fr.utf8');
-
-_bindtextdomain("statusnet", INSTALLDIR . '/locale');
-_bindtextdomain("FeedSub", INSTALLDIR . '/plugins/FeedSub/locale');
-
-$times = 10000;
-$delta = array();
-
-$start = microtime(true);
-for($i = 0; $i < $times; $i++) {
-    $result = _("Send");
-}
-$delta["_"] = array((microtime(true) - $start) / $times, $result);
-
-$start = microtime(true);
-for($i = 0; $i < $times; $i++) {
-    $result = __("Send");
-}
-$delta["__"] = array((microtime(true) - $start) / $times, $result);
-
-$start = microtime(true);
-for($i = 0; $i < $times; $i++) {
-    $result = dgettext("FeedSub", "Feeds");
-}
-$delta["dgettext"] = array((microtime(true) - $start) / $times, $result);
-
-$start = microtime(true);
-for($i = 0; $i < $times; $i++) {
-    $result = _dgettext("FeedSub", "Feeds");
-}
-$delta["_dgettext"] = array((microtime(true) - $start) / $times, $result);
-
-
-$start = microtime(true);
-for($i = 0; $i < $times; $i++) {
-    $result = _m("Feeds");
-}
-$delta["_m"] = array((microtime(true) - $start) / $times, $result);
-
-
-$start = microtime(true);
-for($i = 0; $i < $times; $i++) {
-    $result = fake("Feeds");
-}
-$delta["fake"] = array((microtime(true) - $start) / $times, $result);
-
-foreach ($delta as $func => $bits) {
-    list($time, $result) = $bits;
-    $ms = $time * 1000.0;
-    printf("%10s %2.4fms %s\n", $func, $ms, $result);
-}
-
-
-function fake($str) {
-    return $str;
-}
-
index 93a0583fe8e703527bc715066ffbf5c4c891a214..c3ca5c135928e52e9c4d5a897f2b030f227a84f1 100644 (file)
@@ -115,7 +115,7 @@ class MemcachePlugin extends Plugin
      *
      * @param string  &$key     in; Key to use for lookups
      * @param mixed   &$value   in; Value to associate
-     * @param integer &$flag    in; Flag (passed through to Memcache)
+     * @param integer &$flag    in; Flag empty or Cache::COMPRESSED
      * @param integer &$expiry  in; Expiry (passed through to Memcache)
      * @param boolean &$success out; Whether the set was successful
      *
@@ -128,7 +128,7 @@ class MemcachePlugin extends Plugin
         if ($expiry === null) {
             $expiry = $this->defaultExpiry;
         }
-        $success = $this->_conn->set($key, $value, $flag, $expiry);
+        $success = $this->_conn->set($key, $value, $this->flag(intval($flag)), $expiry);
         Event::handle('EndCacheSet', array($key, $value, $flag,
                                            $expiry));
         return false;
@@ -228,6 +228,20 @@ class MemcachePlugin extends Plugin
         }
     }
 
+    /**
+     * Translate general flags to Memcached-specific flags
+     * @param int $flag
+     * @return int
+     */
+    protected function flag($flag)
+    {
+        $out = 0;
+        if ($flag & Cache::COMPRESSED == Cache::COMPRESSED) {
+            $out |= MEMCACHE_COMPRESSED;
+        }
+        return $out;
+    }
+
     function onPluginVersion(&$versions)
     {
         $versions[] = array('name' => 'Memcache',
index 5c913836dccd7db6f6375e88bf24626ee596c8d2..cd2531fa727a8f3b7d4d2fd41ed645a8f3d4dc7d 100644 (file)
@@ -240,6 +240,8 @@ class MobileProfilePlugin extends WAP20Plugin
             return true;
         }
 
+        $action->cssLink('css/display.css');
+
         if (file_exists(Theme::file('css/mp-screen.css'))) {
             $action->cssLink('css/mp-screen.css', null, 'screen');
         } else {
@@ -256,6 +258,14 @@ class MobileProfilePlugin extends WAP20Plugin
     }
 
 
+    function onStartShowUAStyles($action) {
+        if (!$this->serveMobile) {
+            return true;
+        }
+
+        return false;
+    }
+
     function onStartShowHeader($action)
     {
         if (!$this->serveMobile) {
index 04fa5fb0021054e871bfd81673b2e15dd0a580eb..0fc801612be9d4848bc4318057c5ea29741d6fef 100644 (file)
@@ -1,15 +1,12 @@
 /** theme: mobile profile screen
  *
  * @package   StatusNet
- * @author Sarven Capadisli <csarven@status.net>
+ * @author    Sarven Capadisli <csarven@status.net>
  * @copyright 2009 StatusNet, Inc.
  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  * @link      http://status.net/
  */
 
-@import url(../../theme/base/css/display.css);
-@import url(../../theme/identica/css/display.css);
-
 #wrap {
 min-width:0;
 max-width:100%;
diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php
new file mode 100644 (file)
index 0000000..b6c9fa1
--- /dev/null
@@ -0,0 +1,305 @@
+<?php
+/*
+StatusNet Plugin: 0.9
+Plugin Name: FeedSub
+Plugin URI: http://status.net/wiki/Feed_subscription
+Description: FeedSub allows subscribing to real-time updates from external feeds supporting PubHubSubbub protocol.
+Version: 0.1
+Author: Brion Vibber <brion@status.net>
+Author URI: http://status.net/
+*/
+
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package FeedSubPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+define('FEEDSUB_SERVICE', 100); // fixme -- avoid hardcoding these?
+
+// We bundle the XML_Parse_Feed library...
+set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib');
+
+class FeedSubException extends Exception
+{
+}
+
+class OStatusPlugin extends Plugin
+{
+    /**
+     * Hook for RouterInitialized event.
+     *
+     * @param Net_URL_Mapper $m path-to-action mapper
+     * @return boolean hook return
+     */
+    function onRouterInitialized($m)
+    {
+        // Discovery actions
+        $m->connect('.well-known/host-meta',
+                    array('action' => 'hostmeta'));
+        $m->connect('main/webfinger',
+                    array('action' => 'webfinger'));
+        $m->connect('main/ostatus',
+                    array('action' => 'ostatusinit'));
+        $m->connect('main/ostatus?nickname=:nickname',
+                  array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
+        $m->connect('main/ostatussub',
+                    array('action' => 'ostatussub'));
+        $m->connect('main/ostatussub',
+                    array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
+
+        // PuSH actions
+        $m->connect('main/push/hub', array('action' => 'pushhub'));
+
+        $m->connect('main/push/callback/:feed',
+                    array('action' => 'pushcallback'),
+                    array('feed' => '[0-9]+'));
+        $m->connect('settings/feedsub',
+                    array('action' => 'feedsubsettings'));
+
+        // Salmon endpoint
+        $m->connect('main/salmon/user/:id',
+                    array('action' => 'salmon'),
+                    array('id' => '[0-9]+'));
+        $m->connect('main/salmon/group/:id',
+                    array('action' => 'salmongroup'),
+                    array('id' => '[0-9]+'));
+        return true;
+    }
+
+    /**
+     * Set up queue handlers for outgoing hub pushes
+     * @param QueueManager $qm
+     * @return boolean hook return
+     */
+    function onEndInitializeQueueManager(QueueManager $qm)
+    {
+        $qm->connect('hubverify', 'HubVerifyQueueHandler');
+        $qm->connect('hubdistrib', 'HubDistribQueueHandler');
+        $qm->connect('hubout', 'HubOutQueueHandler');
+        return true;
+    }
+
+    /**
+     * Put saved notices into the queue for pubsub distribution.
+     */
+    function onStartEnqueueNotice($notice, &$transports)
+    {
+        $transports[] = 'hubdistrib';
+        return true;
+    }
+
+    /**
+     * Set up a PuSH hub link to our internal link for canonical timeline
+     * Atom feeds for users and groups.
+     */
+    function onStartApiAtom(AtomNoticeFeed $feed)
+    {
+        $id = null;
+
+        if ($feed instanceof AtomUserNoticeFeed) {
+            $salmonAction = 'salmon';
+            $id = $feed->getUser()->id;
+        } else if ($feed instanceof AtomGroupNoticeFeed) {
+            $salmonAction = 'salmongroup';
+            $id = $feed->getGroup()->id;
+        } else {
+            return;
+        }
+
+       if (!empty($id)) {
+            $hub = common_config('ostatus', 'hub');
+            if (empty($hub)) {
+                // Updates will be handled through our internal PuSH hub.
+                $hub = common_local_url('pushhub');
+            }
+            $feed->addLink($hub, array('rel' => 'hub'));
+
+            // Also, we'll add in the salmon link
+            $salmon = common_local_url($salmonAction, array('id' => $id));
+            $feed->addLink($salmon, array('rel' => 'salmon'));
+        }
+    }
+
+    /**
+     * Add the feed settings page to the Connect Settings menu
+     *
+     * @param Action &$action The calling page
+     *
+     * @return boolean hook return
+     */
+    function onEndConnectSettingsNav(&$action)
+    {
+        $action_name = $action->trimmed('action');
+
+        $action->menuItem(common_local_url('feedsubsettings'),
+                          _m('Feeds'),
+                          _m('Feed subscription options'),
+                          $action_name === 'feedsubsettings');
+
+        return true;
+    }
+
+    /**
+     * Automatically load the actions and libraries used by the plugin
+     *
+     * @param Class $cls the class
+     *
+     * @return boolean hook return
+     *
+     */
+    function onAutoload($cls)
+    {
+        $base = dirname(__FILE__);
+        $lower = strtolower($cls);
+        $files = array("$base/classes/$cls.php",
+                       "$base/lib/$lower.php");
+        if (substr($lower, -6) == 'action') {
+            $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php";
+        }
+        foreach ($files as $file) {
+            if (file_exists($file)) {
+                include_once $file;
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Add in an OStatus subscribe button
+     */
+    function onStartProfileRemoteSubscribe($output, $profile)
+    {
+        $cur = common_current_user();
+
+        if (empty($cur)) {
+            // Add an OStatus subscribe
+            $output->elementStart('li', 'entity_subscribe');
+            $url = common_local_url('ostatusinit',
+                                    array('nickname' => $profile->nickname));
+            $output->element('a', array('href' => $url,
+                                        'class' => 'entity_remote_subscribe'),
+                                _m('Subscribe'));
+
+            $output->elementEnd('li');
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if we've got remote replies to send via Salmon.
+     *
+     * @fixme push webfinger lookup & sending to a background queue
+     * @fixme also detect short-form name for remote subscribees where not ambiguous
+     */
+    function onEndNoticeSave($notice)
+    {
+        $count = preg_match_all('/(\w+\.)*\w+@(\w+\.)*\w+(\w+\-\w+)*\.\w+/', $notice->content, $matches);
+        if ($count) {
+            foreach ($matches[0] as $webfinger) {
+
+                // FIXME: look up locally first
+
+                // Check to see if we've got an actual webfinger
+                $w = new Webfinger;
+
+                $endpoint_uri = '';
+
+                $result = $w->lookup($webfinger);
+                if (empty($result)) {
+                    continue;
+                }
+
+                foreach ($result->links as $link) {
+                    if ($link['rel'] == 'salmon') {
+                        $endpoint_uri = $link['href'];
+                    }
+                }
+
+                if (empty($endpoint_uri)) {
+                    continue;
+                }
+
+                // FIXME: this needs to go out in a queue handler
+
+                $xml = '<?xml version="1.0" encoding="UTF-8" ?>';
+                $xml .= $notice->asAtomEntry();
+
+                $salmon = new Salmon();
+                $salmon->post($endpoint_uri, $xml);
+            }
+        }
+    }
+
+    /**
+     * Garbage collect unused feeds on unsubscribe
+     */
+    function onEndUnsubscribe($user, $other)
+    {
+        $profile = Ostatus_profile::staticGet('profile_id', $other->id);
+        if ($feed) {
+            $sub = new Subscription();
+            $sub->subscribed = $other->id;
+            $sub->limit(1);
+            if (!$sub->find(true)) {
+                common_log(LOG_INFO, "Unsubscribing from now-unused feed $feed->feeduri on hub $feed->huburi");
+                $profile->unsubscribe();
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Make sure necessary tables are filled out.
+     */
+    function onCheckSchema() {
+        $schema = Schema::get();
+        $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
+        $schema->ensureTable('hubsub', HubSub::schemaDef());
+        return true;
+    }
+
+    function onEndShowStatusNetStyles($action) {
+        $action->cssLink(common_path('plugins/OStatus/theme/base/css/ostatus.css'));
+        return true;
+    }
+
+    function onEndShowStatusNetScripts($action) {
+        $action->script(common_path('plugins/OStatus/js/ostatus.js'));
+        return true;
+    }
+
+    function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
+    {
+        if ($notice->source == 'ostatus') {
+            $bits = parse_url($notice->uri);
+            $domain = $bits['host'];
+
+            $name = $domain;
+            $url = $notice->uri;
+            $title = sprintf(_m("Sent from %s via OStatus"), $domain);
+            return false;
+        }
+    }
+}
diff --git a/plugins/OStatus/README b/plugins/OStatus/README
new file mode 100644 (file)
index 0000000..cbf3adb
--- /dev/null
@@ -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 <link rel="alternate" href="..."/> but don't have the type
+  (such as http://atomgen.appspot.com/feed/5 demo feed); currently it's not recognized and we end up with the feed's master URI
+* make it easier to see what you're subscribed to and unsub from things
+* saner treatment of fullname/nickname?
+* make use of tags/categories from feeds
+* update feed profile data when it changes
+* XML_Feed_Parser has major problems with category and link tags; consider replacing?
diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php
new file mode 100644 (file)
index 0000000..6933c9b
--- /dev/null
@@ -0,0 +1,269 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package FeedSubPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class FeedSubSettingsAction extends ConnectSettingsAction
+{
+    protected $feedurl;
+    protected $preview;
+    protected $munger;
+
+    /**
+     * Title of the page
+     *
+     * @return string Title of the page
+     */
+
+    function title()
+    {
+        return _m('Feed subscriptions');
+    }
+
+    /**
+     * Instructions for use
+     *
+     * @return instructions for use
+     */
+
+    function getInstructions()
+    {
+        return _m('You can subscribe to feeds from other sites; ' .
+                  'updates will appear in your personal timeline.');
+    }
+
+    /**
+     * Content area of the page
+     *
+     * Shows a form for associating a Twitter account with this
+     * StatusNet account. Also lets the user set preferences.
+     *
+     * @return void
+     */
+
+    function showContent()
+    {
+        $user = common_current_user();
+
+        $profile = $user->getProfile();
+
+        $fuser = null;
+
+        $flink = Foreign_link::getByUserID($user->id, FEEDSUB_SERVICE);
+
+        if (!empty($flink)) {
+            $fuser = $flink->getForeignUser();
+        }
+
+        $this->elementStart('form', array('method' => 'post',
+                                          'id' => 'form_settings_feedsub',
+                                          'class' => 'form_settings',
+                                          'action' =>
+                                          common_local_url('feedsubsettings')));
+
+        $this->hidden('token', common_session_token());
+
+        $this->elementStart('fieldset', array('id' => 'settings_feeds'));
+
+        $this->elementStart('ul', 'form_data');
+        $this->elementStart('li', array('id' => 'settings_twitter_login_button'));
+        $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
+        $this->elementEnd('li');
+        $this->elementEnd('ul');
+
+        if ($this->preview) {
+            $this->submit('subscribe', _m('Subscribe'));
+        } else {
+            $this->submit('validate', _m('Continue'));
+        }
+
+        $this->elementEnd('fieldset');
+
+        $this->elementEnd('form');
+
+        if ($this->preview) {
+            $this->previewFeed();
+        }
+    }
+
+    /**
+     * Handle posts to this form
+     *
+     * Based on the button that was pressed, muxes out to other functions
+     * to do the actual task requested.
+     *
+     * All sub-functions reload the form with a message -- success or failure.
+     *
+     * @return void
+     */
+
+    function handlePost()
+    {
+        // CSRF protection
+        $token = $this->trimmed('token');
+        if (!$token || $token != common_session_token()) {
+            $this->showForm(_('There was a problem with your session token. '.
+                              'Try again, please.'));
+            return;
+        }
+
+        if ($this->arg('validate')) {
+            $this->validateAndPreview();
+        } else if ($this->arg('subscribe')) {
+            $this->saveFeed();
+        } else {
+            $this->showForm(_('Unexpected form submission.'));
+        }
+    }
+
+    /**
+     * Set up and add a feed
+     *
+     * @return boolean true if feed successfully read
+     * Sends you back to input form if not.
+     */
+    function validateFeed()
+    {
+        $feedurl = trim($this->arg('feedurl'));
+        
+        if ($feedurl == '') {
+            $this->showForm(_m('Empty feed URL!'));
+            return;
+        }
+        $this->feedurl = $feedurl;
+        
+        // Get the canonical feed URI and check it
+        try {
+            $discover = new FeedDiscovery();
+            $uri = $discover->discoverFromURL($feedurl);
+        } catch (FeedSubBadURLException $e) {
+            $this->showForm(_m('Invalid URL or could not reach server.'));
+            return false;
+        } catch (FeedSubBadResponseException $e) {
+            $this->showForm(_m('Cannot read feed; server returned error.'));
+            return false;
+        } catch (FeedSubEmptyException $e) {
+            $this->showForm(_m('Cannot read feed; server returned an empty page.'));
+            return false;
+        } catch (FeedSubBadHTMLException $e) {
+            $this->showForm(_m('Bad HTML, could not find feed link.'));
+            return false;
+        } catch (FeedSubNoFeedException $e) {
+            $this->showForm(_m('Could not find a feed linked from this URL.'));
+            return false;
+        } catch (FeedSubUnrecognizedTypeException $e) {
+            $this->showForm(_m('Not a recognized feed type.'));
+            return false;
+        } catch (FeedSubException $e) {
+            // Any new ones we forgot about
+            $this->showForm(_m('Bad feed URL.'));
+            return false;
+        }
+        
+        $this->munger = $discover->feedMunger();
+        $this->profile = $this->munger->ostatusProfile();
+
+        if ($this->profile->huburi == '' && !common_config('feedsub', 'nohub')) {
+            $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
+            return false;
+        }
+        
+        return true;
+    }
+
+    function saveFeed()
+    {
+        if ($this->validateFeed()) {
+            $this->preview = true;
+            $this->profile = Ostatus_profile::ensureProfile($this->munger);
+            if (!$this->profile) {
+                throw new ServerException("Feed profile was not saved properly.");
+            }
+
+            // If not already in use, subscribe to updates via the hub
+            if ($this->profile->sub_start) {
+                common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
+            } else {
+                $ok = $this->profile->subscribe();
+                common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
+                if (!$ok) {
+                    $this->showForm(_m('Feed subscription failed! Bad response from hub.'));
+                    return;
+                }
+            }
+
+            // And subscribe the current user to the local profile
+            $user = common_current_user();
+
+            if ($this->profile->isGroup()) {
+                $group = $this->profile->localGroup();
+                if ($user->isMember($group)) {
+                    $this->showForm(_m('Already a member!'));
+                } elseif (Group_member::join($this->profile->group_id, $user->id)) {
+                    $this->showForm(_m('Joined remote group!'));
+                } else {
+                    $this->showForm(_m('Remote group join failed!'));
+                }
+            } else {
+                $local = $this->profile->localProfile();
+                if ($user->isSubscribed($local)) {
+                    $this->showForm(_m('Already subscribed!'));
+                } elseif ($user->subscribeTo($local)) {
+                    $this->showForm(_m('Feed subscribed!'));
+                } else {
+                    $this->showForm(_m('Feed subscription failed!'));
+                }
+            }
+        }
+    }
+
+    function validateAndPreview()
+    {
+        if ($this->validateFeed()) {
+            $this->preview = true;
+            $this->showForm(_m('Previewing feed:'));
+        }
+    }
+
+    function previewFeed()
+    {
+        $profile = $this->munger->ostatusProfile();
+        $notice = $this->munger->notice(0, true); // preview
+
+        if ($notice) {
+            $this->element('b', null, 'Preview of latest post from this feed:');
+
+            $item = new NoticeList($notice, $this);
+            $item->show();
+        } else {
+            $this->element('b', null, 'No posts in this feed yet.');
+        }
+    }
+
+    function showScripts()
+    {
+        parent::showScripts();
+        $this->autofocus('feedurl');
+    }
+}
diff --git a/plugins/OStatus/actions/hostmeta.php b/plugins/OStatus/actions/hostmeta.php
new file mode 100644 (file)
index 0000000..850b8a0
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class HostMetaAction extends Action
+{
+
+    function handle()
+    {
+        parent::handle();
+
+        $w = new Webfinger();
+
+
+        $domain = common_config('site', 'server');
+        $url = common_local_url('webfinger');
+        $url.= '?uri={uri}';
+        print $w->getHostMeta($domain, $url);
+    }
+}
diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php
new file mode 100644 (file)
index 0000000..d217744
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+
+class OStatusInitAction extends Action
+{
+
+    var $nickname;
+    var $acct;
+    var $err;
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        if (common_logged_in()) {
+            $this->clientError(_('You can use the local subscription!'));
+            return false;
+        }
+
+        $this->nickname    = $this->trimmed('nickname');
+        $this->acct = $this->trimmed('acct');
+
+        return true;
+    }
+    
+    function handle($args)
+    {
+        parent::handle($args);
+
+        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+            /* Use a session token for CSRF protection. */
+            $token = $this->trimmed('token');
+            if (!$token || $token != common_session_token()) {
+                $this->showForm(_('There was a problem with your session token. '.
+                                  'Try again, please.'));
+                return;
+            }
+            $this->ostatusConnect();
+        } else {
+            $this->showForm();
+        }
+    }
+    
+    function showForm($err = null)
+    {
+        $this->err = $err;
+        if ($this->boolean('ajax')) {
+            header('Content-Type: text/xml;charset=utf-8');
+            $this->xw->startDocument('1.0', 'UTF-8');
+            $this->elementStart('html');
+            $this->elementStart('head');
+            $this->element('title', null, _('Subscribe to user'));
+            $this->elementEnd('head');
+            $this->elementStart('body');
+            $this->showContent();
+            $this->elementEnd('body');
+            $this->elementEnd('html');
+        } else {
+            $this->showPage();
+        }
+    }
+
+    function showContent()
+    {
+        $this->elementStart('form', array('id' => 'form_ostatus_connect',
+                                          'method' => 'post',
+                                          'class' => 'form_settings',
+                                          'action' => common_local_url('ostatusinit')));
+        $this->elementStart('fieldset');
+        $this->element('legend', null,  sprintf(_('Subscribe to %s'), $this->nickname));
+        $this->hidden('token', common_session_token());
+
+        $this->elementStart('ul', 'form_data');
+        $this->elementStart('li', array('id' => 'ostatus_nickname'));
+        $this->input('nickname', _('User nickname'), $this->nickname,
+                     _('Nickname of the user you want to follow'));
+        $this->elementEnd('li');
+        $this->elementStart('li', array('id' => 'ostatus_profile'));
+        $this->input('acct', _('Profile Account'), $this->acct,
+                     _('Your account id (i.e. user@identi.ca)'));
+        $this->elementEnd('li');
+        $this->elementEnd('ul');
+        $this->submit('submit', _('Subscribe'));
+        $this->elementEnd('fieldset');
+        $this->elementEnd('form');
+    }
+
+    function ostatusConnect()
+    {
+      $w = new Webfinger;
+
+      $result = $w->lookup($this->acct);
+      foreach ($result->links as $link) {
+          if ($link['rel'] == 'http://ostatus.org/schema/1.0/subscribe') {
+              // We found a URL - let's redirect!
+
+              $user = User::staticGet('nickname', $this->nickname);
+
+              $feed_url = common_local_url('ApiTimelineUser',
+                                           array('id' => $user->id,
+                                                 'format' => 'atom'));
+              $url = $w->applyTemplate($link['template'], $feed_url);
+
+              common_redirect($url, 303);
+          }
+
+      }
+      
+    }
+    
+    function title()
+    {
+      return _('OStatus Connect');  
+    }
+  
+}
diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php
new file mode 100644 (file)
index 0000000..2391225
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class OStatusSubAction extends Action
+{
+
+    protected $feedurl;
+    
+    function title()
+    {
+        return _m("OStatus Subscribe");
+    }
+
+    function handle($args)
+    {
+        if ($this->validateFeed()) {
+            $this->showForm();
+        }
+
+        return true;
+
+    }
+
+    function showForm($err = null)
+    {
+        $this->err = $err;
+        $this->showPage();
+    }
+
+
+    function showContent()
+    {
+        $user = common_current_user();
+
+        $profile = $user->getProfile();
+
+        $fuser = null;
+
+        $flink = Foreign_link::getByUserID($user->id, FEEDSUB_SERVICE);
+
+        if (!empty($flink)) {
+            $fuser = $flink->getForeignUser();
+        }
+
+        $this->elementStart('form', array('method' => 'post',
+                                          'id' => 'form_settings_feedsub',
+                                          'class' => 'form_settings',
+                                          'action' =>
+                                          common_local_url('feedsubsettings')));
+
+        $this->hidden('token', common_session_token());
+
+        $this->elementStart('fieldset', array('id' => 'settings_feeds'));
+
+        $this->elementStart('ul', 'form_data');
+        $this->elementStart('li');
+        $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
+        $this->elementEnd('li');
+        $this->elementEnd('ul');
+
+        $this->submit('subscribe', _m('Subscribe'));
+
+        $this->elementEnd('fieldset');
+
+        $this->elementEnd('form');
+
+        $this->previewFeed();
+    }
+
+    /**
+     * Handle posts to this form
+     *
+     * Based on the button that was pressed, muxes out to other functions
+     * to do the actual task requested.
+     *
+     * All sub-functions reload the form with a message -- success or failure.
+     *
+     * @return void
+     */
+
+    function handlePost()
+    {
+        // CSRF protection
+        $token = $this->trimmed('token');
+        if (!$token || $token != common_session_token()) {
+            $this->showForm(_('There was a problem with your session token. '.
+                              'Try again, please.'));
+            return;
+        }
+
+        if ($this->arg('subscribe')) {
+            $this->saveFeed();
+        } else {
+            $this->showForm(_('Unexpected form submission.'));
+        }
+    }
+
+    
+    /**
+     * Set up and add a feed
+     *
+     * @return boolean true if feed successfully read
+     * Sends you back to input form if not.
+     */
+    function validateFeed()
+    {
+        $feedurl = $this->trimmed('feed');
+        
+        if ($feedurl == '') {
+            $this->showForm(_m('Empty feed URL!'));
+            return;
+        }
+        $this->feedurl = $feedurl;
+        
+        // Get the canonical feed URI and check it
+        try {
+            $discover = new FeedDiscovery();
+            $uri = $discover->discoverFromURL($feedurl);
+        } catch (FeedSubBadURLException $e) {
+            $this->showForm(_m('Invalid URL or could not reach server.'));
+            return false;
+        } catch (FeedSubBadResponseException $e) {
+            $this->showForm(_m('Cannot read feed; server returned error.'));
+            return false;
+        } catch (FeedSubEmptyException $e) {
+            $this->showForm(_m('Cannot read feed; server returned an empty page.'));
+            return false;
+        } catch (FeedSubBadHTMLException $e) {
+            $this->showForm(_m('Bad HTML, could not find feed link.'));
+            return false;
+        } catch (FeedSubNoFeedException $e) {
+            $this->showForm(_m('Could not find a feed linked from this URL.'));
+            return false;
+        } catch (FeedSubUnrecognizedTypeException $e) {
+            $this->showForm(_m('Not a recognized feed type.'));
+            return false;
+        } catch (FeedSubException $e) {
+            // Any new ones we forgot about
+            $this->showForm(_m('Bad feed URL.'));
+            return false;
+        }
+        
+        $this->munger = $discover->feedMunger();
+        $this->profile = $this->munger->ostatusProfile();
+
+        if ($this->profile->huburi == '') {
+            $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
+            return false;
+        }
+        
+        return true;
+    }
+
+    function saveFeed()
+    {
+        if ($this->validateFeed()) {
+            $this->preview = true;
+            $this->profile = Ostatus_profile::ensureProfile($this->munger);
+
+            // If not already in use, subscribe to updates via the hub
+            if ($this->profile->sub_start) {
+                common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
+            } else {
+                $ok = $this->profile->subscribe();
+                common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
+                if (!$ok) {
+                    $this->showForm(_m('Feed subscription failed! Bad response from hub.'));
+                    return;
+                }
+            }
+            
+            // And subscribe the current user to the local profile
+            $user = common_current_user();
+            $profile = $this->profile->getProfile();
+            
+            if ($user->isSubscribed($profile)) {
+                $this->showForm(_m('Already subscribed!'));
+            } elseif ($user->subscribeTo($profile)) {
+                $this->showForm(_m('Feed subscribed!'));
+            } else {
+                $this->showForm(_m('Feed subscription failed!'));
+            }
+        }
+    }
+
+    
+    function previewFeed()
+    {
+        $profile = $this->munger->ostatusProfile();
+        $notice = $this->munger->notice(0, true); // preview
+
+        if ($notice) {
+            $this->element('b', null, 'Preview of latest post from this feed:');
+
+            $item = new NoticeList($notice, $this);
+            $item->show();
+        } else {
+            $this->element('b', null, 'No posts in this feed yet.');
+        }
+    }
+
+
+}
diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php
new file mode 100644 (file)
index 0000000..388c8f9
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package FeedSubPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+
+class PushCallbackAction extends Action
+{
+    function handle()
+    {
+        parent::handle();
+        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+            $this->handlePost();
+        } else {
+            $this->handleGet();
+        }
+    }
+    
+    /**
+     * Handler for POST content updates from the hub
+     */
+    function handlePost()
+    {
+        $feedid = $this->arg('feed');
+        common_log(LOG_INFO, "POST for feed id $feedid");
+        if (!$feedid) {
+            throw new ServerException('Empty or invalid feed id', 400);
+        }
+
+        $profile = Ostatus_profile::staticGet('id', $feedid);
+        if (!$profile) {
+            throw new ServerException('Unknown OStatus/PuSH feed id ' . $feedid, 400);
+        }
+
+        $hmac = '';
+        if (isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) {
+            $hmac = $_SERVER['HTTP_X_HUB_SIGNATURE'];
+        }
+
+        $post = file_get_contents('php://input');
+
+        // @fixme Queue this to a background process; we should return
+        // as quickly as possible from a distribution POST.
+        $profile->postUpdates($post, $hmac);
+    }
+    
+    /**
+     * Handler for GET verification requests from the hub
+     */
+    function handleGet()
+    {
+        $mode = $this->arg('hub_mode');
+        $topic = $this->arg('hub_topic');
+        $challenge = $this->arg('hub_challenge');
+        $lease_seconds = $this->arg('hub_lease_seconds');
+        $verify_token = $this->arg('hub_verify_token');
+        
+        if ($mode != 'subscribe' && $mode != 'unsubscribe') {
+            common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\"");
+            throw new ServerException("Bogus hub callback: bad mode", 404);
+        }
+        
+        $profile = Ostatus_profile::staticGet('feeduri', $topic);
+        if (!$profile) {
+            common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic");
+            throw new ServerException("Bogus hub callback: unknown feed", 404);
+        }
+
+        if ($profile->verify_token !== $verify_token) {
+            common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
+            throw new ServerError("Bogus hub callback: bad token", 404);
+        }
+
+        if ($mode != $profile->sub_state) {
+            common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$profile->sub_state}\"");
+            throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 404);
+        }
+
+        // OK!
+        if ($mode == 'subscribe') {
+            common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
+            $profile->confirmSubscribe($lease_seconds);
+        } else {
+            common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic");
+            $profile->confirmUnsubscribe();
+        }
+        print $challenge;
+    }
+}
diff --git a/plugins/OStatus/actions/pushhub.php b/plugins/OStatus/actions/pushhub.php
new file mode 100644 (file)
index 0000000..13ec09d
--- /dev/null
@@ -0,0 +1,180 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Integrated PuSH hub; lets us only ping them what need it.
+ * @package Hub
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+/**
+
+
+Things to consider...
+* should we purge incomplete subscriptions that never get a verification pingback?
+* when can we send subscription renewal checks?
+    - at next send time probably ok
+* when can we handle trimming of subscriptions?
+    - at next send time probably ok
+* should we keep a fail count?
+
+*/
+
+
+class PushHubAction extends Action
+{
+    function arg($arg, $def=null)
+    {
+        // PHP converts '.'s in incoming var names to '_'s.
+        // It also merges multiple values, which'll break hub.verify and hub.topic for publishing
+        // @fixme handle multiple args
+        $arg = str_replace('hub.', 'hub_', $arg);
+        return parent::arg($arg, $def);
+    }
+
+    function prepare($args)
+    {
+        StatusNet::setApi(true); // reduce exception reports to aid in debugging
+        return parent::prepare($args);
+    }
+
+    function handle()
+    {
+        $mode = $this->trimmed('hub.mode');
+        switch ($mode) {
+        case "subscribe":
+            $this->subscribe();
+            break;
+        case "unsubscribe":
+            $this->unsubscribe();
+            break;
+        case "publish":
+            throw new ServerException("Publishing outside feeds not supported.", 400);
+        default:
+            throw new ServerException("Unrecognized mode '$mode'.", 400);
+        }
+    }
+
+    /**
+     * Process a PuSH feed subscription request.
+     *
+     * HTTP return codes:
+     *   202 Accepted - request saved and awaiting verification
+     *   204 No Content - already subscribed
+     *   403 Forbidden - rejecting this (not specifically spec'd)
+     */
+    function subscribe()
+    {
+        $feed = $this->argUrl('hub.topic');
+        $callback = $this->argUrl('hub.callback');
+
+        common_log(LOG_DEBUG, __METHOD__ . ": checking sub'd to $feed $callback");
+        if ($this->getSub($feed, $callback)) {
+            // Already subscribed; return 204 per spec.
+            header('HTTP/1.1 204 No Content');
+            common_log(LOG_DEBUG, __METHOD__ . ': already subscribed');
+            return;
+        }
+
+        common_log(LOG_DEBUG, __METHOD__ . ': setting up');
+        $sub = new HubSub();
+        $sub->topic = $feed;
+        $sub->callback = $callback;
+        $sub->verify_token = $this->arg('hub.verify_token', null);
+        $sub->secret = $this->arg('hub.secret', null);
+        if (strlen($sub->secret) > 200) {
+            throw new ClientException("hub.secret must be no longer than 200 chars", 400);
+        }
+        $sub->setLease(intval($this->arg('hub.lease_seconds')));
+
+        // @fixme check for feeds we don't manage
+        // @fixme check the verification mode, might want a return immediately?
+
+        common_log(LOG_DEBUG, __METHOD__ . ': inserting');
+        $ok = $sub->insert();
+        
+        if (!$ok) {
+            throw new ServerException("Failed to save subscription record", 500);
+        }
+
+        // @fixme check errors ;)
+
+        $data = array('sub' => $sub, 'mode' => 'subscribe');
+        $qm = QueueManager::get();
+        $qm->enqueue($data, 'hubverify');
+        
+        header('HTTP/1.1 202 Accepted');
+        common_log(LOG_DEBUG, __METHOD__ . ': done');
+    }
+
+    /**
+     * Process a PuSH feed unsubscription request.
+     *
+     * HTTP return codes:
+     *   202 Accepted - request saved and awaiting verification
+     *   204 No Content - already subscribed
+     *   400 Bad Request - invalid params or rejected feed
+     */
+    function unsubscribe()
+    {
+        $feed = $this->argUrl('hub.topic');
+        $callback = $this->argUrl('hub.callback');
+        $sub = $this->getSub($feed, $callback);
+        
+        if ($sub) {
+            if ($sub->verify('unsubscribe')) {
+                $sub->delete();
+                common_log(LOG_INFO, "PuSH unsubscribed $feed for $callback");
+            } else {
+                throw new ServerException("Failed PuSH unsubscription: verification failed! $feed for $callback");
+            }
+        } else {
+            throw new ServerException("Failed PuSH unsubscription: not subscribed! $feed for $callback");
+        }
+    }
+
+    /**
+     * Grab and validate a URL from POST parameters.
+     * @throws ServerException for malformed or non-http/https URLs
+     */
+    protected function argUrl($arg)
+    {
+        $url = $this->arg($arg);
+        $params = array('domain_check' => false, // otherwise breaks my local tests :P
+                        'allowed_schemes' => array('http', 'https'));
+        if (Validate::uri($url, $params)) {
+            return $url;
+        } else {
+            throw new ServerException("Invalid URL passed for $arg: '$url'", 400);
+        }
+    }
+
+    /**
+     * Get HubSub subscription record for a given feed & subscriber.
+     *
+     * @param string $feed
+     * @param string $callback
+     * @return mixed HubSub or false
+     */
+    protected function getSub($feed, $callback)
+    {
+        return HubSub::staticGet($feed, $callback);
+    }
+}
+
diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php
new file mode 100644 (file)
index 0000000..c79d09c
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @author James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+class SalmonAction extends Action
+{
+    var $user     = null;
+    var $xml      = null;
+    var $activity = null;
+
+    function prepare($args)
+    {
+        if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+            $this->clientError(_('This method requires a POST.'));
+        }
+
+        if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
+            $this->clientError(_('Salmon requires application/atom+xml'));
+        }
+
+        $id = $this->trimmed('id');
+
+        if (!$id) {
+            $this->clientError(_('No ID.'));
+        }
+
+        $this->user = User::staticGet($id);
+
+        if (empty($this->user)) {
+            $this->clientError(_('No such user.'));
+        }
+
+        $xml = file_get_contents('php://input');
+
+        $dom = DOMDocument::loadXML($xml);
+
+        // XXX: check that document element is Atom entry
+        // XXX: check the signature
+
+        $this->act = new Activity($dom->documentElement);
+    }
+
+    function handle($args)
+    {
+        common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id);
+
+        // TODO : Insert new $xml -> notice code
+
+        switch ($this->act->verb)
+        {
+        case Activity::POST:
+        case Activity::SHARE:
+        case Activity::FAVORITE:
+        case Activity::FOLLOW:
+        }
+    }
+}
diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/webfinger.php
new file mode 100644 (file)
index 0000000..75ba166
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class WebfingerAction extends Action
+{
+
+    public $uri;
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $this->uri = $this->trimmed('uri');
+
+        return true;
+    }
+        
+    function handle()
+    {
+        $acct = Webfinger::normalize($this->uri);
+
+        $xrd = new XRD();
+
+        list($nick, $domain) = explode('@', urldecode($acct));
+        $nick = common_canonical_nickname($nick);
+
+        $this->user = User::staticGet('nickname', $nick);
+        if (!$this->user) {
+            $this->clientError(_('No such user.'), 404);
+            return false;
+        }
+
+        $xrd->subject = $this->uri;
+        $xrd->alias[] = common_profile_url($nick);
+        $xrd->links[] = array('rel' => 'http://webfinger.net/rel/profile-page',
+                              'type' => 'text/html',
+                              'href' => common_profile_url($nick));
+
+        $salmon_url = common_local_url('salmon',
+                                       array('id' => $this->user->id));
+
+        $xrd->links[] = array('rel' => 'salmon',
+                              'href' => $salmon_url);
+        
+        // TODO - finalize where the redirect should go on the publisher
+        $url = common_local_url('ostatussub') . '?feed={uri}';
+        $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe',
+                              'template' => $url );
+
+        header('Content-type: text/xml');
+        print $xrd->toXML();
+    }
+
+}
diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php
new file mode 100644 (file)
index 0000000..7071ee5
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PuSH feed subscription record
+ * @package Hub
+ * @author Brion Vibber <brion@status.net>
+ */
+class HubSub extends Memcached_DataObject
+{
+    public $__table = 'hubsub';
+
+    public $hashkey; // sha1(topic . '|' . $callback); (topic, callback) key is too long for myisam in utf8
+    public $topic;
+    public $callback;
+    public $secret;
+    public $verify_token;
+    public $challenge;
+    public $lease;
+    public $sub_start;
+    public $sub_end;
+    public $created;
+
+    public /*static*/ function staticGet($topic, $callback)
+    {
+        return parent::staticGet(__CLASS__, 'hashkey', self::hashkey($topic, $callback));
+    }
+
+    protected static function hashkey($topic, $callback)
+    {
+        return sha1($topic . '|' . $callback);
+    }
+
+    /**
+     * return table definition for DB_DataObject
+     *
+     * DB_DataObject needs to know something about the table to manipulate
+     * instances. This method provides all the DB_DataObject needs to know.
+     *
+     * @return array array of column definitions
+     */
+
+    function table()
+    {
+        return array('hashkey' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+                     'topic' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+                     'callback' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+                     'secret' => DB_DATAOBJECT_STR,
+                     'verify_token' => DB_DATAOBJECT_STR,
+                     'challenge' => DB_DATAOBJECT_STR,
+                     'lease' =>  DB_DATAOBJECT_INT,
+                     'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+                     'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+                     'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
+    }
+
+    static function schemaDef()
+    {
+        return array(new ColumnDef('hashkey', 'char',
+                                   /*size*/40,
+                                   /*nullable*/false,
+                                   /*key*/'PRI'),
+                     new ColumnDef('topic', 'varchar',
+                                   /*size*/255,
+                                   /*nullable*/false,
+                                   /*key*/'KEY'),
+                     new ColumnDef('callback', 'varchar',
+                                   255, false),
+                     new ColumnDef('secret', 'text',
+                                   null, true),
+                     new ColumnDef('verify_token', 'text',
+                                   null, true),
+                     new ColumnDef('challenge', 'varchar',
+                                   32, true),
+                     new ColumnDef('lease', 'int',
+                                   null, true),
+                     new ColumnDef('sub_start', 'datetime',
+                                   null, true),
+                     new ColumnDef('sub_end', 'datetime',
+                                   null, true),
+                     new ColumnDef('created', 'datetime',
+                                   null, false));
+    }
+
+    function keys()
+    {
+        return array_keys($this->keyTypes());
+    }
+
+    function sequenceKeys()
+    {
+        return array(false, false, false);
+    }
+
+    /**
+     * return key definitions for DB_DataObject
+     *
+     * DB_DataObject needs to know about keys that the table has; this function
+     * defines them.
+     *
+     * @return array key definitions
+     */
+
+    function keyTypes()
+    {
+        return array('hashkey' => 'K');
+    }
+
+    /**
+     * Validates a requested lease length, sets length plus
+     * subscription start & end dates.
+     *
+     * Does not save to database -- use before insert() or update().
+     *
+     * @param int $length in seconds
+     */
+    function setLease($length)
+    {
+        assert(is_int($length));
+
+        $min = 86400;
+        $max = 86400 * 30;
+
+        if ($length == 0) {
+            // We want to garbage collect dead subscriptions!
+            $length = $max;
+        } elseif( $length < $min) {
+            $length = $min;
+        } else if ($length > $max) {
+            $length = $max;
+        }
+
+        $this->lease = $length;
+        $this->start_sub = common_sql_now();
+        $this->end_sub = common_sql_date(time() + $length);
+    }
+
+    /**
+     * Send a verification ping to subscriber
+     * @param string $mode 'subscribe' or 'unsubscribe'
+     */
+    function verify($mode)
+    {
+        assert($mode == 'subscribe' || $mode == 'unsubscribe');
+
+        // Is this needed? data object fun...
+        $clone = clone($this);
+        $clone->challenge = common_good_rand(16);
+        $clone->update($this);
+        $this->challenge = $clone->challenge;
+        unset($clone);
+
+        $params = array('hub.mode' => $mode,
+                        'hub.topic' => $this->topic,
+                        'hub.challenge' => $this->challenge);
+        if ($mode == 'subscribe') {
+            $params['hub.lease_seconds'] = $this->lease;
+        }
+        if ($this->verify_token) {
+            $params['hub.verify_token'] = $this->verify_token;
+        }
+        $url = $this->callback . '?' . http_build_query($params, '', '&'); // @fixme ugly urls
+
+        try {
+            $request = new HTTPClient();
+            $response = $request->get($url);
+            $status = $response->getStatus();
+
+            if ($status >= 200 && $status < 300) {
+                $fail = false;
+            } else {
+                // @fixme how can we schedule a second attempt?
+                // Or should we?
+                $fail = "Returned HTTP $status";
+            }
+        } catch (Exception $e) {
+            $fail = $e->getMessage();
+        }
+        if ($fail) {
+            // @fixme how can we schedule a second attempt?
+            // or save a fail count?
+            // Or should we?
+            common_log(LOG_ERR, "Failed to verify $mode for $this->topic at $this->callback: $fail");
+            return false;
+        } else {
+            if ($mode == 'subscribe') {
+                // Establish or renew the subscription!
+                // This seems unnecessary... dataobject fun!
+                $clone = clone($this);
+                $clone->challenge = null;
+                $clone->setLease($this->lease);
+                $clone->update($this);
+                unset($clone);
+
+                $this->challenge = null;
+                $this->setLease($this->lease);
+                common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic for $this->lease seconds");
+            } else if ($mode == 'unsubscribe') {
+                common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic");
+                $this->delete();
+            }
+            return true;
+        }
+    }
+
+    /**
+     * Insert wrapper; transparently set the hash key from topic and callback columns.
+     * @return boolean success
+     */
+    function insert()
+    {
+        $this->hashkey = self::hashkey($this->topic, $this->callback);
+        return parent::insert();
+    }
+
+    /**
+     * Send a 'fat ping' to the subscriber's callback endpoint
+     * containing the given Atom feed chunk.
+     *
+     * Determination of which items to send should be done at
+     * a higher level; don't just shove in a complete feed!
+     *
+     * @param string $atom well-formed Atom feed
+     */
+    function push($atom)
+    {
+        $headers = array('Content-Type: application/atom+xml');
+        if ($this->secret) {
+            $hmac = hash_hmac('sha1', $atom, $this->secret);
+            $headers[] = "X-Hub-Signature: sha1=$hmac";
+        } else {
+            $hmac = '(none)';
+        }
+        common_log(LOG_INFO, "About to push feed to $this->callback for $this->topic, HMAC $hmac");
+        try {
+            $request = new HTTPClient();
+            $request->setBody($atom);
+            $response = $request->post($this->callback, $headers);
+
+            if ($response->isOk()) {
+                return true;
+            }
+            common_log(LOG_ERR, "Error sending PuSH content " .
+                                "to $this->callback for $this->topic: " .
+                                $response->getStatus());
+            return false;
+
+        } catch (Exception $e) {
+            common_log(LOG_ERR, "Error sending PuSH content " .
+                                "to $this->callback for $this->topic: " .
+                                $e->getMessage());
+            return false;
+        }
+    }
+}
+
diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php
new file mode 100644 (file)
index 0000000..be01cdf
--- /dev/null
@@ -0,0 +1,936 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009-2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package FeedSubPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+/*
+PuSH subscription flow:
+
+    $profile->subscribe()
+        generate random verification token
+            save to verify_token
+        sends a sub request to the hub...
+
+    main/push/callback
+        hub sends confirmation back to us via GET
+        We verify the request, then echo back the challenge.
+        On our end, we save the time we subscribed and the lease expiration
+
+    main/push/callback
+        hub sends us updates via POST
+
+*/
+
+class FeedDBException extends FeedSubException
+{
+    public $obj;
+
+    function __construct($obj)
+    {
+        parent::__construct('Database insert failure');
+        $this->obj = $obj;
+    }
+}
+
+class Ostatus_profile extends Memcached_DataObject
+{
+    public $__table = 'ostatus_profile';
+
+    public $id;
+    public $profile_id;
+    public $group_id;
+
+    public $feeduri;
+    public $homeuri;
+
+    // PuSH subscription data
+    public $huburi;
+    public $secret;
+    public $verify_token;
+    public $sub_state; // subscribe, active, unsubscribe
+    public $sub_start;
+    public $sub_end;
+
+    public $salmonuri;
+
+    public $created;
+    public $lastupdate;
+
+    public /*static*/ function staticGet($k, $v=null)
+    {
+        return parent::staticGet(__CLASS__, $k, $v);
+    }
+
+    /**
+     * return table definition for DB_DataObject
+     *
+     * DB_DataObject needs to know something about the table to manipulate
+     * instances. This method provides all the DB_DataObject needs to know.
+     *
+     * @return array array of column definitions
+     */
+
+    function table()
+    {
+        return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+                     'profile_id' => DB_DATAOBJECT_INT,
+                     'group_id' => DB_DATAOBJECT_INT,
+                     'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+                     'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+                     'huburi' =>  DB_DATAOBJECT_STR,
+                     'secret' => DB_DATAOBJECT_STR,
+                     'verify_token' => DB_DATAOBJECT_STR,
+                     'sub_state' => DB_DATAOBJECT_STR,
+                     'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+                     'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+                     'salmonuri' =>  DB_DATAOBJECT_STR,
+                     'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
+                     'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
+    }
+
+    static function schemaDef()
+    {
+        return array(new ColumnDef('id', 'integer',
+                                   /*size*/ null,
+                                   /*nullable*/ false,
+                                   /*key*/ 'PRI',
+                                   /*default*/ '0',
+                                   /*extra*/ null,
+                                   /*auto_increment*/ true),
+                     new ColumnDef('profile_id', 'integer',
+                                   null, true, 'UNI'),
+                     new ColumnDef('group_id', 'integer',
+                                   null, true, 'UNI'),
+                     new ColumnDef('feeduri', 'varchar',
+                                   255, false, 'UNI'),
+                     new ColumnDef('homeuri', 'varchar',
+                                   255, false),
+                     new ColumnDef('huburi', 'text',
+                                   null, true),
+                     new ColumnDef('verify_token', 'varchar',
+                                   32, true),
+                     new ColumnDef('secret', 'varchar',
+                                   64, true),
+                     new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe')",
+                                   null, true),
+                     new ColumnDef('sub_start', 'datetime',
+                                   null, true),
+                     new ColumnDef('sub_end', 'datetime',
+                                   null, true),
+                     new ColumnDef('salmonuri', 'text',
+                                   null, true),
+                     new ColumnDef('created', 'datetime',
+                                   null, false),
+                     new ColumnDef('lastupdate', 'datetime',
+                                   null, false));
+    }
+
+    /**
+     * return key definitions for DB_DataObject
+     *
+     * DB_DataObject needs to know about keys that the table has; this function
+     * defines them.
+     *
+     * @return array key definitions
+     */
+
+    function keys()
+    {
+        return array_keys($this->keyTypes());
+    }
+
+    /**
+     * return key definitions for Memcached_DataObject
+     *
+     * Our caching system uses the same key definitions, but uses a different
+     * method to get them.
+     *
+     * @return array key definitions
+     */
+
+    function keyTypes()
+    {
+        return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U');
+    }
+
+    function sequenceKey()
+    {
+        return array('id', true, false);
+    }
+
+    /**
+     * Fetch the StatusNet-side profile for this feed
+     * @return Profile
+     */
+    public function localProfile()
+    {
+        if ($this->profile_id) {
+            return Profile::staticGet('id', $this->profile_id);
+        }
+        return null;
+    }
+
+    /**
+     * Fetch the StatusNet-side profile for this feed
+     * @return Profile
+     */
+    public function localGroup()
+    {
+        if ($this->group_id) {
+            return User_group::staticGet('id', $this->group_id);
+        }
+        return null;
+    }
+
+    /**
+     * @param FeedMunger $munger
+     * @param boolean $isGroup is this a group record?
+     * @return Ostatus_profile
+     */
+    public static function ensureProfile($munger)
+    {
+        $profile = $munger->ostatusProfile();
+
+        $current = self::staticGet('feeduri', $profile->feeduri);
+        if ($current) {
+            // @fixme we should probably update info as necessary
+            return $current;
+        }
+
+        $profile->query('BEGIN');
+
+        try {
+            $local = $munger->profile();
+
+            if ($profile->isGroup()) {
+                $group = new User_group();
+                $group->nickname = $local->nickname . '@remote'; // @fixme
+                $group->fullname = $local->fullname;
+                $group->homepage = $local->homepage;
+                $group->location = $local->location;
+                $group->created = $local->created;
+                $group->insert();
+                if (empty($result)) {
+                    throw new FeedDBException($group);
+                }
+                $profile->group_id = $group->id;
+            } else {
+                $result = $local->insert();
+                if (empty($result)) {
+                    throw new FeedDBException($local);
+                }
+                $profile->profile_id = $local->id;
+            }
+
+            $profile->created = common_sql_now();
+            $profile->lastupdate = common_sql_now();
+            $result = $profile->insert();
+            if (empty($result)) {
+                throw new FeedDBException($profile);
+            }
+
+            $profile->query('COMMIT');
+        } catch (FeedDBException $e) {
+            common_log_db_error($e->obj, 'INSERT', __FILE__);
+            $profile->query('ROLLBACK');
+            return false;
+        }
+
+        $avatar = $munger->getAvatar();
+        if ($avatar) {
+            try {
+                $profile->updateAvatar($avatar);
+            } catch (Exception $e) {
+                common_log(LOG_ERR, "Exception setting OStatus avatar: " .
+                                    $e->getMessage());
+            }
+        }
+
+        return $profile;
+    }
+
+    /**
+     * Download and update given avatar image
+     * @param string $url
+     * @throws Exception in various failure cases
+     */
+    public function updateAvatar($url)
+    {
+        // @fixme this should be better encapsulated
+        // ripped from oauthstore.php (for old OMB client)
+        $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
+        copy($url, $temp_filename);
+        
+        // @fixme should we be using different ids?
+        $imagefile = new ImageFile($this->id, $temp_filename);
+        $filename = Avatar::filename($this->id,
+                                     image_type_to_extension($imagefile->type),
+                                     null,
+                                     common_timestamp());
+        rename($temp_filename, Avatar::path($filename));
+        if ($this->isGroup()) {
+            $group = $this->localGroup();
+            $group->setOriginal($filename);
+        } else {
+            $profile = $this->localProfile();
+            $profile->setOriginal($filename);
+        }
+    }
+
+    /**
+     * Returns an XML string fragment with profile information as an
+     * Activity Streams noun object with the given element type.
+     *
+     * Assumes that 'activity' namespace has been previously defined.
+     *
+     * @param string $element one of 'actor', 'subject', 'object', 'target'
+     * @return string
+     */
+    function asActivityNoun($element)
+    {
+        $xs = new XMLStringer(true);
+
+        $avatarHref = Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+        $avatarType = 'image/png';
+        if ($this->isGroup()) {
+            $type = 'http://activitystrea.ms/schema/1.0/group';
+            $self = $this->localGroup();
+
+            // @fixme put a standard getAvatar() interface on groups too
+            if ($self->homepage_logo) {
+                $avatarHref = $self->homepage_logo;
+                $map = array('png' => 'image/png',
+                             'jpg' => 'image/jpeg',
+                             'jpeg' => 'image/jpeg',
+                             'gif' => 'image/gif');
+                $extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION);
+                if (isset($map[$extension])) {
+                    $avatarType = $map[$extension];
+                }
+            }
+        } else {
+            $type = 'http://activitystrea.ms/schema/1.0/person';
+            $self = $this->localProfile();
+            $avatar = $self->getAvatar(AVATAR_PROFILE_SIZE);
+            if ($avatar) {
+                $avatarHref = $avatar->
+                $avatarType = $avatar->mediatype;
+            }
+        }
+        $xs->elementStart('activity:' . $element);
+        $xs->element(
+            'activity:object-type',
+            null,
+            $type
+        );
+        $xs->element(
+            'id',
+            null,
+            $this->homeuri); // ?
+        $xs->element('title', null, $self->getBestName());
+
+        $xs->element(
+            'link', array(
+                'type' => $avatarType,
+                'href' => $avatarHref
+            ),
+            ''
+        );
+
+        $xs->elementEnd('activity:' . $element);
+
+        return $xs->getString();
+    }
+
+    /**
+     * Damn dirty hack!
+     */
+    function isGroup()
+    {
+        return (strpos($this->feeduri, '/groups/') !== false);
+    }
+
+    /**
+     * Send a subscription request to the hub for this feed.
+     * The hub will later send us a confirmation POST to /main/push/callback.
+     *
+     * @return bool true on success, false on failure
+     * @throws ServerException if feed state is not valid
+     */
+    public function subscribe($mode='subscribe')
+    {
+        if ($this->sub_state != '') {
+            throw new ServerException("Attempting to start PuSH subscription to feed in state $this->sub_state");
+        }
+        if (empty($this->huburi)) {
+            if (common_config('feedsub', 'nohub')) {
+                // Fake it! We're just testing remote feeds w/o hubs.
+                return true;
+            } else {
+                throw new ServerException("Attempting to start PuSH subscription for feed with no hub");
+            }
+        }
+
+        return $this->doSubscribe('subscribe');
+    }
+
+    /**
+     * Send a PuSH unsubscription request to the hub for this feed.
+     * The hub will later send us a confirmation POST to /main/push/callback.
+     *
+     * @return bool true on success, false on failure
+     * @throws ServerException if feed state is not valid
+     */
+    public function unsubscribe() {
+        if ($this->sub_state != 'active') {
+            throw new ServerException("Attempting to end PuSH subscription to feed in state $this->sub_state");
+        }
+        if (empty($this->huburi)) {
+            if (common_config('feedsub', 'nohub')) {
+                // Fake it! We're just testing remote feeds w/o hubs.
+                return true;
+            } else {
+                throw new ServerException("Attempting to end PuSH subscription for feed with no hub");
+            }
+        }
+
+        return $this->doSubscribe('unsubscribe');
+    }
+
+    protected function doSubscribe($mode)
+    {
+        $orig = clone($this);
+        $this->verify_token = common_good_rand(16);
+        if ($mode == 'subscribe') {
+            $this->secret = common_good_rand(32);
+        }
+        $this->sub_state = $mode;
+        $this->update($orig);
+        unset($orig);
+
+        try {
+            $callback = common_local_url('pushcallback', array('feed' => $this->id));
+            $headers = array('Content-Type: application/x-www-form-urlencoded');
+            $post = array('hub.mode' => $mode,
+                          'hub.callback' => $callback,
+                          'hub.verify' => 'async',
+                          'hub.verify_token' => $this->verify_token,
+                          'hub.secret' => $this->secret,
+                          //'hub.lease_seconds' => 0,
+                          'hub.topic' => $this->feeduri);
+            $client = new HTTPClient();
+            $response = $client->post($this->huburi, $headers, $post);
+            $status = $response->getStatus();
+            if ($status == 202) {
+                common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
+                return true;
+            } else if ($status == 204) {
+                common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
+                return true;
+            } else if ($status >= 200 && $status < 300) {
+                common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
+                return false;
+            } else {
+                common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
+                return false;
+            }
+        } catch (Exception $e) {
+            // wtf!
+            common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
+
+            $orig = clone($this);
+            $this->verify_token = null;
+            $this->sub_state = null;
+            $this->update($orig);
+            unset($orig);
+
+            return false;
+        }
+    }
+
+    /**
+     * Save PuSH subscription confirmation.
+     * Sets approximate lease start and end times and finalizes state.
+     *
+     * @param int $lease_seconds provided hub.lease_seconds parameter, if given
+     */
+    public function confirmSubscribe($lease_seconds=0)
+    {
+        $original = clone($this);
+
+        $this->sub_state = 'active';
+        $this->sub_start = common_sql_date(time());
+        if ($lease_seconds > 0) {
+            $this->sub_end = common_sql_date(time() + $lease_seconds);
+        } else {
+            $this->sub_end = null;
+        }
+        $this->lastupdate = common_sql_date();
+
+        return $this->update($original);
+    }
+
+    /**
+     * Save PuSH unsubscription confirmation.
+     * Wipes active PuSH sub info and resets state.
+     */
+    public function confirmUnsubscribe()
+    {
+        $original = clone($this);
+
+        $this->verify_token = null;
+        $this->secret = null;
+        $this->sub_state = null;
+        $this->sub_start = null;
+        $this->sub_end = null;
+        $this->lastupdate = common_sql_date();
+
+        return $this->update($original);
+    }
+
+    /**
+     * Send an Activity Streams notification to the remote Salmon endpoint,
+     * if so configured.
+     *
+     * @param Profile $actor
+     * @param $verb eg Activity::SUBSCRIBE or Activity::JOIN
+     * @param $object object of the action; if null, the remote entity itself is assumed
+     */
+    public function notify(Profile $actor, $verb, $object=null)
+    {
+        if ($object == null) {
+            $object = $this;
+        }
+        if ($this->salmonuri) {
+            $text = 'update'; // @fixme
+            $id = 'tag:' . common_config('site', 'server') .
+                ':' . $verb .
+                ':' . $actor->id .
+                ':' . time(); // @fixme
+
+            $entry = new Atom10Entry();
+            $entry->elementStart('entry');
+            $entry->element('id', null, $id);
+            $entry->element('title', null, $text);
+            $entry->element('summary', null, $text);
+            $entry->element('published', null, common_date_w3dtf());
+
+            $entry->element('activity:verb', null, $verb);
+            $entry->raw($profile->asAtomAuthor());
+            $entry->raw($profile->asActivityActor());
+            $entry->raw($object->asActivityNoun('object'));
+            $entry->elmentEnd('entry');
+
+            $feed = $this->atomFeed($actor);
+            $feed->initFeed();
+            $feed->addEntry($entry);
+            $feed->renderEntries();
+            $feed->endFeed();
+
+            $xml = $feed->getString();
+            common_log(LOG_INFO, "Posting to Salmon endpoint $salmon: $xml");
+
+            $salmon = new Salmon(); // ?
+            $salmon->post($this->salmonuri, $xml);
+        }
+    }
+
+    function getBestName()
+    {
+        if ($this->isGroup()) {
+            return $this->localGroup()->getBestName();
+        } else {
+            return $this->localProfile()->getBestName();
+        }
+    }
+
+    function atomFeed($actor)
+    {
+        $feed = new Atom10Feed();
+        // @fixme should these be set up somewhere else?
+        $feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
+        $feed->addNamesapce('thr', 'http://purl.org/syndication/thread/1.0');
+        $feed->addNamespace('georss', 'http://www.georss.org/georss');
+        $feed->addNamespace('ostatus', 'http://ostatus.org/schema/1.0');
+
+        $taguribase = common_config('integration', 'taguri');
+        $feed->setId("tag:{$taguribase}:UserTimeline:{$actor->id}"); // ???
+
+        $feed->setTitle($actor->getBestName() . ' timeline'); // @fixme
+        $feed->setUpdated(time());
+        $feed->setPublished(time());
+
+        $feed->addLink(common_url('ApiTimelineUser',
+                                  array('id' => $actor->id,
+                                        'type' => 'atom')),
+                       array('rel' => 'self',
+                             'type' => 'application/atom+xml'));
+
+        $feed->addLink(common_url('userbyid',
+                                  array('id' => $actor->id)),
+                       array('rel' => 'alternate',
+                             'type' => 'text/html'));
+
+        return $feed;
+    }
+
+    /**
+     * Read and post notices for updates from the feed.
+     * Currently assumes that all items in the feed are new,
+     * coming from a PuSH hub.
+     *
+     * @param string $post source of Atom or RSS feed
+     * @param string $hmac X-Hub-Signature header, if present
+     */
+    public function postUpdates($post, $hmac)
+    {
+        common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $post");
+
+        if ($this->sub_state != 'active') {
+            common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH for inactive feed $this->feeduri (in state '$this->sub_state')");
+            return;
+        }
+
+        if ($post === '') {
+            common_log(LOG_ERR, __METHOD__ . ": ignoring empty post");
+            return;
+        }
+
+        if (!$this->validatePushSig($post, $hmac)) {
+            // Per spec we silently drop input with a bad sig,
+            // while reporting receipt to the server.
+            return;
+        }
+
+        $feed = new DOMDocument();
+        if (!$feed->loadXML($post)) {
+            // @fixme might help to include the err message
+            common_log(LOG_ERR, __METHOD__ . ": ignoring invalid XML");
+            return;
+        }
+
+        $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
+        if ($entries->length == 0) {
+            common_log(LOG_ERR, __METHOD__ . ": no entries in feed update, ignoring");
+            return;
+        }
+
+        for ($i = 0; $i < $entries->length; $i++) {
+            $entry = $entries->item($i);
+            $this->processEntry($entry, $feed);
+        }
+    }
+
+    /**
+     * Validate the given Atom chunk and HMAC signature against our
+     * shared secret that was set up at subscription time.
+     *
+     * If we don't have a shared secret, there should be no signature.
+     * If we we do, our the calculated HMAC should match theirs.
+     *
+     * @param string $post raw XML source as POSTed to us
+     * @param string $hmac X-Hub-Signature HTTP header value, or empty
+     * @return boolean true for a match
+     */
+    protected function validatePushSig($post, $hmac)
+    {
+        if ($this->secret) {
+            if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
+                $their_hmac = strtolower($matches[1]);
+                $our_hmac = hash_hmac('sha1', $post, $this->secret);
+                if ($their_hmac === $our_hmac) {
+                    return true;
+                }
+                common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
+            } else {
+                common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
+            }
+        } else {
+            if (empty($hmac)) {
+                return true;
+            } else {
+                common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Process a posted entry from this feed source.
+     *
+     * @param DOMElement $entry
+     * @param DOMElement $feed for context
+     */
+    protected function processEntry($entry, $feed)
+    {
+        $activity = new Activity($entry, $feed);
+
+        $debug = var_export($activity, true);
+        common_log(LOG_DEBUG, $debug);
+
+        if ($activity->verb == ActivityVerb::POST) {
+            $this->processPost($activity);
+        } else {
+            common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb");
+        }
+    }
+
+    /**
+     * Process an incoming post activity from this remote feed.
+     * @param Activity $activity
+     */
+    protected function processPost($activity)
+    {
+        if ($this->isGroup()) {
+            // @fixme validate these profiles in some way!
+            $oprofile = $this->ensureActorProfile($activity);
+        } else {
+            $actorUri = $this->getActorProfileURI($activity);
+            if ($actorUri == $this->homeuri) {
+                // @fixme check if profile info has changed and update it
+            } else {
+                // @fixme drop or reject the messages once we've got the canonical profile URI recorded sanely
+                common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->homeuri");
+                //return;
+            }
+            $oprofile = $this;
+        }
+
+        if ($activity->object->link) {
+            $sourceUri = $activity->object->link;
+        } else if (preg_match('!^https?://!', $activity->object->id)) {
+            $sourceUri = $activity->object->id;
+        } else {
+            common_log(LOG_INFO, "OStatus: ignoring post with no source link: id $activity->object->id");
+            return;
+        }
+
+        $dupe = Notice::staticGet('uri', $sourceUri);
+        if ($dupe) {
+            common_log(LOG_INFO, "OStatus: ignoring duplicate post: $noticeLink");
+            return;
+        }
+
+        // @fixme sanitize and save HTML content if available
+        $content = $activity->object->title;
+
+        $params = array('is_local' => Notice::REMOTE_OMB,
+                        'uri' => $sourceUri);
+
+        $location = $this->getEntryLocation($activity->entry);
+        if ($location) {
+            $params['lat'] = $location->lat;
+            $params['lon'] = $location->lon;
+            if ($location->location_id) {
+                $params['location_ns'] = $location->location_ns;
+                $params['location_id'] = $location->location_id;
+            }
+        }
+
+        // @fixme save detailed ostatus source info
+        // @fixme ensure that groups get handled correctly
+
+        $saved = Notice::saveNew($oprofile->localProfile()->id,
+                                 $content,
+                                 'ostatus',
+                                 $params);
+    }
+
+    /**
+     * Parse location given as a GeoRSS-simple point, if provided.
+     * http://www.georss.org/simple
+     *
+     * @param feed item $entry
+     * @return mixed Location or false
+     */
+    function getLocation($dom)
+    {
+        $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point');
+        
+        for ($i = 0; $i < $points->length; $i++) {
+            $point = $points->item(0)->textContent;
+            $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
+            $point = preg_replace('/\s+/', ' ', $point);
+            $point = trim($point);
+            $coords = explode(' ', $point);
+            if (count($coords) == 2) {
+                list($lat, $lon) = $coords;
+                if (is_numeric($lat) && is_numeric($lon)) {
+                    common_log(LOG_INFO, "Looking up location for $lat $lon from georss");
+                    return Location::fromLatLon($lat, $lon);
+                }
+            }
+            common_log(LOG_ERR, "Ignoring bogus georss:point value $point");
+        }
+
+        return false;
+    }
+
+    /**
+     * Get an appropriate avatar image source URL, if available.
+     *
+     * @param ActivityObject $actor
+     * @param DOMElement $feed
+     * @return string
+     */
+    function getAvatar($actor, $feed)
+    {
+        $url = '';
+        $icon = '';
+        if ($actor->avatar) {
+            $url = trim($actor->avatar);
+        }
+        if (!$url) {
+            // Check <atom:logo> and <atom:icon> on the feed
+            $els = $feed->childNodes();
+            if ($els && $els->length) {
+                for ($i = 0; $i < $els->length; $i++) {
+                    $el = $els->item($i);
+                    if ($el->namespaceURI == Activity::ATOM) {
+                        if (empty($url) && $el->localName == 'logo') {
+                            $url = trim($el->textContent);
+                            break;
+                        }
+                        if (empty($icon) && $el->localName == 'icon') {
+                            // Use as a fallback
+                            $icon = trim($el->textContent);
+                        }
+                    }
+                }
+            }
+            if ($icon && !$url) {
+                $url = $icon;
+            }
+        }
+        if ($url) {
+            $opts = array('allowed_schemes' => array('http', 'https'));
+            if (Validate::uri($url, $opts)) {
+                return $url;
+            }
+        }
+        return common_path('plugins/OStatus/images/96px-Feed-icon.svg.png');
+    }
+
+    /**
+     * @fixme move off of ostatus_profile or static?
+     */
+    function ensureActorProfile($activity)
+    {
+        $profile = $this->getActorProfile($activity);
+        if (!$profile) {
+            $profile = $this->createActorProfile($activity);
+        }
+        return $profile;
+    }
+
+    /**
+     * @param Activity $activity
+     * @return mixed matching Ostatus_profile or false if none known
+     */
+    function getActorProfile($activity)
+    {
+        $homeuri = $this->getActorProfileURI($activity);
+        return Ostatus_profile::staticGet('homeuri', $homeuri);
+    }
+
+    /**
+     * @param Activity $activity
+     * @return string
+     * @throws ServerException
+     */
+    function getActorProfileURI($activity)
+    {
+        $opts = array('allowed_schemes' => array('http', 'https'));
+        $actor = $activity->actor;
+        if ($actor->id && Validate::uri($actor->id, $opts)) {
+            return $actor->id;
+        }
+        if ($actor->link && Validate::uri($actor->link, $opts)) {
+            return $actor->link;
+        }
+        throw new ServerException("No author ID URI found");
+    }
+
+    /**
+     *
+     */
+    function createActorProfile($activity)
+    {
+        $actor = $activity->actor();
+        $homeuri = $this->getActivityProfileURI($activity);
+        $nickname = $this->getAuthorNick($activity);
+        $avatar = $this->getAvatar($actor, $feed);
+
+        $profile = new Profile();
+        $profile->nickname   = $nickname;
+        $profile->fullname   = $actor->displayName;
+        $profile->homepage   = $actor->link; // @fixme
+        $profile->profileurl = $homeuri;
+        // @fixme bio
+        // @fixme tags/categories
+        // @fixme location?
+        // @todo tags from categories
+        // @todo lat/lon/location?
+
+        $ok = $profile->insert();
+        if ($ok) {
+            $this->updateAvatar($profile, $avatar);
+        } else {
+            throw new ServerException("Can't save local profile");
+        }
+
+        // @fixme either need to do feed discovery here
+        // or need to split out some of the feed stuff
+        // so we can leave it empty until later.
+        $oprofile = new Ostatus_profile();
+        $oprofile->homeuri = $homeuri;
+        $oprofile->profile_id = $profile->id;
+
+        $ok = $oprofile->insert();
+        if ($ok) {
+            return $oprofile;
+        } else {
+            throw new ServerException("Can't save OStatus profile");
+        }
+    }
+
+    /**
+     * @fixme move this into Activity?
+     * @param Activity $activity
+     * @return string
+     */
+    function getAuthorNick($activity)
+    {
+        // @fixme not technically part of the actor?
+        foreach (array($activity->entry, $activity->feed) as $source) {
+            $author = ActivityUtil::child($source, 'author', Activity::ATOM);
+            if ($author) {
+                $name = ActivityUtil::child($author, 'name', Activity::ATOM);
+                if ($name) {
+                    return trim($name->textContent);
+                }
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/plugins/OStatus/extlib/README b/plugins/OStatus/extlib/README
new file mode 100644 (file)
index 0000000..799b40c
--- /dev/null
@@ -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
+<atom:link> self-link element as well.
+
+Patch from this bug report is included:
+http://pear.php.net/bugs/bug.php?id=16416
+
+If upgrading, be sure that fix is included with the future upgrade!
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser.php b/plugins/OStatus/extlib/XML/Feed/Parser.php
new file mode 100755 (executable)
index 0000000..ffe8220
--- /dev/null
@@ -0,0 +1,351 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Key gateway class for XML_Feed_Parser package
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL
+ * @version    CVS: $Id: Parser.php,v 1.24 2006/08/15 13:04:00 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * XML_Feed_Parser_Type is an abstract class required by all of our
+ * feed types. It makes sense to load it here to keep the other files
+ * clean.
+ */
+require_once 'XML/Feed/Parser/Type.php';
+
+/**
+ * We will throw exceptions when errors occur.
+ */
+require_once 'XML/Feed/Parser/Exception.php';
+
+/**
+ * This is the core of the XML_Feed_Parser package. It identifies feed types 
+ * and abstracts access to them. It is an iterator, allowing for easy access 
+ * to the entire feed.
+ *
+ * @author  James Stewart <james@jystewart.net>
+ * @version Release: 1.0.3
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser implements Iterator
+{
+    /**
+     * This is where we hold the feed object 
+     * @var Object
+     */
+    private $feed;
+
+    /**
+     * To allow for extensions, we make a public reference to the feed model 
+     * @var DOMDocument
+     */
+    public $model;
+    
+    /**
+     * A map between entry ID and offset
+     * @var array
+     */
+    protected $idMappings = array();
+
+    /**
+     * A storage space for Namespace URIs.
+     * @var array
+     */
+    private $feedNamespaces = array(
+        'rss2' => array(
+            'http://backend.userland.com/rss',
+            'http://backend.userland.com/rss2',
+            'http://blogs.law.harvard.edu/tech/rss'));
+    /**
+     * Detects feed types and instantiate appropriate objects.
+     *
+     * Our constructor takes care of detecting feed types and instantiating
+     * appropriate classes. For now we're going to treat Atom 0.3 as Atom 1.0
+     * but raise a warning. I do not intend to introduce full support for 
+     * Atom 0.3 as it has been deprecated, but others are welcome to.
+     *
+     * @param    string    $feed    XML serialization of the feed
+     * @param    bool    $strict    Whether or not to validate the feed
+     * @param    bool    $suppressWarnings Trigger errors for deprecated feed types?
+     * @param    bool    $tidy    Whether or not to try and use the tidy library on input
+     */
+    function __construct($feed, $strict = false, $suppressWarnings = false, $tidy = false)
+    {
+        $this->model = new DOMDocument;
+        if (! $this->model->loadXML($feed)) {
+            if (extension_loaded('tidy') && $tidy) {
+                $tidy = new tidy;
+                $tidy->parseString($feed, 
+                    array('input-xml' => true, 'output-xml' => true));
+                $tidy->cleanRepair();
+                if (! $this->model->loadXML((string) $tidy)) {
+                    throw new XML_Feed_Parser_Exception('Invalid input: this is not ' .
+                        'valid XML');
+                }
+            } else {
+                throw new XML_Feed_Parser_Exception('Invalid input: this is not valid XML');
+            }
+
+        }
+
+        /* detect feed type */
+        $doc_element = $this->model->documentElement;
+        $error = false;
+
+        switch (true) {
+            case ($doc_element->namespaceURI == 'http://www.w3.org/2005/Atom'):
+                require_once 'XML/Feed/Parser/Atom.php';
+                require_once 'XML/Feed/Parser/AtomElement.php';
+                $class = 'XML_Feed_Parser_Atom';
+                break;
+            case ($doc_element->namespaceURI == 'http://purl.org/atom/ns#'):
+                require_once 'XML/Feed/Parser/Atom.php';
+                require_once 'XML/Feed/Parser/AtomElement.php';
+                $class = 'XML_Feed_Parser_Atom';
+                $error = 'Atom 0.3 deprecated, using 1.0 parser which won\'t provide ' .
+                    'all options';
+                break;
+            case ($doc_element->namespaceURI == 'http://purl.org/rss/1.0/' || 
+                ($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1 
+                && $doc_element->childNodes->item(1)->namespaceURI == 
+                'http://purl.org/rss/1.0/')):
+                require_once 'XML/Feed/Parser/RSS1.php';
+                require_once 'XML/Feed/Parser/RSS1Element.php';
+                $class = 'XML_Feed_Parser_RSS1';
+                break;
+            case ($doc_element->namespaceURI == 'http://purl.org/rss/1.1/' || 
+                ($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1 
+                && $doc_element->childNodes->item(1)->namespaceURI == 
+                'http://purl.org/rss/1.1/')):
+                require_once 'XML/Feed/Parser/RSS11.php';
+                require_once 'XML/Feed/Parser/RSS11Element.php';
+                $class = 'XML_Feed_Parser_RSS11';
+                break;
+            case (($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1
+                && $doc_element->childNodes->item(1)->namespaceURI == 
+                'http://my.netscape.com/rdf/simple/0.9/') || 
+                $doc_element->namespaceURI == 'http://my.netscape.com/rdf/simple/0.9/'):
+                require_once 'XML/Feed/Parser/RSS09.php';
+                require_once 'XML/Feed/Parser/RSS09Element.php';
+                $class = 'XML_Feed_Parser_RSS09';
+                break;
+            case ($doc_element->tagName == 'rss' and
+                $doc_element->hasAttribute('version') && 
+                $doc_element->getAttribute('version') == 0.91):
+                $error = 'RSS 0.91 has been superceded by RSS2.0. Using RSS2.0 parser.';
+                require_once 'XML/Feed/Parser/RSS2.php';
+                require_once 'XML/Feed/Parser/RSS2Element.php';
+                $class = 'XML_Feed_Parser_RSS2';
+                break;
+            case ($doc_element->tagName == 'rss' and
+                $doc_element->hasAttribute('version') && 
+                $doc_element->getAttribute('version') == 0.92):
+                $error = 'RSS 0.92 has been superceded by RSS2.0. Using RSS2.0 parser.';
+                require_once 'XML/Feed/Parser/RSS2.php';
+                require_once 'XML/Feed/Parser/RSS2Element.php';
+                $class = 'XML_Feed_Parser_RSS2';
+                break;
+            case (in_array($doc_element->namespaceURI, $this->feedNamespaces['rss2'])
+                || $doc_element->tagName == 'rss'):
+                if (! $doc_element->hasAttribute('version') || 
+                    $doc_element->getAttribute('version') != 2) {
+                    $error = 'RSS version not specified. Parsing as RSS2.0';
+                }
+                require_once 'XML/Feed/Parser/RSS2.php';
+                require_once 'XML/Feed/Parser/RSS2Element.php';
+                $class = 'XML_Feed_Parser_RSS2';
+                break;
+            default:
+                throw new XML_Feed_Parser_Exception('Feed type unknown');
+                break;
+        }
+
+        if (! $suppressWarnings && ! empty($error)) {
+            trigger_error($error, E_USER_WARNING);
+        }
+
+        /* Instantiate feed object */
+        $this->feed = new $class($this->model, $strict);
+    }
+
+    /**
+     * Proxy to allow feed element names to be used as method names
+     *
+     * For top-level feed elements we will provide access using methods or 
+     * attributes. This function simply passes on a request to the appropriate 
+     * feed type object.
+     *
+     * @param   string  $call - the method being called
+     * @param   array   $attributes
+     */
+    function __call($call, $attributes)
+    {
+        $attributes = array_pad($attributes, 5, false);
+        list($a, $b, $c, $d, $e) = $attributes;
+        return $this->feed->$call($a, $b, $c, $d, $e);
+    }
+
+    /**
+     * Proxy to allow feed element names to be used as attribute names
+     *
+     * To allow variable-like access to feed-level data we use this
+     * method. It simply passes along to __call() which in turn passes
+     * along to the relevant object.
+     *
+     * @param   string  $val - the name of the variable required
+     */
+    function __get($val)
+    {
+        return $this->feed->$val;
+    }
+
+    /**
+     * Provides iteration functionality.
+     *
+     * Of course we must be able to iterate... This function simply increases
+     * our internal counter.
+     */
+    function next()
+    {
+        if (isset($this->current_item) && 
+            $this->current_item <= $this->feed->numberEntries - 1) {
+            ++$this->current_item;
+        } else if (! isset($this->current_item)) {
+            $this->current_item = 0;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Return XML_Feed_Type object for current element
+     *
+     * @return    XML_Feed_Parser_Type Object
+     */
+    function current()
+    {
+        return $this->getEntryByOffset($this->current_item);
+    }
+
+    /**
+     * For iteration -- returns the key for the current stage in the array.
+     *
+     * @return    int
+     */    
+    function key()
+    {
+        return $this->current_item;
+    }
+
+    /**
+     * For iteration -- tells whether we have reached the 
+     * end.
+     *
+     * @return    bool
+     */
+    function valid()
+    {
+        return $this->current_item < $this->feed->numberEntries;
+    }
+
+    /**
+     * For iteration -- resets the internal counter to the beginning.
+     */
+    function rewind()
+    {
+        $this->current_item = 0;
+    }
+
+    /**
+     * Provides access to entries by ID if one is specified in the source feed.
+     *
+     * As well as allowing the items to be iterated over we want to allow
+     * users to be able to access a specific entry. This is one of two ways of
+     * doing that, the other being by offset. This method can be quite slow
+     * if dealing with a large feed that hasn't yet been processed as it
+     * instantiates objects for every entry until it finds the one needed.
+     *
+     * @param    string    $id  Valid ID for the given feed format
+     * @return    XML_Feed_Parser_Type|false
+     */            
+    function getEntryById($id)
+    {
+        if (isset($this->idMappings[$id])) {
+            return $this->getEntryByOffset($this->idMappings[$id]);
+        }
+
+        /* 
+         * Since we have not yet encountered that ID, let's go through all the
+         * remaining entries in order till we find it.
+         * This is a fairly slow implementation, but it should work.
+         */
+        return $this->feed->getEntryById($id);
+    }
+
+    /**
+     * Retrieve entry by numeric offset, starting from zero.
+     *
+     * As well as allowing the items to be iterated over we want to allow
+     * users to be able to access a specific entry. This is one of two ways of
+     * doing that, the other being by ID.
+     *
+     * @param    int    $offset The position of the entry within the feed, starting from 0
+     * @return    XML_Feed_Parser_Type|false
+     */
+    function getEntryByOffset($offset)
+    {
+        if ($offset < $this->feed->numberEntries) {
+            if (isset($this->feed->entries[$offset])) {
+                return $this->feed->entries[$offset];
+            } else {
+                try {
+                    $this->feed->getEntryByOffset($offset);
+                } catch (Exception $e) {
+                    return false;
+                }
+                $id = $this->feed->entries[$offset]->getID();
+                $this->idMappings[$id] = $offset;
+                return $this->feed->entries[$offset];
+            }
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Retrieve version details from feed type class.
+     *
+     * @return void
+     * @author James Stewart
+     */
+    function version()
+    {
+        return $this->feed->version;
+    }
+    
+    /**
+     * Returns a string representation of the feed.
+     * 
+     * @return String
+     **/
+    function __toString()
+    {
+        return $this->feed->__toString();
+    }
+}
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/Atom.php b/plugins/OStatus/extlib/XML/Feed/Parser/Atom.php
new file mode 100644 (file)
index 0000000..c7e218a
--- /dev/null
@@ -0,0 +1,365 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Atom feed class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: Atom.php,v 1.29 2008/03/30 22:00:36 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+*/
+
+/**
+ * This is the class that determines how we manage Atom 1.0 feeds
+ * 
+ * How we deal with constructs:
+ *  date - return as unix datetime for use with the 'date' function unless specified otherwise
+ *  text - return as is. optional parameter will give access to attributes
+ *  person - defaults to name, but parameter based access
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.3
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_Atom extends XML_Feed_Parser_Type
+{
+    /**
+     * The URI of the RelaxNG schema used to (optionally) validate the feed 
+     * @var string
+     */
+    private $relax = 'atom.rnc';
+
+    /**
+     * We're likely to use XPath, so let's keep it global 
+     * @var DOMXPath
+     */
+    public $xpath;
+
+    /**
+     * When performing XPath queries we will use this prefix 
+     * @var string
+     */
+    private $xpathPrefix = '//';
+
+    /**
+     * The feed type we are parsing 
+     * @var string
+     */
+    public $version = 'Atom 1.0';
+
+    /** 
+     * The class used to represent individual items 
+     * @var string
+     */
+    protected $itemClass = 'XML_Feed_Parser_AtomElement';
+    
+    /** 
+     * The element containing entries 
+     * @var string
+     */
+    protected $itemElement = 'entry';
+
+    /**
+     * Here we map those elements we're not going to handle individually
+     * to the constructs they are. The optional second parameter in the array
+     * tells the parser whether to 'fall back' (not apt. at the feed level) or
+     * fail if the element is missing. If the parameter is not set, the function
+     * will simply return false and leave it to the client to decide what to do.
+     * @var array
+     */
+    protected $map = array(
+        'author' => array('Person'),
+        'contributor' => array('Person'),
+        'icon' => array('Text'),
+        'logo' => array('Text'),
+        'id' => array('Text', 'fail'),
+        'rights' => array('Text'),
+        'subtitle' => array('Text'),
+        'title' => array('Text', 'fail'),
+        'updated' => array('Date', 'fail'),
+        'link' => array('Link'),
+        'generator' => array('Text'),
+        'category' => array('Category'));
+
+    /**
+     * Here we provide a few mappings for those very special circumstances in
+     * which it makes sense to map back to the RSS2 spec. Key is RSS2 version
+     * value is an array consisting of the equivalent in atom and any attributes
+     * needed to make the mapping.
+     * @var array
+     */
+    protected $compatMap = array(
+        'guid' => array('id'),
+        'links' => array('link'),
+        'tags' => array('category'),
+        'contributors' => array('contributor'));
+
+    /**
+     * Our constructor does nothing more than its parent.
+     * 
+     * @param    DOMDocument    $xml    A DOM object representing the feed
+     * @param    bool (optional) $string    Whether or not to validate this feed
+     */
+    function __construct(DOMDocument $model, $strict = false)
+    {
+        $this->model = $model;
+
+        if ($strict) {
+            if (! $this->model->relaxNGValidateSource($this->relax)) {
+                throw new XML_Feed_Parser_Exception('Failed required validation');
+            }
+        }
+
+        $this->xpath = new DOMXPath($this->model);
+        $this->xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
+        $this->numberEntries = $this->count('entry');
+    }
+
+    /**
+     * Implement retrieval of an entry based on its ID for atom feeds.
+     *
+     * This function uses XPath to get the entry based on its ID. If DOMXPath::evaluate
+     * is available, we also use that to store a reference to the entry in the array
+     * used by getEntryByOffset so that method does not have to seek out the entry
+     * if it's requested that way.
+     * 
+     * @param    string    $id    any valid Atom ID.
+     * @return    XML_Feed_Parser_AtomElement
+     */
+    function getEntryById($id)
+    {
+        if (isset($this->idMappings[$id])) {
+            return $this->entries[$this->idMappings[$id]];
+        }
+
+        $entries = $this->xpath->query("//atom:entry[atom:id='$id']");
+
+        if ($entries->length > 0) {
+            $xmlBase = $entries->item(0)->baseURI;
+            $entry = new $this->itemClass($entries->item(0), $this, $xmlBase);
+            
+            if (in_array('evaluate', get_class_methods($this->xpath))) {
+                $offset = $this->xpath->evaluate("count(preceding-sibling::atom:entry)", $entries->item(0));
+                $this->entries[$offset] = $entry;
+            }
+
+            $this->idMappings[$id] = $entry;
+
+            return $entry;
+        }
+        
+    }
+
+    /**
+     * Retrieves data from a person construct.
+     *
+     * Get a person construct. We default to the 'name' element but allow
+     * access to any of the elements.
+     * 
+     * @param    string    $method    The name of the person construct we want
+     * @param    array     $arguments    An array which we hope gives a 'param'
+     * @return    string|false
+     */
+    protected function getPerson($method, $arguments)
+    {
+        $offset = empty($arguments[0]) ? 0 : $arguments[0];
+        $parameter = empty($arguments[1]['param']) ? 'name' : $arguments[1]['param'];
+        $section = $this->model->getElementsByTagName($method);
+        
+        if ($parameter == 'url') {
+            $parameter = 'uri';
+        }
+
+        if ($section->length <= $offset) {
+            return false;
+        }
+
+        $param = $section->item($offset)->getElementsByTagName($parameter);
+        if ($param->length == 0) {
+            return false;
+        }
+        return $param->item(0)->nodeValue;
+    }
+
+    /**
+     * Retrieves an element's content where that content is a text construct.
+     *
+     * Get a text construct. When calling this method, the two arguments
+     * allowed are 'offset' and 'attribute', so $parser->subtitle() would
+     * return the content of the element, while $parser->subtitle(false, 'type')
+     * would return the value of the type attribute.
+     *
+     * @todo    Clarify overlap with getContent()
+     * @param    string    $method    The name of the text construct we want
+     * @param    array     $arguments    An array which we hope gives a 'param'
+     * @return    string
+     */
+    protected function getText($method, $arguments)
+    {
+        $offset = empty($arguments[0]) ? 0: $arguments[0];
+        $attribute = empty($arguments[1]) ? false : $arguments[1];
+        $tags = $this->model->getElementsByTagName($method);
+
+        if ($tags->length <= $offset) {
+            return false;
+        }
+
+        $content = $tags->item($offset);
+
+        if (! $content->hasAttribute('type')) {
+            $content->setAttribute('type', 'text');
+        }
+        $type = $content->getAttribute('type');
+
+        if (! empty($attribute) and 
+            ! ($method == 'generator' and $attribute == 'name')) {
+            if ($content->hasAttribute($attribute)) {
+                return $content->getAttribute($attribute);
+            } else if ($attribute == 'href' and $content->hasAttribute('uri')) {
+                return $content->getAttribute('uri');
+            }
+            return false;
+        }
+
+        return $this->parseTextConstruct($content);
+    }
+    
+    /**
+     * Extract content appropriately from atom text constructs
+     *
+     * Because of different rules applied to the content element and other text
+     * constructs, they are deployed as separate functions, but they share quite
+     * a bit of processing. This method performs the core common process, which is
+     * to apply the rules for different mime types in order to extract the content.
+     *
+     * @param   DOMNode $content    the text construct node to be parsed
+     * @return String
+     * @author James Stewart
+     **/
+    protected function parseTextConstruct(DOMNode $content)
+    {
+        if ($content->hasAttribute('type')) {
+            $type = $content->getAttribute('type');
+        } else {
+            $type = 'text';
+        }
+
+        if (strpos($type, 'text/') === 0) {
+            $type = 'text';
+        }
+
+        switch ($type) {
+            case 'text':
+            case 'html':
+                return $content->textContent;
+                break;
+            case 'xhtml':
+                $container = $content->getElementsByTagName('div');
+                if ($container->length == 0) {
+                    return false;
+                }
+                $contents = $container->item(0);
+                if ($contents->hasChildNodes()) {
+                    /* Iterate through, applying xml:base and store the result */
+                    $result = '';
+                    foreach ($contents->childNodes as $node) {
+                        $result .= $this->traverseNode($node);
+                    }
+                    return $result;
+                }
+                break;
+            case preg_match('@^[a-zA-Z]+/[a-zA-Z+]*xml@i', $type) > 0:
+                return $content;
+                break;
+            case 'application/octet-stream':
+            default:
+                return base64_decode(trim($content->nodeValue));
+                break;
+        }
+        return false;
+    }
+    /**
+     * Get a category from the entry.
+     *
+     * A feed or entry can have any number of categories. A category can have the
+     * attributes term, scheme and label.
+     * 
+     * @param    string    $method    The name of the text construct we want
+     * @param    array     $arguments    An array which we hope gives a 'param'
+     * @return    string
+     */
+    function getCategory($method, $arguments)
+    {
+        $offset = empty($arguments[0]) ? 0: $arguments[0];
+        $attribute = empty($arguments[1]) ? 'term' : $arguments[1];
+        $categories = $this->model->getElementsByTagName('category');
+        if ($categories->length <= $offset) {
+            $category = $categories->item($offset);
+            if ($category->hasAttribute($attribute)) {
+                return $category->getAttribute($attribute);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * This element must be present at least once with rel="feed". This element may be 
+     * present any number of further times so long as there is no clash. If no 'rel' is 
+     * present and we're asked for one, we follow the example of the Universal Feed
+     * Parser and presume 'alternate'.
+     *
+     * @param    int    $offset    the position of the link within the container
+     * @param    string    $attribute    the attribute name required
+     * @param    array     an array of attributes to search by
+     * @return    string    the value of the attribute
+     */
+    function getLink($offset = 0, $attribute = 'href', $params = false)
+    {
+        if (is_array($params) and !empty($params)) {
+            $terms = array();
+            $alt_predicate = '';
+            $other_predicate = '';
+
+            foreach ($params as $key => $value) {
+                if ($key == 'rel' && $value == 'alternate') {
+                    $alt_predicate = '[not(@rel) or @rel="alternate"]';
+                } else {
+                    $terms[] = "@$key='$value'";
+                }
+            }
+            if (!empty($terms)) {
+                $other_predicate = '[' . join(' and ', $terms) . ']';
+            }
+            $query =  $this->xpathPrefix . 'atom:link' . $alt_predicate . $other_predicate;
+            $links = $this->xpath->query($query);
+        } else {
+            $links = $this->model->getElementsByTagName('link');
+        }
+        if ($links->length > $offset) {
+            if ($links->item($offset)->hasAttribute($attribute)) {
+                $value = $links->item($offset)->getAttribute($attribute);
+                if ($attribute == 'href') {
+                    $value = $this->addBase($value, $links->item($offset));
+                }
+                return $value;
+            } else if ($attribute == 'rel') {
+                return 'alternate';
+            }
+        }
+        return false;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/AtomElement.php b/plugins/OStatus/extlib/XML/Feed/Parser/AtomElement.php
new file mode 100755 (executable)
index 0000000..063ecb6
--- /dev/null
@@ -0,0 +1,261 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * AtomElement class for XML_Feed_Parser package
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: AtomElement.php,v 1.19 2007/03/26 12:43:11 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class provides support for atom entries. It will usually be called by
+ * XML_Feed_Parser_Atom with which it shares many methods.
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.3
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_AtomElement extends XML_Feed_Parser_Atom
+{
+    /**
+     * This will be a reference to the parent object for when we want
+     * to use a 'fallback' rule 
+     * @var XML_Feed_Parser_Atom
+     */
+    protected $parent;
+
+    /**
+     * When performing XPath queries we will use this prefix 
+     * @var string
+     */
+    private $xpathPrefix = '';
+    
+    /**
+     * xml:base values inherited by the element 
+     * @var string
+     */
+    protected $xmlBase;
+
+    /**
+     * Here we provide a few mappings for those very special circumstances in
+     * which it makes sense to map back to the RSS2 spec or to manage other
+     * compatibilities (eg. with the Univeral Feed Parser). Key is the other version's
+     * name for the command, value is an array consisting of the equivalent in our atom 
+     * api and any attributes needed to make the mapping.
+     * @var array
+     */
+    protected $compatMap = array(
+        'guid' => array('id'),
+        'links' => array('link'),
+        'tags' => array('category'),
+        'contributors' => array('contributor'));
+        
+    /**
+     * Our specific element map 
+     * @var array
+     */
+    protected $map = array(
+        'author' => array('Person', 'fallback'),
+        'contributor' => array('Person'),
+        'id' => array('Text', 'fail'),
+        'published' => array('Date'),
+        'updated' => array('Date', 'fail'),
+        'title' => array('Text', 'fail'),
+        'rights' => array('Text', 'fallback'),
+        'summary' => array('Text'),
+        'content' => array('Content'),
+        'link' => array('Link'),
+        'enclosure' => array('Enclosure'),
+        'category' => array('Category'));
+
+    /**
+     * Store useful information for later.
+     *
+     * @param   DOMElement  $element - this item as a DOM element
+     * @param   XML_Feed_Parser_Atom    $parent - the feed of which this is a member
+     */
+    function __construct(DOMElement $element, $parent, $xmlBase = '')
+    {
+        $this->model = $element;
+        $this->parent = $parent;
+        $this->xmlBase = $xmlBase;
+        $this->xpathPrefix = "//atom:entry[atom:id='" . $this->id . "']/";
+        $this->xpath = $this->parent->xpath;
+    }
+
+    /**
+     * Provides access to specific aspects of the author data for an atom entry
+     *
+     * Author data at the entry level is more complex than at the feed level.
+     * If atom:author is not present for the entry we need to look for it in
+     * an atom:source child of the atom:entry. If it's not there either, then
+     * we look to the parent for data.
+     *
+     * @param   array
+     * @return  string
+     */
+    function getAuthor($arguments)
+    {
+        /* Find out which part of the author data we're looking for */
+        if (isset($arguments['param'])) {
+            $parameter = $arguments['param'];
+        } else {
+            $parameter = 'name';
+        }
+        
+        $test = $this->model->getElementsByTagName('author');
+        if ($test->length > 0) {
+            $item = $test->item(0);
+            return $item->getElementsByTagName($parameter)->item(0)->nodeValue;
+        }
+        
+        $source = $this->model->getElementsByTagName('source');
+        if ($source->length > 0) {
+            $test = $this->model->getElementsByTagName('author');
+            if ($test->length > 0) {
+                $item = $test->item(0);
+                return $item->getElementsByTagName($parameter)->item(0)->nodeValue;
+            }
+        }
+        return $this->parent->getAuthor($arguments);
+    }
+
+    /**
+     * Returns the content of the content element or info on a specific attribute
+     *
+     * This element may or may not be present. It cannot be present more than
+     * once. It may have a 'src' attribute, in which case there's no content
+     * If not present, then the entry must have link with rel="alternate".
+     * If there is content we return it, if not and there's a 'src' attribute
+     * we return the value of that instead. The method can take an 'attribute'
+     * argument, in which case we return the value of that attribute if present.
+     * eg. $item->content("type") will return the type of the content. It is
+     * recommended that all users check the type before getting the content to
+     * ensure that their script is capable of handling the type of returned data.
+     * (data carried in the content element can be either 'text', 'html', 'xhtml', 
+     * or any standard MIME type).
+     *
+     * @return  string|false
+     */
+    protected function getContent($method, $arguments = array())
+    {
+        $attribute = empty($arguments[0]) ? false : $arguments[0];
+        $tags = $this->model->getElementsByTagName('content');
+
+        if ($tags->length == 0) {
+            return false;
+        }
+
+        $content = $tags->item(0);
+
+        if (! $content->hasAttribute('type')) {
+            $content->setAttribute('type', 'text');
+        }
+        if (! empty($attribute)) {
+            return $content->getAttribute($attribute);
+        }
+
+        $type = $content->getAttribute('type');
+
+        if (! empty($attribute)) {
+            if ($content->hasAttribute($attribute))
+            {
+                return $content->getAttribute($attribute);
+            }
+            return false;
+        }
+
+        if ($content->hasAttribute('src')) {
+            return $content->getAttribute('src');
+        }
+
+        return $this->parseTextConstruct($content);
+     }
+
+    /**
+     * For compatibility, this method provides a mapping to access enclosures.
+     *
+     * The Atom spec doesn't provide for an enclosure element, but it is
+     * generally supported using the link element with rel='enclosure'.
+     *
+     * @param   string  $method - for compatibility with our __call usage
+     * @param   array   $arguments - for compatibility with our __call usage
+     * @return  array|false
+     */
+    function getEnclosure($method, $arguments = array())
+    {
+        $offset = isset($arguments[0]) ? $arguments[0] : 0;
+        $query = "//atom:entry[atom:id='" . $this->getText('id', false) . 
+            "']/atom:link[@rel='enclosure']";
+
+        $encs = $this->parent->xpath->query($query);
+        if ($encs->length > $offset) {
+            try {
+                if (! $encs->item($offset)->hasAttribute('href')) {
+                    return false;
+                }
+                $attrs = $encs->item($offset)->attributes;
+                $length = $encs->item($offset)->hasAttribute('length') ? 
+                    $encs->item($offset)->getAttribute('length') : false;
+                return array(
+                    'url' => $attrs->getNamedItem('href')->value,
+                    'type' => $attrs->getNamedItem('type')->value,
+                    'length' => $length);
+            } catch (Exception $e) {
+                return false;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Get details of this entry's source, if available/relevant
+     *
+     * Where an atom:entry is taken from another feed then the aggregator
+     * is supposed to include an atom:source element which replicates at least
+     * the atom:id, atom:title, and atom:updated metadata from the original
+     * feed. Atom:source therefore has a very similar structure to atom:feed
+     * and if we find it we will return it as an XML_Feed_Parser_Atom object.
+     *
+     * @return  XML_Feed_Parser_Atom|false
+     */
+    function getSource()
+    {
+        $test = $this->model->getElementsByTagName('source');
+        if ($test->length == 0) {
+            return false;
+        }
+        $source = new XML_Feed_Parser_Atom($test->item(0));
+    }
+
+    /**
+     * Get the entry as an XML string
+     *
+     * Return an XML serialization of the feed, should it be required. Most 
+     * users however, will already have a serialization that they used when 
+     * instantiating the object.
+     *
+     * @return    string    XML serialization of element
+     */    
+    function __toString()
+    {
+        $simple = simplexml_import_dom($this->model);
+        return $simple->asXML();
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/Exception.php b/plugins/OStatus/extlib/XML/Feed/Parser/Exception.php
new file mode 100755 (executable)
index 0000000..1e76e3f
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Keeps the exception class for XML_Feed_Parser.
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL
+ * @version    CVS: $Id: Exception.php,v 1.3 2005/11/07 01:52:35 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+/**
+ * We are extending PEAR_Exception
+ */
+require_once 'PEAR/Exception.php';
+
+/**
+ * XML_Feed_Parser_Exception is a simple extension of PEAR_Exception, existing
+ * to help with identification of the source of exceptions.
+ *
+ * @author  James Stewart <james@jystewart.net>
+ * @version Release: 1.0.3
+ * @package XML_Feed_Parser
+ */ 
+class XML_Feed_Parser_Exception extends PEAR_Exception
+{
+
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS09.php
new file mode 100755 (executable)
index 0000000..07f38f9
--- /dev/null
@@ -0,0 +1,214 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS0.9 class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS09.php,v 1.5 2006/07/26 21:18:46 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class handles RSS0.9 feeds.
+ * 
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.3
+ * @package XML_Feed_Parser
+ * @todo    Find a Relax NG URI we can use
+ */
+class XML_Feed_Parser_RSS09 extends XML_Feed_Parser_Type
+{
+    /**
+     * The URI of the RelaxNG schema used to (optionally) validate the feed 
+     * @var string
+     */
+    private $relax = '';
+
+    /**
+     * We're likely to use XPath, so let's keep it global
+     * @var DOMXPath
+     */
+    protected $xpath;
+
+    /**
+     * The feed type we are parsing 
+     * @var string
+     */
+    public $version = 'RSS 0.9';
+
+    /**
+     * The class used to represent individual items 
+     * @var string
+     */
+    protected $itemClass = 'XML_Feed_Parser_RSS09Element';
+    
+    /**
+     * The element containing entries 
+     * @var string
+     */
+    protected $itemElement = 'item';
+
+    /**
+     * Here we map those elements we're not going to handle individually
+     * to the constructs they are. The optional second parameter in the array
+     * tells the parser whether to 'fall back' (not apt. at the feed level) or
+     * fail if the element is missing. If the parameter is not set, the function
+     * will simply return false and leave it to the client to decide what to do.
+     * @var array
+     */
+    protected $map = array(
+        'title' => array('Text'),
+        'link' => array('Text'),
+        'description' => array('Text'),
+        'image' => array('Image'),
+        'textinput' => array('TextInput'));
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS2.
+     * @var array
+     */
+    protected $compatMap = array(
+        'title' => array('title'),
+        'link' => array('link'),
+        'subtitle' => array('description'));
+
+    /**
+     * We will be working with multiple namespaces and it is useful to 
+     * keep them together 
+     * @var array
+     */
+    protected $namespaces = array(
+        'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
+
+    /**
+     * Our constructor does nothing more than its parent.
+     * 
+     * @todo    RelaxNG validation
+     * @param    DOMDocument    $xml    A DOM object representing the feed
+     * @param    bool (optional) $string    Whether or not to validate this feed
+     */
+    function __construct(DOMDocument $model, $strict = false)
+    {
+        $this->model = $model;
+
+        $this->xpath = new DOMXPath($model);
+        foreach ($this->namespaces as $key => $value) {
+            $this->xpath->registerNamespace($key, $value);
+        }            
+        $this->numberEntries = $this->count('item');
+    }
+
+    /**
+     * Included for compatibility -- will not work with RSS 0.9
+     *
+     * This is not something that will work with RSS0.9 as it does not have
+     * clear restrictions on the global uniqueness of IDs.
+     *
+     * @param    string    $id    any valid ID.
+     * @return    false
+     */
+    function getEntryById($id)
+    {
+        return false;        
+    }
+
+    /**
+     * Get details of the image associated with the feed.
+     *
+     * @return  array|false an array simply containing the child elements
+     */
+    protected function getImage()
+    {
+        $images = $this->model->getElementsByTagName('image');
+        if ($images->length > 0) {
+            $image = $images->item(0);
+            $details = array();
+            if ($image->hasChildNodes()) {
+                $details = array(
+                    'title' => $image->getElementsByTagName('title')->item(0)->value,
+                    'link' => $image->getElementsByTagName('link')->item(0)->value,
+                    'url' => $image->getElementsByTagName('url')->item(0)->value);
+            } else {
+                $details = array('title' => false,
+                    'link' => false,
+                    'url' => $image->attributes->getNamedItem('resource')->nodeValue);
+            }
+            $details = array_merge($details, 
+                array('description' => false, 'height' => false, 'width' => false));
+            if (! empty($details)) {
+                return $details;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * The textinput element is little used, but in the interests of
+     * completeness we will support it.
+     *
+     * @return  array|false
+     */
+    protected function getTextInput()
+    {
+        $inputs = $this->model->getElementsByTagName('textinput');
+        if ($inputs->length > 0) {
+            $input = $inputs->item(0);
+            $results = array();
+            $results['title'] = isset(
+                $input->getElementsByTagName('title')->item(0)->value) ? 
+                $input->getElementsByTagName('title')->item(0)->value : null;
+            $results['description'] = isset(
+                $input->getElementsByTagName('description')->item(0)->value) ? 
+                $input->getElementsByTagName('description')->item(0)->value : null;
+            $results['name'] = isset(
+                $input->getElementsByTagName('name')->item(0)->value) ? 
+                $input->getElementsByTagName('name')->item(0)->value : null;
+            $results['link'] = isset(
+                   $input->getElementsByTagName('link')->item(0)->value) ? 
+                   $input->getElementsByTagName('link')->item(0)->value : null;
+            if (empty($results['link']) && 
+                $input->attributes->getNamedItem('resource')) {
+                $results['link'] = $input->attributes->getNamedItem('resource')->nodeValue;
+            }
+            if (! empty($results)) {
+                return $results;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Get details of a link from the feed.
+     *
+     * In RSS1 a link is a text element but in order to ensure that we resolve
+     * URLs properly we have a special function for them.
+     *
+     * @return  string
+     */
+    function getLink($offset = 0, $attribute = 'href', $params = false)
+    {
+        $links = $this->model->getElementsByTagName('link');
+        if ($links->length <= $offset) {
+            return false;
+        }
+        $link = $links->item($offset);
+        return $this->addBase($link->nodeValue, $link);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS09Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS09Element.php
new file mode 100755 (executable)
index 0000000..d41f36e
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS0.9 Element class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS09Element.php,v 1.4 2006/06/30 17:41:56 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/*
+ * This class provides support for RSS 0.9 entries. It will usually be called by
+ * XML_Feed_Parser_RSS09 with which it shares many methods.
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.3
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_RSS09Element extends XML_Feed_Parser_RSS09
+{
+    /**
+     * This will be a reference to the parent object for when we want
+     * to use a 'fallback' rule 
+     * @var XML_Feed_Parser_RSS09
+     */
+    protected $parent;
+
+    /**
+     * Our specific element map 
+     * @var array
+     */
+    protected $map = array(
+        'title' => array('Text'),
+        'link' => array('Link'));
+
+    /**
+     * Store useful information for later.
+     *
+     * @param   DOMElement  $element - this item as a DOM element
+     * @param   XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
+     */
+    function __construct(DOMElement $element, $parent, $xmlBase = '')
+    {
+        $this->model = $element;
+        $this->parent = $parent;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS1.php
new file mode 100755 (executable)
index 0000000..60c9938
--- /dev/null
@@ -0,0 +1,277 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS1 class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS1.php,v 1.10 2006/07/27 13:52:05 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class handles RSS1.0 feeds.
+ * 
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.3
+ * @package XML_Feed_Parser
+ * @todo    Find a Relax NG URI we can use
+ */
+class XML_Feed_Parser_RSS1 extends XML_Feed_Parser_Type
+{
+    /**
+     * The URI of the RelaxNG schema used to (optionally) validate the feed 
+     * @var string
+     */
+    private $relax = 'rss10.rnc';
+
+    /**
+     * We're likely to use XPath, so let's keep it global
+     * @var DOMXPath
+     */
+    protected $xpath;
+
+    /**
+     * The feed type we are parsing 
+     * @var string
+     */
+    public $version = 'RSS 1.0';
+
+    /**
+     * The class used to represent individual items 
+     * @var string
+     */
+    protected $itemClass = 'XML_Feed_Parser_RSS1Element';
+    
+    /**
+     * The element containing entries 
+     * @var string
+     */
+    protected $itemElement = 'item';
+
+    /**
+     * Here we map those elements we're not going to handle individually
+     * to the constructs they are. The optional second parameter in the array
+     * tells the parser whether to 'fall back' (not apt. at the feed level) or
+     * fail if the element is missing. If the parameter is not set, the function
+     * will simply return false and leave it to the client to decide what to do.
+     * @var array
+     */
+    protected $map = array(
+        'title' => array('Text'),
+        'link' => array('Text'),
+        'description' => array('Text'),
+        'image' => array('Image'),
+        'textinput' => array('TextInput'),
+        'updatePeriod' => array('Text'),
+        'updateFrequency' => array('Text'),
+        'updateBase' => array('Date'),
+        'rights' => array('Text'), # dc:rights
+        'description' => array('Text'), # dc:description
+        'creator' => array('Text'), # dc:creator
+        'publisher' => array('Text'), # dc:publisher
+        'contributor' => array('Text'), # dc:contributor
+        'date' => array('Date') # dc:contributor
+        );
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS2.
+     * @var array
+     */
+    protected $compatMap = array(
+        'title' => array('title'),
+        'link' => array('link'),
+        'subtitle' => array('description'),
+        'author' => array('creator'),
+        'updated' => array('date'));
+
+    /**
+     * We will be working with multiple namespaces and it is useful to 
+     * keep them together 
+     * @var array
+     */
+    protected $namespaces = array(
+        'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
+        'rss' => 'http://purl.org/rss/1.0/',
+        'dc' => 'http://purl.org/rss/1.0/modules/dc/',
+        'content' => 'http://purl.org/rss/1.0/modules/content/',
+        'sy' => 'http://web.resource.org/rss/1.0/modules/syndication/');
+
+    /**
+     * Our constructor does nothing more than its parent.
+     * 
+     * @param    DOMDocument    $xml    A DOM object representing the feed
+     * @param    bool (optional) $string    Whether or not to validate this feed
+     */
+    function __construct(DOMDocument $model, $strict = false)
+    {
+        $this->model = $model;
+        if ($strict) {
+            $validate = $this->model->relaxNGValidate(self::getSchemaDir . 
+                DIRECTORY_SEPARATOR . $this->relax);
+            if (! $validate) {
+                throw new XML_Feed_Parser_Exception('Failed required validation');
+            }
+        }
+
+        $this->xpath = new DOMXPath($model);
+        foreach ($this->namespaces as $key => $value) {
+            $this->xpath->registerNamespace($key, $value);
+        }
+        $this->numberEntries = $this->count('item');
+    }
+
+    /**
+     * Allows retrieval of an entry by ID where the rdf:about attribute is used
+     *
+     * This is not really something that will work with RSS1 as it does not have
+     * clear restrictions on the global uniqueness of IDs. We will employ the
+     * _very_ hit and miss method of selecting entries based on the rdf:about
+     * attribute. If DOMXPath::evaluate is available, we also use that to store 
+     * a reference to the entry in the array used by getEntryByOffset so that 
+     * method does not have to seek out the entry if it's requested that way.
+     *
+     * @param    string    $id    any valid ID.
+     * @return    XML_Feed_Parser_RSS1Element
+     */
+    function getEntryById($id)
+    {
+        if (isset($this->idMappings[$id])) {
+            return $this->entries[$this->idMappings[$id]];
+        }
+
+        $entries = $this->xpath->query("//rss:item[@rdf:about='$id']");
+        if ($entries->length > 0) {
+            $classname = $this->itemClass;
+            $entry = new $classname($entries->item(0), $this);
+            if (in_array('evaluate', get_class_methods($this->xpath))) {
+                $offset = $this->xpath->evaluate("count(preceding-sibling::rss:item)", $entries->item(0));
+                $this->entries[$offset] = $entry;
+            }
+            $this->idMappings[$id] = $entry;
+            return $entry;
+        }
+        return false;
+    }
+
+    /**
+     * Get details of the image associated with the feed.
+     *
+     * @return  array|false an array simply containing the child elements
+     */
+    protected function getImage()
+    {
+        $images = $this->model->getElementsByTagName('image');
+        if ($images->length > 0) {
+            $image = $images->item(0);
+            $details = array();
+            if ($image->hasChildNodes()) {
+                $details = array(
+                    'title' => $image->getElementsByTagName('title')->item(0)->value,
+                    'link' => $image->getElementsByTagName('link')->item(0)->value,
+                    'url' => $image->getElementsByTagName('url')->item(0)->value);
+            } else {
+                $details = array('title' => false,
+                    'link' => false,
+                    'url' => $image->attributes->getNamedItem('resource')->nodeValue);
+            }
+            $details = array_merge($details, array('description' => false, 'height' => false, 'width' => false));
+            if (! empty($details)) {
+                return $details;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * The textinput element is little used, but in the interests of
+     * completeness we will support it.
+     *
+     * @return  array|false
+     */
+    protected function getTextInput()
+    {
+        $inputs = $this->model->getElementsByTagName('textinput');
+        if ($inputs->length > 0) {
+            $input = $inputs->item(0);
+            $results = array();
+            $results['title'] = isset(
+                $input->getElementsByTagName('title')->item(0)->value) ? 
+                $input->getElementsByTagName('title')->item(0)->value : null;
+            $results['description'] = isset(
+                $input->getElementsByTagName('description')->item(0)->value) ? 
+                $input->getElementsByTagName('description')->item(0)->value : null;
+            $results['name'] = isset(
+                $input->getElementsByTagName('name')->item(0)->value) ? 
+                $input->getElementsByTagName('name')->item(0)->value : null;
+            $results['link'] = isset(
+                   $input->getElementsByTagName('link')->item(0)->value) ? 
+                   $input->getElementsByTagName('link')->item(0)->value : null;
+            if (empty($results['link']) and 
+                $input->attributes->getNamedItem('resource')) {
+                $results['link'] = 
+                    $input->attributes->getNamedItem('resource')->nodeValue;
+            }
+            if (! empty($results)) {
+                return $results;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Employs various techniques to identify the author
+     *
+     * Dublin Core provides the dc:creator, dc:contributor, and dc:publisher
+     * elements for defining authorship in RSS1. We will try each of those in
+     * turn in order to simulate the atom author element and will return it
+     * as text.
+     *
+     * @return  array|false
+     */
+    function getAuthor()
+    {
+        $options = array('creator', 'contributor', 'publisher');
+        foreach ($options as $element) {
+            $test = $this->model->getElementsByTagName($element);
+            if ($test->length > 0) {
+                return $test->item(0)->value;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Retrieve a link
+     * 
+     * In RSS1 a link is a text element but in order to ensure that we resolve
+     * URLs properly we have a special function for them.
+     *
+     * @return  string
+     */
+    function getLink($offset = 0, $attribute = 'href', $params = false)
+    {
+        $links = $this->model->getElementsByTagName('link');
+        if ($links->length <= $offset) {
+            return false;
+        }
+        $link = $links->item($offset);
+        return $this->addBase($link->nodeValue, $link);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS11.php
new file mode 100755 (executable)
index 0000000..3cd1ef1
--- /dev/null
@@ -0,0 +1,276 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS1.1 class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS11.php,v 1.6 2006/07/27 13:52:05 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class handles RSS1.1 feeds. RSS1.1 is documented at:
+ * http://inamidst.com/rss1.1/
+ * 
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.3
+ * @package XML_Feed_Parser
+ * @todo    Support for RDF:List
+ * @todo    Ensure xml:lang is accessible to users
+ */
+class XML_Feed_Parser_RSS11 extends XML_Feed_Parser_Type
+{
+    /**
+     * The URI of the RelaxNG schema used to (optionally) validate the feed 
+     * @var string
+     */
+    private $relax = 'rss11.rnc';
+
+    /**
+     * We're likely to use XPath, so let's keep it global
+     * @var DOMXPath
+     */
+    protected $xpath;
+
+    /**
+     * The feed type we are parsing 
+     * @var string
+     */
+    public $version = 'RSS 1.0';
+
+    /**
+     * The class used to represent individual items 
+     * @var string
+     */
+    protected $itemClass = 'XML_Feed_Parser_RSS1Element';
+    
+    /**
+     * The element containing entries 
+     * @var string
+     */
+    protected $itemElement = 'item';
+
+    /**
+     * Here we map those elements we're not going to handle individually
+     * to the constructs they are. The optional second parameter in the array
+     * tells the parser whether to 'fall back' (not apt. at the feed level) or
+     * fail if the element is missing. If the parameter is not set, the function
+     * will simply return false and leave it to the client to decide what to do.
+     * @var array
+     */
+    protected $map = array(
+        'title' => array('Text'),
+        'link' => array('Text'),
+        'description' => array('Text'),
+        'image' => array('Image'),
+        'updatePeriod' => array('Text'),
+        'updateFrequency' => array('Text'),
+        'updateBase' => array('Date'),
+        'rights' => array('Text'), # dc:rights
+        'description' => array('Text'), # dc:description
+        'creator' => array('Text'), # dc:creator
+        'publisher' => array('Text'), # dc:publisher
+        'contributor' => array('Text'), # dc:contributor
+        'date' => array('Date') # dc:contributor
+        );
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS2.
+     * @var array
+     */
+    protected $compatMap = array(
+        'title' => array('title'),
+        'link' => array('link'),
+        'subtitle' => array('description'),
+        'author' => array('creator'),
+        'updated' => array('date'));
+
+    /**
+     * We will be working with multiple namespaces and it is useful to 
+     * keep them together. We will retain support for some common RSS1.0 modules
+     * @var array
+     */
+    protected $namespaces = array(
+        'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
+        'rss' => 'http://purl.org/net/rss1.1#',
+        'dc' => 'http://purl.org/rss/1.0/modules/dc/',
+        'content' => 'http://purl.org/rss/1.0/modules/content/',
+        'sy' => 'http://web.resource.org/rss/1.0/modules/syndication/');
+
+    /**
+     * Our constructor does nothing more than its parent.
+     * 
+     * @param    DOMDocument    $xml    A DOM object representing the feed
+     * @param    bool (optional) $string    Whether or not to validate this feed
+     */
+    function __construct(DOMDocument $model, $strict = false)
+    {
+        $this->model = $model;
+
+        if ($strict) {
+            $validate = $this->model->relaxNGValidate(self::getSchemaDir . 
+                DIRECTORY_SEPARATOR . $this->relax);
+            if (! $validate) {
+                throw new XML_Feed_Parser_Exception('Failed required validation');
+            }
+        }
+
+        $this->xpath = new DOMXPath($model);
+        foreach ($this->namespaces as $key => $value) {
+            $this->xpath->registerNamespace($key, $value);
+        }            
+        $this->numberEntries = $this->count('item');
+    }
+
+    /**
+     * Attempts to identify an element by ID given by the rdf:about attribute
+     *
+     * This is not really something that will work with RSS1.1 as it does not have
+     * clear restrictions on the global uniqueness of IDs. We will employ the
+     * _very_ hit and miss method of selecting entries based on the rdf:about
+     * attribute. Please note that this is even more hit and miss with RSS1.1 than
+     * with RSS1.0 since RSS1.1 does not require the rdf:about attribute for items.
+     *
+     * @param    string    $id    any valid ID.
+     * @return    XML_Feed_Parser_RSS1Element
+     */
+    function getEntryById($id)
+    {
+        if (isset($this->idMappings[$id])) {
+            return $this->entries[$this->idMappings[$id]];
+        }
+
+        $entries = $this->xpath->query("//rss:item[@rdf:about='$id']");
+        if ($entries->length > 0) {
+            $classname = $this->itemClass;
+            $entry = new $classname($entries->item(0), $this);
+            return $entry;
+        }
+        return false;
+    }
+
+    /**
+     * Get details of the image associated with the feed.
+     *
+     * @return  array|false an array simply containing the child elements
+     */
+    protected function getImage()
+    {
+        $images = $this->model->getElementsByTagName('image');
+        if ($images->length > 0) {
+            $image = $images->item(0);
+            $details = array();
+            if ($image->hasChildNodes()) {
+                $details = array(
+                    'title' => $image->getElementsByTagName('title')->item(0)->value,
+                    'url' => $image->getElementsByTagName('url')->item(0)->value);
+                if ($image->getElementsByTagName('link')->length > 0) {
+                    $details['link'] = 
+                        $image->getElementsByTagName('link')->item(0)->value;
+                }
+            } else {
+                $details = array('title' => false,
+                    'link' => false,
+                    'url' => $image->attributes->getNamedItem('resource')->nodeValue);
+            }
+            $details = array_merge($details, 
+                array('description' => false, 'height' => false, 'width' => false));
+            if (! empty($details)) {
+                return $details;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * The textinput element is little used, but in the interests of
+     * completeness we will support it.
+     *
+     * @return  array|false
+     */
+    protected function getTextInput()
+    {
+        $inputs = $this->model->getElementsByTagName('textinput');
+        if ($inputs->length > 0) {
+            $input = $inputs->item(0);
+            $results = array();
+            $results['title'] = isset(
+                $input->getElementsByTagName('title')->item(0)->value) ? 
+                $input->getElementsByTagName('title')->item(0)->value : null;
+            $results['description'] = isset(
+                $input->getElementsByTagName('description')->item(0)->value) ? 
+                $input->getElementsByTagName('description')->item(0)->value : null;
+            $results['name'] = isset(
+                $input->getElementsByTagName('name')->item(0)->value) ? 
+                $input->getElementsByTagName('name')->item(0)->value : null;
+            $results['link'] = isset(
+                   $input->getElementsByTagName('link')->item(0)->value) ? 
+                   $input->getElementsByTagName('link')->item(0)->value : null;
+            if (empty($results['link']) and 
+                $input->attributes->getNamedItem('resource')) {
+                $results['link'] = $input->attributes->getNamedItem('resource')->nodeValue;
+            }
+            if (! empty($results)) {
+                return $results;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Attempts to discern authorship
+     *
+     * Dublin Core provides the dc:creator, dc:contributor, and dc:publisher
+     * elements for defining authorship in RSS1. We will try each of those in
+     * turn in order to simulate the atom author element and will return it
+     * as text.
+     *
+     * @return  array|false
+     */
+    function getAuthor()
+    {
+        $options = array('creator', 'contributor', 'publisher');
+        foreach ($options as $element) {
+            $test = $this->model->getElementsByTagName($element);
+            if ($test->length > 0) {
+                return $test->item(0)->value;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Retrieve a link
+     *
+     * In RSS1 a link is a text element but in order to ensure that we resolve
+     * URLs properly we have a special function for them.
+     *
+     * @return  string
+     */
+    function getLink($offset = 0, $attribute = 'href', $params = false)
+    {
+        $links = $this->model->getElementsByTagName('link');
+        if ($links->length <= $offset) {
+            return false;
+        }
+        $link = $links->item($offset);
+        return $this->addBase($link->nodeValue, $link);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS11Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS11Element.php
new file mode 100755 (executable)
index 0000000..75918be
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS1 Element class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS11Element.php,v 1.4 2006/06/30 17:41:56 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/*
+ * This class provides support for RSS 1.1 entries. It will usually be called by
+ * XML_Feed_Parser_RSS11 with which it shares many methods.
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.3
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_RSS11Element extends XML_Feed_Parser_RSS11
+{
+    /**
+     * This will be a reference to the parent object for when we want
+     * to use a 'fallback' rule 
+     * @var XML_Feed_Parser_RSS1
+     */
+    protected $parent;
+
+    /**
+     * Our specific element map 
+     * @var array
+     */
+    protected $map = array(
+        'id' => array('Id'),
+        'title' => array('Text'),
+        'link' => array('Link'),
+        'description' => array('Text'), # or dc:description
+        'category' => array('Category'),
+        'rights' => array('Text'), # dc:rights
+        'creator' => array('Text'), # dc:creator
+        'publisher' => array('Text'), # dc:publisher
+        'contributor' => array('Text'), # dc:contributor
+        'date' => array('Date'), # dc:date
+        'content' => array('Content')
+        );
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS1.
+     * @var array
+     */
+    protected $compatMap = array(
+        'content' => array('content'),
+        'updated' => array('lastBuildDate'),
+        'published' => array('pubdate'),
+        'subtitle' => array('description'),
+        'updated' => array('date'),
+        'author' => array('creator'),
+        'contributor' => array('contributor')
+    );
+
+    /**
+     * Store useful information for later.
+     *
+     * @param   DOMElement  $element - this item as a DOM element
+     * @param   XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
+     */
+    function __construct(DOMElement $element, $parent, $xmlBase = '')
+    {
+        $this->model = $element;
+        $this->parent = $parent;
+    }
+
+    /**
+     * If an rdf:about attribute is specified, return that as an ID
+     *
+     * There is no established way of showing an ID for an RSS1 entry. We will 
+     * simulate it using the rdf:about attribute of the entry element. This cannot
+     * be relied upon for unique IDs but may prove useful.
+     *
+     * @return  string|false
+     */
+    function getId()
+    {
+        if ($this->model->attributes->getNamedItem('about')) {
+            return $this->model->attributes->getNamedItem('about')->nodeValue;
+        }
+        return false;
+    }
+
+    /**
+     * Return the entry's content
+     *
+     * The official way to include full content in an RSS1 entry is to use
+     * the content module's element 'encoded'. Often, however, the 'description'
+     * element is used instead. We will offer that as a fallback.
+     *
+     * @return  string|false
+     */
+    function getContent()
+    {
+        $options = array('encoded', 'description');
+        foreach ($options as $element) {
+            $test = $this->model->getElementsByTagName($element);
+            if ($test->length == 0) {
+                continue;
+            }
+            if ($test->item(0)->hasChildNodes()) {
+                $value = '';
+                foreach ($test->item(0)->childNodes as $child) {
+                    if ($child instanceof DOMText) {
+                        $value .= $child->nodeValue;
+                    } else {
+                        $simple = simplexml_import_dom($child);
+                        $value .= $simple->asXML();
+                    }
+                }
+                return $value;
+            } else if ($test->length > 0) {
+                return $test->item(0)->nodeValue;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * How RSS1.1 should support for enclosures is not clear. For now we will return
+     * false.
+     *
+     * @return  false
+     */
+    function getEnclosure()
+    {
+        return false;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS1Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS1Element.php
new file mode 100755 (executable)
index 0000000..8e36d5a
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS1 Element class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS1Element.php,v 1.6 2006/06/30 17:41:56 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/*
+ * This class provides support for RSS 1.0 entries. It will usually be called by
+ * XML_Feed_Parser_RSS1 with which it shares many methods.
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.3
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_RSS1Element extends XML_Feed_Parser_RSS1
+{
+    /**
+     * This will be a reference to the parent object for when we want
+     * to use a 'fallback' rule 
+     * @var XML_Feed_Parser_RSS1
+     */
+    protected $parent;
+
+    /**
+     * Our specific element map 
+     * @var array
+     */
+    protected $map = array(
+        'id' => array('Id'),
+        'title' => array('Text'),
+        'link' => array('Link'),
+        'description' => array('Text'), # or dc:description
+        'category' => array('Category'),
+        'rights' => array('Text'), # dc:rights
+        'creator' => array('Text'), # dc:creator
+        'publisher' => array('Text'), # dc:publisher
+        'contributor' => array('Text'), # dc:contributor
+        'date' => array('Date'), # dc:date
+        'content' => array('Content')
+        );
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS1.
+     * @var array
+     */
+    protected $compatMap = array(
+        'content' => array('content'),
+        'updated' => array('lastBuildDate'),
+        'published' => array('pubdate'),
+        'subtitle' => array('description'),
+        'updated' => array('date'),
+        'author' => array('creator'),
+        'contributor' => array('contributor')
+    );
+
+    /**
+     * Store useful information for later.
+     *
+     * @param   DOMElement  $element - this item as a DOM element
+     * @param   XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
+     */
+    function __construct(DOMElement $element, $parent, $xmlBase = '')
+    {
+        $this->model = $element;
+        $this->parent = $parent;
+    }
+
+    /**
+     * If an rdf:about attribute is specified, return it as an ID
+     *
+     * There is no established way of showing an ID for an RSS1 entry. We will 
+     * simulate it using the rdf:about attribute of the entry element. This cannot
+     * be relied upon for unique IDs but may prove useful.
+     *
+     * @return  string|false
+     */
+    function getId()
+    {
+        if ($this->model->attributes->getNamedItem('about')) {
+            return $this->model->attributes->getNamedItem('about')->nodeValue;
+        }
+        return false;
+    }
+
+    /**
+     * How RSS1 should support for enclosures is not clear. For now we will return
+     * false.
+     *
+     * @return  false
+     */
+    function getEnclosure()
+    {
+        return false;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS2.php
new file mode 100644 (file)
index 0000000..0936bd2
--- /dev/null
@@ -0,0 +1,335 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Class representing feed-level data for an RSS2 feed
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS2.php,v 1.12 2008/03/08 18:16:45 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class handles RSS2 feeds.
+ * 
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.3
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_RSS2 extends XML_Feed_Parser_Type
+{
+    /**
+     * The URI of the RelaxNG schema used to (optionally) validate the feed
+     * @var string
+     */
+    private $relax = 'rss20.rnc';
+
+    /**
+     * We're likely to use XPath, so let's keep it global
+     * @var DOMXPath
+     */
+    protected $xpath;
+
+    /**
+     * The feed type we are parsing
+     * @var string
+     */
+    public $version = 'RSS 2.0';
+
+    /**
+     * The class used to represent individual items
+     * @var string
+     */     
+    protected $itemClass = 'XML_Feed_Parser_RSS2Element';
+    
+    /**
+     * The element containing entries 
+     * @var string
+     */
+    protected $itemElement = 'item';
+
+    /**
+     * Here we map those elements we're not going to handle individually
+     * to the constructs they are. The optional second parameter in the array
+     * tells the parser whether to 'fall back' (not apt. at the feed level) or
+     * fail if the element is missing. If the parameter is not set, the function
+     * will simply return false and leave it to the client to decide what to do.
+     * @var array
+     */
+    protected $map = array(
+        'ttl' => array('Text'),
+        'pubDate' => array('Date'),
+        'lastBuildDate' => array('Date'),
+        'title' => array('Text'),
+        'link' => array('Link'),
+        'description' => array('Text'),
+        'language' => array('Text'),
+        'copyright' => array('Text'),
+        'managingEditor' => array('Text'),
+        'webMaster' => array('Text'),
+        'category' => array('Text'),
+        'generator' => array('Text'),
+        'docs' => array('Text'),
+        'ttl' => array('Text'),
+        'image' => array('Image'),
+        'skipDays' => array('skipDays'),
+        'skipHours' => array('skipHours'));
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS2.
+     * @var array
+     */
+    protected $compatMap = array(
+        'title' => array('title'),
+        'rights' => array('copyright'),
+        'updated' => array('lastBuildDate'),
+        'subtitle' => array('description'),
+        'date' => array('pubDate'),
+        'author' => array('managingEditor'));
+
+    protected $namespaces = array(
+        'dc' => 'http://purl.org/rss/1.0/modules/dc/',
+        'content' => 'http://purl.org/rss/1.0/modules/content/');
+
+    /**
+     * Our constructor does nothing more than its parent.
+     * 
+     * @param    DOMDocument    $xml    A DOM object representing the feed
+     * @param    bool (optional) $string    Whether or not to validate this feed
+     */
+    function __construct(DOMDocument $model, $strict = false)
+    {
+        $this->model = $model;
+
+        if ($strict) {
+            if (! $this->model->relaxNGValidate($this->relax)) {
+                throw new XML_Feed_Parser_Exception('Failed required validation');
+            }
+        }
+
+        $this->xpath = new DOMXPath($this->model);
+        foreach ($this->namespaces as $key => $value) {
+            $this->xpath->registerNamespace($key, $value);
+        }
+        $this->numberEntries = $this->count('item');
+    }
+
+    /**
+     * Retrieves an entry by ID, if the ID is specified with the guid element
+     *
+     * This is not really something that will work with RSS2 as it does not have
+     * clear restrictions on the global uniqueness of IDs. But we can emulate
+     * it by allowing access based on the 'guid' element. If DOMXPath::evaluate
+     * is available, we also use that to store a reference to the entry in the array
+     * used by getEntryByOffset so that method does not have to seek out the entry
+     * if it's requested that way.
+     *
+     * @param    string    $id    any valid ID.
+     * @return    XML_Feed_Parser_RSS2Element
+     */
+    function getEntryById($id)
+    {
+        if (isset($this->idMappings[$id])) {
+            return $this->entries[$this->idMappings[$id]];
+        }
+
+        $entries = $this->xpath->query("//item[guid='$id']");
+        if ($entries->length > 0) {
+            $entry = new $this->itemElement($entries->item(0), $this);
+            if (in_array('evaluate', get_class_methods($this->xpath))) {
+                $offset = $this->xpath->evaluate("count(preceding-sibling::item)", $entries->item(0));
+                $this->entries[$offset] = $entry;
+            }
+            $this->idMappings[$id] = $entry;
+            return $entry;
+        }        
+    }
+
+    /**
+     * Get a category from the element
+     *
+     * The category element is a simple text construct which can occur any number
+     * of times. We allow access by offset or access to an array of results.
+     *
+     * @param    string    $call    for compatibility with our overloading
+     * @param   array $arguments - arg 0 is the offset, arg 1 is whether to return as array
+     * @return  string|array|false
+     */
+    function getCategory($call, $arguments = array())
+    {
+        $categories = $this->model->getElementsByTagName('category');
+        $offset = empty($arguments[0]) ? 0 : $arguments[0];
+        $array = empty($arguments[1]) ? false : true;
+        if ($categories->length <= $offset) {
+            return false;
+        }
+        if ($array) {
+            $list = array();
+            foreach ($categories as $category) {
+                array_push($list, $category->nodeValue);
+            }
+            return $list;
+        }
+        return $categories->item($offset)->nodeValue;
+    }
+
+    /**
+     * Get details of the image associated with the feed.
+     *
+     * @return  array|false an array simply containing the child elements
+     */
+    protected function getImage()
+    {
+        $images = $this->xpath->query("//image");
+        if ($images->length > 0) {
+            $image = $images->item(0);
+            $desc = $image->getElementsByTagName('description');
+            $description = $desc->length ? $desc->item(0)->nodeValue : false;
+            $heigh = $image->getElementsByTagName('height'); 
+            $height = $heigh->length ? $heigh->item(0)->nodeValue : false;
+            $widt = $image->getElementsByTagName('width'); 
+            $width = $widt->length ? $widt->item(0)->nodeValue : false;
+            return array(
+                'title' => $image->getElementsByTagName('title')->item(0)->nodeValue,
+                'link' => $image->getElementsByTagName('link')->item(0)->nodeValue,
+                'url' => $image->getElementsByTagName('url')->item(0)->nodeValue,
+                'description' => $description,
+                'height' => $height,
+                'width' => $width);
+        }
+        return false;
+    }
+
+    /**
+     * The textinput element is little used, but in the interests of
+     * completeness...
+     *
+     * @return  array|false
+     */
+    function getTextInput()
+    {
+        $inputs = $this->model->getElementsByTagName('input');
+        if ($inputs->length > 0) {
+            $input = $inputs->item(0);
+            return array(
+                'title' => $input->getElementsByTagName('title')->item(0)->value,
+                'description' => 
+                    $input->getElementsByTagName('description')->item(0)->value,
+                'name' => $input->getElementsByTagName('name')->item(0)->value,
+                'link' => $input->getElementsByTagName('link')->item(0)->value);
+        }
+        return false;
+    }
+
+    /**
+     * Utility function for getSkipDays and getSkipHours
+     *
+     * This is a general function used by both getSkipDays and getSkipHours. It simply
+     * returns an array of the values of the children of the appropriate tag.
+     *
+     * @param   string      $tagName    The tag name (getSkipDays or getSkipHours)
+     * @return  array|false
+     */
+    protected function getSkips($tagName)
+    {
+        $hours = $this->model->getElementsByTagName($tagName);
+        if ($hours->length == 0) {
+            return false;
+        }
+        $skipHours = array();
+        foreach($hours->item(0)->childNodes as $hour) {
+            if ($hour instanceof DOMElement) {
+                array_push($skipHours, $hour->nodeValue);
+            }
+        }
+        return $skipHours;
+    }
+
+    /**
+     * Retrieve skipHours data
+     *
+     * The skiphours element provides a list of hours on which this feed should
+     * not be checked. We return an array of those hours (integers, 24 hour clock)
+     *
+     * @return  array
+     */    
+    function getSkipHours()
+    {
+        return $this->getSkips('skipHours');
+    }
+
+    /**
+     * Retrieve skipDays data
+     *
+     * The skipdays element provides a list of days on which this feed should
+     * not be checked. We return an array of those days.
+     *
+     * @return  array
+     */
+    function getSkipDays()
+    {
+        return $this->getSkips('skipDays');
+    }
+
+    /**
+     * Return content of the little-used 'cloud' element
+     *
+     * The cloud element is rarely used. It is designed to provide some details
+     * of a location to update the feed.
+     *
+     * @return  array   an array of the attributes of the element
+     */
+    function getCloud()
+    {
+        $cloud = $this->model->getElementsByTagName('cloud');
+        if ($cloud->length == 0) {
+            return false;
+        }
+        $cloudData = array();
+        foreach ($cloud->item(0)->attributes as $attribute) {
+            $cloudData[$attribute->name] = $attribute->value;
+        }
+        return $cloudData;
+    }
+    
+    /**
+     * Get link URL
+     *
+     * In RSS2 a link is a text element but in order to ensure that we resolve
+     * URLs properly we have a special function for them. We maintain the 
+     * parameter used by the atom getLink method, though we only use the offset
+     * parameter.
+     *
+     * @param   int     $offset The position of the link within the feed. Starts from 0
+     * @param   string  $attribute  The attribute of the link element required
+     * @param   array   $params An array of other parameters. Not used.
+     * @return  string
+     */
+    function getLink($offset, $attribute = 'href', $params = array())
+    {
+        $xPath = new DOMXPath($this->model);
+        $links = $xPath->query('//link');
+
+        if ($links->length <= $offset) {
+            return false;
+        }
+        $link = $links->item($offset);
+        return $this->addBase($link->nodeValue, $link);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/RSS2Element.php b/plugins/OStatus/extlib/XML/Feed/Parser/RSS2Element.php
new file mode 100755 (executable)
index 0000000..6edf910
--- /dev/null
@@ -0,0 +1,171 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Class representing entries in an RSS2 feed.
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS2Element.php,v 1.11 2006/07/26 21:18:47 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class provides support for RSS 2.0 entries. It will usually be 
+ * called by XML_Feed_Parser_RSS2 with which it shares many methods.
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.3
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_RSS2Element extends XML_Feed_Parser_RSS2
+{
+    /**
+     * This will be a reference to the parent object for when we want
+     * to use a 'fallback' rule
+     * @var XML_Feed_Parser_RSS2
+     */
+    protected $parent;
+
+    /**
+     * Our specific element map 
+     * @var array
+     */
+    protected $map = array(
+        'title' => array('Text'),
+        'guid' => array('Guid'),
+        'description' => array('Text'),
+        'author' => array('Text'),
+        'comments' => array('Text'),
+        'enclosure' => array('Enclosure'),
+        'pubDate' => array('Date'),
+        'source' => array('Source'),
+        'link' => array('Text'),
+        'content' => array('Content'));
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS2.
+     * @var array
+     */
+    protected $compatMap = array(
+        'id' => array('guid'),
+        'updated' => array('lastBuildDate'),
+        'published' => array('pubdate'),
+        'guidislink' => array('guid', 'ispermalink'),
+        'summary' => array('description'));
+
+    /**
+     * Store useful information for later.
+     *
+     * @param   DOMElement  $element - this item as a DOM element
+     * @param   XML_Feed_Parser_RSS2    $parent - the feed of which this is a member
+     */
+    function __construct(DOMElement $element, $parent, $xmlBase = '')
+    {
+        $this->model = $element;
+        $this->parent = $parent;
+    }
+
+    /**
+     * Get the value of the guid element, if specified
+     *
+     * guid is the closest RSS2 has to atom's ID. It is usually but not always a
+     * URI. The one attribute that RSS2 can posess is 'ispermalink' which specifies
+     * whether the guid is itself dereferencable. Use of guid is not obligatory,
+     * but is advisable. To get the guid you would call $item->id() (for atom
+     * compatibility) or $item->guid(). To check if this guid is a permalink call
+     * $item->guid("ispermalink").
+     *
+     * @param   string  $method - the method name being called
+     * @param   array   $params - parameters required
+     * @return  string  the guid or value of ispermalink
+     */
+    protected function getGuid($method, $params)
+    {
+        $attribute = (isset($params[0]) and $params[0] == 'ispermalink') ? 
+            true : false;
+        $tag = $this->model->getElementsByTagName('guid');
+        if ($tag->length > 0) {
+            if ($attribute) {
+                if ($tag->hasAttribute("ispermalink")) {
+                    return $tag->getAttribute("ispermalink");
+                }
+            }
+            return $tag->item(0)->nodeValue;
+        }
+        return false;
+    }
+
+    /**
+     * Access details of file enclosures
+     *
+     * The RSS2 spec is ambiguous as to whether an enclosure element must be
+     * unique in a given entry. For now we will assume it needn't, and allow
+     * for an offset.
+     *
+     * @param   string $method - the method being called
+     * @param   array   $parameters - we expect the first of these to be our offset
+     * @return  array|false
+     */
+    protected function getEnclosure($method, $parameters)
+    {
+        $encs = $this->model->getElementsByTagName('enclosure');
+        $offset = isset($parameters[0]) ? $parameters[0] : 0;
+        if ($encs->length > $offset) {
+            try {
+                if (! $encs->item($offset)->hasAttribute('url')) {
+                    return false;
+                }
+                $attrs = $encs->item($offset)->attributes;
+                return array(
+                    'url' => $attrs->getNamedItem('url')->value,
+                    'length' => $attrs->getNamedItem('length')->value,
+                    'type' => $attrs->getNamedItem('type')->value);
+            } catch (Exception $e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get the entry source if specified
+     *
+     * source is an optional sub-element of item. Like atom:source it tells
+     * us about where the entry came from (eg. if it's been copied from another
+     * feed). It is not a rich source of metadata in the same way as atom:source
+     * and while it would be good to maintain compatibility by returning an
+     * XML_Feed_Parser_RSS2 element, it makes a lot more sense to return an array.
+     *
+     * @return array|false
+     */
+    protected function getSource()
+    {
+        $get = $this->model->getElementsByTagName('source');
+        if ($get->length) {
+            $source = $get->item(0);
+            $array = array(
+                'content' => $source->nodeValue);
+            foreach ($source->attributes as $attribute) {
+                $array[$attribute->name] = $attribute->value;
+            }
+            return $array;
+        }
+        return false;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/Parser/Type.php b/plugins/OStatus/extlib/XML/Feed/Parser/Type.php
new file mode 100644 (file)
index 0000000..7505261
--- /dev/null
@@ -0,0 +1,467 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Abstract class providing common methods for XML_Feed_Parser feeds.
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: Type.php,v 1.25 2008/03/08 18:39:09 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This abstract class provides some general methods that are likely to be
+ * implemented exactly the same way for all feed types.
+ *
+ * @package XML_Feed_Parser
+ * @author  James Stewart <james@jystewart.net>
+ * @version Release: 1.0.3
+ */
+abstract class XML_Feed_Parser_Type
+{
+    /**
+     * Where we store our DOM object for this feed 
+     * @var DOMDocument
+     */
+    public $model;
+
+    /**
+     * For iteration we'll want a count of the number of entries 
+     * @var int
+     */
+    public $numberEntries;
+
+    /**
+     * Where we store our entry objects once instantiated 
+     * @var array
+     */
+    public $entries = array();
+
+    /**
+     * Store mappings between entry IDs and their position in the feed
+     */
+    public $idMappings = array();
+
+    /**
+     * Proxy to allow use of element names as method names
+     *
+     * We are not going to provide methods for every entry type so this
+     * function will allow for a lot of mapping. We rely pretty heavily
+     * on this to handle our mappings between other feed types and atom.
+     *
+     * @param   string  $call - the method attempted
+     * @param   array   $arguments - arguments to that method
+     * @return  mixed
+     */
+    function __call($call, $arguments = array())
+    {
+        if (! is_array($arguments)) {
+            $arguments = array();
+        }
+
+        if (isset($this->compatMap[$call])) {
+            $tempMap = $this->compatMap;
+            $tempcall = array_pop($tempMap[$call]);
+            if (! empty($tempMap)) {
+                $arguments = array_merge($arguments, $tempMap[$call]);
+            }
+            $call = $tempcall;
+        }
+
+        /* To be helpful, we allow a case-insensitive search for this method */
+        if (! isset($this->map[$call])) {
+            foreach (array_keys($this->map) as $key) {
+                if (strtoupper($key) == strtoupper($call)) {
+                    $call = $key;
+                    break;
+                }
+            }
+        }
+
+        if (empty($this->map[$call])) {
+            return false;
+        }
+
+        $method = 'get' . $this->map[$call][0];
+        if ($method == 'getLink') {
+            $offset = empty($arguments[0]) ? 0 : $arguments[0];
+            $attribute = empty($arguments[1]) ? 'href' : $arguments[1];
+            $params = isset($arguments[2]) ? $arguments[2] : array();
+            return $this->getLink($offset, $attribute, $params);
+        }
+        if (method_exists($this, $method)) {
+            return $this->$method($call, $arguments);
+        }
+
+        return false;
+    }
+
+    /**
+     * Proxy to allow use of element names as attribute names
+     *
+     * For many elements variable-style access will be desirable. This function
+     * provides for that.
+     *
+     * @param   string  $value - the variable required
+     * @return  mixed
+     */
+    function __get($value)
+    {
+        return $this->__call($value, array());
+    }
+
+    /**
+     * Utility function to help us resolve xml:base values
+     *
+     * We have other methods which will traverse the DOM and work out the different
+     * xml:base declarations we need to be aware of. We then need to combine them.
+     * If a declaration starts with a protocol then we restart the string. If it 
+     * starts with a / then we add on to the domain name. Otherwise we simply tag 
+     * it on to the end.
+     *
+     * @param   string  $base - the base to add the link to
+     * @param   string  $link
+     */
+    function combineBases($base, $link)
+    {
+        if (preg_match('/^[A-Za-z]+:\/\//', $link)) {
+            return $link;
+        } else if (preg_match('/^\//', $link)) {
+            /* Extract domain and suffix link to that */
+            preg_match('/^([A-Za-z]+:\/\/.*)?\/*/', $base, $results);
+            $firstLayer = $results[0];
+            return $firstLayer . "/" . $link;
+        } else if (preg_match('/^\.\.\//', $base)) {
+            /* Step up link to find place to be */
+            preg_match('/^((\.\.\/)+)(.*)$/', $link, $bases);
+            $suffix = $bases[3];
+            $count = preg_match_all('/\.\.\//', $bases[1], $steps);
+            $url = explode("/", $base);
+            for ($i = 0; $i <= $count; $i++) {
+                array_pop($url);
+            }
+            return implode("/", $url) . "/" . $suffix;
+        } else if (preg_match('/^(?!\/$)/', $base)) {
+            $base = preg_replace('/(.*\/).*$/', '$1', $base)  ;
+            return $base . $link;
+        } else {
+            /* Just stick it on the end */
+            return $base . $link;
+        }
+    }
+
+    /**
+     * Determine whether we need to apply our xml:base rules
+     *
+     * Gets us the xml:base data and then processes that with regard
+     * to our current link.
+     *
+     * @param   string
+     * @param   DOMElement
+     * @return  string
+     */
+    function addBase($link, $element)
+    {
+        if (preg_match('/^[A-Za-z]+:\/\//', $link)) {
+            return $link;
+        }
+
+        return $this->combineBases($element->baseURI, $link);
+    }
+
+    /**
+     * Get an entry by its position in the feed, starting from zero
+     *
+     * As well as allowing the items to be iterated over we want to allow
+     * users to be able to access a specific entry. This is one of two ways of
+     * doing that, the other being by ID.
+     * 
+     * @param   int $offset
+     * @return  XML_Feed_Parser_RSS1Element
+     */
+    function getEntryByOffset($offset)
+    {
+        if (! isset($this->entries[$offset])) {
+            $entries = $this->model->getElementsByTagName($this->itemElement);
+            if ($entries->length > $offset) {
+                $xmlBase = $entries->item($offset)->baseURI;
+                $this->entries[$offset] = new $this->itemClass(
+                    $entries->item($offset), $this, $xmlBase);
+                if ($id = $this->entries[$offset]->id) {
+                    $this->idMappings[$id] = $this->entries[$offset];
+                }
+            } else {
+                throw new XML_Feed_Parser_Exception('No entries found');
+            }
+        }
+
+        return $this->entries[$offset];
+    }
+
+    /**
+     * Return a date in seconds since epoch.
+     *
+     * Get a date construct. We use PHP's strtotime to return it as a unix datetime, which
+     * is the number of seconds since 1970-01-01 00:00:00.
+     * 
+     * @link    http://php.net/strtotime
+     * @param    string    $method        The name of the date construct we want
+     * @param    array     $arguments    Included for compatibility with our __call usage
+     * @return    int|false datetime
+     */
+    protected function getDate($method, $arguments)
+    {
+        $time = $this->model->getElementsByTagName($method);
+        if ($time->length == 0 || empty($time->item(0)->nodeValue)) {
+            return false;
+        }
+        return strtotime($time->item(0)->nodeValue);
+    }
+
+    /**
+     * Get a text construct. 
+     *
+     * @param    string    $method    The name of the text construct we want
+     * @param    array     $arguments    Included for compatibility with our __call usage
+     * @return    string
+     */
+    protected function getText($method, $arguments = array())
+    {
+        $tags = $this->model->getElementsByTagName($method);
+        if ($tags->length > 0) {
+            $value = $tags->item(0)->nodeValue;
+            return $value;
+        }
+        return false;
+    }
+
+    /**
+     * Apply various rules to retrieve category data.
+     *
+     * There is no single way of declaring a category in RSS1/1.1 as there is in RSS2 
+     * and  Atom. Instead the usual approach is to use the dublin core namespace to 
+     * declare  categories. For example delicious use both: 
+     * <dc:subject>PEAR</dc:subject> and: <taxo:topics><rdf:Bag>
+     * <rdf:li resource="http://del.icio.us/tag/PEAR" /></rdf:Bag></taxo:topics>
+     * to declare a categorisation of 'PEAR'.
+     *
+     * We need to be sensitive to this where possible.
+     *
+     * @param    string    $call    for compatibility with our overloading
+     * @param   array $arguments - arg 0 is the offset, arg 1 is whether to return as array
+     * @return  string|array|false
+     */
+    protected function getCategory($call, $arguments)
+    {
+        $categories = $this->model->getElementsByTagName('subject');
+        $offset = empty($arguments[0]) ? 0 : $arguments[0];
+        $array = empty($arguments[1]) ? false : true;
+        if ($categories->length <= $offset) {
+            return false;
+        }
+        if ($array) {
+            $list = array();
+            foreach ($categories as $category) {
+                array_push($list, $category->nodeValue);
+            }
+            return $list;
+        }
+        return $categories->item($offset)->nodeValue;
+    }
+
+    /**
+     * Count occurrences of an element
+     *
+     * This function will tell us how many times the element $type
+     * appears at this level of the feed.
+     * 
+     * @param    string    $type    the element we want to get a count of
+     * @return    int
+     */
+    protected function count($type)
+    {
+        if ($tags = $this->model->getElementsByTagName($type)) {
+            return $tags->length;
+        }
+        return 0;
+    }
+
+    /**
+     * Part of our xml:base processing code
+     *
+     * We need a couple of methods to access XHTML content stored in feeds. 
+     * This is because we dereference all xml:base references before returning
+     * the element. This method handles the attributes.
+     *
+     * @param   DOMElement $node    The DOM node we are iterating over
+     * @return  string
+     */
+    function processXHTMLAttributes($node) {
+        $return = '';
+        foreach ($node->attributes as $attribute) {
+            if ($attribute->name == 'src' or $attribute->name == 'href') {
+                $attribute->value = $this->addBase(htmlentities($attribute->value, NULL, 'utf-8'), $attribute);
+            }
+            if ($attribute->name == 'base') {
+                continue;
+            }
+            $return .= $attribute->name . '="' . htmlentities($attribute->value, NULL, 'utf-8') .'" ';
+        }
+        if (! empty($return)) {
+            return ' ' . trim($return);
+        }
+        return '';
+    }
+
+    /**
+     * Convert HTML entities based on the current character set.
+     * 
+     * @param String
+     * @return String
+     */
+    function processEntitiesForNodeValue($node) 
+    {
+        if (function_exists('iconv')) {
+          $current_encoding = $node->ownerDocument->encoding;
+          $value = iconv($current_encoding, 'UTF-8', $node->nodeValue);
+        } else if ($current_encoding == 'iso-8859-1') {
+          $value = utf8_encode($node->nodeValue);
+        } else {
+          $value = $node->nodeValue;
+        }
+
+        $decoded = html_entity_decode($value, NULL, 'UTF-8');
+        return htmlentities($decoded, NULL, 'UTF-8');
+    }
+
+    /**
+     * Part of our xml:base processing code
+     *
+     * We need a couple of methods to access XHTML content stored in feeds. 
+     * This is because we dereference all xml:base references before returning
+     * the element. This method recurs through the tree descending from the node
+     * and builds our string.
+     *
+     * @param   DOMElement $node    The DOM node we are processing
+     * @return   string
+     */
+    function traverseNode($node)
+    {
+        $content = '';
+
+        /* Add the opening of this node to the content */
+        if ($node instanceof DOMElement) {
+            $content .= '<' . $node->tagName . 
+                $this->processXHTMLAttributes($node) . '>';
+        }
+
+        /* Process children */
+        if ($node->hasChildNodes()) {
+            foreach ($node->childNodes as $child) {
+                $content .= $this->traverseNode($child);
+            }
+        }
+
+        if ($node instanceof DOMText) {
+            $content .= $this->processEntitiesForNodeValue($node);
+        }
+
+        /* Add the closing of this node to the content */
+        if ($node instanceof DOMElement) {
+            $content .= '</' . $node->tagName . '>';
+        }
+
+        return $content;
+    }
+
+    /**
+     * Get content from RSS feeds (atom has its own implementation)
+     *
+     * The official way to include full content in an RSS1 entry is to use
+     * the content module's element 'encoded', and RSS2 feeds often duplicate that.
+     * Often, however, the 'description' element is used instead. We will offer that 
+     * as a fallback. Atom uses its own approach and overrides this method.
+     *
+     * @return  string|false
+     */
+    protected function getContent()
+    {
+        $options = array('encoded', 'description');
+        foreach ($options as $element) {
+            $test = $this->model->getElementsByTagName($element);
+            if ($test->length == 0) {
+                continue;
+            }
+            if ($test->item(0)->hasChildNodes()) {
+                $value = '';
+                foreach ($test->item(0)->childNodes as $child) {
+                    if ($child instanceof DOMText) {
+                        $value .= $child->nodeValue;
+                    } else {
+                        $simple = simplexml_import_dom($child);
+                        $value .= $simple->asXML();
+                    }
+                }
+                return $value;
+            } else if ($test->length > 0) {
+                return $test->item(0)->nodeValue;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks if this element has a particular child element.
+     *
+     * @param   String
+     * @param   Integer
+     * @return  bool
+     **/
+    function hasKey($name, $offset = 0)
+    {
+        $search = $this->model->getElementsByTagName($name);
+        return $search->length > $offset;
+    }
+
+    /**
+     * Return an XML serialization of the feed, should it be required. Most 
+     * users however, will already have a serialization that they used when 
+     * instantiating the object.
+     *
+     * @return    string    XML serialization of element
+     */    
+    function __toString()
+    {
+        $simple = simplexml_import_dom($this->model);
+        return $simple->asXML();
+    }
+    
+    /**
+     * Get directory holding RNG schemas. Method is based on that 
+     * found in Contact_AddressBook.
+     *
+     * @return string PEAR data directory.
+     * @access public
+     * @static
+     */
+    static function getSchemaDir()
+    {
+        require_once 'PEAR/Config.php';
+        $config = new PEAR_Config;
+        return $config->get('data_dir') . '/XML_Feed_Parser/schemas';
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/atom10-entryonly.xml b/plugins/OStatus/extlib/XML/Feed/samples/atom10-entryonly.xml
new file mode 100755 (executable)
index 0000000..02e1c58
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<entry xmlns="http://www.w3.org/2005/Atom">
+    <title>Atom draft-07 snapshot</title>
+    <link rel="alternate" type="text/html" 
+     href="http://example.org/2005/04/02/atom"/>
+    <link rel='enclosure' type="audio/mpeg" length="1337"
+     href="http://example.org/audio/ph34r_my_podcast.mp3"/>
+    <id>tag:example.org,2003:3.2397</id>
+    <updated>2005-07-10T12:29:29Z</updated>
+    <published>2003-12-13T08:29:29-04:00</published>
+    <author>
+      <name>Mark Pilgrim</name>
+      <uri>http://example.org/</uri>
+      <email>f8dy@example.com</email>
+    </author>
+    <contributor>
+      <name>Sam Ruby</name>
+    </contributor>
+    <contributor>
+      <name>Joe Gregorio</name>
+    </contributor>
+    <content type="xhtml" xml:lang="en" 
+     xml:base="http://diveintomark.org/">
+      <div xmlns="http://www.w3.org/1999/xhtml">
+        <p><i>[Update: The Atom draft is finished.]</i></p>
+      </div>
+    </content>
+  </entry>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/atom10-example1.xml b/plugins/OStatus/extlib/XML/Feed/samples/atom10-example1.xml
new file mode 100755 (executable)
index 0000000..d181d2b
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+   <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+   <title>Atom-Powered Robots Run Amok</title>
+   <link href="http://example.org/2003/12/13/atom03"/>
+   <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+   <updated>2003-12-13T18:30:02Z</updated>
+   <summary>Some text.</summary>
+ </entry>
+
+</feed>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/atom10-example2.xml b/plugins/OStatus/extlib/XML/Feed/samples/atom10-example2.xml
new file mode 100755 (executable)
index 0000000..98abf9d
--- /dev/null
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+   <feed xmlns="http://www.w3.org/2005/Atom">
+     <title type="text">dive into mark</title>
+     <subtitle type="html">
+       A &lt;em&gt;lot&lt;/em&gt; of effort
+       went into making this effortless
+     </subtitle>
+     <updated>2005-07-31T12:29:29Z</updated>
+     <id>tag:example.org,2003:3</id>
+     <link rel="alternate" type="text/html"
+      hreflang="en" href="http://example.org/"/>
+     <link rel="self" type="application/atom+xml"
+      href="http://example.org/feed.atom"/>
+     <rights>Copyright (c) 2003, Mark Pilgrim</rights>
+     <generator uri="http://www.example.com/" version="1.0">
+       Example Toolkit
+     </generator>
+     <entry>
+       <title>Atom draft-07 snapshot</title>
+       <link rel="alternate" type="text/html"
+        href="http://example.org/2005/04/02/atom"/>
+       <link rel='enclosure' type="audio/mpeg" length="1337"
+        href="http://example.org/audio/ph34r_my_podcast.mp3"/>
+       <id>tag:example.org,2003:3.2397</id>
+       <updated>2005-07-31T12:29:29Z</updated>
+       <published>2003-12-13T08:29:29-04:00</published>
+       <author>
+         <name>Mark Pilgrim</name>
+         <uri>http://example.org/</uri>
+         <email>f8dy@example.com</email>
+       </author>
+       <contributor>
+         <name>Sam Ruby</name>
+       </contributor>
+       <contributor>
+         <name>Joe Gregorio</name>
+       </contributor>
+       <content type="xhtml" xml:lang="en"
+        xml:base="http://diveintomark.org/">
+         <div xmlns="http://www.w3.org/1999/xhtml">
+           <p><i>[Update: The Atom draft is finished.]</i></p>
+         </div>
+       </content>
+     </entry>
+   </feed>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/delicious.feed b/plugins/OStatus/extlib/XML/Feed/samples/delicious.feed
new file mode 100755 (executable)
index 0000000..32f9fa4
--- /dev/null
@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
+ xmlns:admin="http://webns.net/mvcb/"
+>
+<channel rdf:about="http://del.icio.us/tag/greenbelt">
+<title>del.icio.us/tag/greenbelt</title>
+<link>http://del.icio.us/tag/greenbelt</link>
+<description>Text</description>
+<items>
+ <rdf:Seq>
+  <rdf:li rdf:resource="http://www.greenbelt.org.uk/" />
+  <rdf:li rdf:resource="http://www.greenbelt.org.uk/" />
+  <rdf:li rdf:resource="http://www.natuerlichwien.at/rundumadum/dergruenguertel/" />
+  <rdf:li rdf:resource="http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars" />
+  <rdf:li rdf:resource="http://www.greenbelt.ca/home.htm" />
+  <rdf:li rdf:resource="http://pipwilsonbhp.blogspot.com/" />
+  <rdf:li rdf:resource="http://maggidawn.typepad.com/maggidawn/" />
+  <rdf:li rdf:resource="http://www.johndavies.org/" />
+  <rdf:li rdf:resource="http://jonnybaker.blogs.com/" />
+ </rdf:Seq>
+</items>
+</channel>
+
+<item rdf:about="http://www.greenbelt.org.uk/">
+<dc:title>Greenbelt - Homepage Section</dc:title>
+<link>http://www.greenbelt.org.uk/</link>
+<dc:creator>jonnybaker</dc:creator>
+<dc:date>2005-05-16T16:30:38Z</dc:date>
+<dc:subject>greenbelt</dc:subject>
+<taxo:topics>
+  <rdf:Bag>
+    <rdf:li resource="http://del.icio.us/tag/greenbelt" />
+  </rdf:Bag>
+</taxo:topics>
+</item>
+
+<item rdf:about="http://www.greenbelt.org.uk/">
+<title>Greenbelt festival (uk)</title>
+<link>http://www.greenbelt.org.uk/</link>
+<dc:creator>sssshhhh</dc:creator>
+<dc:date>2005-05-14T18:19:40Z</dc:date>
+<dc:subject>audiology festival gigs greenbelt</dc:subject>
+<taxo:topics>
+  <rdf:Bag>
+    <rdf:li resource="http://del.icio.us/tag/gigs" />
+    <rdf:li resource="http://del.icio.us/tag/audiology" />
+    <rdf:li resource="http://del.icio.us/tag/festival" />
+    <rdf:li resource="http://del.icio.us/tag/greenbelt" />
+  </rdf:Bag>
+</taxo:topics>
+</item>
+
+<item rdf:about="http://www.natuerlichwien.at/rundumadum/dergruenguertel/">
+<title>Natuerlichwien.at - Rundumadum</title>
+<link>http://www.natuerlichwien.at/rundumadum/dergruenguertel/</link>
+<dc:creator>egmilman47</dc:creator>
+<dc:date>2005-05-06T21:33:41Z</dc:date>
+<dc:subject>Austria Vienna Wien greenbelt nature walking</dc:subject>
+<taxo:topics>
+  <rdf:Bag>
+    <rdf:li resource="http://del.icio.us/tag/Vienna" />
+    <rdf:li resource="http://del.icio.us/tag/Wien" />
+    <rdf:li resource="http://del.icio.us/tag/Austria" />
+    <rdf:li resource="http://del.icio.us/tag/walking" />
+    <rdf:li resource="http://del.icio.us/tag/nature" />
+    <rdf:li resource="http://del.icio.us/tag/greenbelt" />
+  </rdf:Bag>
+</taxo:topics>
+</item>
+
+<item rdf:about="http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars">
+<title>Tank - GBMediaWiki</title>
+<link>http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars</link>
+<dc:creator>jystewart</dc:creator>
+<dc:date>2005-03-21T22:44:11Z</dc:date>
+<dc:subject>greenbelt</dc:subject>
+<taxo:topics>
+  <rdf:Bag>
+    <rdf:li resource="http://del.icio.us/tag/greenbelt" />
+  </rdf:Bag>
+</taxo:topics>
+</item>
+
+<item rdf:about="http://www.greenbelt.ca/home.htm">
+<title>Greenbelt homepage</title>
+<link>http://www.greenbelt.ca/home.htm</link>
+<dc:creator>Gooberoo</dc:creator>
+<dc:date>2005-03-01T22:43:17Z</dc:date>
+<dc:subject>greenbelt ontario</dc:subject>
+<taxo:topics>
+  <rdf:Bag>
+    <rdf:li resource="http://del.icio.us/tag/ontario" />
+    <rdf:li resource="http://del.icio.us/tag/greenbelt" />
+  </rdf:Bag>
+</taxo:topics>
+</item>
+
+<item rdf:about="http://pipwilsonbhp.blogspot.com/">
+<title>Pip Wilson bhp ...... blog</title>
+<link>http://pipwilsonbhp.blogspot.com/</link>
+<dc:creator>sssshhhh</dc:creator>
+<dc:date>2004-12-27T11:20:51Z</dc:date>
+<dc:subject>Greenbelt friend ideas links thinking weblog</dc:subject>
+<taxo:topics>
+  <rdf:Bag>
+    <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
+    <rdf:li resource="http://del.icio.us/tag/thinking" />
+    <rdf:li resource="http://del.icio.us/tag/ideas" />
+    <rdf:li resource="http://del.icio.us/tag/links" />
+    <rdf:li resource="http://del.icio.us/tag/friend" />
+    <rdf:li resource="http://del.icio.us/tag/weblog" />
+  </rdf:Bag>
+</taxo:topics>
+</item>
+
+<item rdf:about="http://maggidawn.typepad.com/maggidawn/">
+<title>maggi dawn</title>
+<link>http://maggidawn.typepad.com/maggidawn/</link>
+<dc:creator>sssshhhh</dc:creator>
+<dc:date>2004-12-27T11:20:11Z</dc:date>
+<dc:subject>Greenbelt ideas links thinking weblog</dc:subject>
+<taxo:topics>
+  <rdf:Bag>
+    <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
+    <rdf:li resource="http://del.icio.us/tag/thinking" />
+    <rdf:li resource="http://del.icio.us/tag/ideas" />
+    <rdf:li resource="http://del.icio.us/tag/links" />
+    <rdf:li resource="http://del.icio.us/tag/weblog" />
+  </rdf:Bag>
+</taxo:topics>
+</item>
+
+<item rdf:about="http://www.johndavies.org/">
+<title>John Davies</title>
+<link>http://www.johndavies.org/</link>
+<dc:creator>sssshhhh</dc:creator>
+<dc:date>2004-12-27T11:18:37Z</dc:date>
+<dc:subject>Greenbelt ideas links thinking weblog</dc:subject>
+<taxo:topics>
+  <rdf:Bag>
+    <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
+    <rdf:li resource="http://del.icio.us/tag/thinking" />
+    <rdf:li resource="http://del.icio.us/tag/ideas" />
+    <rdf:li resource="http://del.icio.us/tag/links" />
+    <rdf:li resource="http://del.icio.us/tag/weblog" />
+  </rdf:Bag>
+</taxo:topics>
+</item>
+
+<item rdf:about="http://jonnybaker.blogs.com/">
+<title>jonnybaker</title>
+<link>http://jonnybaker.blogs.com/</link>
+<dc:creator>sssshhhh</dc:creator>
+<dc:date>2004-12-27T11:18:17Z</dc:date>
+<dc:subject>Greenbelt event ideas links resources thinking weblog youth</dc:subject>
+<taxo:topics>
+  <rdf:Bag>
+    <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
+    <rdf:li resource="http://del.icio.us/tag/thinking" />
+    <rdf:li resource="http://del.icio.us/tag/ideas" />
+    <rdf:li resource="http://del.icio.us/tag/links" />
+    <rdf:li resource="http://del.icio.us/tag/weblog" />
+    <rdf:li resource="http://del.icio.us/tag/youth" />
+    <rdf:li resource="http://del.icio.us/tag/event" />
+    <rdf:li resource="http://del.icio.us/tag/resources" />
+  </rdf:Bag>
+</taxo:topics>
+</item>
+
+</rdf:RDF>
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/flickr.feed b/plugins/OStatus/extlib/XML/Feed/samples/flickr.feed
new file mode 100755 (executable)
index 0000000..57e83af
--- /dev/null
@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>\r
+<feed version="0.3" xmlns="http://purl.org/atom/ns#" \r
+    xmlns:dc="http://purl.org/dc/elements/1.1/">\r
+\r
+       <title>jamesstewart - Everyone's Tagged Photos</title>\r
+       <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/tags/jamesstewart/"/>\r
+       <link rel="icon" type="image/jpeg" href="http://www.flickr.com/images/buddyicon.jpg"/>\r
+       <info type="text/html" mode="escaped">A feed of jamesstewart - Everyone's Tagged Photos</info>\r
+       <modified>2005-08-01T18:50:26Z</modified>\r
+       <generator url="http://www.flickr.com/">Flickr</generator>\r
+\r
+       <entry>\r
+               <title>Oma and James</title>\r
+               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/30484029@N00/30367516/"/>\r
+               <link rel='enclosure' type="application/xml" href="http://james.anthropiccollective.org" />\r
+               <id>tag:flickr.com,2004:/photo/30367516</id>\r
+               <issued>2005-08-01T18:50:26Z</issued>\r
+               <modified>2005-08-01T18:50:26Z</modified>\r
+               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/30484029@N00/&quot;&gt;kstewart&lt;/a&gt; posted a photo:&lt;/p&gt;\r
+\r
+&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/30484029@N00/30367516/&quot; title=&quot;Oma and James&quot;&gt;&lt;img src=&quot;http://photos23.flickr.com/30367516_1f685a16e8_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;Oma and James&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
+\r
+&lt;p&gt;I have a beautiful Oma and a gorgeous husband.&lt;/p&gt;</content>\r
+               <author>\r
+                       <name>kstewart</name>\r
+                       <url>http://www.flickr.com/people/30484029@N00/</url>\r
+               </author>\r
+                               <dc:subject>jamesstewart oma stoelfamily</dc:subject>\r
+       </entry>\r
+       <entry>\r
+               <title></title>\r
+               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21376174/"/>\r
+               <id>tag:flickr.com,2004:/photo/21376174</id>\r
+               <issued>2005-06-25T02:00:35Z</issued>\r
+               <modified>2005-06-25T02:00:35Z</modified>\r
+               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/buddscreek/&quot;&gt;Lan Rover&lt;/a&gt; posted a photo:&lt;/p&gt;\r
+\r
+&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/buddscreek/21376174/&quot; title=&quot;&quot;&gt;&lt;img src=&quot;http://photos17.flickr.com/21376174_4314fd8d5c_m.jpg&quot; width=&quot;240&quot; height=&quot;160&quot; alt=&quot;&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
+\r
+&lt;p&gt;AMA Motocross Championship 2005, Budds Creek, Maryland&lt;/p&gt;</content>\r
+               <author>\r
+                       <name>Lan Rover</name>\r
+                       <url>http://www.flickr.com/people/buddscreek/</url>\r
+               </author>\r
+                               <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational rickycarmichael 259 jamesstewart 4</dc:subject>\r
+       </entry>\r
+       <entry>\r
+               <title></title>\r
+               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21375650/"/>\r
+               <id>tag:flickr.com,2004:/photo/21375650</id>\r
+               <issued>2005-06-25T01:56:24Z</issued>\r
+               <modified>2005-06-25T01:56:24Z</modified>\r
+               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/buddscreek/&quot;&gt;Lan Rover&lt;/a&gt; posted a photo:&lt;/p&gt;\r
+\r
+&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/buddscreek/21375650/&quot; title=&quot;&quot;&gt;&lt;img src=&quot;http://photos16.flickr.com/21375650_5c60e0dab1_m.jpg&quot; width=&quot;240&quot; height=&quot;160&quot; alt=&quot;&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
+\r
+</content>\r
+               <author>\r
+                       <name>Lan Rover</name>\r
+                       <url>http://www.flickr.com/people/buddscreek/</url>\r
+               </author>\r
+                               <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart</dc:subject>\r
+       </entry>\r
+       <entry>\r
+               <title></title>\r
+               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21375345/"/>\r
+               <id>tag:flickr.com,2004:/photo/21375345</id>\r
+               <issued>2005-06-25T01:54:11Z</issued>\r
+               <modified>2005-06-25T01:54:11Z</modified>\r
+               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/buddscreek/&quot;&gt;Lan Rover&lt;/a&gt; posted a photo:&lt;/p&gt;\r
+\r
+&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/buddscreek/21375345/&quot; title=&quot;&quot;&gt;&lt;img src=&quot;http://photos15.flickr.com/21375345_4205fdd22b_m.jpg&quot; width=&quot;160&quot; height=&quot;240&quot; alt=&quot;&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
+\r
+</content>\r
+               <author>\r
+                       <name>Lan Rover</name>\r
+                       <url>http://www.flickr.com/people/buddscreek/</url>\r
+               </author>\r
+                               <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart</dc:subject>\r
+       </entry>\r
+       <entry>\r
+               <title>Lunch with Kari &amp; James, café in the crypt of St Martin in the fields</title>\r
+               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/16516618/"/>\r
+               <id>tag:flickr.com,2004:/photo/16516618</id>\r
+               <issued>2005-05-30T21:56:39Z</issued>\r
+               <modified>2005-05-30T21:56:39Z</modified>\r
+               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;\r
+\r
+&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/16516618/&quot; title=&quot;Lunch with Kari &amp;amp; James, café in the crypt of St Martin in the fields&quot;&gt;&lt;img src=&quot;http://photos14.flickr.com/16516618_afaa4a395e_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;Lunch with Kari &amp;amp; James, café in the crypt of St Martin in the fields&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
+\r
+</content>\r
+               <author>\r
+                       <name>fidothe</name>\r
+                       <url>http://www.flickr.com/people/fidothe/</url>\r
+               </author>\r
+                               <dc:subject>nokia7610 london stmartininthefields clarepatterson jamesstewart parvinstewart jimstewart susanstewart</dc:subject>\r
+       </entry>\r
+       <entry>\r
+               <title>Stewart keeping it low over the obstacle.</title>\r
+               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/pqbon/10224728/"/>\r
+               <id>tag:flickr.com,2004:/photo/10224728</id>\r
+               <issued>2005-04-21T07:30:29Z</issued>\r
+               <modified>2005-04-21T07:30:29Z</modified>\r
+               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/pqbon/&quot;&gt;pqbon&lt;/a&gt; posted a photo:&lt;/p&gt;\r
+\r
+&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/pqbon/10224728/&quot; title=&quot;Stewart keeping it low over the obstacle.&quot;&gt;&lt;img src=&quot;http://photos7.flickr.com/10224728_b756341957_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;Stewart keeping it low over the obstacle.&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
+\r
+</content>\r
+               <author>\r
+                       <name>pqbon</name>\r
+                       <url>http://www.flickr.com/people/pqbon/</url>\r
+               </author>\r
+                               <dc:subject>ama hangtown motocross jamesstewart bubba</dc:subject>\r
+       </entry>\r
+       <entry>\r
+               <title>king james stewart</title>\r
+               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/jjlook/7152910/"/>\r
+               <id>tag:flickr.com,2004:/photo/7152910</id>\r
+               <issued>2005-03-22T21:53:37Z</issued>\r
+               <modified>2005-03-22T21:53:37Z</modified>\r
+               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/jjlook/&quot;&gt;jj look&lt;/a&gt; posted a photo:&lt;/p&gt;\r
+\r
+&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/jjlook/7152910/&quot; title=&quot;king james stewart&quot;&gt;&lt;img src=&quot;http://photos7.flickr.com/7152910_a02ab5a750_m.jpg&quot; width=&quot;180&quot; height=&quot;240&quot; alt=&quot;king james stewart&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
+\r
+&lt;p&gt;11th&lt;/p&gt;</content>\r
+               <author>\r
+                       <name>jj look</name>\r
+                       <url>http://www.flickr.com/people/jjlook/</url>\r
+               </author>\r
+                               <dc:subject>dilomar05 eastside austin texas 78702 kingjames stewart jamesstewart borrowed</dc:subject>\r
+       </entry>\r
+       <entry>\r
+               <title>It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)</title>\r
+               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586562/"/>\r
+               <id>tag:flickr.com,2004:/photo/1586562</id>\r
+               <issued>2004-11-20T09:34:28Z</issued>\r
+               <modified>2004-11-20T09:34:28Z</modified>\r
+               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;\r
+\r
+&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/1586562/&quot; title=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot;&gt;&lt;img src=&quot;http://photos2.flickr.com/1586562_0bc5313a3e_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
+\r
+</content>\r
+               <author>\r
+                       <name>fidothe</name>\r
+                       <url>http://www.flickr.com/people/fidothe/</url>\r
+               </author>\r
+                               <dc:subject>holiday grandrapids jamesstewart</dc:subject>\r
+       </entry>\r
+       <entry>\r
+               <title>It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)</title>\r
+               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586539/"/>\r
+               <id>tag:flickr.com,2004:/photo/1586539</id>\r
+               <issued>2004-11-20T09:28:16Z</issued>\r
+               <modified>2004-11-20T09:28:16Z</modified>\r
+               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;\r
+\r
+&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/1586539/&quot; title=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot;&gt;&lt;img src=&quot;http://photos2.flickr.com/1586539_c51e5f2e7a_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
+\r
+</content>\r
+               <author>\r
+                       <name>fidothe</name>\r
+                       <url>http://www.flickr.com/people/fidothe/</url>\r
+               </author>\r
+                               <dc:subject>holiday grandrapids jamesstewart</dc:subject>\r
+       </entry>\r
+       <entry>\r
+               <title>It's a Grind, James and Jim can't decide)</title>\r
+               <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586514/"/>\r
+               <id>tag:flickr.com,2004:/photo/1586514</id>\r
+               <issued>2004-11-20T09:25:05Z</issued>\r
+               <modified>2004-11-20T09:25:05Z</modified>\r
+               <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;\r
+\r
+&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/1586514/&quot; title=&quot;It's a Grind, James and Jim can't decide)&quot;&gt;&lt;img src=&quot;http://photos2.flickr.com/1586514_733c2dfa3e_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;It's a Grind, James and Jim can't decide)&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;\r
+\r
+</content>\r
+               <author>\r
+                       <name>fidothe</name>\r
+                       <url>http://www.flickr.com/people/fidothe/</url>\r
+               </author>\r
+                               <dc:subject>holiday grandrapids jamesstewart johnkentish</dc:subject>\r
+       </entry>\r
+\r
+</feed>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/grwifi-atom.xml b/plugins/OStatus/extlib/XML/Feed/samples/grwifi-atom.xml
new file mode 100755 (executable)
index 0000000..c351d3c
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="iso-8859-1"?>\r<feed xmlns="http://www.w3.org/2005/Atom"\r xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en">\r<title>Updates to Grand Rapids WiFi hotspot details</title>\r<link rel="alternate" type="text/html" href="http://grwifi.net/"/>\r<link rel="self" type="application/atom+xml" href="http://grwifi.net/atom/locations"/>\r<updated>2005-09-01T15:43:01-05:00</updated>\r<subtitle>WiFi Hotspots in Grand Rapids, MI</subtitle>\r<id>http://grwifi.net/atom/locations</id>\r<rights>Creative Commons Attribution-NonCommercial-ShareAlike 2.0 http://creativecommons.org/licenses/by-nc-sa/2.0/ </rights>\r\r\r<entry>\r    <title>Hotspot Details Updated: Sweetwaters</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/sweetwaters"/>\r    <id>http://grwifi.net/location/sweetwaters</id>\r    <updated>2005-09-01T15:43:01-05:00</updated>\r\r        <summary type="html">\r          The details of the WiFi hotspot at: Sweetwaters have been updated. Find out more at:
+http://grwifi.net/location/sweetwaters\r        </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r\r<entry>\r    <title>Hotspot Details Updated: Common Ground Coffee Shop</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/common-ground"/>\r    <id>http://grwifi.net/location/common-ground</id>\r    <updated>2005-09-01T15:42:39-05:00</updated>\r\r     <summary type="html">\r          The details of the WiFi hotspot at: Common Ground Coffee Shop have been updated. Find out more at:
+http://grwifi.net/location/common-ground\r      </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r\r<entry>\r    <title>Hotspot Details Updated: Grand Rapids Public Library, Main Branch</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/grpl-main-branch"/>\r    <id>http://grwifi.net/location/grpl-main-branch</id>\r    <updated>2005-09-01T15:42:20-05:00</updated>\r\r        <summary type="html">\r          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\r   </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r\r<entry>\r    <title>Hotspot Details Updated: Four Friends Coffee House</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/four-friends"/>\r    <id>http://grwifi.net/location/four-friends</id>\r    <updated>2005-09-01T15:41:35-05:00</updated>\r\r       <summary type="html">\r          The details of the WiFi hotspot at: Four Friends Coffee House have been updated. Find out more at:
+http://grwifi.net/location/four-friends\r       </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r\r<entry>\r    <title>Hotspot Details Updated: Barnes and Noble, Rivertown Crossings</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/barnes-noble-rivertown"/>\r    <id>http://grwifi.net/location/barnes-noble-rivertown</id>\r    <updated>2005-09-01T15:40:41-05:00</updated>\r\r       <summary type="html">\r          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\r     </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r\r<entry>\r    <title>Hotspot Details Updated: The Boss Sports Bar &amp; Grille</title>\r    <link rel="alternate" type="text/html" href="http://grwifi.net/location/boss-sports-bar"/>\r    <id>http://grwifi.net/location/boss-sports-bar</id>\r    <updated>2005-09-01T15:40:19-05:00</updated>\r\r  <summary type="html">\r          The details of the WiFi hotspot at: The Boss Sports Bar &amp; Grille have been updated. Find out more at:
+http://grwifi.net/location/boss-sports-bar\r    </summary>\r\r    <author>\r               <name>James</name>\r             <uri>http://jystewart.net</uri>\r                <email>james@jystewart.net</email>      </author>\r      <dc:subject>wifi hotspot</dc:subject>\r</entry>\r</feed>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/hoder.xml b/plugins/OStatus/extlib/XML/Feed/samples/hoder.xml
new file mode 100755 (executable)
index 0000000..0994635
--- /dev/null
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<rss version="2.0" 
+  xmlns:dc="http://purl.org/dc/elements/1.1/"
+  xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
+  xmlns:admin="http://webns.net/mvcb/"
+  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+
+<channel>
+<title>Editor: Myself (Persian)</title>
+<link>http://editormyself.info</link>
+<description>This is a Persian (Farsi) weblog, written by Hossein Derakhshan (aka, Hoder), an Iranian Multimedia designer and a journalist who lives in Toronto since Dec 2000. He also keeps an English weblog with the same name.</description>
+<dc:language>en-us</dc:language>
+<dc:creator>hoder@hotmail.com</dc:creator>
+<dc:date>2005-10-12T19:45:32-05:00</dc:date>
+<admin:generatorAgent rdf:resource="http://www.movabletype.org/?v=3.15" />
+<sy:updatePeriod>hourly</sy:updatePeriod>
+<sy:updateFrequency>1</sy:updateFrequency>
+<sy:updateBase>2000-01-01T12:00+00:00</sy:updateBase>
+
+
+<item>
+<title>لينکدونی‌ | جلسه‌ی امریکن انترپرایز برای تقسیم قومی ایران</title>
+<link>http://www.aei.org/events/type.upcoming,eventID.1166,filter.all/event_detail.asp</link>
+<description>چطور بعضی‌ها فکر می‌کنند دست راستی‌های آمریکا از خامنه‌ای ملی‌گراترند</description>
+<guid isPermaLink="false">14645@http://i.hoder.com/</guid>
+<dc:subject>iran</dc:subject>
+<dc:date>2005-10-12T19:45:32-05:00</dc:date>
+</item>
+
+<item>
+<title>لينکدونی‌ | به صبحانه آگهی بدهید</title>
+<link>http://www.adbrite.com/mb/commerce/purchase_form.php?opid=24346&amp;afsid=1</link>
+<description>خیلی ارزان و راحت است</description>
+<guid isPermaLink="false">14644@http://i.hoder.com/</guid>
+<dc:subject>media/journalism</dc:subject>
+<dc:date>2005-10-12T17:23:15-05:00</dc:date>
+</item>
+
+<item>
+<title>لينکدونی‌ | نیروی انتظامی چگونه تابوهای هم‌جنس‌گرایانه را می‌شکند؛ فرنگوپولیس</title>
+<link>http://farangeopolis.blogspot.com/2005/10/blog-post_08.html</link>
+<description>از پس و پیش و حاشیه‌ی این ماجرا می‌توان یک مستند بی‌نظیر ساخت</description>
+<guid isPermaLink="false">14643@http://i.hoder.com/</guid>
+<dc:subject>soc_popculture</dc:subject>
+<dc:date>2005-10-12T17:06:40-05:00</dc:date>
+</item>
+
+<item>
+<title>لينکدونی‌ | بازتاب توقیف شد</title>
+<link>http://www.baztab.com/news/30201.php</link>
+<description>اگر گفتید یک وب‌سایت را چطور توقیف می‌کنند؟ لابد ماوس‌شان را قایم می‌کنند.</description>
+<guid isPermaLink="false">14642@http://i.hoder.com/</guid>
+<dc:subject>media/journalism</dc:subject>
+<dc:date>2005-10-12T14:41:57-05:00</dc:date>
+</item>
+
+<item>
+<title>لينکدونی‌ | رشد وب در سال 2005 از همیشه بیشتر بوده است&quot; بی.بی.سی</title>
+<link>http://news.bbc.co.uk/2/hi/technology/4325918.stm</link>
+<description></description>
+<guid isPermaLink="false">14640@http://i.hoder.com/</guid>
+<dc:subject>tech</dc:subject>
+<dc:date>2005-10-12T13:04:46-05:00</dc:date>
+</item>
+
+
+
+<item>
+<title>==قرعه کشی گرین کارد به زودی شروع می‌شود==</title>
+<link>http://nice.newsxphotos.biz/05/09/2007_dv_lottery_registration_to_begin_oct_5_14589.php</link>
+<description></description>
+<guid isPermaLink="false">14613@http://vagrantly.com</guid>
+<dc:subject>ads03</dc:subject>
+<dc:date>2005-09-27T04:49:22-05:00</dc:date>
+</item>
+
+
+
+
+
+
+<item>
+<title>پروژه‌ی هاروارد، قدم دوم</title>
+<link>http://editormyself.info/archives/2005/10/051012_014641.shtml</link>
+<description><![CDATA[<p>اگر یادتان باشد <a href="/archives/2005/09/050906_014504.shtml">چند وقت پیش نوشتم</a> که دانشگاه هاروارد پروژه‌ای دارد با نام آواهای جهانی که در آن به وبلاگ‌های غیر انگلیسی‌زبان می‌پردازد. خواشتم که اگر کسی علاقه دارد ایمیل بزند. تعداد زیادی جواب دادند و ابراز علاقه کردند. حالا وقت قدم دوم است.</p>
+
+<p>قدم دوم این است که برای اینکه مسوولین پروژه بتوانند تصمیم بگیرند که با چه کسی کار کنند، می‌خواهند نمونه‌ی کارهای علاقمندان مشارکت در این پرزو‌ه را ببینند.</p>
+
+<p>برای همین از همه‌ی علاقماندان، حتی کسانی که قبلا اعلام آمادگی نکرده بودند، می‌‌خواهم که یک موضوع رایج این روزهای وبلاگستان فارسی را انتخاب کنند و در هفتصد کلمه، به انگلیسی، بنویسند که وبلاگ‌دارهای درباره‌اش چه می‌گویند. لینک به پنج، شش وبلاگ و بازنویسی آنچه آنها از جنبه‌های گوناگون درباره‌ی آن موضوع نوشته‌اند با نقل قول مستقیم از آنها (البته ترجمه شده از فارسی) کافی است. دو سه جمله هم اول کار توضیح دهید که چرا این موضوع مهم است.</p>
+
+<p>متن نمونه را به آدرس ایمیل من hoder@hoder.com و نیز برای افراد زیر تا روز دوشنبه بفرستید:<br />
+ربکا : rmackinnon@cyber.law.harvard.edu<br />
+هیثم: haitham.sabbah@gmail.com</p>]]></description>
+<guid isPermaLink="false">14641@http://editormyself.info</guid>
+<dc:subject>weblog</dc:subject>
+<dc:date>2005-10-12T14:04:23-05:00</dc:date>
+</item>
+
+
+
+</channel>
+</rss>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/illformed_atom10.xml b/plugins/OStatus/extlib/XML/Feed/samples/illformed_atom10.xml
new file mode 100755 (executable)
index 0000000..6121868
--- /dev/null
@@ -0,0 +1,13 @@
+<!--\r
+Description: entry author name\r
+Expect:      bozo and entries[0]['author_detail']['name'] == u'Example author'\r
+-->\r
+<feed xmlns="http://www.w3.org/2005/Atom">\r
+<entry>\r
+  <author>\r
+    <name>Example author</name>\r
+    <email>me@example.com</email>\r
+    <uri>http://example.com/</uri>\r
+  </author>\r
+</entry>\r
+</feed
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss091-complete.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss091-complete.xml
new file mode 100755 (executable)
index 0000000..b0a1fee
--- /dev/null
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd">
+<rss version="0.91">
+<channel>
+<copyright>Copyright 1997-1999 UserLand Software, Inc.</copyright>
+<pubDate>Thu, 08 Jul 1999 07:00:00 GMT</pubDate>
+<lastBuildDate>Thu, 08 Jul 1999 16:20:26 GMT</lastBuildDate>
+<docs>http://my.userland.com/stories/storyReader$11</docs>
+<description>News and commentary from the cross-platform scripting community.</description>
+<link>http://www.scripting.com/</link>
+<title>Scripting News</title>
+<image>
+<link>http://www.scripting.com/</link>
+<title>Scripting News</title>
+<url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
+<height>40</height>
+<width>78</width>
+<description>What is this used for?</description>
+</image>
+<managingEditor>dave@userland.com (Dave Winer)</managingEditor>
+<webMaster>dave@userland.com (Dave Winer)</webMaster>
+<language>en-us</language>
+<skipHours>
+<hour>6</hour>
+<hour>7</hour>
+<hour>8</hour>
+<hour>9</hour>
+<hour>10</hour>
+<hour>11</hour>
+</skipHours>
+<skipDays>
+<day>Sunday</day>
+</skipDays>
+<rating>(PICS-1.1 "http://www.rsac.org/ratingsv01.html" l gen true comment "RSACi North America Server" for "http://www.rsac.org" on "1996.04.16T08:15-0500" r (n 0 s 0 v 0 l 0))</rating>
+<item>
+<title>stuff</title>
+<link>http://bar</link>
+<description>This is an article about some stuff</description>
+</item>
+<textinput>
+<title>Search Now!</title>
+<description>Enter your search &lt;terms&gt;</description>
+<name>find</name>
+<link>http://my.site.com/search.cgi</link>
+</textinput>
+</channel>
+</rss>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss091-international.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss091-international.xml
new file mode 100755 (executable)
index 0000000..cfe9169
--- /dev/null
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="EuC-JP"?>  
+<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd">
+<rss version="0.91">
+<channel>
+<title>膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ</title>
+<link>http://www.mozilla.org</link>
+<description>膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ</description>
+<language>ja</language>  <!-- tagged as Japanese content -->
+<item>
+<title>NYÒ™Á¢¸»ÌêÛì15285.25´ƒ´‘Á£´Û´—´ÀÁ¹´ê´Ì´éÒ™Ûì¡êçÒÕ‰ÌêÁ£</title>
+<link>http://www.mozilla.org/status/</link>
+<description>This is an item description...</description>
+</item>
+<item>
+<title>‚§±Çç¡ËßÛÂÒ\8féøÓ¸Á£Ë²®Ÿè†Ûèå\8d±ÇÌ’¡Íæ—éøë‡Á£</title>
+<link>http://www.mozilla.org/status/</link>
+<description>This is an item description...</description>
+</item>
+<item>
+<title>ËÜË”\81ïÌëÈšÁ¢È†Ë§æàÀ豎ˉۂÁ¢Ë‚åܼšÛ˜íËüËÁ£</title>
+<link>http://www.mozilla.org/status/</link>
+<description>This is an item description...</description>
+</item>
+<item>
+<title>2000‚øíŠå\90Á¢«‘¦éÛë¹\8fÛ\90çéÛ§ÛÂè†ÒæÓ¸Á£Ì¾«…æ—ÕÝéøƒ¸Á£</title>
+<link>http://www.mozilla.org/status/</link>
+<description>This is an item description...</description>
+</item>
+</channel>
+</rss>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss091-simple.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss091-simple.xml
new file mode 100755 (executable)
index 0000000..f0964a2
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd">
+<rss version="0.91">
+<channel>
+<language>en</language>
+<description>News and commentary from the cross-platform scripting community.</description>
+<link>http://www.scripting.com/</link>
+<title>Scripting News</title>
+<image>
+<link>http://www.scripting.com/</link>
+<title>Scripting News</title>
+<url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
+</image>
+</channel>
+</rss>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss092-sample.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss092-sample.xml
new file mode 100755 (executable)
index 0000000..5d75c35
--- /dev/null
@@ -0,0 +1,103 @@
+<?xml version="1.0"?>
+<!-- RSS generation done by 'Radio UserLand' on Fri, 13 Apr 2001 19:23:02 GMT -->
+<rss version="0.92">
+       <channel>
+               <title>Dave Winer: Grateful Dead</title>
+               <link>http://www.scripting.com/blog/categories/gratefulDead.html</link>
+               <description>A high-fidelity Grateful Dead song every day. This is where we&apos;re experimenting with enclosures on RSS news items that download when you&apos;re not using your computer. If it works (it will) it will be the end of the Click-And-Wait multimedia experience on the Internet. </description>
+               <lastBuildDate>Fri, 13 Apr 2001 19:23:02 GMT</lastBuildDate>
+               <docs>http://backend.userland.com/rss092</docs>
+               <managingEditor>dave@userland.com (Dave Winer)</managingEditor>
+               <webMaster>dave@userland.com (Dave Winer)</webMaster>
+               <cloud domain="data.ourfavoritesongs.com" port="80" path="/RPC2" registerProcedure="ourFavoriteSongs.rssPleaseNotify" protocol="xml-rpc"/>
+               <item>
+                       <description>It&apos;s been a few days since I added a song to the Grateful Dead channel. Now that there are all these new Radio users, many of whom are tuned into this channel (it&apos;s #16 on the hotlist of upstreaming Radio users, there&apos;s no way of knowing how many non-upstreaming users are subscribing, have to do something about this..). Anyway, tonight&apos;s song is a live version of Weather Report Suite from Dick&apos;s Picks Volume 7. It&apos;s wistful music. Of course a beautiful song, oft-quoted here on Scripting News. &lt;i&gt;A little change, the wind and rain.&lt;/i&gt;
+</description>
+                       <enclosure url="http://www.scripting.com/mp3s/weatherReportDicksPicsVol7.mp3" length="6182912" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>Kevin Drennan started a &lt;a href=&quot;http://deadend.editthispage.com/&quot;&gt;Grateful Dead Weblog&lt;/a&gt;. Hey it&apos;s cool, he even has a &lt;a href=&quot;http://deadend.editthispage.com/directory/61&quot;&gt;directory&lt;/a&gt;. &lt;i&gt;A Frontier 7 feature.&lt;/i&gt;</description>
+                       <source url="http://scriptingnews.userland.com/xml/scriptingNews2.xml">Scripting News</source>
+                       </item>
+               <item>
+                       <description>&lt;a href=&quot;http://arts.ucsc.edu/GDead/AGDL/other1.html&quot;&gt;The Other One&lt;/a&gt;, live instrumental, One From The Vault. Very rhythmic very spacy, you can listen to it many times, and enjoy something new every time.</description>
+                       <enclosure url="http://www.scripting.com/mp3s/theOtherOne.mp3" length="6666097" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>This is a test of a change I just made. Still diggin..</description>
+                       </item>
+               <item>
+                       <description>The HTML rendering almost &lt;a href=&quot;http://validator.w3.org/check/referer&quot;&gt;validates&lt;/a&gt;. Close. Hey I wonder if anyone has ever published a style guide for ALT attributes on images? What are you supposed to say in the ALT attribute? I sure don&apos;t know. If you&apos;re blind send me an email if u cn rd ths. </description>
+                       </item>
+               <item>
+                       <description>&lt;a href=&quot;http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Franklin&apos;s_Tower.txt&quot;&gt;Franklin&apos;s Tower&lt;/a&gt;, a live version from One From The Vault.</description>
+                       <enclosure url="http://www.scripting.com/mp3s/franklinsTower.mp3" length="6701402" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>Moshe Weitzman says Shakedown Street is what I&apos;m lookin for for tonight. I&apos;m listening right now. It&apos;s one of my favorites. &quot;Don&apos;t tell me this town ain&apos;t got no heart.&quot; Too bright. I like the jazziness of Weather Report Suite. Dreamy and soft. How about The Other One? &quot;Spanish lady come to me..&quot;</description>
+                       <source url="http://scriptingnews.userland.com/xml/scriptingNews2.xml">Scripting News</source>
+                       </item>
+               <item>
+                       <description>&lt;a href=&quot;http://www.scripting.com/mp3s/youWinAgain.mp3&quot;&gt;The news is out&lt;/a&gt;, all over town..&lt;p&gt;
+You&apos;ve been seen, out runnin round. &lt;p&gt;
+The lyrics are &lt;a href=&quot;http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/You_Win_Again.txt&quot;&gt;here&lt;/a&gt;, short and sweet. &lt;p&gt;
+&lt;i&gt;You win again!&lt;/i&gt;
+</description>
+                       <enclosure url="http://www.scripting.com/mp3s/youWinAgain.mp3" length="3874816" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>&lt;a href=&quot;http://www.getlyrics.com/lyrics/grateful-dead/wake-of-the-flood/07.htm&quot;&gt;Weather Report Suite&lt;/a&gt;: &quot;Winter rain, now tell me why, summers fade, and roses die? The answer came. The wind and rain. Golden hills, now veiled in grey, summer leaves have blown away. Now what remains? The wind and rain.&quot;</description>
+                       <enclosure url="http://www.scripting.com/mp3s/weatherReportSuite.mp3" length="12216320" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>&lt;a href=&quot;http://arts.ucsc.edu/gdead/agdl/darkstar.html&quot;&gt;Dark Star&lt;/a&gt; crashes, pouring its light into ashes.</description>
+                       <enclosure url="http://www.scripting.com/mp3s/darkStar.mp3" length="10889216" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>DaveNet: &lt;a href=&quot;http://davenet.userland.com/2001/01/21/theUsBlues&quot;&gt;The U.S. Blues&lt;/a&gt;.</description>
+                       </item>
+               <item>
+                       <description>Still listening to the US Blues. &lt;i&gt;&quot;Wave that flag, wave it wide and high..&quot;&lt;/i&gt; Mistake made in the 60s. We gave our country to the assholes. Ah ah. Let&apos;s take it back. Hey I&apos;m still a hippie. &lt;i&gt;&quot;You could call this song The United States Blues.&quot;&lt;/i&gt;</description>
+                       </item>
+               <item>
+                       <description>&lt;a href=&quot;http://www.sixties.com/html/garcia_stack_0.html&quot;&gt;&lt;img src=&quot;http://www.scripting.com/images/captainTripsSmall.gif&quot; height=&quot;51&quot; width=&quot;42&quot; border=&quot;0&quot; hspace=&quot;10&quot; vspace=&quot;10&quot; align=&quot;right&quot;&gt;&lt;/a&gt;In celebration of today&apos;s inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the &lt;a href=&quot;http://searchlyrics2.homestead.com/gd_usblues.html&quot;&gt;lyrics&lt;/a&gt;. Click on the audio icon to the left to give it a listen. &quot;Red and white, blue suede shoes, I&apos;m Uncle Sam, how do you do?&quot; It&apos;s a different kind of patriotic music, but man I love my country and I love Jerry and the band. &lt;i&gt;I truly do!&lt;/i&gt;</description>
+                       <enclosure url="http://www.scripting.com/mp3s/usBlues.mp3" length="5272510" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>Grateful Dead: &quot;Tennessee, Tennessee, ain&apos;t no place I&apos;d rather be.&quot;</description>
+                       <enclosure url="http://www.scripting.com/mp3s/tennesseeJed.mp3" length="3442648" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>Ed Cone: &quot;Had a nice Deadhead experience with my wife, who never was one but gets the vibe and knows and likes a lot of the music. Somehow she made it to the age of 40 without ever hearing Wharf Rat. We drove to Jersey and back over Christmas with the live album commonly known as Skull and Roses in the CD player much of the way, and it was cool to see her discover one the band&apos;s finest moments. That song is unique and underappreciated. Fun to hear that disc again after a few years off -- you get Jerry as blues-guitar hero on Big Railroad Blues and a nice version of Bertha.&quot;</description>
+                       <enclosure url="http://www.scripting.com/mp3s/darkStarWharfRat.mp3" length="27503386" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>&lt;a href=&quot;http://arts.ucsc.edu/GDead/AGDL/fotd.html&quot;&gt;Tonight&apos;s Song&lt;/a&gt;: &quot;If I get home before daylight I just might get some sleep tonight.&quot; </description>
+                       <enclosure url="http://www.scripting.com/mp3s/friendOfTheDevil.mp3" length="3219742" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>&lt;a href=&quot;http://arts.ucsc.edu/GDead/AGDL/uncle.html&quot;&gt;Tonight&apos;s song&lt;/a&gt;: &quot;Come hear Uncle John&apos;s Band by the river side. Got some things to talk about here beside the rising tide.&quot;</description>
+                       <enclosure url="http://www.scripting.com/mp3s/uncleJohnsBand.mp3" length="4587102" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>&lt;a href=&quot;http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Me_and_My_Uncle.txt&quot;&gt;Me and My Uncle&lt;/a&gt;: &quot;I loved my uncle, God rest his soul, taught me good, Lord, taught me all I know. Taught me so well, I grabbed that gold and I left his dead ass there by the side of the road.&quot;
+</description>
+                       <enclosure url="http://www.scripting.com/mp3s/meAndMyUncle.mp3" length="2949248" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>Truckin, like the doo-dah man, once told me gotta play your hand. Sometimes the cards ain&apos;t worth a dime, if you don&apos;t lay em down.</description>
+                       <enclosure url="http://www.scripting.com/mp3s/truckin.mp3" length="4847908" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>Two-Way-Web: &lt;a href=&quot;http://www.thetwowayweb.com/payloadsForRss&quot;&gt;Payloads for RSS&lt;/a&gt;. &quot;When I started talking with Adam late last year, he wanted me to think about high quality video on the Internet, and I totally didn&apos;t want to hear about it.&quot;</description>
+                       </item>
+               <item>
+                       <description>A touch of gray, kinda suits you anyway..</description>
+                       <enclosure url="http://www.scripting.com/mp3s/touchOfGrey.mp3" length="5588242" type="audio/mpeg"/>
+                       </item>
+               <item>
+                       <description>&lt;a href=&quot;http://www.sixties.com/html/garcia_stack_0.html&quot;&gt;&lt;img src=&quot;http://www.scripting.com/images/captainTripsSmall.gif&quot; height=&quot;51&quot; width=&quot;42&quot; border=&quot;0&quot; hspace=&quot;10&quot; vspace=&quot;10&quot; align=&quot;right&quot;&gt;&lt;/a&gt;In celebration of today&apos;s inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the &lt;a href=&quot;http://searchlyrics2.homestead.com/gd_usblues.html&quot;&gt;lyrics&lt;/a&gt;. Click on the audio icon to the left to give it a listen. &quot;Red and white, blue suede shoes, I&apos;m Uncle Sam, how do you do?&quot; It&apos;s a different kind of patriotic music, but man I love my country and I love Jerry and the band. &lt;i&gt;I truly do!&lt;/i&gt;</description>
+                       <enclosure url="http://www.scripting.com/mp3s/usBlues.mp3" length="5272510" type="audio/mpeg"/>
+                       </item>
+               </channel>
+       </rss>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss10-example1.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss10-example1.xml
new file mode 100755 (executable)
index 0000000..0edecf5
--- /dev/null
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+
+<rdf:RDF 
+  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+  xmlns="http://purl.org/rss/1.0/"
+>
+
+  <channel rdf:about="http://www.xml.com/xml/news.rss">
+    <title>XML.com</title>
+    <link>http://xml.com/pub</link>
+    <description>
+      XML.com features a rich mix of information and services 
+      for the XML community.
+    </description>
+
+    <image rdf:resource="http://xml.com/universal/images/xml_tiny.gif" />
+
+    <items>
+      <rdf:Seq>
+        <rdf:li resource="http://xml.com/pub/2000/08/09/xslt/xslt.html" />
+        <rdf:li resource="http://xml.com/pub/2000/08/09/rdfdb/index.html" />
+      </rdf:Seq>
+    </items>
+
+    <textinput rdf:resource="http://search.xml.com" />
+
+  </channel>
+  
+  <image rdf:about="http://xml.com/universal/images/xml_tiny.gif">
+    <title>XML.com</title>
+    <link>http://www.xml.com</link>
+    <url>http://xml.com/universal/images/xml_tiny.gif</url>
+  </image>
+  
+  <item rdf:about="http://xml.com/pub/2000/08/09/xslt/xslt.html">
+    <title>Processing Inclusions with XSLT</title>
+    <link>http://xml.com/pub/2000/08/09/xslt/xslt.html</link>
+    <description>
+     Processing document inclusions with general XML tools can be 
+     problematic. This article proposes a way of preserving inclusion 
+     information through SAX-based processing.
+    </description>
+  </item>
+  
+  <item rdf:about="http://xml.com/pub/2000/08/09/rdfdb/index.html">
+    <title>Putting RDF to Work</title>
+    <link>http://xml.com/pub/2000/08/09/rdfdb/index.html</link>
+    <description>
+     Tool and API support for the Resource Description Framework 
+     is slowly coming of age. Edd Dumbill takes a look at RDFDB, 
+     one of the most exciting new RDF toolkits.
+    </description>
+  </item>
+
+  <textinput rdf:about="http://search.xml.com">
+    <title>Search XML.com</title>
+    <description>Search XML.com's XML collection</description>
+    <name>s</name>
+    <link>http://search.xml.com</link>
+  </textinput>
+
+</rdf:RDF>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss10-example2.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss10-example2.xml
new file mode 100755 (executable)
index 0000000..26235f7
--- /dev/null
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?> 
+
+<rdf:RDF 
+  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 
+  xmlns:dc="http://purl.org/dc/elements/1.1/"
+  xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
+  xmlns:co="http://purl.org/rss/1.0/modules/company/"
+  xmlns:ti="http://purl.org/rss/1.0/modules/textinput/"
+  xmlns="http://purl.org/rss/1.0/"
+> 
+
+  <channel rdf:about="http://meerkat.oreillynet.com/?_fl=rss1.0">
+    <title>Meerkat</title>
+    <link>http://meerkat.oreillynet.com</link>
+    <description>Meerkat: An Open Wire Service</description>
+    <dc:publisher>The O'Reilly Network</dc:publisher>
+    <dc:creator>Rael Dornfest (mailto:rael@oreilly.com)</dc:creator>
+    <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+    <dc:date>2000-01-01T12:00+00:00</dc:date>
+    <sy:updatePeriod>hourly</sy:updatePeriod>
+    <sy:updateFrequency>2</sy:updateFrequency>
+    <sy:updateBase>2000-01-01T12:00+00:00</sy:updateBase>
+
+    <image rdf:resource="http://meerkat.oreillynet.com/icons/meerkat-powered.jpg" />
+
+    <items>
+      <rdf:Seq>
+        <rdf:li resource="http://c.moreover.com/click/here.pl?r123" />
+      </rdf:Seq>
+    </items>
+
+    <textinput rdf:resource="http://meerkat.oreillynet.com" />
+
+  </channel>
+
+  <image rdf:about="http://meerkat.oreillynet.com/icons/meerkat-powered.jpg">
+    <title>Meerkat Powered!</title>
+    <url>http://meerkat.oreillynet.com/icons/meerkat-powered.jpg</url>
+    <link>http://meerkat.oreillynet.com</link>
+  </image>
+
+  <item rdf:about="http://c.moreover.com/click/here.pl?r123">
+    <title>XML: A Disruptive Technology</title> 
+    <link>http://c.moreover.com/click/here.pl?r123</link>
+    <dc:description>
+      XML is placing increasingly heavy loads on the existing technical
+      infrastructure of the Internet.
+    </dc:description>
+    <dc:publisher>The O'Reilly Network</dc:publisher>
+    <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+    <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+    <dc:subject>XML</dc:subject>
+    <co:name>XML.com</co:name>
+    <co:market>NASDAQ</co:market>
+    <co:symbol>XML</co:symbol>
+  </item> 
+
+  <textinput rdf:about="http://meerkat.oreillynet.com">
+    <title>Search Meerkat</title>
+    <description>Search Meerkat's RSS Database...</description>
+    <name>s</name>
+    <link>http://meerkat.oreillynet.com/</link>
+    <ti:function>search</ti:function>
+    <ti:inputType>regex</ti:inputType>
+  </textinput>
+
+</rdf:RDF>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/rss2sample.xml b/plugins/OStatus/extlib/XML/Feed/samples/rss2sample.xml
new file mode 100755 (executable)
index 0000000..53483cc
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>\r
+<rss version="2.0" xmlns:content="http://purl.org/rss/1.0.modules/content/">\r
+   <channel>\r
+      <title>Liftoff News</title>\r
+      <link>http://liftoff.msfc.nasa.gov/</link>\r
+      <description>Liftoff to Space Exploration.</description>\r
+      <language>en-us</language>\r
+      <pubDate>Tue, 10 Jun 2003 04:00:00 GMT</pubDate>\r
+      <lastBuildDate>Tue, 10 Jun 2003 09:41:01 GMT</lastBuildDate>\r
+      <docs>http://blogs.law.harvard.edu/tech/rss</docs>\r
+      <generator>Weblog Editor 2.0</generator>\r
+      <managingEditor>editor@example.com</managingEditor>\r
+      <webMaster>webmaster@example.com</webMaster>\r
+      <item>\r
+         <title>Star City</title>\r
+         <link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>\r
+         <description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's &lt;a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm"&gt;Star City&lt;/a&gt;.</description>\r
+         <pubDate>Tue, 03 Jun 2003 09:39:21 GMT</pubDate>\r
+         <guid>http://liftoff.msfc.nasa.gov/2003/06/03.html#item573</guid>\r
+      </item>\r
+      <item>\r
+         <description>Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a &lt;a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm"&gt;partial eclipse of the Sun&lt;/a&gt; on Saturday, May 31st.</description>\r
+         <pubDate>Fri, 30 May 2003 11:06:42 GMT</pubDate>\r
+         <guid>http://liftoff.msfc.nasa.gov/2003/05/30.html#item572</guid>\r
+      </item>\r
+      <item>\r
+         <title>The Engine That Does More</title>\r
+         <link>http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp</link>\r
+         <description>Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly.  The proposed VASIMR engine would do that.</description>\r
+         <pubDate>Tue, 27 May 2003 08:37:32 GMT</pubDate>\r
+         <guid>http://liftoff.msfc.nasa.gov/2003/05/27.html#item571</guid>\r
+                <content:encoded><![CDATA[<p>Test content</p>]]></content:encoded>\r
+      </item>\r
+      <item>\r
+         <title>Astronauts' Dirty Laundry</title>\r
+         <link>http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp</link>\r
+         <description>Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them.  Instead, astronauts have other options.</description>\r
+         <pubDate>Tue, 20 May 2003 08:56:02 GMT</pubDate>\r
+         <guid>http://liftoff.msfc.nasa.gov/2003/05/20.html#item570</guid>\r
+      </item>\r
+   </channel>\r
+</rss>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/sixapart-jp.xml b/plugins/OStatus/extlib/XML/Feed/samples/sixapart-jp.xml
new file mode 100755 (executable)
index 0000000..f8a04bb
--- /dev/null
@@ -0,0 +1,226 @@
+<?xml version="1.0" encoding="utf-8"?>
+<rss version="2.0">
+<channel>
+<title>Six Apart - News</title>
+<link>http://www.sixapart.jp/</link>
+<description></description>
+<language>ja</language>
+<copyright>Copyright 2005</copyright>
+<lastBuildDate>Fri, 07 Oct 2005 19:09:34 +0900</lastBuildDate>
+<generator>http://www.movabletype.org/?v=3.2-ja</generator>
+<docs>http://blogs.law.harvard.edu/tech/rss</docs> 
+
+<item>
+<title>ファイブ・ディーが、Movable Typeでブログプロモーションをスタート</title>
+<description><![CDATA[<p><img alt="MIYAZAWAblog_banner.jpg" src="http://www.sixapart.jp/MIYAZAWAblog_banner.jpg" width="200" height="88" align="right" /><br />
+ファイブ・ディーは、Movable Typeで構築したプロモーション ブログ『宮沢和史 中南米ツアーblog Latin America 2005』を開設しました。</p>
+
+<p>9月21日に開設されたこのブログは、ブラジル、ホンジュラス、ニカラグア、メキシコ、キューバの5か国を巡る「Latin America 2005」ツアーに合わせ、そのツアーの模様を同行マネージャーがレポートしていきます。<br />
+さらに今月2日からは宮沢和史自身が日々録音した声をPodcastingするという点でも、ブログを使ったユニークなプロモーションとなっています。</p>
+
+<p><a href="http://www.five-d.co.jp/miyazawa/jp/blog/la2005/">「宮沢和史 中南米ツアーblog Latin America 2005」</a></p>
+
+<p>※シックス・アパートではこうしたブログを使ったプロモーションに最適な製品をご用意しております。<br />
+<ul><li><a href="/movabletype/">Movable Type</a><br />
+<li><a href="/typepad/typepad_promotion.html">TypePad Promotion</a><br />
+</ul></p>]]></description>
+<link>http://www.sixapart.jp/news/2005/10/07-1909.html</link>
+<guid>http://www.sixapart.jp/news/2005/10/07-1909.html</guid>
+<category>news</category>
+<pubDate>Fri, 07 Oct 2005 19:09:34 +0900</pubDate>
+</item>
+<item>
+<title>Movable Type 3.2日本語版の提供を開始</title>
+<description><![CDATA[<p><img alt="Movable Type Logo" src="/images/mt3-logo-small.gif" width="151" height="37"/></p>
+<p>シックス・アパートは、Movable Type 3.2日本語版の提供を開始いたしました。<br />
+ベータテストにご協力いただいた多くの皆様に、スタッフ一同、心から感謝いたします。</p>
+<p>製品概要など、詳しくは<a href="http://www.sixapart.jp/press_releases/2005/09/29-1529.html" title="Six Apart - News: シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始">プレスリリース</a>をご参照下さい。</p>
+<p>ご購入のご検討は、<a href="http://www.sixapart.jp/movabletype/purchase-mt.html">Movable Typeのご購入</a>からどうぞ。</p>]]></description>
+<link>http://www.sixapart.jp/news/2005/09/29-1530.html</link>
+<guid>http://www.sixapart.jp/news/2005/09/29-1530.html</guid>
+<category>news</category>
+<pubDate>Thu, 29 Sep 2005 15:30:00 +0900</pubDate>
+</item>
+<item>
+<title>シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始</title>
+<description><![CDATA[<p><プレスリリース資料></p>
+<ul>
+  <li><a href="http://www.sixapart.jp/sixapart20050929.pdf">印刷用(PDF版)</a></li>
+</ul>
+<p><strong>シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始 ~ スパムの自動判別機能や新ユーザー・インターフェースで、運用管理の機能を強化 ~</strong></p>
+<p>2005年9月29日<br />
+シックス・アパート株式会社</p>
+<p>ブログ・ソフトウェア大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、「Movable Type(ムーバブル・タイプ) 3.2 日本語版」(URL:<a href="http://www.sixapart.jp/movabletype/">http://www.sixapart.jp/movabletype/</a>)を9月29日より提供開始いたします。</p>]]></description>
+<link>http://www.sixapart.jp/press_releases/2005/09/29-1529.html</link>
+<guid>http://www.sixapart.jp/press_releases/2005/09/29-1529.html</guid>
+<category>Press Releases</category>
+<pubDate>Thu, 29 Sep 2005 15:29:00 +0900</pubDate>
+</item>
+<item>
+<title>スタッフを募集しています</title>
+<description><![CDATA[<p>シックス・アパートはMovable TypeやTypePadの開発エンジニアなど、スタッフを広く募集しています。具体的な募集職種は次の通りです。</p>
+
+<ul>
+<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0007.html">Movable Type開発エンジニア</a></li>
+<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0004.html">TypePad開発エンジニア</a></li>
+<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0003.html">カスタマーサポート・ディレクター</a></li>
+<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0002.html">マーケティング・広報アシスタント</a></li>
+<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0001.html">開発アシスタント</a></li>
+</ul>
+
+<p>拡大を続ける、日本のブログ市場を積極的にリードする人材を、シックス・アパートは募集しています。上記以外の職種につきましても、お気軽にお問い合わせください。詳しい募集要項や応募方法については、<a href="/jobs/">求人情報のページ</a>をご覧ください。<br />
+</p>]]></description>
+<link>http://www.sixapart.jp/news/2005/09/27-0906.html</link>
+<guid>http://www.sixapart.jp/news/2005/09/27-0906.html</guid>
+<category>news</category>
+<pubDate>Tue, 27 Sep 2005 09:06:10 +0900</pubDate>
+</item>
+<item>
+<title>サイト接続不具合に関するお詫びと復旧のお知らせ</title>
+<description><![CDATA[<p>9月24日(土)の14:45ごろから、同日18:30ごろまで、シックス・アパート社のウェブサイトが不安定になっており、断続的に接続できない不具合が発生しておりました。このため、この期間中にウェブサイトの閲覧や製品のダウンロードができませんでした。</p>
+
+<p>なお現在は不具合は解消しております。みなさまにご迷惑をおかけしたことをお詫びいたします。</p>]]></description>
+<link>http://www.sixapart.jp/news/2005/09/26-1000.html</link>
+<guid>http://www.sixapart.jp/news/2005/09/26-1000.html</guid>
+<category>news</category>
+<pubDate>Mon, 26 Sep 2005 10:00:56 +0900</pubDate>
+</item>
+<item>
+<title>企業ブログ向けパッケージ「TypePad Promotion」を新発売</title>
+<description><![CDATA[<p>シックス・アパートは、ウェブログ・サービスTypePadの企業ブログ向けパッケージ「TypePad Promotion」(タイプパッド・プロモーションの発売を10月下旬から開始いたします。</p>
+
+<p>詳しくは、<a href="http://www.sixapart.jp/press_releases/2005/09/20-1500.html" title="プレスリリース: 「TypePad Promotion」新発売">プレスリリース</a>をご参照下さい。</p>]]></description>
+<link>http://www.sixapart.jp/news/2005/09/20-1500.html</link>
+<guid>http://www.sixapart.jp/news/2005/09/20-1500.html</guid>
+<category>news</category>
+<pubDate>Tue, 20 Sep 2005 15:00:01 +0900</pubDate>
+</item>
+<item>
+<title>シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売</title>
+<description><![CDATA[<p><プレスリリース資料><br />
+<a href="http://www.sixapart.jp/sixapart20050920.pdf">印刷用(PDF版)</a></p>
+
+<p><br />
+<strong>シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売<br />
+~PR/IRサイトやキャンペーンサイトなど企業のプロモーションニーズに特化~<br />
+</strong><br />
+2005年9月20日<br />
+シックス・アパート株式会社</p>
+
+<p>ブログ・サービス大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、法人向けプロモーションブログ・パッケージ「TypePad Promotion(タイプパッド・プロモーション)」(URL:<a href="http://www.sixapart.jp/typepad/typepad_promotion.html">http://www.sixapart.jp/typepad/typepad_promotion.html</a>)を10月下旬より販売開始いたします。</p>]]></description>
+<link>http://www.sixapart.jp/press_releases/2005/09/20-1500.html</link>
+<guid>http://www.sixapart.jp/press_releases/2005/09/20-1500.html</guid>
+<category>Press Releases</category>
+<pubDate>Tue, 20 Sep 2005 15:00:00 +0900</pubDate>
+</item>
+<item>
+<title>Six [days] Apart Week</title>
+<description><![CDATA[<p>本日、9月16日はSix Apartの創業者ミナ・トロットの誕生日です。<br />
+私たちの会社は、創業者のトロット夫妻(ベンとミナ)の誕生日が、6日離れていることからSix  [days] Apart →Six Apartという風に名付けられています。本日から22日までの6日間を社名の由来となる Six [days] Apart Weekとして、私たちのプロダクトをご紹介させていただきます。</p>
+
+<p>今日は、ブログ・サービスのTypePad(タイプパッド)をご紹介します。<br />
+<img alt="tp-logo.gif" src="http://www.sixapart.jp/tp-logo.gif" width="227" height="52" /></p>
+
+<p>TypePadは、米国PC MAGAZINE誌の2003年EDITOR'S CHOICE とBEST OF THE YEARに選ばれております。<br />
+<img alt="pcmag-ad.gif" src="http://www.sixapart.jp/pcmag-ad.gif" width="297" height="100" /><br />
+</p>]]></description>
+<link>http://www.sixapart.jp/news/2005/09/16-1941.html</link>
+<guid>http://www.sixapart.jp/news/2005/09/16-1941.html</guid>
+<category>news</category>
+<pubDate>Fri, 16 Sep 2005 19:41:47 +0900</pubDate>
+</item>
+<item>
+<title>ハイパーワークスが商用フォントを利用できるMovable Typeホスティングサービスを開始</title>
+<description><![CDATA[<p>ソフト開発会社の<a href="http://www.hyperwrx.co.jp/">有限会社ハイパーワークス</a>は、商用フォントなど多彩なフォントをブログ上で利用できるブログ・サービス「<a href="http://glyph-on.jp/">Glyph-On!(グリフォン) Movable Type ホスティング サービス</a>」の提供を開始しました。<br />
+</p>]]></description>
+<link>http://www.sixapart.jp/news/2005/09/14-1700.html</link>
+<guid>http://www.sixapart.jp/news/2005/09/14-1700.html</guid>
+<category>news</category>
+<pubDate>Wed, 14 Sep 2005 17:00:00 +0900</pubDate>
+</item>
+<item>
+<title>Movable Type開発エンジニアの募集</title>
+<description><![CDATA[<p>
+勤務形態: フルタイム<br />
+勤務地: 東京 (赤坂)<br />
+職種: ソフトウェア・エンジニア<br />
+職務内容: Movable Typeの開発業務全般<br />
+募集人数: 若干名
+</p>]]></description>
+<link>http://www.sixapart.jp/jobs/2005/09/13-0007.html</link>
+<guid>http://www.sixapart.jp/jobs/2005/09/13-0007.html</guid>
+<category>Jobs</category>
+<pubDate>Tue, 13 Sep 2005 00:07:00 +0900</pubDate>
+</item>
+<item>
+<title>TypePad開発エンジニアの募集</title>
+<description><![CDATA[<p>
+勤務形態: フルタイム<br />
+勤務地: 東京 (赤坂)<br />
+職種: アプリケーション・エンジニア<br />
+職務内容: TypePadのカスタマイズ、周辺開発<br />
+募集人数: 若干名
+</p>]]></description>
+<link>http://www.sixapart.jp/jobs/2005/09/13-0004.html</link>
+<guid>http://www.sixapart.jp/jobs/2005/09/13-0004.html</guid>
+<category>Jobs</category>
+<pubDate>Tue, 13 Sep 2005 00:04:00 +0900</pubDate>
+</item>
+<item>
+<title>カスタマーサポート・ディレクターの募集</title>
+<description><![CDATA[<p>勤務形態: フルタイム<br />
+勤務地: 東京(赤坂)<br />
+職種: カスタマーサポート・ディレクター<br />
+職務内容: TypePadやMovable Typeのカスタマーサポート業務の統括<br />
+募集人数: 若干名
+</p>
+]]></description>
+<link>http://www.sixapart.jp/jobs/2005/09/13-0003.html</link>
+<guid>http://www.sixapart.jp/jobs/2005/09/13-0003.html</guid>
+<category>Jobs</category>
+<pubDate>Tue, 13 Sep 2005 00:03:30 +0900</pubDate>
+</item>
+<item>
+<title>アルバイト(マーケティング・広報アシスタント)の募集</title>
+<description><![CDATA[<p>勤務形態: アルバイト<br />
+勤務地: 東京(港区)<br />
+職種:マーケティング・PRのアシスタント業務<br />
+募集人数: 若干名<br />
+時給:1000円~(但し、試用期間終了後に応相談)。交通費支給<br />
+時間:平日10時30分~18時30分まで。週3日以上(応相談)<br />
+</p>]]></description>
+<link>http://www.sixapart.jp/jobs/2005/09/13-0002.html</link>
+<guid>http://www.sixapart.jp/jobs/2005/09/13-0002.html</guid>
+<category>Jobs</category>
+<pubDate>Tue, 13 Sep 2005 00:02:00 +0900</pubDate>
+</item>
+<item>
+<title>アルバイト(開発アシスタント)の募集</title>
+<description><![CDATA[<p>勤務形態: アルバイト<br />
+勤務地: 東京(港区)<br />
+職種: アプリケーション開発のアシスタント業務<br />
+募集人数: 若干名<br />
+時給:1000円~(但し、試用期間終了後に応相談)。交通費支給<br />
+時間:平日10時30分~18時30分まで。週3日以上(応相談)
+</p>]]></description>
+<link>http://www.sixapart.jp/jobs/2005/09/13-0001.html</link>
+<guid>http://www.sixapart.jp/jobs/2005/09/13-0001.html</guid>
+<category>Jobs</category>
+<pubDate>Tue, 13 Sep 2005 00:01:00 +0900</pubDate>
+</item>
+<item>
+<title>TypePad Japan がバージョンアップしました。</title>
+<description><![CDATA[<p><a href="http://www.sixapart.jp/typepad/">「TypePad Japan(タイプパッドジャパン)」</a>において、本日、「TypePad 1.6 日本語版」へのバージョンアップを行いました。最新版となる「TypePad 1.6 日本語版」では、ブログデザインの機能強化、ポッドキャスティング対応、モブログ対応に加え、今回新たに大幅な容量アップが行われております。皆様、新しくなった<a href="http://www.sixapart.jp/typepad/">TypePad Japan</a>にどうぞご期待ください。</p>
+
+<p>なお、TypePadの携帯対応強化に関しましては、本日よりTypePad Japanのお客様を対象にオープン・ベータを開始しております。</p>
+
+<p>2005年9月5日発表のTypePad日本語版 1.6プレスリリースは<a href="http://www.sixapart.jp/press_releases/2005/09/05-1420.html">こちら</a>をご覧下さい。</p>]]></description>
+<link>http://www.sixapart.jp/news/2005/09/12-1953.html</link>
+<guid>http://www.sixapart.jp/news/2005/09/12-1953.html</guid>
+<category>news</category>
+<pubDate>Mon, 12 Sep 2005 19:53:07 +0900</pubDate>
+</item>
+
+
+</channel>
+</rss>
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/samples/technorati.feed b/plugins/OStatus/extlib/XML/Feed/samples/technorati.feed
new file mode 100755 (executable)
index 0000000..6274a32
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<rss version="2.0"
+    xmlns:tapi="http://api.technorati.com/dtd/tapi-002.xml">
+    <channel>
+        <title>[Technorati] Tag results for greenbelt</title>
+        <link>http://www.technorati.com/tag/greenbelt</link>
+        <description>Posts tagged with "greenbelt" on Technorati.</description>
+        <pubDate>Mon, 08 Aug 2005 15:15:08 GMT</pubDate>
+        <category domain="http://www.technorati.com/tag">greenbelt</category>
+        <tapi:inboundblogs>2</tapi:inboundblogs>
+        <tapi:inboundlinks>2</tapi:inboundlinks>
+        <cloud domain="rpc.sys.com" port="80" path="/RPC2" registerProcedure="myCloud.rssPleaseNotify" protocol="xml-rpc" />
+        <generator>Technorati v1.0</generator>
+        <image>
+            <url>http://static.technorati.com/pix/logos/logo_reverse_sm.gif</url>
+            <title>Technorati logo</title>
+            <link>http://www.technorati.com</link>
+        </image>
+        <skipHours>
+            <hour>1</hour>
+            <hour>7</hour>
+            <hour>9</hour>
+        </skipHours>
+        <webMaster>support@technorati.com (Technorati Support)</webMaster>
+        <docs>http://blogs.law.harvad.edu/tech/rss</docs>
+        <ttl>60</ttl>
+        <item>
+            <title>Greenbelt</title>
+            <link>http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html</link>
+            <description>So if the plan goes according to plan (!)... I'll be speaking at Greenbelt at these times: Slot 1...</description>
+            <guid isPermaLink="true">http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html</guid>
+            <pubDate>Mon, 18 Jul 2005 02:11:42 GMT</pubDate>
+            <category>James</category>
+            <tapi:linkcreated>2005-07-11 02:08:12</tapi:linkcreated>
+            <comments>http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fmaggidawn.typepad.com%2Fmaggidawn%2F2005%2F07%2Fgreenbelt.html</comments>
+            <tapi:inboundblogs>190</tapi:inboundblogs>
+            <tapi:inboundlinks>237</tapi:inboundlinks>
+            <source url="http://maggidawn.typepad.com/maggidawn/index.rdf">maggi dawn</source>
+        </item>
+
+        <item>
+            <title>Walking along the Greenbelt</title>
+            <link>http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html</link>
+            <description>[IMG] Photo of homeless man walking near the greenbelt in Boise, Idaho Tags: photo homeless greenbelt Boise Idaho picture</description>
+            <guid isPermaLink="true">http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html</guid>
+            <pubDate>Tue, 28 Jun 2005 01:41:24 GMT</pubDate>
+            <tapi:linkcreated>2005-06-26 17:24:03</tapi:linkcreated>
+            <comments>http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fpictureshomeless.blogspot.com%2F2005%2F06%2Fwalking-along-greenbelt.html</comments>
+            <tapi:inboundblogs>2</tapi:inboundblogs>
+            <tapi:inboundlinks>2</tapi:inboundlinks>
+        </item>
+
+    </channel>
+</rss>
diff --git a/plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc b/plugins/OStatus/extlib/XML/Feed/schemas/atom.rnc
new file mode 100755 (executable)
index 0000000..e662d26
--- /dev/null
@@ -0,0 +1,338 @@
+# -*- rnc -*-
+# RELAX NG Compact Syntax Grammar for the
+# Atom Format Specification Version 11
+
+namespace atom = "http://www.w3.org/2005/Atom"
+namespace xhtml = "http://www.w3.org/1999/xhtml"
+namespace s = "http://www.ascc.net/xml/schematron"
+namespace local = ""
+
+start = atomFeed | atomEntry
+
+# Common attributes
+
+atomCommonAttributes =
+   attribute xml:base { atomUri }?,
+   attribute xml:lang { atomLanguageTag }?,
+   undefinedAttribute*
+
+# Text Constructs
+
+atomPlainTextConstruct =
+   atomCommonAttributes,
+   attribute type { "text" | "html" }?,
+   text
+
+atomXHTMLTextConstruct =
+   atomCommonAttributes,
+   attribute type { "xhtml" },
+   xhtmlDiv
+
+atomTextConstruct = atomPlainTextConstruct | atomXHTMLTextConstruct
+
+# Person Construct
+
+atomPersonConstruct =
+   atomCommonAttributes,
+   (element atom:name { text }
+    & element atom:uri { atomUri }?
+    & element atom:email { atomEmailAddress }?
+    & extensionElement*)
+
+# Date Construct
+
+atomDateConstruct =
+   atomCommonAttributes,
+   xsd:dateTime
+
+# atom:feed
+
+atomFeed =
+   [
+      s:rule [
+         context = "atom:feed"
+         s:assert [
+            test = "atom:author or not(atom:entry[not(atom:author)])"
+            "An atom:feed must have an atom:author unless all "
+            ~ "of its atom:entry children have an atom:author."
+         ]
+      ]
+   ]
+   element atom:feed {
+      atomCommonAttributes,
+      (atomAuthor*
+       & atomCategory*
+       & atomContributor*
+       & atomGenerator?
+       & atomIcon?
+       & atomId
+       & atomLink*
+       & atomLogo?
+       & atomRights?
+       & atomSubtitle?
+       & atomTitle
+       & atomUpdated
+       & extensionElement*),
+      atomEntry*
+   }
+
+# atom:entry
+
+atomEntry =
+   [
+      s:rule [
+         context = "atom:entry"
+         s:assert [
+            test = "atom:link[@rel='alternate'] "
+            ~ "or atom:link[not(@rel)] "
+            ~ "or atom:content"
+            "An atom:entry must have at least one atom:link element "
+            ~ "with a rel attribute of 'alternate' "
+            ~ "or an atom:content."
+         ]
+      ]
+      s:rule [
+         context = "atom:entry"
+         s:assert [
+            test = "atom:author or "
+            ~ "../atom:author or atom:source/atom:author"
+            "An atom:entry must have an atom:author "
+            ~ "if its feed does not."
+         ]
+      ]
+   ]
+   element atom:entry {
+      atomCommonAttributes,
+      (atomAuthor*
+       & atomCategory*
+       & atomContent?
+       & atomContributor*
+       & atomId
+       & atomLink*
+       & atomPublished?
+       & atomRights?
+       & atomSource?
+       & atomSummary?
+       & atomTitle
+       & atomUpdated
+       & extensionElement*)
+   }
+
+# atom:content
+
+atomInlineTextContent =
+   element atom:content {
+      atomCommonAttributes,
+      attribute type { "text" | "html" }?,
+      (text)*
+   }
+
+atomInlineXHTMLContent =
+   element atom:content {
+      atomCommonAttributes,
+      attribute type { "xhtml" },
+      xhtmlDiv
+   }
+
+atomInlineOtherContent =
+   element atom:content {
+      atomCommonAttributes,
+      attribute type { atomMediaType }?,
+      (text|anyElement)*
+   }
+
+atomOutOfLineContent =
+   element atom:content {
+      atomCommonAttributes,
+      attribute type { atomMediaType }?,
+      attribute src { atomUri },
+      empty
+   }
+
+atomContent = atomInlineTextContent
+ | atomInlineXHTMLContent
+ | atomInlineOtherContent
+ | atomOutOfLineContent
+
+# atom:author
+
+atomAuthor = element atom:author { atomPersonConstruct }
+
+# atom:category
+
+atomCategory =
+   element atom:category {
+      atomCommonAttributes,
+      attribute term { text },
+      attribute scheme { atomUri }?,
+      attribute label { text }?,
+      undefinedContent
+   }
+
+# atom:contributor
+
+atomContributor = element atom:contributor { atomPersonConstruct }
+
+# atom:generator
+
+atomGenerator = element atom:generator {
+   atomCommonAttributes,
+   attribute uri { atomUri }?,
+   attribute version { text }?,
+   text
+}
+
+# atom:icon
+
+atomIcon = element atom:icon {
+   atomCommonAttributes,
+   (atomUri)
+}
+
+# atom:id
+
+atomId = element atom:id {
+   atomCommonAttributes,
+   (atomUri)
+}
+
+# atom:logo
+
+atomLogo = element atom:logo {
+   atomCommonAttributes,
+   (atomUri)
+}
+
+# atom:link
+
+atomLink =
+   element atom:link {
+      atomCommonAttributes,
+      attribute href { atomUri },
+      attribute rel { atomNCName | atomUri }?,
+      attribute type { atomMediaType }?,
+      attribute hreflang { atomLanguageTag }?,
+      attribute title { text }?,
+      attribute length { text }?,
+      undefinedContent
+   }
+
+# atom:published
+
+atomPublished = element atom:published { atomDateConstruct }
+
+# atom:rights
+
+atomRights = element atom:rights { atomTextConstruct }
+
+# atom:source
+
+atomSource =
+   element atom:source {
+      atomCommonAttributes,
+      (atomAuthor*
+       & atomCategory*
+       & atomContributor*
+       & atomGenerator?
+       & atomIcon?
+       & atomId?
+       & atomLink*
+       & atomLogo?
+       & atomRights?
+       & atomSubtitle?
+       & atomTitle?
+       & atomUpdated?
+       & extensionElement*)
+   }
+
+# atom:subtitle
+
+atomSubtitle = element atom:subtitle { atomTextConstruct }
+
+# atom:summary
+
+atomSummary = element atom:summary { atomTextConstruct }
+
+# atom:title
+
+atomTitle = element atom:title { atomTextConstruct }
+
+# atom:updated
+
+atomUpdated = element atom:updated { atomDateConstruct }
+
+# Low-level simple types
+
+atomNCName = xsd:string { minLength = "1" pattern = "[^:]*" }
+
+# Whatever a media type is, it contains at least one slash
+atomMediaType = xsd:string { pattern = ".+/.+" }
+
+# As defined in RFC 3066
+atomLanguageTag = xsd:string {
+   pattern = "[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*"
+}
+
+# Unconstrained; it's not entirely clear how IRI fit into
+# xsd:anyURI so let's not try to constrain it here
+atomUri = text
+
+# Whatever an email address is, it contains at least one @
+atomEmailAddress = xsd:string { pattern = ".+@.+" }
+
+# Simple Extension
+
+simpleExtensionElement =
+   element * - atom:* {
+      text
+   }
+
+# Structured Extension
+
+structuredExtensionElement =
+   element * - atom:* {
+      (attribute * { text }+,
+         (text|anyElement)*)
+    | (attribute * { text }*,
+       (text?, anyElement+, (text|anyElement)*))
+   }
+
+# Other Extensibility
+
+extensionElement =
+   simpleExtensionElement | structuredExtensionElement
+
+undefinedAttribute =
+  attribute * - (xml:base | xml:lang | local:*) { text }
+
+undefinedContent = (text|anyForeignElement)*
+
+anyElement =
+   element * {
+      (attribute * { text }
+       | text
+       | anyElement)*
+   }
+
+anyForeignElement =
+   element * - atom:* {
+      (attribute * { text }
+       | text
+       | anyElement)*
+   }
+
+# XHTML
+
+anyXHTML = element xhtml:* {
+   (attribute * { text }
+    | text
+    | anyXHTML)*
+}
+
+xhtmlDiv = element xhtml:div {
+   (attribute * { text }
+    | text
+    | anyXHTML)*
+}
+
+# EOF
\ No newline at end of file
diff --git a/plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc b/plugins/OStatus/extlib/XML/Feed/schemas/rss10.rnc
new file mode 100755 (executable)
index 0000000..7250947
--- /dev/null
@@ -0,0 +1,113 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!-- http://www.xml.com/lpt/a/2002/01/23/relaxng.html -->
+<!-- http://www.oasis-open.org/committees/relax-ng/tutorial-20011203.html -->
+<!-- http://www.zvon.org/xxl/XMLSchemaTutorial/Output/ser_wildcards_st8.html -->
+
+<grammar xmlns='http://relaxng.org/ns/structure/1.0'
+        xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
+        xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
+        ns='http://purl.org/rss/1.0/'
+        datatypeLibrary='http://www.w3.org/2001/XMLSchema-datatypes'>
+
+    <start>
+        <element name='RDF' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
+            <ref name='RDFContent'/>
+        </element>
+    </start>   
+
+    <define name='RDFContent' ns='http://purl.org/rss/1.0/'>
+        <interleave>
+            <element name='channel'>
+                <ref name='channelContent'/>
+            </element>
+            <optional>
+                <element name='image'><ref name='imageContent'/></element>
+            </optional>
+            <oneOrMore>
+                <element name='item'><ref name='itemContent'/></element>
+            </oneOrMore>
+        </interleave>
+    </define>
+
+     <define name='channelContent' combine="interleave">
+        <interleave>
+            <element name='title'><data type='string'/></element>
+            <element name='link'><data type='anyURI'/></element>
+            <element name='description'><data type='string'/></element>
+            <element name='image'>
+                <attribute name='resource' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
+                    <data type='anyURI'/>
+                </attribute>
+            </element>
+            <element name='items'>
+                    <ref name='itemsContent'/>
+            </element>
+            <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
+                <data type='anyURI'/>
+            </attribute>
+        </interleave>
+    </define>
+    
+        <define name="itemsContent">
+            <element name="Seq" ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
+                <oneOrMore>
+                    <element name="li" ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
+                        <choice>
+                            <attribute name='resource'>    <!-- Why doesn't RDF/RSS1.0 ns qualify this attribute? -->
+                                <data type='anyURI'/>
+                            </attribute>
+                            <attribute name='resource' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
+                                <data type='anyURI'/>
+                            </attribute>
+                        </choice>
+                    </element>
+                </oneOrMore>
+            </element>
+        </define>
+        
+    <define name='imageContent'>
+        <interleave>
+            <element name='title'><data type='string'/></element>
+            <element name='link'><data type='anyURI'/></element>
+            <element name='url'><data type='anyURI'/></element>
+            <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
+                <data type='anyURI'/>
+            </attribute>
+        </interleave>
+    </define>
+
+    <define name='itemContent'>
+        <interleave>
+            <element name='title'><data type='string'/></element>
+            <element name='link'><data type='anyURI'/></element>
+            <optional><element name='description'><data type='string'/></element></optional>
+            <ref name="anyThing"/>
+            <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
+                <data type='anyURI'/>
+            </attribute>
+        </interleave>
+    </define>            
+            
+
+        <define name='anyThing'>
+            <zeroOrMore>
+                <choice>
+                    <text/>
+                    <element>
+                        <anyName>
+                            <except>
+                                <nsName/>
+                            </except>
+                        </anyName>
+                        <ref name='anyThing'/>
+                        <zeroOrMore>
+                            <attribute>
+                              <anyName/>
+                            </attribute>
+                        </zeroOrMore>
+                    </element>
+                </choice>
+            </zeroOrMore>
+            </define>
+            
+</grammar>
diff --git a/plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc b/plugins/OStatus/extlib/XML/Feed/schemas/rss11.rnc
new file mode 100755 (executable)
index 0000000..c863376
--- /dev/null
@@ -0,0 +1,218 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  RELAX NG Compact Schema for RSS 1.1
+  Sean B. Palmer, inamidst.com
+  Christopher Schmidt, crschmidt.net
+  License: This schema is in the public domain
+-->
+<grammar xmlns:rss="http://purl.org/net/rss1.1#" xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ns="http://purl.org/net/rss1.1#" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+    <ref name="Channel"/>
+  </start>
+  <define name="Channel">
+    <a:documentation>http://purl.org/net/rss1.1#Channel</a:documentation>
+    <element name="Channel">
+      <ref name="Channel.content"/>
+
+    </element>
+  </define>
+  <define name="Channel.content">
+    <optional>
+      <ref name="AttrXMLLang"/>
+    </optional>
+    <optional>
+      <ref name="AttrXMLBase"/>
+    </optional>
+
+    <ref name="AttrRDFAbout"/>
+    <interleave>
+      <ref name="title"/>
+      <ref name="link"/>
+      <ref name="description"/>
+      <optional>
+        <ref name="image"/>
+      </optional>
+      <zeroOrMore>
+
+        <ref name="Any"/>
+      </zeroOrMore>
+      <ref name="items"/>
+    </interleave>
+  </define>
+  <define name="title">
+    <a:documentation>http://purl.org/net/rss1.1#title</a:documentation>
+    <element name="title">
+
+      <ref name="title.content"/>
+    </element>
+  </define>
+  <define name="title.content">
+    <optional>
+      <ref name="AttrXMLLang"/>
+    </optional>
+    <text/>
+  </define>
+
+  <define name="link">
+    <a:documentation>http://purl.org/net/rss1.1#link</a:documentation>
+    <element name="link">
+      <ref name="link.content"/>
+    </element>
+  </define>
+  <define name="link.content">
+    <data type="anyURI"/>
+
+  </define>
+  <define name="description">
+    <a:documentation>http://purl.org/net/rss1.1#description</a:documentation>
+    <element name="description">
+      <ref name="description.content"/>
+    </element>
+  </define>
+  <define name="description.content">
+
+    <optional>
+      <ref name="AttrXMLLang"/>
+    </optional>
+    <text/>
+  </define>
+  <define name="image">
+    <a:documentation>http://purl.org/net/rss1.1#image</a:documentation>
+    <element name="image">
+
+      <ref name="image.content"/>
+    </element>
+  </define>
+  <define name="image.content">
+    <optional>
+      <ref name="AttrXMLLang"/>
+    </optional>
+    <ref name="AttrRDFResource"/>
+    <interleave>
+
+      <ref name="title"/>
+      <optional>
+        <ref name="link"/>
+      </optional>
+      <ref name="url"/>
+      <zeroOrMore>
+        <ref name="Any"/>
+      </zeroOrMore>
+    </interleave>
+
+  </define>
+  <define name="url">
+    <a:documentation>http://purl.org/net/rss1.1#url</a:documentation>
+    <element name="url">
+      <ref name="url.content"/>
+    </element>
+  </define>
+  <define name="url.content">
+
+    <data type="anyURI"/>
+  </define>
+  <define name="items">
+    <a:documentation>http://purl.org/net/rss1.1#items</a:documentation>
+    <element name="items">
+      <ref name="items.content"/>
+    </element>
+  </define>
+
+  <define name="items.content">
+    <optional>
+      <ref name="AttrXMLLang"/>
+    </optional>
+    <ref name="AttrRDFCollection"/>
+    <zeroOrMore>
+      <ref name="item"/>
+    </zeroOrMore>
+  </define>
+
+  <define name="item">
+    <a:documentation>http://purl.org/net/rss1.1#item</a:documentation>
+    <element name="item">
+      <ref name="item.content"/>
+    </element>
+  </define>
+  <define name="item.content">
+    <optional>
+
+      <ref name="AttrXMLLang"/>
+    </optional>
+    <ref name="AttrRDFAbout"/>
+    <interleave>
+      <ref name="title"/>
+      <ref name="link"/>
+      <optional>
+        <ref name="description"/>
+      </optional>
+
+      <optional>
+        <ref name="image"/>
+      </optional>
+      <zeroOrMore>
+        <ref name="Any"/>
+      </zeroOrMore>
+    </interleave>
+  </define>
+  <define name="Any">
+
+    <a:documentation>http://purl.org/net/rss1.1#Any</a:documentation>
+    <element>
+      <anyName>
+        <except>
+          <nsName/>
+        </except>
+      </anyName>
+      <ref name="Any.content"/>
+
+    </element>
+  </define>
+  <define name="Any.content">
+    <zeroOrMore>
+      <attribute>
+        <anyName>
+          <except>
+            <nsName/>
+            <nsName ns=""/>
+
+          </except>
+        </anyName>
+      </attribute>
+    </zeroOrMore>
+    <mixed>
+      <zeroOrMore>
+        <ref name="Any"/>
+      </zeroOrMore>
+    </mixed>
+
+  </define>
+  <define name="AttrXMLLang">
+    <attribute name="xml:lang">
+      <data type="language"/>
+    </attribute>
+  </define>
+  <define name="AttrXMLBase">
+    <attribute name="xml:base">
+      <data type="anyURI"/>
+
+    </attribute>
+  </define>
+  <define name="AttrRDFAbout">
+    <attribute name="rdf:about">
+      <data type="anyURI"/>
+    </attribute>
+  </define>
+  <define name="AttrRDFResource">
+    <attribute name="rdf:parseType">
+
+      <value>Resource</value>
+    </attribute>
+  </define>
+  <define name="AttrRDFCollection">
+    <attribute name="rdf:parseType">
+      <value>Collection</value>
+    </attribute>
+  </define>
+
+</grammar>
diff --git a/plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch b/plugins/OStatus/extlib/xml-feed-parser-bug-16416.patch
new file mode 100644 (file)
index 0000000..c53bd97
--- /dev/null
@@ -0,0 +1,14 @@
+diff --git a/htdocs/lib/pear/XML/Feed/Parser/RSS2.php b/htdocs/lib/pear/XML/Feed/Parser/RSS2.php
+index c5d79d1..308a4ab 100644
+--- a/htdocs/lib/pear/XML/Feed/Parser/RSS2.php
++++ b/htdocs/lib/pear/XML/Feed/Parser/RSS2.php
+@@ -321,7 +321,8 @@ class XML_Feed_Parser_RSS2 extends XML_Feed_Parser_Type
+      */
+     function getLink($offset, $attribute = 'href', $params = array())
+     {
+-        $links = $this->model->getElementsByTagName('link');
++        $xPath = new DOMXPath($this->model);
++        $links = $xPath->query('//link');
+         if ($links->length <= $offset) {
+             return false;
diff --git a/plugins/OStatus/images/24px-Feed-icon.svg.png b/plugins/OStatus/images/24px-Feed-icon.svg.png
new file mode 100644 (file)
index 0000000..3172258
Binary files /dev/null and b/plugins/OStatus/images/24px-Feed-icon.svg.png differ
diff --git a/plugins/OStatus/images/48px-Feed-icon.svg.png b/plugins/OStatus/images/48px-Feed-icon.svg.png
new file mode 100644 (file)
index 0000000..bd1da4f
Binary files /dev/null and b/plugins/OStatus/images/48px-Feed-icon.svg.png differ
diff --git a/plugins/OStatus/images/96px-Feed-icon.svg.png b/plugins/OStatus/images/96px-Feed-icon.svg.png
new file mode 100644 (file)
index 0000000..bf16571
Binary files /dev/null and b/plugins/OStatus/images/96px-Feed-icon.svg.png differ
diff --git a/plugins/OStatus/images/README b/plugins/OStatus/images/README
new file mode 100644 (file)
index 0000000..d9379c2
--- /dev/null
@@ -0,0 +1,5 @@
+Feed icon rendered from http://commons.wikimedia.org/wiki/File:Feed-icon.svg
+
+Originally distributed by the Mozilla Foundation under a MPL/GPL/LGPL tri-license:
+
+http://www.mozilla.org/MPL/boilerplate-1.1/mpl-tri-license-html
diff --git a/plugins/OStatus/js/ostatus.js b/plugins/OStatus/js/ostatus.js
new file mode 100644 (file)
index 0000000..6717955
--- /dev/null
@@ -0,0 +1,60 @@
+SN.U.DialogBox = {
+    Subscribe: function(a) {
+        var f = a.parent().find('#form_ostatus_connect');
+        if (f.length > 0) {
+            f.show();
+        }
+        else {
+            $.ajax({
+                type: 'GET',
+                dataType: 'xml',
+                url: a[0].href+'&ajax=1',
+                beforeSend: function(formData) {
+                    a.addClass('processing');
+                },
+                error: function (xhr, textStatus, errorThrown) {
+                    alert(errorThrown || textStatus);
+                },
+                success: function(data, textStatus, xhr) {
+                    if (typeof($('form', data)[0]) != 'undefined') {
+                        a.after(document._importNode($('form', data)[0], true));
+
+                        var form = a.parent().find('#form_ostatus_connect');
+
+                        form
+                            .addClass('dialogbox')
+                            .append('<button class="close">&#215;</button>');
+
+                        form
+                            .find('.submit')
+                                .addClass('submit_dialogbox')
+                                .removeClass('submit')
+                                .bind('click', function() {
+                                    form.addClass('processing');
+                                });
+
+                        form.find('button.close').click(function(){
+                            form.hide();
+
+                            return false;
+                        });
+
+                        form.find('#acct').focus();
+                    }
+
+                    a.removeClass('processing');
+                }
+            });
+        }
+    }
+};
+
+SN.Init.Subscribe = function() {
+    $('.entity_subscribe a').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; });
+};
+
+$(document).ready(function() {
+    if ($('.entity_subscribe .entity_remote_subscribe').length > 0) {
+        SN.Init.Subscribe();
+    }
+});
diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php
new file mode 100644 (file)
index 0000000..f137946
--- /dev/null
@@ -0,0 +1,448 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  OStatus
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Utilities for turning DOMish things into Activityish things
+ *
+ * Some common functions that I didn't have the bandwidth to try to factor
+ * into some kind of reasonable superclass, so just dumped here. Might
+ * be useful to have an ActivityObject parent class or something.
+ *
+ * @category  OStatus
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link      http://status.net/
+ */
+
+class ActivityUtils
+{
+    const ATOM = 'http://www.w3.org/2005/Atom';
+
+    const LINK = 'link';
+    const REL  = 'rel';
+    const TYPE = 'type';
+    const HREF = 'href';
+
+    /**
+     * Get the permalink for an Activity object
+     *
+     * @param DOMElement $element A DOM element
+     *
+     * @return string related link, if any
+     */
+
+    static function getPermalink($element)
+    {
+        return self::getLink($element, 'alternate', 'text/html');
+    }
+
+    /**
+     * Get the permalink for an Activity object
+     *
+     * @param DOMElement $element A DOM element
+     *
+     * @return string related link, if any
+     */
+
+    static function getLink($element, $rel, $type=null)
+    {
+        $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
+
+        foreach ($links as $link) {
+
+            $linkRel = $link->getAttribute(self::REL);
+            $linkType = $link->getAttribute(self::TYPE);
+
+            if ($linkRel == $rel &&
+                (is_null($type) || $linkType == $type)) {
+                return $link->getAttribute(self::HREF);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets the first child element with the given tag
+     *
+     * @param DOMElement $element   element to pick at
+     * @param string     $tag       tag to look for
+     * @param string     $namespace Namespace to look under
+     *
+     * @return DOMElement found element or null
+     */
+
+    static function child($element, $tag, $namespace=self::ATOM)
+    {
+        $els = $element->childNodes;
+        if (empty($els) || $els->length == 0) {
+            return null;
+        } else {
+            for ($i = 0; $i < $els->length; $i++) {
+                $el = $els->item($i);
+                if ($el->localName == $tag && $el->namespaceURI == $namespace) {
+                    return $el;
+                }
+            }
+        }
+    }
+
+    /**
+     * Grab the text content of a DOM element child of the current element
+     *
+     * @param DOMElement $element   Element whose children we examine
+     * @param string     $tag       Tag to look up
+     * @param string     $namespace Namespace to use, defaults to Atom
+     *
+     * @return string content of the child
+     */
+
+    static function childContent($element, $tag, $namespace=self::ATOM)
+    {
+        $el = self::child($element, $tag, $namespace);
+
+        if (empty($el)) {
+            return null;
+        } else {
+            return $el->textContent;
+        }
+    }
+}
+
+/**
+ * A noun-ish thing in the activity universe
+ *
+ * The activity streams spec talks about activity objects, while also having
+ * a tag activity:object, which is in fact an activity object. Aaaaaah!
+ *
+ * This is just a thing in the activity universe. Can be the subject, object,
+ * or indirect object (target!) of an activity verb. Rotten name, and I'm
+ * propagating it. *sigh*
+ *
+ * @category  OStatus
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link      http://status.net/
+ */
+
+class ActivityObject
+{
+    const ARTICLE   = 'http://activitystrea.ms/schema/1.0/article';
+    const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
+    const NOTE      = 'http://activitystrea.ms/schema/1.0/note';
+    const STATUS    = 'http://activitystrea.ms/schema/1.0/status';
+    const FILE      = 'http://activitystrea.ms/schema/1.0/file';
+    const PHOTO     = 'http://activitystrea.ms/schema/1.0/photo';
+    const ALBUM     = 'http://activitystrea.ms/schema/1.0/photo-album';
+    const PLAYLIST  = 'http://activitystrea.ms/schema/1.0/playlist';
+    const VIDEO     = 'http://activitystrea.ms/schema/1.0/video';
+    const AUDIO     = 'http://activitystrea.ms/schema/1.0/audio';
+    const BOOKMARK  = 'http://activitystrea.ms/schema/1.0/bookmark';
+    const PERSON    = 'http://activitystrea.ms/schema/1.0/person';
+    const GROUP     = 'http://activitystrea.ms/schema/1.0/group';
+    const PLACE     = 'http://activitystrea.ms/schema/1.0/place';
+    const COMMENT   = 'http://activitystrea.ms/schema/1.0/comment';
+    // ^^^^^^^^^^ tea!
+
+    // Atom elements we snarf
+
+    const TITLE   = 'title';
+    const SUMMARY = 'summary';
+    const CONTENT = 'content';
+    const ID      = 'id';
+    const SOURCE  = 'source';
+
+    const NAME  = 'name';
+    const URI   = 'uri';
+    const EMAIL = 'email';
+
+    public $element;
+    public $type;
+    public $id;
+    public $title;
+    public $summary;
+    public $content;
+    public $link;
+    public $source;
+
+    /**
+     * Constructor
+     *
+     * This probably needs to be refactored
+     * to generate a local class (ActivityPerson, ActivityFile, ...)
+     * based on the object type.
+     *
+     * @param DOMElement $element DOM thing to turn into an Activity thing
+     */
+
+    function __construct($element)
+    {
+        $this->element = $element;
+
+        if ($element->tagName == 'author') {
+
+            $this->type  = self::PERSON; // XXX: is this fair?
+            $this->title = $this->_childContent($element, self::NAME);
+            $this->id    = $this->_childContent($element, self::URI);
+
+            if (empty($this->id)) {
+                $email = $this->_childContent($element, self::EMAIL);
+                if (!empty($email)) {
+                    // XXX: acct: ?
+                    $this->id = 'mailto:'.$email;
+                }
+            }
+
+        } else {
+
+            $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
+                                               Activity::SPEC);
+
+            if (empty($this->type)) {
+                $this->type = ActivityObject::NOTE;
+            }
+
+            $this->id      = $this->_childContent($element, self::ID);
+            $this->title   = $this->_childContent($element, self::TITLE);
+            $this->summary = $this->_childContent($element, self::SUMMARY);
+            $this->content = $this->_childContent($element, self::CONTENT);
+
+            $this->source  = $this->_getSource($element);
+
+            $this->link = ActivityUtils::getPermalink($element);
+
+            // XXX: grab PoCo stuff
+        }
+
+        // Some per-type attributes...
+        if ($this->type == self::PERSON || $this->type == self::GROUP) {
+            $this->displayName = $this->title;
+
+            // @fixme we may have multiple avatars with different resolutions specified
+            $this->avatar = ActivityUtils::getLink($element, 'avatar');
+        }
+    }
+
+    private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
+    {
+        return ActivityUtils::childContent($element, $tag, $namespace);
+    }
+
+    // Try to get a unique id for the source feed
+
+    private function _getSource($element)
+    {
+        $sourceEl = ActivityUtils::child($element, 'source');
+
+        if (empty($sourceEl)) {
+            return null;
+        } else {
+            $href = ActivityUtils::getLink($sourceEl, 'self');
+            if (!empty($href)) {
+                return $href;
+            } else {
+                return ActivityUtils::childContent($sourceEl, 'id');
+            }
+        }
+    }
+}
+
+/**
+ * Utility class to hold a bunch of constant defining default verb types
+ *
+ * @category  OStatus
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link      http://status.net/
+ */
+
+class ActivityVerb
+{
+    const POST     = 'http://activitystrea.ms/schema/1.0/post';
+    const SHARE    = 'http://activitystrea.ms/schema/1.0/share';
+    const SAVE     = 'http://activitystrea.ms/schema/1.0/save';
+    const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite';
+    const PLAY     = 'http://activitystrea.ms/schema/1.0/play';
+    const FOLLOW   = 'http://activitystrea.ms/schema/1.0/follow';
+    const FRIEND   = 'http://activitystrea.ms/schema/1.0/make-friend';
+    const JOIN     = 'http://activitystrea.ms/schema/1.0/join';
+    const TAG      = 'http://activitystrea.ms/schema/1.0/tag';
+}
+
+/**
+ * An activity in the ActivityStrea.ms world
+ *
+ * An activity is kind of like a sentence: someone did something
+ * to something else.
+ *
+ * 'someone' is the 'actor'; 'did something' is the verb;
+ * 'something else' is the object.
+ *
+ * @category  OStatus
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link      http://status.net/
+ */
+
+class Activity
+{
+    const SPEC   = 'http://activitystrea.ms/spec/1.0/';
+    const SCHEMA = 'http://activitystrea.ms/schema/1.0/';
+
+    const VERB       = 'verb';
+    const OBJECT     = 'object';
+    const ACTOR      = 'actor';
+    const SUBJECT    = 'subject';
+    const OBJECTTYPE = 'object-type';
+    const CONTEXT    = 'context';
+    const TARGET     = 'target';
+
+    const ATOM = 'http://www.w3.org/2005/Atom';
+
+    const AUTHOR    = 'author';
+    const PUBLISHED = 'published';
+    const UPDATED   = 'updated';
+
+    public $actor;   // an ActivityObject
+    public $verb;    // a string (the URL)
+    public $object;  // an ActivityObject
+    public $target;  // an ActivityObject
+    public $context; // an ActivityObject
+    public $time;    // Time of the activity
+    public $link;    // an ActivityObject
+    public $entry;   // the source entry
+    public $feed;    // the source feed
+
+    /**
+     * Turns a regular old Atom <entry> into a magical activity
+     *
+     * @param DOMElement $entry Atom entry to poke at
+     * @param DOMElement $feed  Atom feed, for context
+     */
+
+    function __construct($entry, $feed = null)
+    {
+        $this->entry = $entry;
+        $this->feed  = $feed;
+
+        $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM);
+
+        if (!empty($pubEl)) {
+            $this->time = strtotime($pubEl->textContent);
+        } else {
+            // XXX technically an error; being liberal. Good idea...?
+            $updateEl = $this->_child($entry, self::UPDATED, self::ATOM);
+            if (!empty($updateEl)) {
+                $this->time = strtotime($updateEl->textContent);
+            } else {
+                $this->time = null;
+            }
+        }
+
+        $this->link = ActivityUtils::getPermalink($entry);
+
+        $verbEl = $this->_child($entry, self::VERB);
+
+        if (!empty($verbEl)) {
+            $this->verb = trim($verbEl->textContent);
+        } else {
+            $this->verb = ActivityVerb::POST;
+            // XXX: do other implied stuff here
+        }
+
+        $objectEl = $this->_child($entry, self::OBJECT);
+
+        if (!empty($objectEl)) {
+            $this->object = new ActivityObject($objectEl);
+        } else {
+            $this->object = new ActivityObject($entry);
+        }
+
+        $actorEl = $this->_child($entry, self::ACTOR);
+
+        if (!empty($actorEl)) {
+
+            $this->actor = new ActivityObject($actorEl);
+
+        } else if (!empty($feed) &&
+                   $subjectEl = $this->_child($feed, self::SUBJECT)) {
+
+            $this->actor = new ActivityObject($subjectEl);
+
+        } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) {
+
+            $this->actor = new ActivityObject($authorEl);
+
+        } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR,
+                                                              self::ATOM)) {
+
+            $this->actor = new ActivityObject($authorEl);
+        }
+
+        $contextEl = $this->_child($entry, self::CONTEXT);
+
+        if (!empty($contextEl)) {
+            $this->context = new ActivityObject($contextEl);
+        }
+
+        $targetEl = $this->_child($entry, self::TARGET);
+
+        if (!empty($targetEl)) {
+            $this->target = new ActivityObject($targetEl);
+        }
+    }
+
+    /**
+     * Returns an Atom <entry> based on this activity
+     *
+     * @return DOMElement Atom entry
+     */
+
+    function toAtomEntry()
+    {
+        return null;
+    }
+
+    private function _child($element, $tag, $namespace=self::SPEC)
+    {
+        return ActivityUtils::child($element, $tag, $namespace);
+    }
+}
\ No newline at end of file
diff --git a/plugins/OStatus/lib/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php
new file mode 100644 (file)
index 0000000..39985fc
--- /dev/null
@@ -0,0 +1,231 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package FeedSubPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class FeedSubBadURLException extends FeedSubException
+{
+}
+
+class FeedSubBadResponseException extends FeedSubException
+{
+}
+
+class FeedSubEmptyException extends FeedSubException
+{
+}
+
+class FeedSubBadHTMLException extends FeedSubException
+{
+}
+
+class FeedSubUnrecognizedTypeException extends FeedSubException
+{
+}
+
+class FeedSubNoFeedException extends FeedSubException
+{
+}
+
+/**
+ * Given a web page or feed URL, discover the final location of the feed
+ * and return its current contents.
+ *
+ * @example
+ *   $feed = new FeedDiscovery();
+ *   if ($feed->discoverFromURL($url)) {
+ *     print $feed->uri;
+ *     print $feed->type;
+ *     processFeed($feed->body);
+ *   }
+ */
+class FeedDiscovery
+{
+    public $uri;
+    public $type;
+    public $body;
+
+
+    public function feedMunger()
+    {
+        require_once 'XML/Feed/Parser.php';
+        $feed = new XML_Feed_Parser($this->body, false, false, true); // @fixme
+        return new FeedMunger($feed, $this->uri);
+    }
+
+    /**
+     * @param string $url
+     * @param bool $htmlOk pass false here if you don't want to follow web pages.
+     * @return string with validated URL
+     * @throws FeedSubBadURLException
+     * @throws FeedSubBadHtmlException
+     * @throws FeedSubNoFeedException
+     * @throws FeedSubEmptyException
+     * @throws FeedSubUnrecognizedTypeException
+     */
+    function discoverFromURL($url, $htmlOk=true)
+    {
+        try {
+            $client = new HTTPClient();
+            $response = $client->get($url);
+        } catch (HTTP_Request2_Exception $e) {
+            throw new FeedSubBadURLException($e);
+        }
+
+        if ($htmlOk) {
+            $type = $response->getHeader('Content-Type');
+            $isHtml = preg_match('!^(text/html|application/xhtml\+xml)!i', $type);
+            if ($isHtml) {
+                $target = $this->discoverFromHTML($response->getUrl(), $response->getBody());
+                if (!$target) {
+                    throw new FeedSubNoFeedException($url);
+                }
+                return $this->discoverFromURL($target, false);
+            }
+        }
+        
+        return $this->initFromResponse($response);
+    }
+    
+    function initFromResponse($response)
+    {
+        if (!$response->isOk()) {
+            throw new FeedSubBadResponseException($response->getCode());
+        }
+
+        $sourceurl = $response->getUrl();
+        $body = $response->getBody();
+        if (!$body) {
+            throw new FeedSubEmptyException($sourceurl);
+        }
+
+        $type = $response->getHeader('Content-Type');
+        if (preg_match('!^(text/xml|application/xml|application/(rss|atom)\+xml)!i', $type)) {
+            $this->uri = $sourceurl;
+            $this->type = $type;
+            $this->body = $body;
+            return true;
+        } else {
+            common_log(LOG_WARNING, "Unrecognized feed type $type for $sourceurl");
+            throw new FeedSubUnrecognizedTypeException($type);
+        }
+    }
+
+    /**
+     * @param string $url source URL, used to resolve relative links
+     * @param string $body HTML body text
+     * @return mixed string with URL or false if no target found
+     */
+    function discoverFromHTML($url, $body)
+    {
+        // DOMDocument::loadHTML may throw warnings on unrecognized elements.
+        $old = error_reporting(error_reporting() & ~E_WARNING);
+        $dom = new DOMDocument();
+        $ok = $dom->loadHTML($body);
+        error_reporting($old);
+
+        if (!$ok) {
+            throw new FeedSubBadHtmlException();
+        }
+
+        // Autodiscovery links may be relative to the page's URL or <base href>
+        $base = false;
+        $nodes = $dom->getElementsByTagName('base');
+        for ($i = 0; $i < $nodes->length; $i++) {
+            $node = $nodes->item($i);
+            if ($node->hasAttributes()) {
+                $href = $node->attributes->getNamedItem('href');
+                if ($href) {
+                    $base = trim($href->value);
+                }
+            }
+        }
+        if ($base) {
+            $base = $this->resolveURI($base, $url);
+        } else {
+            $base = $url;
+        }
+
+        // Ok... now on to the links!
+        // Types listed in order of priority -- we'll prefer Atom if available.
+        // @fixme merge with the munger link checks
+        $feeds = array(
+            'application/atom+xml' => false,
+            'application/rss+xml' => false,
+        );
+        
+        $nodes = $dom->getElementsByTagName('link');
+        for ($i = 0; $i < $nodes->length; $i++) {
+            $node = $nodes->item($i);
+            if ($node->hasAttributes()) {
+                $rel = $node->attributes->getNamedItem('rel');
+                $type = $node->attributes->getNamedItem('type');
+                $href = $node->attributes->getNamedItem('href');
+                if ($rel && $type && $href) {
+                    $rel = trim($rel->value);
+                    $type = trim($type->value);
+                    $href = trim($href->value);
+
+                    if (trim($rel) == 'alternate' && array_key_exists($type, $feeds) && empty($feeds[$type])) {
+                        // Save the first feed found of each type...
+                        $feeds[$type] = $this->resolveURI($href, $base);
+                    }
+                }
+            }
+        }
+
+        // Return the highest-priority feed found
+        foreach ($feeds as $type => $url) {
+            if ($url) {
+                return $url;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Resolve a possibly relative URL against some absolute base URL
+     * @param string $rel relative or absolute URL
+     * @param string $base absolute URL
+     * @return string absolute URL, or original URL if could not be resolved.
+     */
+    function resolveURI($rel, $base)
+    {
+        require_once "Net/URL2.php";
+        try {
+            $relUrl = new Net_URL2($rel);
+            if ($relUrl->isAbsolute()) {
+                return $rel;
+            }
+            $baseUrl = new Net_URL2($base);
+            $absUrl = $baseUrl->resolve($relUrl);
+            return $absUrl->getURL();
+        } catch (Exception $e) {
+            common_log(LOG_WARNING, 'Unable to resolve relative link "' .
+                $rel . '" against base "' . $base . '": ' . $e->getMessage());
+            return $rel;
+        }
+    }
+}
diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php
new file mode 100644 (file)
index 0000000..e8c46de
--- /dev/null
@@ -0,0 +1,350 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package FeedSubPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class FeedSubPreviewNotice extends Notice
+{
+    protected $fetched = true;
+
+    function __construct($profile)
+    {
+        $this->profile = $profile;
+        $this->profile_id = 0;
+    }
+    
+    function getProfile()
+    {
+        return $this->profile;
+    }
+    
+    function find()
+    {
+        return true;
+    }
+    
+    function fetch()
+    {
+        $got = $this->fetched;
+        $this->fetched = false;
+        return $got;
+    }
+}
+
+class FeedSubPreviewProfile extends Profile
+{
+    function getAvatar($width, $height=null)
+    {
+        return new FeedSubPreviewAvatar($width, $height, $this->avatar);
+    }
+}
+
+class FeedSubPreviewAvatar extends Avatar
+{
+    function __construct($width, $height, $remote)
+    {
+        $this->remoteImage = $remote;
+    }
+
+    function displayUrl() {
+        return $this->remoteImage;
+    }
+}
+
+class FeedMunger
+{
+    /**
+     * @param XML_Feed_Parser $feed
+     */
+    function __construct($feed, $url=null)
+    {
+        $this->feed = $feed;
+        $this->url = $url;
+    }
+    
+    function ostatusProfile()
+    {
+        $profile = new Ostatus_profile();
+        $profile->feeduri = $this->url;
+        $profile->homeuri = $this->feed->link;
+        $profile->huburi = $this->getHubLink();
+        $salmon = $this->getSalmonLink();
+        if ($salmon) {
+            $profile->salmonuri = $salmon;
+        }
+        return $profile;
+    }
+
+    function getAtomLink($item, $attribs=array())
+    {
+        // XML_Feed_Parser gets confused by multiple <link> elements.
+        $dom = $item->model;
+
+        // Note that RSS feeds would embed an <atom:link> so this should work for both.
+        /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds
+        // <link rel='hub' href='http://pubsubhubbub.appspot.com/'/>
+        $links = $dom->getElementsByTagNameNS('http://www.w3.org/2005/Atom', 'link');
+        for ($i = 0; $i < $links->length; $i++) {
+            $node = $links->item($i);
+            if ($node->hasAttributes()) {
+                $href = $node->attributes->getNamedItem('href');
+                if ($href) {
+                    $matches = 0;
+                    foreach ($attribs as $name => $val) {
+                        $attrib = $node->attributes->getNamedItem($name);
+                        if ($attrib && $attrib->value == $val) {
+                            $matches++;
+                        }
+                    }
+                    if ($matches == count($attribs)) {
+                        return $href->value;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    function getRssLink($item)
+    {
+        // XML_Feed_Parser gets confused by multiple <link> elements.
+        $dom = $item->model;
+
+        // Note that RSS feeds would embed an <atom:link> so this should work for both.
+        /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds
+        // <link rel='hub' href='http://pubsubhubbub.appspot.com/'/>
+        $links = $dom->getElementsByTagName('link');
+        for ($i = 0; $i < $links->length; $i++) {
+            $node = $links->item($i);
+            if (!$node->hasAttributes()) {
+                return $node->textContent;
+            }
+        }
+        return false;
+    }
+
+    function getAltLink($item)
+    {
+        // Check for an atom link...
+        $link = $this->getAtomLink($item, array('rel' => 'alternate', 'type' => 'text/html'));
+        if (!$link) {
+            $link = $this->getRssLink($item);
+        }
+        return $link;
+    }
+
+    function getHubLink()
+    {
+        return $this->getAtomLink($this->feed, array('rel' => 'hub'));
+    }
+
+    function getSalmonLink()
+    {
+        return $this->getAtomLink($this->feed, array('rel' => 'salmon'));
+    }
+
+    function getSelfLink()
+    {
+        return $this->getAtomLink($this->feed, array('rel' => 'self'));
+    }
+
+    /**
+     * Get an appropriate avatar image source URL, if available.
+     * @return mixed string or false
+     */
+    function getAvatar()
+    {
+        $logo = $this->feed->logo;
+        if ($logo) {
+            return $logo;
+        }
+        $icon = $this->feed->icon;
+        if ($icon) {
+            return $icon;
+        }
+        return common_path('plugins/OStatus/images/48px-Feed-icon.svg.png');
+    }
+
+    function profile($preview=false)
+    {
+        if ($preview) {
+            $profile = new FeedSubPreviewProfile();
+        } else {
+            $profile = new Profile();
+        }
+        
+        // @todo validate/normalize nick?
+        $profile->nickname   = $this->feed->title;
+        $profile->fullname   = $this->feed->title;
+        $profile->homepage   = $this->getAltLink($this->feed);
+        $profile->bio        = $this->feed->description;
+        $profile->profileurl = $this->getAltLink($this->feed);
+
+        if ($preview) {
+            $profile->avatar = $this->getAvatar();
+        }
+        
+        // @todo tags from categories
+        // @todo lat/lon/location?
+
+        return $profile;
+    }
+
+    function notice($index=1, $preview=false)
+    {
+        $entry = $this->feed->getEntryByOffset($index);
+        if (!$entry) {
+            return null;
+        }
+
+        if ($preview) {
+            $notice = new FeedSubPreviewNotice($this->profile(true));
+            $notice->id = -1;
+        } else {
+            $notice = new Notice();
+            $notice->profile_id = $this->profileIdForEntry($index);
+        }
+
+        $link = $this->getAltLink($entry);
+        if (empty($link)) {
+            if (preg_match('!^https?://!', $entry->id)) {
+                $link = $entry->id;
+                common_log(LOG_DEBUG, "No link on entry, using URL from id: $link");
+            }
+        }
+        $notice->uri = $link;
+        $notice->url = $link;
+        $notice->content = $this->noticeFromEntry($entry);
+        $notice->rendered = common_render_content($notice->content, $notice); // @fixme this is failing on group posts
+        $notice->created = common_sql_date($entry->updated); // @fixme
+        $notice->is_local = Notice::GATEWAY;
+        $notice->source = 'feed';
+
+        $location = $this->getLocation($entry);
+        if ($location) {
+            if ($location->location_id) {
+                $notice->location_ns = $location->location_ns;
+                $notice->location_id = $location->location_id;
+            }
+            $notice->lat = $location->lat;
+            $notice->lon = $location->lon;
+        }
+
+        return $notice;
+    }
+
+    function profileIdForEntry($index=1)
+    {
+        // hack hack hack
+        // should get profile for this entry's author...
+        $feeduri = $this->getSelfLink();
+        $remote = Ostatus_profile::staticGet('feeduri', $feeduri);
+        if ($remote) {
+            return $remote->profile_id;
+        } else {
+            throw new Exception("Can't find feed profile for $feeduri");
+        }
+    }
+
+    /**
+     * Parse location given as a GeoRSS-simple point, if provided.
+     * http://www.georss.org/simple
+     *
+     * @param feed item $entry
+     * @return mixed Location or false
+     */
+    function getLocation($entry)
+    {
+        $dom = $entry->model;
+        $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point');
+        
+        for ($i = 0; $i < $points->length; $i++) {
+            $point = $points->item(0)->textContent;
+            $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
+            $point = preg_replace('/\s+/', ' ', $point);
+            $point = trim($point);
+            $coords = explode(' ', $point);
+            if (count($coords) == 2) {
+                list($lat, $lon) = $coords;
+                if (is_numeric($lat) && is_numeric($lon)) {
+                    common_log(LOG_INFO, "Looking up location for $lat $lon from georss");
+                    return Location::fromLatLon($lat, $lon);
+                }
+            }
+            common_log(LOG_ERR, "Ignoring bogus georss:point value $point");
+        }
+
+        return false;
+    }
+
+    /**
+     * @param XML_Feed_Type $entry
+     * @return string notice text, within post size limit
+     */
+    function noticeFromEntry($entry)
+    {
+        $max = Notice::maxContent();
+        $ellipsis = "\xe2\x80\xa6"; // U+2026 HORIZONTAL ELLIPSIS
+        $title = $entry->title;
+        $link = $entry->link;
+
+        // @todo We can get <category> entries like this:
+        // $cats = $entry->getCategory('category', array(0, true));
+        // but it feels like an awful hack. If it's accessible cleanly,
+        // try adding #hashtags from the categories/tags on a post.
+
+        $title = $entry->title;
+        $link = $this->getAltLink($entry);
+        if ($link) {
+            // Blog post or such...
+            // @todo Should we force a language here?
+            $format = _m('New post: "%1$s" %2$s');
+            $out = sprintf($format, $title, $link);
+
+            // Trim link if needed...
+            if (mb_strlen($out) > $max) {
+                $link = common_shorten_url($link);
+                $out = sprintf($format, $title, $link);
+            }
+
+            // Trim title if needed...
+            if (mb_strlen($out) > $max) {
+                $used = mb_strlen($out) - mb_strlen($title);
+                $available = $max - $used - mb_strlen($ellipsis);
+                $title = mb_substr($title, 0, $available) . $ellipsis;
+                $out = sprintf($format, $title, $link);
+            }
+        } else {
+            // No link? Consider a bare status update.
+            if (mb_strlen($title) > $max) {
+                $available = $max - mb_strlen($ellipsis);
+                $out = mb_substr($title, 0, $available) . $ellipsis;
+            } else {
+                $out = $title;
+            }
+        }
+        
+        return $out;
+    }
+}
diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php
new file mode 100644 (file)
index 0000000..245a57f
--- /dev/null
@@ -0,0 +1,185 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Send a PuSH subscription verification from our internal hub.
+ * Queue up final distribution for 
+ * @package Hub
+ * @author Brion Vibber <brion@status.net>
+ */
+class HubDistribQueueHandler extends QueueHandler
+{
+    function transport()
+    {
+        return 'hubdistrib';
+    }
+
+    function handle($notice)
+    {
+        assert($notice instanceof Notice);
+
+        $this->pushUser($notice);
+        foreach ($notice->getGroups() as $group) {
+            $this->pushGroup($notice, $group->group_id);
+        }
+        return true;
+    }
+    
+    function pushUser($notice)
+    {
+        // See if there's any PuSH subscriptions, including OStatus clients.
+        // @fixme handle group subscriptions as well
+        // http://identi.ca/api/statuses/user_timeline/1.atom
+        $feed = common_local_url('ApiTimelineUser',
+                                 array('id' => $notice->profile_id,
+                                       'format' => 'atom'));
+        $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice);
+    }
+
+    function pushGroup($notice, $group_id)
+    {
+        $feed = common_local_url('ApiTimelineGroup',
+                                 array('id' => $group_id,
+                                       'format' => 'atom'));
+        $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id, $notice);
+    }
+
+    /**
+     * @param string $feed URI to the feed
+     * @param callable $callback function to generate Atom feed update if needed
+     *        any additional params are passed to the callback.
+     */
+    function pushFeed($feed, $callback)
+    {
+        $hub = common_config('ostatus', 'hub');
+        if ($hub) {
+            $this->pushFeedExternal($feed, $hub);
+        }
+
+        $sub = new HubSub();
+        $sub->topic = $feed;
+        if ($sub->find()) {
+            $args = array_slice(func_get_args(), 2);
+            $atom = call_user_func_array($callback, $args);
+            $this->pushFeedInternal($atom, $sub);
+        } else {
+            common_log(LOG_INFO, "No PuSH subscribers for $feed");
+        }
+        return true;
+    }
+
+    /**
+     * Ping external hub about this update.
+     * The hub will pull the feed and check for new items later.
+     * Not guaranteed safe in an environment with database replication.
+     *
+     * @param string $feed feed topic URI
+     * @param string $hub PuSH hub URI
+     * @fixme can consolidate pings for user & group posts
+     */
+    function pushFeedExternal($feed, $hub)
+    {
+        $client = new HTTPClient();
+        try {
+            $data = array('hub.mode' => 'publish',
+                          'hub.url' => $feed);
+            $response = $client->post($hub, array(), $data);
+            if ($response->getStatus() == 204) {
+                common_log(LOG_INFO, "PuSH ping to hub $hub for $feed ok");
+                return true;
+            } else {
+                common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed with HTTP " .
+                                    $response->getStatus() . ': ' .
+                                    $response->getBody());
+            }
+        } catch (Exception $e) {
+            common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed: " . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Queue up direct feed update pushes to subscribers on our internal hub.
+     * @param string $atom update feed, containing only new/changed items
+     * @param HubSub $sub open query of subscribers
+     */
+    function pushFeedInternal($atom, $sub)
+    {
+        common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic");
+        $qm = QueueManager::get();
+        while ($sub->fetch()) {
+            common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $sub->topic");
+            $data = array('sub' => clone($sub),
+                          'atom' => $atom);
+            $qm->enqueue($data, 'hubout');
+        }
+    }
+
+    /**
+     * Build a single-item version of the sending user's Atom feed.
+     * @param Notice $notice
+     * @return string
+     */
+    function userFeedForNotice($notice)
+    {
+        // @fixme this feels VERY hacky...
+        // should probably be a cleaner way to do it
+
+        ob_start();
+        $api = new ApiTimelineUserAction();
+        $api->prepare(array('id' => $notice->profile_id,
+                            'format' => 'atom',
+                            'max_id' => $notice->id,
+                            'since_id' => $notice->id - 1));
+        $api->showTimeline();
+        $feed = ob_get_clean();
+        
+        // ...and override the content-type back to something normal... eww!
+        // hope there's no other headers that got set while we weren't looking.
+        header('Content-Type: text/html; charset=utf-8');
+
+        common_log(LOG_DEBUG, $feed);
+        return $feed;
+    }
+
+    function groupFeedForNotice($group_id, $notice)
+    {
+        // @fixme this feels VERY hacky...
+        // should probably be a cleaner way to do it
+
+        ob_start();
+        $api = new ApiTimelineGroupAction();
+        $args = array('id' => $group_id,
+                      'format' => 'atom',
+                      'max_id' => $notice->id,
+                      'since_id' => $notice->id - 1);
+        $api->prepare($args);
+        $api->handle($args);
+        $feed = ob_get_clean();
+        
+        // ...and override the content-type back to something normal... eww!
+        // hope there's no other headers that got set while we weren't looking.
+        header('Content-Type: text/html; charset=utf-8');
+
+        common_log(LOG_DEBUG, $feed);
+        return $feed;
+    }
+
+}
+
diff --git a/plugins/OStatus/lib/huboutqueuehandler.php b/plugins/OStatus/lib/huboutqueuehandler.php
new file mode 100644 (file)
index 0000000..0791c7e
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Send a raw PuSH atom update from our internal hub.
+ * @package Hub
+ * @author Brion Vibber <brion@status.net>
+ */
+class HubOutQueueHandler extends QueueHandler
+{
+    function transport()
+    {
+        return 'hubout';
+    }
+
+    function handle($data)
+    {
+        $sub = $data['sub'];
+        $atom = $data['atom'];
+
+        assert($sub instanceof HubSub);
+        assert(is_string($atom));
+
+        try {
+            $sub->push($atom);
+        } catch (Exception $e) {
+            common_log(LOG_ERR, "Failed PuSH to $sub->callback for $sub->topic: " .
+                                $e->getMessage());
+            // @fixme Reschedule a later delivery?
+            return true;
+        }
+
+        return true;
+    }
+}
+
diff --git a/plugins/OStatus/lib/hubverifyqueuehandler.php b/plugins/OStatus/lib/hubverifyqueuehandler.php
new file mode 100644 (file)
index 0000000..125d13a
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Send a PuSH subscription verification from our internal hub.
+ * @package Hub
+ * @author Brion Vibber <brion@status.net>
+ */
+class HubVerifyQueueHandler extends QueueHandler
+{
+    function transport()
+    {
+        return 'hubverify';
+    }
+
+    function handle($data)
+    {
+        $sub = $data['sub'];
+        $mode = $data['mode'];
+
+        assert($sub instanceof HubSub);
+        assert($mode === 'subscribe' || $mode === 'unsubscribe');
+
+        common_log(LOG_INFO, __METHOD__ . ": $mode $sub->callback $sub->topic");
+        try {
+            $sub->verify($mode);
+        } catch (Exception $e) {
+            common_log(LOG_ERR, "Failed PuSH $mode verify to $sub->callback for $sub->topic: " .
+                                $e->getMessage());
+            // @fixme schedule retry?
+            // @fixme just kill it?
+        }
+
+        return true;
+    }
+}
+
diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php
new file mode 100644 (file)
index 0000000..8c77222
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+class Salmon
+{
+    public function post($endpoint_uri, $xml)
+    {
+        if (empty($endpoint_uri)) {
+            return FALSE;
+        }
+
+        $headers = array('Content-type: application/atom+xml');
+
+        try {
+            $client = new HTTPClient();
+            $client->setBody($xml);
+            $response = $client->post($endpoint_uri, $headers);
+        } catch (HTTP_Request2_Exception $e) {
+            return false;
+        }
+        if ($response->getStatus() != 200) {
+            return false;
+        }
+
+    }
+
+    public function createMagicEnv($text, $userid)
+    {
+
+
+    }
+
+
+    public function verifyMagicEnv($env)
+    {
+
+
+    }
+}
diff --git a/plugins/OStatus/lib/webfinger.php b/plugins/OStatus/lib/webfinger.php
new file mode 100644 (file)
index 0000000..417d549
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+define('WEBFINGER_SERVICE_REL_VALUE', 'lrdd');
+
+/**
+ * Implement the webfinger protocol.
+ */
+class Webfinger
+{
+    /**
+     * Perform a webfinger lookup given an account.
+     */ 
+    public function lookup($id)
+    {
+        $id = $this->normalize($id);
+        list($name, $domain) = explode('@', $id);
+
+        $links = $this->getServiceLinks($domain);
+        if (!$links) {
+            return false;
+        }
+        
+        $services = array();
+        foreach ($links as $link) {
+            if ($link['template']) {
+                return $this->getServiceDescription($link['template'], $id);
+            }
+            if ($link['href']) {
+                return $this->getServiceDescription($link['href'], $id);
+            }
+        }
+    }
+
+    /**
+     * Normalize an account ID
+     */
+    function normalize($id)
+    {
+        if (substr($id, 0, 7) == 'acct://') {
+            return substr($id, 7); 
+        } else if (substr($id, 0, 5) == 'acct:') {
+            return substr($id, 5);
+        }
+
+        return $id;
+    }
+
+    function getServiceLinks($domain)
+    {
+        $url = 'http://'. $domain .'/.well-known/host-meta';
+        $content = $this->fetchURL($url);
+        if (empty($content)) {
+            common_log(LOG_DEBUG, 'Error fetching host-meta');
+            return false;
+        }
+        $result = XRD::parse($content);
+
+        // Ensure that the host == domain (spec may include signing later)
+        if ($result->host != $domain) {
+            return false;
+        }
+        
+        $links = array();
+        foreach ($result->links as $link) {
+            if ($link['rel'] == WEBFINGER_SERVICE_REL_VALUE) {
+                $links[] = $link;
+            }
+
+        }
+        return $links;
+    }
+
+    function getServiceDescription($template, $id)
+    {
+        $url = $this->applyTemplate($template, 'acct:' . $id);
+
+        $content = $this->fetchURL($url);
+
+        return XRD::parse($content);
+    }
+
+    function fetchURL($url)
+    {
+        try {
+            $client = new HTTPClient();
+            $response = $client->get($url);
+        } catch (HTTP_Request2_Exception $e) {
+            return false;
+        }
+
+        if ($response->getStatus() != 200) {
+            return false;
+        }
+
+        return $response->getBody();
+    }
+
+    function applyTemplate($template, $id)
+    {
+        $template = str_replace('{uri}', urlencode($id), $template);
+
+        return $template;
+    }
+
+    function getHostMeta($domain, $template) {
+        $xrd = new XRD();
+        $xrd->host = $domain;
+        $xrd->links[] = array('rel' => 'lrdd',
+                              'template' => $template,
+                              'title' => array('Resource Descriptor'));
+
+        return $xrd->toXML();
+    }
+}
+
+
diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php
new file mode 100644 (file)
index 0000000..16d27f8
--- /dev/null
@@ -0,0 +1,183 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+
+class XRD
+{
+    const XML_NS = 'http://www.w3.org/2000/xmlns/';
+    
+    const XRD_NS = 'http://docs.oasis-open.org/ns/xri/xrd-1.0';
+
+    const HOST_META_NS = 'http://host-meta.net/xrd/1.0';
+    
+    public $expires;
+
+    public $subject;
+
+    public $host;
+
+    public $alias = array();
+    
+    public $types = array();
+    
+    public $links = array();
+    
+    public static function parse($xml)
+    {
+        $xrd = new XRD();
+
+        $dom = new DOMDocument();
+        $dom->loadXML($xml);
+        $xrd_element = $dom->getElementsByTagName('XRD')->item(0);
+
+        // Check for host-meta host
+        $host = $xrd_element->getElementsByTagName('Host')->item(0)->nodeValue;
+        if ($host) {
+            $xrd->host = $host;
+        }
+
+        // Loop through other elements
+        foreach ($xrd_element->childNodes as $node) {
+            switch ($node->tagName) {
+            case 'Expires':
+                $xrd->expires = $node->nodeValue;
+                break;
+            case 'Subject':
+                $xrd->subject = $node->nodeValue;
+                break;
+                
+            case 'Alias':
+                $xrd->alias[] = $node->nodeValue;
+                break;
+
+            case 'Link':
+                $xrd->links[] = $xrd->parseLink($node);
+                break;
+
+            case 'Type':
+                $xrd->types[] = $xrd->parseType($node);
+                break;
+
+            }
+        }
+        return $xrd;
+    }
+
+    public function toXML()
+    {
+        $dom = new DOMDocument('1.0', 'UTF-8');
+        $dom->formatOutput = true;
+        
+        $xrd_dom = $dom->createElementNS(XRD::XRD_NS, 'XRD');
+        $dom->appendChild($xrd_dom);
+
+        if ($this->host) {
+            $host_dom = $dom->createElement('hm:Host', $this->host);
+            $xrd_dom->setAttributeNS(XRD::XML_NS, 'xmlns:hm', XRD::HOST_META_NS);
+            $xrd_dom->appendChild($host_dom);
+        }
+        
+               if ($this->expires) {
+                       $expires_dom = $dom->createElement('Expires', $this->expires);
+                       $xrd_dom->appendChild($expires_dom);
+               }
+
+               if ($this->subject) {
+                       $subject_dom = $dom->createElement('Subject', $this->subject);
+                       $xrd_dom->appendChild($subject_dom);
+               }
+
+               foreach ($this->alias as $alias) {
+                       $alias_dom = $dom->createElement('Alias', $alias);
+                       $xrd_dom->appendChild($alias_dom);
+               }
+
+               foreach ($this->types as $type) {
+                       $type_dom = $dom->createElement('Type', $type);
+                       $xrd_dom->appendChild($type_dom);
+               }
+
+               foreach ($this->links as $link) {
+                       $link_dom = $this->saveLink($dom, $link);
+                       $xrd_dom->appendChild($link_dom);
+               }
+
+        return $dom->saveXML();
+    }
+
+    function parseType($element)
+    {
+        return array();
+    }
+    
+    function parseLink($element)
+    {
+        $link = array();
+        $link['rel'] = $element->getAttribute('rel');
+        $link['type'] = $element->getAttribute('type');
+        $link['href'] = $element->getAttribute('href');
+        $link['template'] = $element->getAttribute('template');
+        foreach ($element->childNodes as $node) {
+            switch($node->tagName) {
+            case 'Title':
+                $link['title'][] = $node->nodeValue;
+            }
+        }
+
+        return $link;
+    }
+
+    function saveLink($doc, $link)
+    {
+        $link_element = $doc->createElement('Link');
+        if ($link['rel']) {
+            $link_element->setAttribute('rel', $link['rel']);
+        }
+        if ($link['type']) {
+            $link_element->setAttribute('type', $link['type']);
+        }
+        if ($link['href']) {
+            $link_element->setAttribute('href', $link['href']);
+        }
+        if ($link['template']) {
+            $link_element->setAttribute('template', $link['template']);
+        }
+
+        if (is_array($link['title'])) {
+            foreach($link['title'] as $title) {
+                $title = $doc->createElement('Title', $title);
+                $link_element->appendChild($title);
+            }
+        }
+
+        
+        return $link_element;
+    }
+}
+
diff --git a/plugins/OStatus/locale/OStatus.po b/plugins/OStatus/locale/OStatus.po
new file mode 100644 (file)
index 0000000..dedc018
--- /dev/null
@@ -0,0 +1,104 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2009-12-07 20:38-0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: tests/gettext-speedtest.php:57 FeedSubPlugin.php:76
+msgid "Feeds"
+msgstr ""
+
+#: FeedSubPlugin.php:77
+msgid "Feed subscription options"
+msgstr ""
+
+#: feedmunger.php:215
+#, php-format
+msgid "New post: \"%1$s\" %2$s"
+msgstr ""
+
+#: actions/feedsubsettings.php:41
+msgid "Feed subscriptions"
+msgstr ""
+
+#: actions/feedsubsettings.php:52
+msgid ""
+"You can subscribe to feeds from other sites; updates will appear in your "
+"personal timeline."
+msgstr ""
+
+#: actions/feedsubsettings.php:96
+msgid "Subscribe"
+msgstr ""
+
+#: actions/feedsubsettings.php:98
+msgid "Continue"
+msgstr ""
+
+#: actions/feedsubsettings.php:151
+msgid "Empty feed URL!"
+msgstr ""
+
+#: actions/feedsubsettings.php:161
+msgid "Invalid URL or could not reach server."
+msgstr ""
+
+#: actions/feedsubsettings.php:164
+msgid "Cannot read feed; server returned error."
+msgstr ""
+
+#: actions/feedsubsettings.php:167
+msgid "Cannot read feed; server returned an empty page."
+msgstr ""
+
+#: actions/feedsubsettings.php:170
+msgid "Bad HTML, could not find feed link."
+msgstr ""
+
+#: actions/feedsubsettings.php:173
+msgid "Could not find a feed linked from this URL."
+msgstr ""
+
+#: actions/feedsubsettings.php:176
+msgid "Not a recognized feed type."
+msgstr ""
+
+#: actions/feedsubsettings.php:180
+msgid "Bad feed URL."
+msgstr ""
+
+#: actions/feedsubsettings.php:188
+msgid "Feed is not PuSH-enabled; cannot subscribe."
+msgstr ""
+
+#: actions/feedsubsettings.php:208
+msgid "Feed subscription failed! Bad response from hub."
+msgstr ""
+
+#: actions/feedsubsettings.php:218
+msgid "Already subscribed!"
+msgstr ""
+
+#: actions/feedsubsettings.php:220
+msgid "Feed subscribed!"
+msgstr ""
+
+#: actions/feedsubsettings.php:222
+msgid "Feed subscription failed!"
+msgstr ""
+
+#: actions/feedsubsettings.php:231
+msgid "Previewing feed:"
+msgstr ""
diff --git a/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po b/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po
new file mode 100644 (file)
index 0000000..f17dfa5
--- /dev/null
@@ -0,0 +1,106 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2009-12-07 14:14-0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: FeedSubPlugin.php:77
+msgid "Feeds"
+msgstr "Flux"
+
+#: FeedSubPlugin.php:78
+msgid "Feed subscription options"
+msgstr "Préférences pour abonnement flux"
+
+#: feedmunger.php:215
+#, php-format
+msgid "New post: \"%1$s\" %2$s"
+msgstr "Nouveau: \"%1$s\" %2$s"
+
+#: actions/feedsubsettings.php:41
+msgid "Feed subscriptions"
+msgstr "Abonnements aux fluxes"
+
+#: actions/feedsubsettings.php:52
+msgid ""
+"You can subscribe to feeds from other sites; updates will appear in your "
+"personal timeline."
+msgstr ""
+"Abonner aux fluxes RSS ou Atom des autres sites web; les temps se trouverair"
+"en votre flux personnel."
+
+#: actions/feedsubsettings.php:96
+msgid "Subscribe"
+msgstr "Abonner"
+
+#: actions/feedsubsettings.php:98
+msgid "Continue"
+msgstr "Prochaine"
+
+#: actions/feedsubsettings.php:151
+msgid "Empty feed URL!"
+msgstr ""
+
+#: actions/feedsubsettings.php:161
+msgid "Invalid URL or could not reach server."
+msgstr ""
+
+#: actions/feedsubsettings.php:164
+msgid "Cannot read feed; server returned error."
+msgstr ""
+
+#: actions/feedsubsettings.php:167
+msgid "Cannot read feed; server returned an empty page."
+msgstr ""
+
+#: actions/feedsubsettings.php:170
+msgid "Bad HTML, could not find feed link."
+msgstr ""
+
+#: actions/feedsubsettings.php:173
+msgid "Could not find a feed linked from this URL."
+msgstr ""
+
+#: actions/feedsubsettings.php:176
+msgid "Not a recognized feed type."
+msgstr ""
+
+#: actions/feedsubsettings.php:180
+msgid "Bad feed URL."
+msgstr ""
+
+#: actions/feedsubsettings.php:188
+msgid "Feed is not PuSH-enabled; cannot subscribe."
+msgstr ""
+
+#: actions/feedsubsettings.php:208
+msgid "Feed subscription failed! Bad response from hub."
+msgstr ""
+
+#: actions/feedsubsettings.php:218
+msgid "Already subscribed!"
+msgstr ""
+
+#: actions/feedsubsettings.php:220
+msgid "Feed subscribed!"
+msgstr ""
+
+#: actions/feedsubsettings.php:222
+msgid "Feed subscription failed!"
+msgstr ""
+
+#: actions/feedsubsettings.php:231
+msgid "Previewing feed:"
+msgstr ""
diff --git a/plugins/OStatus/tests/ActivityParseTests.php b/plugins/OStatus/tests/ActivityParseTests.php
new file mode 100644 (file)
index 0000000..fa8bcdd
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+    print "This script must be run from the command line\n";
+    exit();
+}
+
+// XXX: we should probably have some common source for this stuff
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+define('STATUSNET', true);
+
+require_once INSTALLDIR . '/lib/common.php';
+require_once INSTALLDIR . '/plugins/OStatus/lib/activity.php';
+
+class ActivityParseTests extends PHPUnit_Framework_TestCase
+{
+    public function testExample1()
+    {
+        global $_example1;
+        $dom = DOMDocument::loadXML($_example1);
+        $act = new Activity($dom->documentElement);
+
+        $this->assertFalse(empty($act));
+        $this->assertEquals($act->time, 1243860840);
+        $this->assertEquals($act->verb, ActivityVerb::POST);
+    }
+
+    public function testExample3()
+    {
+        global $_example3;
+        $dom = DOMDocument::loadXML($_example3);
+
+        $feed = $dom->documentElement;
+
+        $entries = $feed->getElementsByTagName('entry');
+
+        $entry = $entries->item(0);
+
+        $act = new Activity($entry, $feed);
+
+        $this->assertFalse(empty($act));
+        $this->assertEquals($act->time, 1071340202);
+        $this->assertEquals($act->link, 'http://example.org/2003/12/13/atom03.html');
+
+        $this->assertEquals($act->verb, ActivityVerb::POST);
+
+        $this->assertFalse(empty($act->actor));
+        $this->assertEquals($act->actor->type, ActivityObject::PERSON);
+        $this->assertEquals($act->actor->title, 'John Doe');
+        $this->assertEquals($act->actor->id, 'mailto:johndoe@example.com');
+
+        $this->assertFalse(empty($act->object));
+        $this->assertEquals($act->object->type, ActivityObject::NOTE);
+        $this->assertEquals($act->object->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a');
+        $this->assertEquals($act->object->title, 'Atom-Powered Robots Run Amok');
+        $this->assertEquals($act->object->summary, 'Some text.');
+        $this->assertEquals($act->object->link, 'http://example.org/2003/12/13/atom03.html');
+
+        $this->assertTrue(empty($act->context));
+        $this->assertTrue(empty($act->target));
+
+        $this->assertEquals($act->entry, $entry);
+        $this->assertEquals($act->feed, $feed);
+    }
+}
+
+$_example1 = <<<EXAMPLE1
+<?xml version='1.0' encoding='UTF-8'?>
+<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
+  <id>tag:versioncentral.example.org,2009:/commit/1643245</id>
+  <published>2009-06-01T12:54:00Z</published>
+  <title>Geraldine committed a change to yate</title>
+  <content type="xhtml">Geraldine just committed a change to yate on VersionCentral</content>
+  <link rel="alternate" type="text/html"
+        href="http://versioncentral.example.org/geraldine/yate/commit/1643245" />
+  <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+  <activity:verb>http://versioncentral.example.org/activity/commit</activity:verb>
+  <activity:object>
+    <activity:object-type>http://versioncentral.example.org/activity/changeset</activity:object-type>
+    <id>tag:versioncentral.example.org,2009:/change/1643245</id>
+    <title>Punctuation Changeset</title>
+    <summary>Fixing punctuation because it makes it more readable.</summary>
+    <link rel="alternate" type="text/html" href="..." />
+  </activity:object>
+</entry>
+EXAMPLE1;
+
+$_example2 = <<<EXAMPLE2
+<?xml version='1.0' encoding='UTF-8'?>
+<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
+  <id>tag:photopanic.example.com,2008:activity01</id>
+  <title>Geraldine posted a Photo on PhotoPanic</title>
+  <published>2008-11-02T15:29:00Z</published>
+  <link rel="alternate" type="text/html" href="/geraldine/activities/1" />
+  <activity:verb>
+  http://activitystrea.ms/schema/1.0/post
+  </activity:verb>
+  <activity:object>
+    <id>tag:photopanic.example.com,2008:photo01</id>
+    <title>My Cat</title>
+    <published>2008-11-02T15:29:00Z</published>
+    <link rel="alternate" type="text/html" href="/geraldine/photos/1" />
+    <activity:object-type>
+      tag:atomactivity.example.com,2008:photo
+    </activity:object-type>
+    <source>
+      <title>Geraldine's Photos</title>
+      <link rel="self" type="application/atom+xml" href="/geraldine/photofeed.xml" />
+      <link rel="alternate" type="text/html" href="/geraldine/" />
+    </source>
+  </activity:object>
+  <content type="html">
+     &lt;p&gt;Geraldine posted a Photo on PhotoPanic&lt;/p&gt;
+     &lt;img src="/geraldine/photo1.jpg"&gt;
+  </content>
+</entry>
+EXAMPLE2;
+
+$_example3 = <<<EXAMPLE3
+<?xml version="1.0" encoding="utf-8"?>
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+       <title>Example Feed</title>
+       <subtitle>A subtitle.</subtitle>
+       <link href="http://example.org/feed/" rel="self" />
+       <link href="http://example.org/" />
+       <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
+       <updated>2003-12-13T18:30:02Z</updated>
+       <author>
+               <name>John Doe</name>
+               <email>johndoe@example.com</email>
+       </author>
+
+       <entry>
+               <title>Atom-Powered Robots Run Amok</title>
+               <link href="http://example.org/2003/12/13/atom03" />
+               <link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/>
+               <link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/>
+               <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+               <updated>2003-12-13T18:30:02Z</updated>
+               <summary>Some text.</summary>
+       </entry>
+
+</feed>
+EXAMPLE3;
diff --git a/plugins/OStatus/tests/FeedDiscoveryTest.php b/plugins/OStatus/tests/FeedDiscoveryTest.php
new file mode 100644 (file)
index 0000000..1c52497
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+    print "This script must be run from the command line\n";
+    exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+define('STATUSNET', true);
+define('LACONICA', true);
+
+require_once INSTALLDIR . '/lib/common.php';
+require_once INSTALLDIR . '/plugins/FeedSub/feedsub.php';
+
+class FeedDiscoveryTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @dataProvider provider
+     *
+     */
+    public function testProduction($url, $html, $expected)
+    {
+        $sub = new FeedDiscovery();
+        $url = $sub->discoverFromHTML($url, $html);
+        $this->assertEquals($expected, $url);
+    }
+
+    static public function provider()
+    {
+        $sampleHeader = <<<END
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head profile="http://gmpg.org/xfn/11">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
+<title>leŭksman  </title>
+
+<meta name="generator" content="WordPress 2.8.6" /> <!-- leave this for stats -->
+
+<link rel="stylesheet" href="http://leuksman.com/log/wp-content/themes/leuksman/style.css" type="text/css" media="screen" />
+<link rel="alternate" type="application/rss+xml" title="leŭksman RSS Feed" href="http://leuksman.com/log/feed/" />
+<link rel="pingback" href="http://leuksman.com/log/xmlrpc.php" />
+
+<meta name="viewport" content="width = 640" />
+
+<xmeta name="viewport" content="initial-scale=2.3, user-scalable=no" />
+
+<style type="text/css" media="screen">
+
+       #page { background: url("http://leuksman.com/log/wp-content/themes/leuksman/images/kubrickbg.jpg") repeat-y top; border: none; }
+
+</style>
+
+<link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://leuksman.com/log/xmlrpc.php?rsd" />
+<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="http://leuksman.com/log/wp-includes/wlwmanifest.xml" /> 
+<link rel='index' title='leŭksman' href='http://leuksman.com/log' />
+<meta name="generator" content="WordPress 2.8.6" />
+</head>
+<body>
+</body>
+</html>
+END;
+        return array(
+                     array('http://example.com/',
+                           '<html><link rel="alternate" href="http://example.com/feed/rss" type="application/rss+xml">',
+                           'http://example.com/feed/rss'),
+                     array('http://example.com/atom',
+                           '<html><link rel="alternate" href="http://example.com/feed/atom" type="application/atom+xml">',
+                           'http://example.com/feed/atom'),
+                     array('http://example.com/empty',
+                           '<html><link rel="alternate" href="http://example.com/index.pdf" type="application/pdf">',
+                           false),
+                     array('http://example.com/tagsoup',
+                           '<body><pre><LINK rel=alternate hRef=http://example.com/feed/rss type=application/rss+xml><fnork',
+                           'http://example.com/feed/rss'),
+                     // 'rel' attribute must be lowercase, alone per http://www.rssboard.org/rss-autodiscovery
+                     array('http://example.com/tagsoup2',
+                           '<body><pre><LINK rel=" feeders    alternate 467" hRef=http://example.com/feed/rss type=application/rss+xml><fnork',
+                           false),
+                     array('http://example.com/tagsoup3',
+                           '<body><pre><LINK rel=ALTERNATE hRef=http://example.com/feed/rss type=application/rss+xml><fnork',
+                           false),
+                     array('http://example.com/relative/link1',
+                           '<html><link rel="alternate" href="/feed/rss" type="application/rss+xml">',
+                           'http://example.com/feed/rss'),
+                     array('http://example.com/relative/link2',
+                           '<html><link rel="alternate" href="../feed/rss" type="application/rss+xml">',
+                           'http://example.com/feed/rss'),
+                     array('http://example.com/relative/link3',
+                           '<html><link rel="alternate" href="http:/feed/rss" type="application/rss+xml">',
+                           'http://example.com/feed/rss'),
+                     array('http://example.com/base/link1',
+                           '<html><link rel="alternate" href="/feed/rss" type="application/rss+xml"><base href="http://target.example.com/">',
+                           'http://target.example.com/feed/rss'),
+                     array('http://example.com/base/link2',
+                           '<html><link rel="alternate" href="feed/rss" type="application/rss+xml"><base href="http://target.example.com/">',
+                           'http://target.example.com/feed/rss'),
+                     array('http://example.com/base/link3',
+                           '<html><link rel="alternate" href="http:/feed/rss" type="application/rss+xml"><base href="http://target.example.com/">',
+                           'http://target.example.com/feed/rss'),
+                     // Trick question! There's a <base> but no href on it
+                     array('http://example.com/relative/fauxbase',
+                           '<html><link rel="alternate" href="../feed/rss" type="application/rss+xml"><base target="top">',
+                           'http://example.com/feed/rss'),
+                     // Actual WordPress blog header example
+                     array('http://leuksman.com/log/',
+                           $sampleHeader,
+                           'http://leuksman.com/log/feed/'));
+    }
+}
diff --git a/plugins/OStatus/tests/FeedMungerTest.php b/plugins/OStatus/tests/FeedMungerTest.php
new file mode 100644 (file)
index 0000000..0ce24c9
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+    print "This script must be run from the command line\n";
+    exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+define('STATUSNET', true);
+define('LACONICA', true);
+
+require_once INSTALLDIR . '/lib/common.php';
+require_once INSTALLDIR . '/plugins/FeedSub/feedsub.php';
+
+require_once 'XML/Feed/Parser.php';
+
+class FeedMungerTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @dataProvider profileProvider
+     *
+     */
+    public function testProfiles($xml, $expected)
+    {
+        $feed = new XML_Feed_Parser($xml, false, false, true);
+        
+        $munger = new FeedMunger($feed);
+        $profile = $munger->profile();
+
+        foreach ($expected as $field => $val) {
+            $this->assertEquals($expected[$field], $profile->$field, "profile->$field");
+        }
+    }
+
+    static public function profileProvider()
+    {
+        return array(
+                     array(self::samplefeed(),
+                           array('nickname' => 'leŭksman', // @todo does this need to be asciified?
+                                 'fullname' => 'leŭksman',
+                                 'bio' => 'reticula, electronica, & oddities',
+                                 'homepage' => 'http://leuksman.com/log')));
+    }
+
+    /**
+     * @dataProvider noticeProvider
+     *
+     */
+    public function testNotices($xml, $entryIndex, $expected)
+    {
+        $feed = new XML_Feed_Parser($xml, false, false, true);
+        $entry = $feed->getEntryByOffset($entryIndex);
+
+        $munger = new FeedMunger($feed);
+        $notice = $munger->noticeFromEntry($entry);
+
+        $this->assertTrue(mb_strlen($notice) <= Notice::maxContent());
+        $this->assertEquals($expected, $notice);
+    }
+
+    static public function noticeProvider()
+    {
+        return array(
+                     array('<rss version="2.0"><channel><item><title>A fairly short title</title><link>http://example.com/short/link</link></item></channel></rss>', 0,
+                           'New post: "A fairly short title" http://example.com/short/link'),
+                     // Requires URL shortening ...
+                     array('<rss version="2.0"><channel><item><title>A fairly short title</title><link>http://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh</link></item></channel></rss>', 0,
+                           'New post: "A fairly short title" http://ur1.ca/g2o1'),
+                     array('<rss version="2.0"><channel><item><title>A fairly long title in this case, which will have to get cut down at some point alongside its very long link. Really who even makes titles this long? It\'s just ridiculous imo...</title><link>http://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh</link></item></channel></rss>', 0,
+                           'New post: "A fairly long title in this case, which will have to get cut down at some point alongside its very long li…" http://ur1.ca/g2o1'),
+                     // Some real sample feeds
+                     array(self::samplefeed(), 0,
+                           'New post: "Compiling PHP on Snow Leopard" http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/'),
+                     array(self::samplefeedBlogspot(), 0,
+                           'New post: "I love posting" http://briontest.blogspot.com/2009/11/i-love-posting.html'),
+                     array(self::samplefeedBlogspot(), 1,
+                           'New post: "Hey dude" http://briontest.blogspot.com/2009/11/hey-dude.html'),
+        );
+    }
+
+    static protected function samplefeed()
+    {
+        $xml = '<' . '?xml version="1.0" encoding="UTF-8"?' . ">\n";
+        $samplefeed = $xml . <<<END
+<rss version="2.0"
+       xmlns:content="http://purl.org/rss/1.0/modules/content/"
+       xmlns:wfw="http://wellformedweb.org/CommentAPI/"
+       xmlns:dc="http://purl.org/dc/elements/1.1/"
+       xmlns:atom="http://www.w3.org/2005/Atom"
+       xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
+       xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
+       >
+
+<channel>
+       <title>leŭksman</title>
+       <atom:link href="http://leuksman.com/log/feed/" rel="self" type="application/rss+xml" />
+       <link>http://leuksman.com/log</link>
+       <description>reticula, electronica, &#38; oddities</description>
+
+       <lastBuildDate>Thu, 12 Nov 2009 17:44:42 +0000</lastBuildDate>
+       <generator>http://wordpress.org/?v=2.8.6</generator>
+       <language>en</language>
+       <sy:updatePeriod>hourly</sy:updatePeriod>
+       <sy:updateFrequency>1</sy:updateFrequency>
+                       <item>
+
+               <title>Compiling PHP on Snow Leopard</title>
+               <link>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/</link>
+               <comments>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/#comments</comments>
+               <pubDate>Thu, 12 Nov 2009 17:44:42 +0000</pubDate>
+               <dc:creator>brion</dc:creator>
+                               <category><![CDATA[apple]]></category>
+
+               <category><![CDATA[devel]]></category>
+
+               <guid isPermaLink="false">http://leuksman.com/log/?p=649</guid>
+               <description><![CDATA[If you&#8217;ve been having trouble compiling your own PHP installations on Mac OS X 10.6, here&#8217;s the secret to making it not suck! After running the configure script, edit the generated Makefile and make these fixes:
+
+Find the EXTRA_LIBS definition and add -lresolv to the end
+Find the EXE_EXT definition and remove .dSYM
+
+Standard make and make install [...]]]></description>
+                       <content:encoded><![CDATA[<p>If you&#8217;ve been having trouble compiling your own PHP installations on Mac OS X 10.6, here&#8217;s the secret to making it not suck! After running the configure script, edit the generated Makefile and make these fixes:</p>
+<ul>
+<li>Find the <strong>EXTRA_LIBS</strong> definition and add <strong>-lresolv</strong> to the end</li>
+<li>Find the <strong>EXE_EXT</strong> definition and remove <strong>.dSYM</strong></li>
+</ul>
+<p>Standard make and make install should work from here&#8230;</p>
+<p>For reference, here&#8217;s the whole configure line I currently use; MySQL is installed from the downloadable installer; other deps from MacPorts:</p>
+<p>&#8216;./configure&#8217; &#8216;&#8211;prefix=/opt/php52&#8242; &#8216;&#8211;with-mysql=/usr/local/mysql&#8217; &#8216;&#8211;with-zlib&#8217; &#8216;&#8211;with-bz2&#8242; &#8216;&#8211;enable-mbstring&#8217; &#8216;&#8211;enable-exif&#8217; &#8216;&#8211;enable-fastcgi&#8217; &#8216;&#8211;with-xmlrpc&#8217; &#8216;&#8211;with-xsl&#8217; &#8216;&#8211;with-readline=/opt/local&#8217; &#8211;without-iconv &#8211;with-gd &#8211;with-png-dir=/opt/local &#8211;with-jpeg-dir=/opt/local &#8211;with-curl &#8211;with-gettext=/opt/local &#8211;with-mysqli=/usr/local/mysql/bin/mysql_config &#8211;with-tidy=/opt/local &#8211;enable-pcntl &#8211;with-openssl</p>
+]]></content:encoded>
+                       <wfw:commentRss>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/feed/</wfw:commentRss>
+               <slash:comments>0</slash:comments>
+               </item>
+</channel>
+</rss>
+END;
+        return $samplefeed;
+    }
+    
+    static protected function samplefeedBlogspot()
+    {
+        return <<<END
+<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss'><id>tag:blogger.com,1999:blog-7780083508531697167</id><updated>2009-11-19T12:56:11.233-08:00</updated><title type='text'>Brion's Cool Test Blog</title><subtitle type='html'></subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>2</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-7780083508531697167.post-8456671879000290677</id><published>2009-11-19T12:55:00.000-08:00</published><updated>2009-11-19T12:56:11.241-08:00</updated><title type='text'>I love posting</title><content type='html'>It's pretty awesome, if you like that sort of thing.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7780083508531697167-8456671879000290677?l=briontest.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/8456671879000290677/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://briontest.blogspot.com/2009/11/i-love-posting.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8456671879000290677'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8456671879000290677'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/2009/11/i-love-posting.html' title='I love posting'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='05912464053145602436'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7780083508531697167.post-8202296917897346633</id><published>2009-11-18T13:52:00.001-08:00</published><updated>2009-11-18T13:52:48.444-08:00</updated><title type='text'>Hey dude</title><content type='html'>testingggggggggg&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7780083508531697167-8202296917897346633?l=briontest.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/8202296917897346633/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://briontest.blogspot.com/2009/11/hey-dude.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8202296917897346633'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8202296917897346633'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/2009/11/hey-dude.html' title='Hey dude'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='05912464053145602436'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry></feed>
+END;
+    }
+}
diff --git a/plugins/OStatus/tests/gettext-speedtest.php b/plugins/OStatus/tests/gettext-speedtest.php
new file mode 100644 (file)
index 0000000..8bbdf5e
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+    print "This script must be run from the command line\n";
+    exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+define('STATUSNET', true);
+define('LACONICA', true);
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/extlib/php-gettext/gettext.inc';
+
+common_init_locale("en_US");
+common_init_locale('fr');
+
+
+putenv("LANG=fr");
+putenv("LANGUAGE=fr");
+setlocale('fr.utf8');
+_setlocale('fr.utf8');
+
+_bindtextdomain("statusnet", INSTALLDIR . '/locale');
+_bindtextdomain("FeedSub", INSTALLDIR . '/plugins/FeedSub/locale');
+
+$times = 10000;
+$delta = array();
+
+$start = microtime(true);
+for($i = 0; $i < $times; $i++) {
+    $result = _("Send");
+}
+$delta["_"] = array((microtime(true) - $start) / $times, $result);
+
+$start = microtime(true);
+for($i = 0; $i < $times; $i++) {
+    $result = __("Send");
+}
+$delta["__"] = array((microtime(true) - $start) / $times, $result);
+
+$start = microtime(true);
+for($i = 0; $i < $times; $i++) {
+    $result = dgettext("FeedSub", "Feeds");
+}
+$delta["dgettext"] = array((microtime(true) - $start) / $times, $result);
+
+$start = microtime(true);
+for($i = 0; $i < $times; $i++) {
+    $result = _dgettext("FeedSub", "Feeds");
+}
+$delta["_dgettext"] = array((microtime(true) - $start) / $times, $result);
+
+
+$start = microtime(true);
+for($i = 0; $i < $times; $i++) {
+    $result = _m("Feeds");
+}
+$delta["_m"] = array((microtime(true) - $start) / $times, $result);
+
+
+$start = microtime(true);
+for($i = 0; $i < $times; $i++) {
+    $result = fake("Feeds");
+}
+$delta["fake"] = array((microtime(true) - $start) / $times, $result);
+
+foreach ($delta as $func => $bits) {
+    list($time, $result) = $bits;
+    $ms = $time * 1000.0;
+    printf("%10s %2.4fms %s\n", $func, $ms, $result);
+}
+
+
+function fake($str) {
+    return $str;
+}
+
diff --git a/plugins/OStatus/theme/base/css/ostatus.css b/plugins/OStatus/theme/base/css/ostatus.css
new file mode 100644 (file)
index 0000000..9bc90a7
--- /dev/null
@@ -0,0 +1,30 @@
+/** theme: base for OStatus
+ *
+ * @package   StatusNet
+ * @author    Sarven Capadisli <csarven@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+#form_ostatus_connect.dialogbox {
+width:70%;
+background-image:none;
+}
+#form_ostatus_connect.dialogbox .form_data label {
+width:34%;
+}
+#form_ostatus_connect.dialogbox .form_data input {
+width:57%;
+}
+#form_ostatus_connect.dialogbox .form_data .form_guide {
+margin-left:36%;
+}
+
+#form_ostatus_connect.dialogbox #ostatus_nickname {
+display:none;
+}
+
+#form_ostatus_connect.dialogbox .submit_dialogbox  {
+min-width:96px;
+}
index d25ce696cf95ad83e97f9d3173784b4a66cfc9f9..438a728d83fb65c0934887353713cb6e747f05d3 100644 (file)
@@ -438,49 +438,7 @@ class FinishopenidloginAction extends Action
 
     function urlToNickname($openid)
     {
-        static $bad = array('query', 'user', 'password', 'port', 'fragment');
-
-        $parts = parse_url($openid);
-
-        # If any of these parts exist, this won't work
-
-        foreach ($bad as $badpart) {
-            if (array_key_exists($badpart, $parts)) {
-                return null;
-            }
-        }
-
-        # We just have host and/or path
-
-        # If it's just a host...
-        if (array_key_exists('host', $parts) &&
-            (!array_key_exists('path', $parts) || strcmp($parts['path'], '/') == 0))
-        {
-            $hostparts = explode('.', $parts['host']);
-
-            # Try to catch common idiom of nickname.service.tld
-
-            if ((count($hostparts) > 2) &&
-                (strlen($hostparts[count($hostparts) - 2]) > 3) && # try to skip .co.uk, .com.au
-                (strcmp($hostparts[0], 'www') != 0))
-            {
-                return $this->nicknamize($hostparts[0]);
-            } else {
-                # Do the whole hostname
-                return $this->nicknamize($parts['host']);
-            }
-        } else {
-            if (array_key_exists('path', $parts)) {
-                # Strip starting, ending slashes
-                $path = preg_replace('@/$@', '', $parts['path']);
-                $path = preg_replace('@^/@', '', $path);
-                if (strpos($path, '/') === false) {
-                    return $this->nicknamize($path);
-                }
-            }
-        }
-
-        return null;
+        return common_url_to_nickname($openid);
     }
 
     function xriToNickname($xri)
@@ -510,7 +468,6 @@ class FinishopenidloginAction extends Action
 
     function nicknamize($str)
     {
-        $str = preg_replace('/\W/', '', $str);
-        return strtolower($str);
+        return common_nicknamize($str);
     }
 }
diff --git a/plugins/PostDebug/PostDebugPlugin.php b/plugins/PostDebug/PostDebugPlugin.php
new file mode 100644 (file)
index 0000000..48fe28e
--- /dev/null
@@ -0,0 +1,150 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Debugging helper plugin -- records detailed data on POSTs to log
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Sample
+ * @package   StatusNet
+ * @author    Brion Vibber <brionv@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+class PostDebugPlugin extends Plugin
+{
+    /**
+     * Set to a directory to dump individual items instead of
+     * sending to the debug log
+     */
+    public $dir=false;
+
+    public function onArgsInitialize(&$args)
+    {
+        if (isset($_SERVER['REQUEST_METHOD']) &&
+            $_SERVER['REQUEST_METHOD'] == 'POST') {
+            $this->doDebug();
+        }
+    }
+
+    public function onPluginVersion(&$versions)
+    {
+        $versions[] = array('name' => 'PostDebug',
+                            'version' => STATUSNET_VERSION,
+                            'author' => 'Brion Vibber',
+                            'homepage' => 'http://status.net/wiki/Plugin:PostDebug',
+                            'rawdescription' =>
+                            _m('Debugging tool to record request details on POST.'));
+        return true;
+    }
+
+    protected function doDebug()
+    {
+        $data = array('timestamp' => gmdate('r'),
+                      'remote_addr' => @$_SERVER['REMOTE_ADDR'],
+                      'url' => @$_SERVER['REQUEST_URI'],
+                      'have_session' => common_have_session(),
+                      'logged_in' => common_logged_in(),
+                      'is_real_login' => common_is_real_login(),
+                      'user' => common_logged_in() ? common_current_user()->nickname : null,
+                      'headers' => $this->getHttpHeaders(),
+                      'post_data' => $this->sanitizePostData($_POST));
+        $this->saveDebug($data);
+    }
+
+    protected function saveDebug($data)
+    {
+        $output = var_export($data, true);
+        if ($this->dir) {
+            $file = $this->dir . DIRECTORY_SEPARATOR . $this->logFileName();
+            file_put_contents($file, $output);
+        } else {
+            common_log(LOG_DEBUG, "PostDebug: $output");
+        }
+    }
+
+    protected function logFileName()
+    {
+        $base = common_request_id();
+        $base = preg_replace('/^(.+?) .*$/', '$1', $base);
+        $base = str_replace(':', '-', $base);
+        $base = rawurlencode($base);
+        return $base;
+    }
+
+    protected function getHttpHeaders()
+    {
+        if (function_exists('getallheaders')) {
+            $headers = getallheaders();
+        } else {
+            $headers = array();
+            $prefix = 'HTTP_';
+            $prefixLen = strlen($prefix);
+            foreach ($_SERVER as $key => $val) {
+                if (substr($key, 0, $prefixLen) == $prefix) {
+                    $header = $this->normalizeHeader(substr($key, $prefixLen));
+                    $headers[$header] = $val;
+                }
+            }
+        }
+        foreach ($headers as $header => $val) {
+            if (strtolower($header) == 'cookie') {
+                $headers[$header] = $this->sanitizeCookies($val);
+            }
+        }
+        return $headers;
+    }
+
+    protected function normalizeHeader($key)
+    {
+        return implode('-',
+                       array_map('ucfirst',
+                                 explode("_",
+                                         strtolower($key))));
+    }
+
+    function sanitizeCookies($val)
+    {
+        $blacklist = array(session_name(), 'rememberme');
+        foreach ($blacklist as $name) {
+            $val = preg_replace("/(^|;\s*)({$name}=)(.*?)(;|$)/",
+                                "$1$2########$4",
+                                $val);
+        }
+        return $val;
+    }
+
+    function sanitizePostData($data)
+    {
+        $blacklist = array('password', 'confirm', 'token');
+        foreach ($data as $key => $val) {
+            if (in_array($key, $blacklist)) {
+                $data[$key] = '########';
+            }
+        }
+        return $data;
+    }
+
+}
+
index 14d1608d3c8b25bf8311bf82dab0dd94d7e0a02d..fb4eff73894c0791aa37d6de96379a051ba58782 100644 (file)
@@ -45,6 +45,7 @@ class PoweredByStatusNetPlugin extends Plugin
 {
     function onEndAddressData($action)
     {
+        $action->text(' ');
         $action->elementStart('span', 'poweredby');
         $action->raw(sprintf(_m('powered by %s'),
                      sprintf('<a href="http://status.net/">%s</a>',
index 52151f9de8dad1dd42df0e95034b020ff98e9e32..2e5851ae531e539dc07f85a0d0eab80915a87fc8 100644 (file)
@@ -95,9 +95,7 @@ RealtimeUpdate = {
         $("#notices_primary .notice:first").css({display:"none"});
         $("#notices_primary .notice:first").fadeIn(1000);
 
-        SN.U.FormXHR($('#'+noticeItemID+' .form_favor'));
         SN.U.NoticeReplyTo($('#'+noticeItemID));
-        SN.U.FormXHR($('#'+noticeItemID+' .form_repeat'));
         SN.U.NoticeWithAttachment($('#'+noticeItemID));
      },
 
@@ -136,7 +134,7 @@ RealtimeUpdate = {
           ni = "<li class=\"hentry notice\" id=\"notice-"+unique+"\">"+
                "<div class=\"entry-title\">"+
                "<span class=\"vcard author\">"+
-               "<a href=\""+user['profile_url']+"\" class=\"url\">"+
+               "<a href=\""+user['profile_url']+"\" class=\"url\" title=\""+user['name']+"\">"+
                "<img src=\""+user['profile_image_url']+"\" class=\"avatar photo\" width=\"48\" height=\"48\" alt=\""+user['screen_name']+"\"/>"+
                "<span class=\"nickname fn\">"+user['screen_name']+"</span>"+
                "</a>"+
@@ -180,7 +178,7 @@ RealtimeUpdate = {
 
           ni = ni+"</div>";
 
-               "</li>";
+          ni = ni+"</li>";
           return ni;
      },
 
@@ -211,10 +209,10 @@ RealtimeUpdate = {
           var rf;
           rf = "<form id=\"repeat-"+id+"\" class=\"form_repeat\" method=\"post\" action=\""+RealtimeUpdate._repeaturl+"\">"+
                "<fieldset>"+
-               "<legend>Favor this notice</legend>"+
+               "<legend>Repeat this notice?</legend>"+
                "<input name=\"token-"+id+"\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+
-               "<input name=\"notice\" type=\"hidden\" id=\"notice-n"+id+"\" value=\""+id+"\"/>"+
-               "<input type=\"submit\" id=\"repeat-submit-"+id+"\" name=\"repeat-submit-"+id+"\" class=\"submit\" value=\"Favor\" title=\"Repeat this notice\"/>"+
+               "<input name=\"notice\" type=\"hidden\" id=\"notice-"+id+"\" value=\""+id+"\"/>"+
+               "<input type=\"submit\" id=\"repeat-submit-"+id+"\" name=\"repeat-submit-"+id+"\" class=\"submit\" value=\"Yes\" title=\"Repeat this notice\"/>"+
                "</fieldset>"+
                "</form>";
 
index 6822d33dd19ba542c5e11af600cf2b2f3c2033be..c154932bbc003bdac3cabd62604c9ef714800f5e 100644 (file)
@@ -56,6 +56,7 @@ class TwitterauthorizationAction extends Action
     var $tw_fields    = null;
     var $access_token = null;
     var $signin       = null;
+    var $verifier     = null;
 
     /**
      * Initialize class members. Looks for 'oauth_token' parameter.
@@ -70,6 +71,7 @@ class TwitterauthorizationAction extends Action
 
         $this->signin      = $this->boolean('signin');
         $this->oauth_token = $this->arg('oauth_token');
+        $this->verifier    = $this->arg('oauth_verifier');
 
         return true;
     }
@@ -160,8 +162,7 @@ class TwitterauthorizationAction extends Action
             // Get a new request token and authorize it
 
             $client  = new TwitterOAuthClient();
-            $req_tok =
-              $client->getRequestToken(TwitterOAuthClient::$requestTokenURL);
+            $req_tok = $client->getRequestToken();
 
             // Sock the request token away in the session temporarily
 
@@ -171,7 +172,7 @@ class TwitterauthorizationAction extends Action
             $auth_link = $client->getAuthorizeLink($req_tok, $this->signin);
 
         } catch (OAuthClientException $e) {
-            $msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s',
+            $msg = sprintf('OAuth client error - code: %1s, msg: %2s',
                            $e->getCode(), $e->getMessage());
             $this->serverError(_m('Couldn\'t link your Twitter account.'));
         }
@@ -187,7 +188,6 @@ class TwitterauthorizationAction extends Action
      */
     function saveAccessToken()
     {
-
         // Check to make sure Twitter returned the same request
         // token we sent them
 
@@ -204,7 +204,7 @@ class TwitterauthorizationAction extends Action
 
             // Exchange the request token for an access token
 
-            $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL);
+            $atok = $client->getAccessToken($this->verifier);
 
             // Test the access token and get the user's Twitter info
 
index 277e7ab409c69b76598e5a72a093274f263c3d49..ba45b533dc9b50fdf4a72075948dadeba14693cd 100644 (file)
@@ -91,6 +91,19 @@ class TwitterOAuthClient extends OAuthClient
         }
     }
 
+    /**
+     * Gets a request token from Twitter
+     *
+     * @return OAuthToken $token the request token
+     */
+    function getRequestToken()
+    {
+        return parent::getRequestToken(
+            self::$requestTokenURL,
+            common_local_url('twitterauthorization')
+        );
+    }
+
     /**
      * Builds a link to Twitter's endpoint for authorizing a request token
      *
@@ -107,6 +120,21 @@ class TwitterOAuthClient extends OAuthClient
                                         common_local_url('twitterauthorization'));
     }
 
+    /**
+     * Fetches an access token from Twitter
+     *
+     * @param string $verifier 1.0a verifier
+     *
+     * @return OAuthToken $token the access token
+     */
+    function getAccessToken($verifier = null)
+    {
+        return parent::getAccessToken(
+            self::$accessTokenURL,
+            $verifier
+        );
+    }
+
     /**
      * Calls Twitter's /account/verify_credentials API method
      *
index a33869c19ea2be076c4db748a100a1fb9e268b3f..ae3dfe0365155983e6518046afd1def6bb1ec51d 100644 (file)
@@ -182,21 +182,6 @@ class UserFlagPlugin extends Plugin
         return true;
     }
 
-    /**
-     * Add our plugin's CSS to page output
-     *
-     * @param Action $action action being shown
-     *
-     * @return boolean hook result
-     */
-
-    function onEndShowStatusNetStyles($action)
-    {
-        $action->cssLink(common_path('plugins/UserFlag/userflag.css'),
-                         null, 'screen, projection, tv');
-        return true;
-    }
-
     /**
      * Initialize any flagging buttons on the page
      *
@@ -208,8 +193,8 @@ class UserFlagPlugin extends Plugin
     function onEndShowScripts($action)
     {
         $action->inlineScript('if ($(".form_entity_flag").length > 0) { '.
-                              'SN.U.FormXHR($(".form_entity_flag")); '.
-                              '}');
+                              '$(".form_entity_flag").bind("click", function() {'.
+                              'SN.U.FormXHR($(this)); return false; }); }');
         return true;
     }
 
index 5ad6055d331a85ceaba61ccf5d83c116399f2ca1..eefd15c3680bb9c48426540b32846a9be03ba2d1 100644 (file)
@@ -54,7 +54,7 @@ class ClearFlagForm extends ProfileActionForm
 
     function formClass()
     {
-        return 'form_entity_clearflag';
+        return 'form_user_clearflag';
     }
 
     /**
diff --git a/plugins/UserFlag/icon_flag.gif b/plugins/UserFlag/icon_flag.gif
deleted file mode 100644 (file)
index 68c8aee..0000000
Binary files a/plugins/UserFlag/icon_flag.gif and /dev/null differ
diff --git a/plugins/UserFlag/userflag.css b/plugins/UserFlag/userflag.css
deleted file mode 100644 (file)
index 98da24c..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-.entity_flag input.submit,
-.entity_flag p {
-background:url(icon_flag.gif) 5px 5px no-repeat;
-}
old mode 100644 (file)
new mode 100755 (executable)
index b102f99..fe0e46d
@@ -28,6 +28,7 @@ setconfig.php [options] [section] [setting] <value>
 With three args, set the setting to the value.
 With two args, just show the setting.
 With -d, delete the setting.
+With no args, lists all currently set values.
 
   [section]   section to use (required)
   [setting]   setting to use (required)
@@ -39,6 +40,21 @@ END_OF_SETCONFIG_HELP;
 
 require_once INSTALLDIR.'/scripts/commandline.inc';
 
+if (empty($args)) {
+    $count = 0;
+    $config = new Config();
+    $config->find();
+    while ($config->fetch()) {
+        $count++;
+        printf("%-20s %-20s %s\n", $config->section, $config->setting,
+               var_export($config->value, true));
+    }
+    if ($count == 0) {
+        print "No configuration set in database for this site.\n";
+    }
+    exit(0);
+}
+
 if (count($args) < 2 || count($args) > 3) {
     show_help();
     exit(1);
index ed8853e57e665dfbd66bd0b09f970ef1f629a077..89fe810c6955b9082bb1270aa2d9ac1cecb949ad 100644 (file)
@@ -288,7 +288,7 @@ margin-left:18px;
 }
 #site_nav_global_primary li {
 display:inline;
-margin-left:11px;
+margin-left:18px;
 }
 
 .system_notice dt {
@@ -370,7 +370,7 @@ margin-bottom:11px;
 
 #site_nav_global_secondary ul li {
 display:inline;
-margin-right:11px;
+margin-right:18px;
 }
 #export_data li a {
 padding-left:20px;
@@ -383,15 +383,13 @@ padding-left:28px;
 }
 
 #export_data ul {
-display:inline;
+width:100%;
+float:left;
 }
 #export_data li {
 list-style-type:none;
-display:inline;
-margin-left:11px;
-}
-#export_data li:first-child {
-margin-left:0;
+float:left;
+margin-right:11px;
 }
 
 #licenses {
@@ -801,8 +799,8 @@ list-style-type:none;
 display:inline;
 }
 .entity_tags li {
-display:inline;
-margin-right:4px;
+float:left;
+margin-right:11px;
 }
 
 .aside .section {
@@ -820,6 +818,7 @@ font-size:1em;
 #entity_statistics dt,
 #entity_statistics dd {
 display:inline;
+margin-right:11px;
 }
 #entity_statistics dt:after {
 content: ":";
@@ -1104,25 +1103,22 @@ left:0;
 
 .dialogbox {
 position:absolute;
-top:-4px;
-right:29px;
+top:-1px;
+right:-1px;
 z-index:9;
-min-width:199px;
 float:none;
-background-color:#FFF;
 padding:11px;
 border-radius:7px;
 -moz-border-radius:7px;
 -webkit-border-radius:7px;
 border-style:solid;
 border-width:1px;
-border-color:#DDDDDD;
--moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
 }
 
 .dialogbox legend {
 display:block !important;
 margin-right:18px;
+margin-bottom:18px;
 }
 
 .dialogbox button.close {
@@ -1131,11 +1127,22 @@ right:3px;
 top:3px;
 }
 
+.dialogbox .form_guide {
+font-weight:normal;
+padding:0;
+}
+
 .dialogbox .submit_dialogbox {
 font-weight:bold;
 text-indent:0;
 min-width:46px;
 }
+.dialogbox input {
+padding-left:4px;
+}
+.dialogbox fieldset {
+margin-bottom:0;
+}
 
 #wrap form.processing input.submit,
 .entity_actions a.processing,
@@ -1145,6 +1152,12 @@ outline:none;
 text-indent:-9999px;
 }
 
+.form_repeat.dialogbox {
+top:-4px;
+right:29px;
+min-width:199px;
+}
+
 .notice-options {
 position:relative;
 font-size:0.95em;
@@ -1413,6 +1426,9 @@ margin-bottom:18px;
 .hentry .entry-content li li {
 margin-left:18px;
 }
+.hentry .entry-content .form_settings ul {
+margin-left:0;
+}
 
 #content #plugin_authors {
 min-width:122px;
@@ -1479,6 +1495,11 @@ display:inline;
 margin-right:7px;
 line-height:1.25;
 }
+
+.tag-cloud li:before {
+content:'\0009';
+}
+
 .aside .tag-cloud li {
 line-height:1.5;
 }
index f93d33d79bce50e1e407c98d534c6cbff24d4366..6f284f023ee7c7c1fef480c26e7ccfd2b27ba32c 100644 (file)
Binary files a/theme/base/images/icons/icons-01.gif and b/theme/base/images/icons/icons-01.gif differ
diff --git a/theme/base/images/icons/twotone/green/against.gif b/theme/base/images/icons/twotone/green/against.gif
new file mode 100644 (file)
index 0000000..ca796c8
Binary files /dev/null and b/theme/base/images/icons/twotone/green/against.gif differ
diff --git a/theme/base/images/icons/twotone/green/checkmark.gif b/theme/base/images/icons/twotone/green/checkmark.gif
new file mode 100644 (file)
index 0000000..892429d
Binary files /dev/null and b/theme/base/images/icons/twotone/green/checkmark.gif differ
diff --git a/theme/base/images/icons/twotone/green/clear.gif b/theme/base/images/icons/twotone/green/clear.gif
new file mode 100644 (file)
index 0000000..2666430
Binary files /dev/null and b/theme/base/images/icons/twotone/green/clear.gif differ
diff --git a/theme/base/images/icons/twotone/green/flag.gif b/theme/base/images/icons/twotone/green/flag.gif
new file mode 100644 (file)
index 0000000..68c8aee
Binary files /dev/null and b/theme/base/images/icons/twotone/green/flag.gif differ
index 7c68b34f61008753d12394806c3cf46f7b04effd..cf1839194a6d8e91d3ec988abe7d5be227143d28 100644 (file)
Binary files a/theme/base/logo.png and b/theme/base/logo.png differ
index 06711850fc3856c3452290b977f031ac9a8a9c76..a2f10134283210534a36c9f08a13cc25f9fcf31e 100644 (file)
@@ -1,7 +1,7 @@
 /** theme: default
  *
  * @package   StatusNet
- * @author Sarven Capadisli <csarven@status.net>
+ * @author    Sarven Capadisli <csarven@status.net>
  * @copyright 2009 StatusNet, Inc.
  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  * @link      http://status.net/
@@ -18,7 +18,7 @@ font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
 font-size:1em;
 }
 address {
-margin-right:5.7%;
+margin-right:5.3%;
 }
 input, textarea, select {
 border-width:2px;
@@ -30,7 +30,9 @@ border-radius:4px;
 input, textarea, select, option {
 font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
 }
-input, textarea, select {
+input, textarea, select,
+.entity_actions .dialogbox input,
+.mark-top {
 border-color:#AAAAAA;
 }
 
@@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3);
 .pagination .nav_prev a,
 .pagination .nav_next a,
 .form_settings fieldset fieldset,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
 border-color:#DDDDDD;
 }
 
@@ -78,7 +81,8 @@ background-color:transparent;
 input:focus, textarea:focus, select:focus,
 .form_notice.warning #notice_data-text,
 .form_notice.warning #notice_text-count,
-.form_settings .form_note {
+.form_settings .form_note,
+.entity_actions .dialogbox .form_data input:focus {
 border-color:#9BB43E;
 }
 input.submit {
@@ -133,9 +137,6 @@ color:#002FA7;
 #content tbody tr {
 border-top-color:#C8D1D5;
 }
-.mark-top {
-border-color:#AAAAAA;
-}
 
 #aside_primary {
 background-color:#C8D1D5;
@@ -144,7 +145,9 @@ background-color:#C8D1D5;
 #notice_text-count {
 color:#333333;
 }
-.form_notice.warning #notice_text-count {
+.form_notice.warning #notice_text-count,
+.dialogbox,
+.entity_actions .dialogbox input {
 color:#000000;
 }
 .form_notice label[for=notice_data-attach] {
@@ -189,7 +192,11 @@ button.close,
 .notice-options .repeated,
 .form_notice label[for=notice_data-geo],
 button.minimize,
-.form_reset_key input.submit {
+.form_reset_key input.submit,
+.entity_clear input.submit,
+.entity_flag input.submit,
+.entity_flag p,
+.entity_subscribe input.submit {
 background-image:url(../../base/images/icons/icons-01.gif);
 background-repeat:no-repeat;
 background-color:transparent;
@@ -217,7 +224,8 @@ border-color:transparent;
 #content,
 #site_nav_local_views .current a,
 .entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
 background-color:#FFFFFF;
 }
 
@@ -304,7 +312,8 @@ background-position: 5px -718px;
 background-position: 5px -852px;
 }
 .entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
 box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
 -moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
 -webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
@@ -338,6 +347,19 @@ background-position: 5px -1511px;
 .form_reset_key input.submit {
 background-position: 5px -1973px;
 }
+.entity_clear input.submit {
+background-position: 5px -2039px;
+}
+.entity_flag input.submit,
+.entity_flag p {
+background-position: 5px -2105px;
+}
+.entity_subscribe input.accept {
+background-position: 5px -2171px;
+}
+.entity_subscribe input.reject {
+background-position: 5px -2237px;
+}
 
 /* NOTICES */
 .notice .attachment {
index 550d373fef4005342c4e1daa7cb2c115db54f46c..cf1839194a6d8e91d3ec988abe7d5be227143d28 100644 (file)
Binary files a/theme/default/logo.png and b/theme/default/logo.png differ
index b72f7aff5b114e134e3fb1b510d1c47baf76306f..66bb5f67871596723fcd928c80ed5ebc18899a32 100644 (file)
Binary files a/theme/default/mobilelogo.png and b/theme/default/mobilelogo.png differ
index 1ac96ab5be4b3ad8a2210d5cb0145c0a27b71f99..e214047451efbd8c79a0f33c98a4f61bd3e86de7 100644 (file)
@@ -1,7 +1,7 @@
 /** theme: identica
  *
  * @package   StatusNet
- * @author Sarven Capadisli <csarven@status.net>
+ * @author    Sarven Capadisli <csarven@status.net>
  * @copyright 2009 StatusNet, Inc.
  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  * @link      http://status.net/
@@ -30,7 +30,9 @@ border-radius:4px;
 input, textarea, select, option {
 font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
 }
-input, textarea, select {
+input, textarea, select,
+.entity_actions .dialogbox input,
+.mark-top {
 border-color:#AAAAAA;
 }
 
@@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3);
 .pagination .nav_prev a,
 .pagination .nav_next a,
 .form_settings fieldset fieldset,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
 border-color:#DDDDDD;
 }
 
@@ -88,6 +91,7 @@ color:#FFFFFF;
 border-color:transparent;
 text-shadow:none;
 }
+
 .dialogbox .submit_dialogbox,
 input.submit,
 .form_notice input.submit {
@@ -133,9 +137,6 @@ color:#002FA7;
 #content tbody tr {
 border-top-color:#CEE1E9;
 }
-.mark-top {
-border-color:#AAAAAA;
-}
 
 #aside_primary {
 background-color:#CEE1E9;
@@ -144,7 +145,9 @@ background-color:#CEE1E9;
 #notice_text-count {
 color:#333333;
 }
-.form_notice.warning #notice_text-count {
+.form_notice.warning #notice_text-count,
+.dialogbox,
+.entity_actions .dialogbox input {
 color:#000000;
 }
 .form_notice label[for=notice_data-attach] {
@@ -189,7 +192,11 @@ button.close,
 .notice-options .repeated,
 .form_notice label[for=notice_data-geo],
 button.minimize,
-.form_reset_key input.submit {
+.form_reset_key input.submit,
+.entity_clear input.submit,
+.entity_flag input.submit,
+.entity_flag p,
+.entity_subscribe input.submit {
 background-image:url(../../base/images/icons/icons-01.gif);
 background-repeat:no-repeat;
 background-color:transparent;
@@ -217,7 +224,8 @@ border-color:transparent;
 #content,
 #site_nav_local_views .current a,
 .entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
 background-color:#FFFFFF;
 }
 
@@ -303,7 +311,8 @@ background-position: 5px -718px;
 background-position: 5px -852px;
 }
 .entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
 box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
 -moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
 -webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
@@ -337,6 +346,19 @@ background-position: 5px -1511px;
 .form_reset_key input.submit {
 background-position: 5px -1973px;
 }
+.entity_clear input.submit {
+background-position: 5px -2039px;
+}
+.entity_flag input.submit,
+.entity_flag p {
+background-position: 5px -2105px;
+}
+.entity_subscribe input.accept {
+background-position: 5px -2171px;
+}
+.entity_subscribe input.reject {
+background-position: 5px -2237px;
+}
 
 /* NOTICES */
 .notice .attachment {
index b72f7aff5b114e134e3fb1b510d1c47baf76306f..66bb5f67871596723fcd928c80ed5ebc18899a32 100644 (file)
Binary files a/theme/identica/mobilelogo.png and b/theme/identica/mobilelogo.png differ