]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch 'testing' of git@gitorious.org:statusnet/mainline into testing
authorZach Copley <zach@status.net>
Mon, 15 Feb 2010 21:14:32 +0000 (21:14 +0000)
committerZach Copley <zach@status.net>
Mon, 15 Feb 2010 21:14:32 +0000 (21:14 +0000)
65 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
classes/Avatar.php
classes/Design.php
classes/File.php
classes/Nonce.php
classes/Notice.php
classes/Profile.php
classes/User_group.php
classes/statusnet.links.ini
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/default.php
lib/grouplist.php
lib/groupsection.php
lib/htmloutputter.php
lib/noticelist.php
lib/noticesection.php
lib/profilelist.php
lib/profilesection.php
lib/queuemanager.php
lib/stompqueuemanager.php
lib/theme.php
lib/userprofile.php
lib/util.php
plugins/OStatus/OStatusPlugin.php
plugins/OStatus/actions/feedsubsettings.php
plugins/OStatus/actions/ostatusinit.php
plugins/OStatus/actions/ostatussub.php
plugins/OStatus/actions/pushcallback.php
plugins/OStatus/actions/salmon.php
plugins/OStatus/classes/Feedinfo.php [deleted file]
plugins/OStatus/classes/HubSub.php
plugins/OStatus/classes/Ostatus_profile.php [new file with mode: 0644]
plugins/OStatus/js/ostatus.js [new file with mode: 0644]
plugins/OStatus/lib/Salmon.php [deleted file]
plugins/OStatus/lib/Webfinger.php [deleted file]
plugins/OStatus/lib/XRD.php [deleted file]
plugins/OStatus/lib/activity.php [new file with mode: 0644]
plugins/OStatus/lib/feedmunger.php
plugins/OStatus/lib/hubdistribqueuehandler.php
plugins/OStatus/lib/huboutqueuehandler.php
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/tests/ActivityParseTests.php [new file with mode: 0644]
plugins/OStatus/theme/base/css/ostatus.css [new file with mode: 0644]
plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php
theme/base/css/display.css
theme/default/css/display.css
theme/identica/css/display.css

index 6bf12bf13fb15db9ea94507971c635266db383ee..69fe2ddccb37f24de7cd429b2151abdabe4e3f6c 100644 (file)
@@ -1,4 +1,4 @@
-InitializePlugin: a chance to initialize a plugin in a complete environment
+\InitializePlugin: a chance to initialize a plugin in a complete environment
 
 CleanupPlugin: a chance to cleanup a plugin at the end of a program
 
@@ -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.)
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 fd2ed9ff93f228f2cfcff616278b36ef9f8a6927..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->id . '.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 3f4a46c0fa16e2bc6b55483d32304cce6e0807da..c1fa72a3ee372f72e944931f86c6480bff17aabc 100644 (file)
@@ -75,6 +75,10 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
 
         $this->notices = $this->getNotices();
 
+        if ($this->since) {
+            throw new ServerException("since parameter is disabled for performance; use since_id", 403);
+        }
+
         return true;
     }
 
@@ -118,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);
@@ -145,7 +166,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
 
         $notice = Notice::publicStream(
             ($this->page - 1) * $this->count, $this->count, $this->since_id,
-            $this->max_id, $this->since
+            $this->max_id
         );
 
         while ($notice->fetch()) {
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 ed9104905de1f316c1066df4a9fdbf3332e5836f..24752e45fddf8a709c2ac62f1ee1982290e0ea3c 100644 (file)
@@ -145,19 +145,59 @@ class ApiTimelineUserAction extends ApiBareAuthAction
             );
             break;
         case '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 ($id) {
-                $selfuri = common_root_url() .
-                    'api/statuses/user_timeline/' .
-                    rawurlencode($id) . '.atom';
+
+            if (strval(intval($id)) === strval($id)) {
+                $atom = new AtomUserNoticeFeed($this->user);
             } else {
-                $selfuri = common_root_url() .
-                    'api/statuses/user_timeline.atom';
+                $atom = new AtomUserNoticeFeed();
             }
-            $this->showAtomTimeline(
-                $this->notices, $title, $id, $link,
-                $subtitle, $suplink, $selfuri, $logo
+
+            $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;
+            }
+
+            $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());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
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()
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 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 247440f29cbcf3eae2874658e4a3f823db623cdd..73b22d58a030cea41a8afbd67121e755f26954ad 100644 (file)
@@ -957,7 +957,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 +986,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 +993,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 +1015,43 @@ class Notice extends Memcached_DataObject
             }
         }
 
+        if (!empty($this->conversation)
+            && $this->conversation != $this->id) {
+            $xs->element(
+                'link', array(
+                    'rel' => 'ostatus:conversation',
+                    'href' => common_local_url(
+                        'conversation',
+                        array('id' => $this->conversation)
+                        )
+                    )
+                );
+        }
+
+        $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->getAcctUri()
+                    )
+                );
+            }
+        }
+
+        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 +1079,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');
index feabc250870c2effcff82b065b3b65eb021c8764..ab05bb8546bfe83d22ce66ce6164e509d32b6bc4 100644 (file)
@@ -754,4 +754,89 @@ 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->profileurl);
+        $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,
+            common_local_url(
+                'userbyid',
+                array('id' => $this->id)
+                )
+            );
+        $xs->element('title', null, $this->getBestName());
+
+        $avatar = $this->getAvatar(AVATAR_PROFILE_SIZE);
+
+        $xs->element(
+            'link', array(
+                'type' => empty($avatar) ? 'image/png' : $avatar->mediatype,
+                'href' => empty($avatar)
+                ? Avatar::defaultImage(AVATAR_PROFILE_SIZE)
+                : $avatar->displayUrl()
+            ),
+            ''
+        );
+
+        $xs->elementEnd('activity:' . $element);
+
+        return $xs->getString();
+    }
+
+    function getAcctUri()
+    {
+        return $this->nickname . '@' . common_config('site', 'server');
+    }
+
 }
index 1fbb50a6eb2b59cbecf7cc02b3768c2ad57f9b31..379e6b7219fb6d84eed97b069d3e6c410c12ba36 100644 (file)
@@ -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 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 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 fd07bbbbe080a6644c9e69dcd7b624d82942db95..22eef7436dfc8ac1f010882f699786e480ff1b8a 100644 (file)
@@ -1103,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');
 
@@ -1154,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()
@@ -1321,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 bf4b83718d04dd4e934d626bbd31a30618e04c56..8b1fe27694f938e84b801fafede1e5fd84c59b4d 100644 (file)
@@ -111,11 +111,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(),
@@ -123,10 +125,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
@@ -184,6 +188,7 @@ $default =
         array('server' => null,
               'dir' => INSTALLDIR . '/file/',
               'path' => $_path . '/file/',
+              'ssl' => null,
               'supported' => array('image/png',
                                    'image/jpeg',
                                    'image/gif',
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 317f5ea612a79b486488875120188583a10d1e01..47e56fc8f8ba6041c63821c3f534ed2ddaf072a1 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 a4a0f2651a190084f67e811fc540f914cecc0050..c05b990245f01acdce345b4866d94212367b9c46 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':
@@ -540,6 +545,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 +597,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 +624,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 +643,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 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 afe710e884dbc9d4aea2168a4f9548c4fc053b9c..149617eb508f1139c2502a110f57756b1a251744 100644 (file)
@@ -155,26 +155,26 @@ abstract class QueueManager extends IoManager
     }
 
     /**
-     * Encode an object for queued storage.
-     * Next gen may use serialization.
+     * Encode an object or variable for queued storage.
+     * Notice objects are currently stored as an id reference;
+     * other items are serialized.
      *
-     * @param mixed $object
+     * @param mixed $item
      * @return string
      */
-    protected function encode($object)
+    protected function encode($item)
     {
-        if ($object instanceof Notice) {
-            return $object->id;
-        } else if (is_string($object)) {
-            return $object;
+        if ($item instanceof Notice) {
+            // Backwards compat
+            return $item->id;
         } else {
-            throw new ServerException("Can't queue this type", 500);
+            return serialize($item);
         }
     }
 
     /**
      * Decode an object from queued storage.
-     * Accepts back-compat notice reference entries and strings for now.
+     * Accepts notice reference entries and serialized items.
      *
      * @param string
      * @return mixed
@@ -182,9 +182,23 @@ abstract class QueueManager extends IoManager
     protected function decode($frame)
     {
         if (is_numeric($frame)) {
+            // Back-compat for notices...
             return Notice::staticGet(intval($frame));
-        } else {
+        } elseif (substr($frame, 0, 1) == '<') {
+            // Back-compat for XML source
             return $frame;
+        } else {
+            // Deserialize!
+            #$old = error_reporting();
+            #error_reporting($old & ~E_NOTICE);
+            $out = unserialize($frame);
+            #error_reporting($old);
+
+            if ($out === false && $frame !== 'b:0;') {
+                common_log(LOG_ERR, "Couldn't unserialize queued frame: $frame");
+                return false;
+            }
+            return $out;
         }
     }
 
index 6730cd213d789dcd1e4de8ed273a27f8653a4ebf..cd62c25bd828c9a6bc37b158e2b9dc3ae5bb5180 100644 (file)
@@ -107,9 +107,10 @@ class StompQueueManager extends QueueManager
             $message .= ':' . $param;
         }
         $this->_connect();
-        $result = $this->_send($this->control,
-                               $message,
-                               array ('created' => common_sql_now()));
+        $con = $this->cons[$this->defaultIdx];
+        $result = $con->send($this->control,
+                             $message,
+                             array ('created' => common_sql_now()));
         if ($result) {
             $this->_log(LOG_INFO, "Sent control ping to queue daemons: $message");
             return true;
@@ -368,17 +369,10 @@ class StompQueueManager extends QueueManager
         foreach ($this->cons as $i => $con) {
             if ($con) {
                 $this->rollback($i);
-                $con->unsubscribe($this->control);
+                $con->disconnect();
+                $this->cons[$i] = null;
             }
         }
-        if ($this->sites) {
-            foreach ($this->sites as $server) {
-                StatusNet::init($server);
-                $this->doUnsubscribe();
-            }
-        } else {
-            $this->doUnsubscribe();
-        }
         return true;
     }
 
@@ -555,26 +549,14 @@ class StompQueueManager extends QueueManager
         }
 
         $host = $this->cons[$idx]->getServer();
-        if (is_numeric($frame->body)) {
-            $id = intval($frame->body);
-            $info = "notice $id posted at {$frame->headers['created']} in queue $queue from $host";
-
-            $notice = Notice::staticGet('id', $id);
-            if (empty($notice)) {
-                $this->_log(LOG_WARNING, "Skipping missing $info");
-                $this->ack($idx, $frame);
-                $this->commit($idx);
-                $this->begin($idx);
-                $this->stats('badnotice', $queue);
-                return false;
-            }
-
-            $item = $notice;
-        } else {
-            // @fixme should we serialize, or json, or what here?
-            $info = "string posted at {$frame->headers['created']} in queue $queue from $host";
-            $item = $frame->body;
+        $item = $this->decode($frame->body);
+        if (empty($item)) {
+            $this->_log(LOG_ERR, "Skipping empty or deleted item in queue $queue from $host");
+            return true;
         }
+        $info = $this->logrep($item) . " posted at " .
+                $frame->headers['created'] . " in queue $queue from $host";
+        $this->_log(LOG_DEBUG, "Dequeued $info");
 
         $handler = $this->getHandler($queue);
         if (!$handler) {
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 a07fe49e33d4502506fd3fc04450abe334d48321..209dc2254ed321c5da7418df812f7026d1ce0c2d 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) {
index 62ecaf6310cb842cc75d30ed24fc95d478236567..276ca1b3d0507a93c183ec174ab93887da1edd3c 100644 (file)
@@ -63,9 +63,9 @@ class OStatusPlugin extends Plugin
         $m->connect('main/ostatus?nickname=:nickname',
                   array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
         $m->connect('main/ostatussub',
-                    array('action' => 'ostatussub'));          
+                    array('action' => 'ostatussub'));
         $m->connect('main/ostatussub',
-                    array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));          
+                    array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
 
         // PuSH actions
         $m->connect('main/push/hub', array('action' => 'pushhub'));
@@ -80,6 +80,9 @@ class OStatusPlugin extends Plugin
         $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;
     }
 
@@ -109,24 +112,34 @@ class OStatusPlugin extends Plugin
      * Set up a PuSH hub link to our internal link for canonical timeline
      * Atom feeds for users and groups.
      */
-    function onStartApiAtom(Action $action)
+    function onStartApiAtom(AtomNoticeFeed $feed)
     {
-        if ($action instanceof ApiTimelineUserAction || $action instanceof ApiTimelineGroupAction) {
-            $id = $action->arg('id');
-            if (strval(intval($id)) === strval($id)) {
-                // Canonical form of id in URL?
-                // Updates will be handled for our internal PuSH hub.
-                $action->element('link', array('rel' => 'hub',
-                                               'href' => common_local_url('pushhub')));
-
-                // Also, we'll add in the salmon link
-                $action->element('link', array('rel' => 'salmon',
-                                               'href' => common_local_url('salmon')));
+        $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'));
         }
-        return true;
     }
-    
+
     /**
      * Add the feed settings page to the Connect Settings menu
      *
@@ -175,7 +188,7 @@ class OStatusPlugin extends Plugin
     /**
      * Add in an OStatus subscribe button
      */
-    function onStartProfilePageActionsElements($output, $profile)
+    function onStartProfileRemoteSubscribe($output, $profile)
     {
         $cur = common_current_user();
 
@@ -186,14 +199,19 @@ class OStatusPlugin extends Plugin
                                     array('nickname' => $profile->nickname));
             $output->element('a', array('href' => $url,
                                         'class' => 'entity_remote_subscribe'),
-                                _('OStatus'));
-            
+                                _m('Subscribe'));
+
             $output->elementEnd('li');
         }
+
+        return false;
     }
 
     /**
-     * Check if we've got some Salmon stuff to send
+     * 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)
     {
@@ -204,38 +222,66 @@ class OStatusPlugin extends Plugin
                 $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;
                 }
 
                 $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() {
-        // warning: the autoincrement doesn't seem to set.
-        // alter table feedinfo change column id id int(11) not null  auto_increment;
         $schema = Schema::get();
-        $schema->ensureTable('feedinfo', Feedinfo::schemaDef());
+        $schema->ensureTable('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;
+    }
 }
index 6f592bf5b0890a8f15e2279cc3d8c0dd5cf86b28..6933c9bf21a8ab5f6e3c41362f0d0f61125db5e0 100644 (file)
@@ -182,9 +182,9 @@ class FeedSubSettingsAction extends ConnectSettingsAction
         }
         
         $this->munger = $discover->feedMunger();
-        $this->feedinfo = $this->munger->feedInfo();
+        $this->profile = $this->munger->ostatusProfile();
 
-        if ($this->feedinfo->huburi == '' && !common_config('feedsub', 'nohub')) {
+        if ($this->profile->huburi == '' && !common_config('feedsub', 'nohub')) {
             $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
             return false;
         }
@@ -196,13 +196,16 @@ class FeedSubSettingsAction extends ConnectSettingsAction
     {
         if ($this->validateFeed()) {
             $this->preview = true;
-            $this->feedinfo = Feedinfo::ensureProfile($this->munger);
+            $this->profile = Ostatus_profile::ensureProfile($this->munger);
+            if (!$this->profile) {
+                throw new ServerException("Feed profile was not saved properly.");
+            }
 
             // If not already in use, subscribe to updates via the hub
-            if ($this->feedinfo->sub_start) {
-                common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}");
+            if ($this->profile->sub_start) {
+                common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
             } else {
-                $ok = $this->feedinfo->subscribe();
+                $ok = $this->profile->subscribe();
                 common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
                 if (!$ok) {
                     $this->showForm(_m('Feed subscription failed! Bad response from hub.'));
@@ -212,23 +215,21 @@ class FeedSubSettingsAction extends ConnectSettingsAction
 
             // And subscribe the current user to the local profile
             $user = common_current_user();
-            $profile = $this->feedinfo->getProfile();
-            if (!$profile) {
-                throw new ServerException("Feed profile was not saved properly.");
-            }
 
-            if ($this->feedinfo->isGroup()) {
-                if ($user->isMember($profile)) {
+            if ($this->profile->isGroup()) {
+                $group = $this->profile->localGroup();
+                if ($user->isMember($group)) {
                     $this->showForm(_m('Already a member!'));
-                } elseif (Group_member::join($this->feedinfo->group_id, $user->id)) {
+                } 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 {
-                if ($user->isSubscribed($profile)) {
+                $local = $this->profile->localProfile();
+                if ($user->isSubscribed($local)) {
                     $this->showForm(_m('Already subscribed!'));
-                } elseif ($user->subscribeTo($profile)) {
+                } elseif ($user->subscribeTo($local)) {
                     $this->showForm(_m('Feed subscribed!'));
                 } else {
                     $this->showForm(_m('Feed subscription failed!'));
@@ -247,7 +248,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction
 
     function previewFeed()
     {
-        $feedinfo = $this->munger->feedinfo();
+        $profile = $this->munger->ostatusProfile();
         $notice = $this->munger->notice(0, true); // preview
 
         if ($notice) {
index bac2c4d4382b56737b2d89f7e6005912c467e7b4..d21774420dd4c984d7a894bffe707663ade6cf13 100644 (file)
@@ -67,9 +67,21 @@ class OStatusInitAction extends Action
     
     function showForm($err = null)
     {
-      $this->err = $err;
-      $this->showPage();
-
+        $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()
@@ -79,15 +91,15 @@ class OStatusInitAction extends Action
                                           'class' => 'form_settings',
                                           'action' => common_local_url('ostatusinit')));
         $this->elementStart('fieldset');
-        $this->element('legend', _('Subscribe to a remote user'));
+        $this->element('legend', null,  sprintf(_('Subscribe to %s'), $this->nickname));
         $this->hidden('token', common_session_token());
 
         $this->elementStart('ul', 'form_data');
-        $this->elementStart('li');
+        $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');
+        $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');
@@ -95,7 +107,7 @@ class OStatusInitAction extends Action
         $this->submit('submit', _('Subscribe'));
         $this->elementEnd('fieldset');
         $this->elementEnd('form');
-    }        
+    }
 
     function ostatusConnect()
     {
@@ -125,4 +137,4 @@ class OStatusInitAction extends Action
       return _('OStatus Connect');  
     }
   
-}
\ No newline at end of file
+}
index ffc4ae8dfe24f46c66044d7b8296622ff71a8b4d..23912250163f0330585ff737bb6bc2c893aa46b5 100644 (file)
@@ -76,7 +76,7 @@ class OStatusSubAction extends Action
         $this->elementStart('fieldset', array('id' => 'settings_feeds'));
 
         $this->elementStart('ul', 'form_data');
-        $this->elementStart('li', array('id' => 'settings_twitter_login_button'));
+        $this->elementStart('li');
         $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
         $this->elementEnd('li');
         $this->elementEnd('ul');
@@ -164,9 +164,9 @@ class OStatusSubAction extends Action
         }
         
         $this->munger = $discover->feedMunger();
-        $this->feedinfo = $this->munger->feedInfo();
+        $this->profile = $this->munger->ostatusProfile();
 
-        if ($this->feedinfo->huburi == '') {
+        if ($this->profile->huburi == '') {
             $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
             return false;
         }
@@ -178,13 +178,13 @@ class OStatusSubAction extends Action
     {
         if ($this->validateFeed()) {
             $this->preview = true;
-            $this->feedinfo = Feedinfo::ensureProfile($this->munger);
+            $this->profile = Ostatus_profile::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}");
+            if ($this->profile->sub_start) {
+                common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
             } else {
-                $ok = $this->feedinfo->subscribe();
+                $ok = $this->profile->subscribe();
                 common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
                 if (!$ok) {
                     $this->showForm(_m('Feed subscription failed! Bad response from hub.'));
@@ -194,7 +194,7 @@ class OStatusSubAction extends Action
             
             // And subscribe the current user to the local profile
             $user = common_current_user();
-            $profile = $this->feedinfo->getProfile();
+            $profile = $this->profile->getProfile();
             
             if ($user->isSubscribed($profile)) {
                 $this->showForm(_m('Already subscribed!'));
@@ -209,7 +209,7 @@ class OStatusSubAction extends Action
     
     function previewFeed()
     {
-        $feedinfo = $this->munger->feedinfo();
+        $profile = $this->munger->ostatusProfile();
         $notice = $this->munger->notice(0, true); // preview
 
         if ($notice) {
@@ -223,4 +223,4 @@ class OStatusSubAction extends Action
     }
 
 
-}
\ No newline at end of file
+}
index a5e02e08f1442f9603174fc2fcace621b84b1048..2601a377a0bdf2d5b32246a05d3e43cc9dfb6686 100644 (file)
@@ -48,9 +48,9 @@ class PushCallbackAction extends Action
             throw new ServerException('Empty or invalid feed id', 400);
         }
 
-        $feedinfo = Feedinfo::staticGet('id', $feedid);
-        if (!$feedinfo) {
-            throw new ServerException('Unknown feed id ' . $feedid, 400);
+        $profile = Ostatus_profile::staticGet('id', $feedid);
+        if (!$profile) {
+            throw new ServerException('Unknown OStatus/PuSH feed id ' . $feedid, 400);
         }
 
         $hmac = '';
@@ -59,7 +59,7 @@ class PushCallbackAction extends Action
         }
 
         $post = file_get_contents('php://input');
-        $feedinfo->postUpdates($post, $hmac);
+        $profile->postUpdates($post, $hmac);
     }
     
     /**
@@ -78,28 +78,30 @@ class PushCallbackAction extends Action
             throw new ServerException("Bogus hub callback: bad mode", 404);
         }
         
-        $feedinfo = Feedinfo::staticGet('feeduri', $topic);
-        if (!$feedinfo) {
+        $profile = Ostatus_profile::staticGet('feeduri', $topic);
+        if (!$profile) {
             common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic");
             throw new ServerException("Bogus hub callback: unknown feed", 404);
         }
 
-        # Can't currently set the token in our sub api
-        #if ($feedinfo->verify_token !== $verify_token) {
-        #    common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
-        #    throw new ServerError("Bogus hub callback: bad token", 404);
-        #}
-        
+        if ($profile->verify_token !== $verify_token) {
+            common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
+            throw new ServerError("Bogus hub callback: bad token", 404);
+        }
+
+        if ($mode != $profile->sub_state) {
+            common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$profile->sub_state}\"");
+            throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 404);
+        }
+
         // OK!
-        common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
-        $feedinfo->sub_start = common_sql_date(time());
-        if ($lease_seconds > 0) {
-            $feedinfo->sub_end = common_sql_date(time() + $lease_seconds);
+        if ($mode == 'subscribe') {
+            common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
+            $profile->confirmSubscribe($lease_seconds);
         } else {
-            $feedinfo->sub_end = null;
+            common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic");
+            $profile->confirmUnsubscribe();
         }
-        $feedinfo->update();
-        
         print $challenge;
     }
 }
index 012869cf735d2015b5d1eaf13433900901d8c853..c79d09c95aa692e30371c55c504895df350731b2 100644 (file)
  * @author James Walker <james@status.net>
  */
 
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+if (!defined('STATUSNET')) {
+    exit(1);
+}
 
 class SalmonAction extends Action
 {
+    var $user     = null;
+    var $xml      = null;
+    var $activity = null;
 
-    function handle()
+    function prepare($args)
     {
-        parent::handle();
-        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
-            $this->handlePost();
+        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'));
+        }
 
-    function handlePost()
-    {
-        $user_id = $this->arg('id');
-        common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id);
+        $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/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php
deleted file mode 100644 (file)
index 792ea60..0000000
+++ /dev/null
@@ -1,390 +0,0 @@
-<?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:
-
-    $feedinfo->subscribe()
-        generate random verification token
-            save to verify_token
-        sends a sub request to the hub...
-    
-    feedsub/callback
-        hub sends confirmation back to us via GET
-        We verify the request, then echo back the challenge.
-        On our end, we save the time we subscribed and the lease expiration
-    
-    feedsub/callback
-        hub sends us updates via POST
-    
-*/
-
-class FeedDBException extends FeedSubException
-{
-    public $obj;
-
-    function __construct($obj)
-    {
-        parent::__construct('Database insert failure');
-        $this->obj = $obj;
-    }
-}
-
-class Feedinfo extends Memcached_DataObject
-{
-    public $__table = 'feedinfo';
-
-    public $id;
-    public $profile_id;
-
-    public $feeduri;
-    public $homeuri;
-    public $huburi;
-
-    // PuSH subscription data
-    public $secret;
-    public $verify_token;
-    public $sub_start;
-    public $sub_end;
-
-    public $created;
-    public $lastupdate;
-
-
-    public /*static*/ function staticGet($k, $v=null)
-    {
-        return parent::staticGet(__CLASS__, $k, $v);
-    }
-
-    /**
-     * return table definition for DB_DataObject
-     *
-     * DB_DataObject needs to know something about the table to manipulate
-     * instances. This method provides all the DB_DataObject needs to know.
-     *
-     * @return array array of column definitions
-     */
-
-    function table()
-    {
-        return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
-                     'profile_id' => DB_DATAOBJECT_INT,
-                     'group_id' => DB_DATAOBJECT_INT,
-                     'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
-                     'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
-                     'huburi' =>  DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
-                     'secret' => DB_DATAOBJECT_STR,
-                     'verify_token' => DB_DATAOBJECT_STR,
-                     'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
-                     'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
-                     'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
-                     'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
-    }
-    
-    static function schemaDef()
-    {
-        return array(new ColumnDef('id', 'integer',
-                                   /*size*/ null,
-                                   /*nullable*/ false,
-                                   /*key*/ 'PRI',
-                                   /*default*/ '0',
-                                   /*extra*/ null,
-                                   /*auto_increment*/ true),
-                     new ColumnDef('profile_id', 'integer',
-                                   null, true),
-                     new ColumnDef('group_id', 'integer',
-                                   null, true),
-                     new ColumnDef('feeduri', 'varchar',
-                                   255, false, 'UNI'),
-                     new ColumnDef('homeuri', 'varchar',
-                                   255, false),
-                     new ColumnDef('huburi', 'varchar',
-                                   255, false),
-                     new ColumnDef('verify_token', 'varchar',
-                                   32, true),
-                     new ColumnDef('secret', 'varchar',
-                                   64, true),
-                     new ColumnDef('sub_start', 'datetime',
-                                   null, true),
-                     new ColumnDef('sub_end', 'datetime',
-                                   null, true),
-                     new ColumnDef('created', 'datetime',
-                                   null, false),
-                     new ColumnDef('lastupdate', 'datetime',
-                                   null, false));
-    }
-
-    /**
-     * return key definitions for DB_DataObject
-     *
-     * DB_DataObject needs to know about keys that the table has; this function
-     * defines them.
-     *
-     * @return array key definitions
-     */
-
-    function keys()
-    {
-        return array_keys($this->keyTypes());
-    }
-
-    /**
-     * return key definitions for Memcached_DataObject
-     *
-     * Our caching system uses the same key definitions, but uses a different
-     * method to get them.
-     *
-     * @return array key definitions
-     */
-
-    function keyTypes()
-    {
-        return array('id' => 'K'); // @fixme we'll need a profile_id key at least
-    }
-
-    function sequenceKey()
-    {
-        return array('id', true, false);
-    }
-
-    /**
-     * Fetch the StatusNet-side profile for this feed
-     * @return Profile
-     */
-    public function getProfile()
-    {
-        return Profile::staticGet('id', $this->profile_id);
-    }
-
-    /**
-     * @param FeedMunger $munger
-     * @param boolean $isGroup is this a group record?
-     * @return Feedinfo
-     */
-    public static function ensureProfile($munger)
-    {
-        $feedinfo = $munger->feedinfo();
-
-        $current = self::staticGet('feeduri', $feedinfo->feeduri);
-        if ($current) {
-            // @fixme we should probably update info as necessary
-            return $current;
-        }
-
-        $feedinfo->query('BEGIN');
-
-        // Awful hack! Awful hack!
-        $feedinfo->verify = common_good_rand(16);
-        $feedinfo->secret = common_good_rand(32);
-
-        try {
-            $profile = $munger->profile();
-            $result = $profile->insert();
-            if (empty($result)) {
-                throw new FeedDBException($profile);
-            }
-
-            $avatar = $munger->getAvatar();
-            if ($avatar) {
-                // @fixme this should be better encapsulated
-                // ripped from oauthstore.php (for old OMB client)
-                $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
-                copy($avatar, $temp_filename);
-                $imagefile = new ImageFile($profile->id, $temp_filename);
-                $filename = Avatar::filename($profile->id,
-                                             image_type_to_extension($imagefile->type),
-                                             null,
-                                             common_timestamp());
-                rename($temp_filename, Avatar::path($filename));
-                $profile->setOriginal($filename);
-            }
-
-            $feedinfo->profile_id = $profile->id;
-            if ($feedinfo->isGroup()) {
-                $group = new User_group();
-                $group->nickname = $profile->nickname . '@remote'; // @fixme
-                $group->fullname = $profile->fullname;
-                $group->homepage = $profile->homepage;
-                $group->location = $profile->location;
-                $group->created = $profile->created;
-                $group->insert();
-
-                if ($avatar) {
-                    $group->setOriginal($filename);
-                }
-
-                $feedinfo->group_id = $group->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;
-    }
-
-    /**
-     * 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 /feedsub/callback.
-     *
-     * @return bool true on success, false on failure
-     */
-    public function subscribe()
-    {
-        if (common_config('feedsub', 'nohub')) {
-            // Fake it! We're just testing remote feeds w/o hubs.
-            return true;
-        }
-        // @fixme use the verification token
-        #$token = md5(mt_rand() . ':' . $this->feeduri);
-        #$this->verify_token = $token;
-        #$this->update(); // @fixme
-        try {
-            $callback = common_local_url('pushcallback', array('feed' => $this->id));
-            $headers = array('Content-Type: application/x-www-form-urlencoded');
-            $post = array('hub.mode' => 'subscribe',
-                          'hub.callback' => $callback,
-                          'hub.verify' => 'async',
-                          'hub.verify_token' => $this->verify_token,
-                          'hub.secret' => $this->secret,
-                          //'hub.lease_seconds' => 0,
-                          'hub.topic' => $this->feeduri);
-            $client = new HTTPClient();
-            $response = $client->post($this->huburi, $headers, $post);
-            $status = $response->getStatus();
-            if ($status == 202) {
-                common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
-                return true;
-            } else if ($status == 204) {
-                common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
-                return true;
-            } else if ($status >= 200 && $status < 300) {
-                common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
-                return false;
-            } else {
-                common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
-                return false;
-            }
-        } catch (Exception $e) {
-            // wtf!
-            common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
-            return false;
-        }
-    }
-
-    /**
-     * Read and post notices for updates from the feed.
-     * Currently assumes that all items in the feed are new,
-     * coming from a PuSH hub.
-     *
-     * @param string $xml source of Atom or RSS feed
-     * @param string $hmac X-Hub-Signature header, if present
-     */
-    public function postUpdates($xml, $hmac)
-    {
-        common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
-
-        if ($this->secret) {
-            if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
-                $their_hmac = strtolower($matches[1]);
-                $our_hmac = sha1($xml . $this->secret);
-                if ($their_hmac !== $our_hmac) {
-                    common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
-                    return;
-                }
-            } else {
-                common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
-                return;
-            }
-        } else if ($hmac) {
-            common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
-            return;
-        }
-
-        require_once "XML/Feed/Parser.php";
-        $feed = new XML_Feed_Parser($xml, false, false, true);
-        $munger = new FeedMunger($feed);
-        
-        $hits = 0;
-        foreach ($feed as $index => $entry) {
-            // @fixme this might sort in wrong order if we get multiple updates
-            
-            $notice = $munger->notice($index);
-            $notice->profile_id = $this->profile_id;
-            
-            // Double-check for oldies
-            // @fixme this could explode horribly for multiple feeds on a blog. sigh
-            $dupe = new Notice();
-            $dupe->uri = $notice->uri;
-            if ($dupe->find(true)) {
-                // @fixme we might have to do individual and group delivery separately!
-                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));
-            }
-            common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\"");
-
-            common_log(LOG_DEBUG, "going to check group delivery...");
-            if ($this->group_id) {
-                $group = User_group::staticGet($this->group_id);
-                if ($group) {
-                    common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname");
-                    $groups = array($group);
-                } else {
-                    common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?");
-                }
-            } else {
-                common_log(LOG_INFO, __METHOD__ . ": no local shadow groups");
-                $groups = array();
-            }
-            common_log(LOG_DEBUG, "going to add to inboxes...");
-            $notice->addToInboxes($groups, array());
-            common_log(LOG_DEBUG, "added to inboxes.");
-
-            $hits++;
-        }
-        if ($hits == 0) {
-            common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
-        }
-    }
-}
index 1769f6c9417f0c213c00fc9ebe8180d2676b343f..7071ee5b4f82e9933973b7a3afc705ef58934233 100644 (file)
@@ -242,7 +242,7 @@ class HubSub extends Memcached_DataObject
     {
         $headers = array('Content-Type: application/atom+xml');
         if ($this->secret) {
-            $hmac = sha1($atom . $this->secret);
+            $hmac = hash_hmac('sha1', $atom, $this->secret);
             $headers[] = "X-Hub-Signature: sha1=$hmac";
         } else {
             $hmac = '(none)';
diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php
new file mode 100644 (file)
index 0000000..733d884
--- /dev/null
@@ -0,0 +1,644 @@
+<?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');
+
+        // Awful hack! Awful hack!
+        $profile->verify = common_good_rand(16);
+        $profile->secret = common_good_rand(32);
+
+        try {
+            $local = $munger->profile();
+
+            if ($entity->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 = sql_common_date();
+            $profile->lastupdate = sql_common_date();
+            $result = $profile->insert();
+            if (empty($result)) {
+                throw new FeedDBException($profile);
+            }
+
+            $entity->query('COMMIT');
+        } catch (FeedDBException $e) {
+            common_log_db_error($e->obj, 'INSERT', __FILE__);
+            $entity->query('ROLLBACK');
+            return false;
+        }
+
+        $avatar = $munger->getAvatar();
+        if ($avatar) {
+            try {
+                $this->updateAvatar($avatar);
+            } catch (Exception $e) {
+                common_log(LOG_ERR, "Exception setting OStatus avatar: " .
+                                    $e->getMessage());
+            }
+        }
+
+        return $entity;
+    }
+
+    /**
+     * 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);
+        $imagefile = new ImageFile($profile->id, $temp_filename);
+        $filename = Avatar::filename($profile->id,
+                                     image_type_to_extension($imagefile->type),
+                                     null,
+                                     common_timestamp());
+        rename($temp_filename, Avatar::path($filename));
+        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
+     */
+    public function subscribe($mode='subscribe')
+    {
+        if (common_config('feedsub', 'nohub')) {
+            // Fake it! We're just testing remote feeds w/o hubs.
+            return true;
+        }
+        // @fixme use the verification token
+        #$token = md5(mt_rand() . ':' . $this->feeduri);
+        #$this->verify_token = $token;
+        #$this->update(); // @fixme
+        try {
+            $callback = common_local_url('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");
+            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 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
+     */
+    public function unsubscribe() {
+        return $this->subscribe('unsubscribe');
+    }
+
+    /**
+     * 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 $xml source of Atom or RSS feed
+     * @param string $hmac X-Hub-Signature header, if present
+     */
+    public function postUpdates($xml, $hmac)
+    {
+        common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
+
+        if ($this->secret) {
+            if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
+                $their_hmac = strtolower($matches[1]);
+                $our_hmac = hash_hmac('sha1', $xml, $this->secret);
+                if ($their_hmac !== $our_hmac) {
+                    common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
+                    return;
+                }
+            } else {
+                common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
+                return;
+            }
+        } else if ($hmac) {
+            common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
+            return;
+        }
+
+        require_once "XML/Feed/Parser.php";
+        $feed = new XML_Feed_Parser($xml, false, false, true);
+        $munger = new FeedMunger($feed);
+        
+        $hits = 0;
+        foreach ($feed as $index => $entry) {
+            // @fixme this might sort in wrong order if we get multiple updates
+
+            $notice = $munger->notice($index);
+
+            // Double-check for oldies
+            // @fixme this could explode horribly for multiple feeds on a blog. sigh
+            $dupe = new Notice();
+            $dupe->uri = $notice->uri;
+            if ($dupe->find(true)) {
+                common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
+                continue;
+            }
+
+            // @fixme need to ensure that groups get handled correctly
+            $saved = Notice::saveNew($notice->profile_id,
+                                     $notice->content,
+                                     'ostatus',
+                                     array('is_local' => Notice::REMOTE_OMB,
+                                           'uri' => $notice->uri,
+                                           'lat' => $notice->lat,
+                                           'lon' => $notice->lon,
+                                           'location_ns' => $notice->location_ns,
+                                           'location_id' => $notice->location_id));
+
+            /*
+            common_log(LOG_DEBUG, "going to check group delivery...");
+            if ($this->group_id) {
+                $group = User_group::staticGet($this->group_id);
+                if ($group) {
+                    common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname");
+                    $groups = array($group);
+                } else {
+                    common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?");
+                }
+            } else {
+                common_log(LOG_INFO, __METHOD__ . ": no local shadow groups");
+                $groups = array();
+            }
+            common_log(LOG_DEBUG, "going to add to inboxes...");
+            $notice->addToInboxes($groups, array());
+            common_log(LOG_DEBUG, "added to inboxes.");
+            */
+
+            $hits++;
+        }
+        if ($hits == 0) {
+            common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
+        }
+    }
+}
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/Salmon.php b/plugins/OStatus/lib/Salmon.php
deleted file mode 100644 (file)
index 8c77222..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-/**
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * A sample module to show best practices for StatusNet plugins
- *
- * PHP version 5
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   StatusNet
- * @author    James Walker <james@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
- * @link      http://status.net/
- */
-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
deleted file mode 100644 (file)
index 417d549..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-<?php
-/**
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * A sample module to show best practices for StatusNet plugins
- *
- * PHP version 5
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   StatusNet
- * @author    James Walker <james@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
- * @link      http://status.net/
- */
-
-define('WEBFINGER_SERVICE_REL_VALUE', 'lrdd');
-
-/**
- * Implement the webfinger protocol.
- */
-class Webfinger
-{
-    /**
-     * Perform a webfinger lookup given an account.
-     */ 
-    public function lookup($id)
-    {
-        $id = $this->normalize($id);
-        list($name, $domain) = explode('@', $id);
-
-        $links = $this->getServiceLinks($domain);
-        if (!$links) {
-            return false;
-        }
-        
-        $services = array();
-        foreach ($links as $link) {
-            if ($link['template']) {
-                return $this->getServiceDescription($link['template'], $id);
-            }
-            if ($link['href']) {
-                return $this->getServiceDescription($link['href'], $id);
-            }
-        }
-    }
-
-    /**
-     * Normalize an account ID
-     */
-    function normalize($id)
-    {
-        if (substr($id, 0, 7) == 'acct://') {
-            return substr($id, 7); 
-        } else if (substr($id, 0, 5) == 'acct:') {
-            return substr($id, 5);
-        }
-
-        return $id;
-    }
-
-    function getServiceLinks($domain)
-    {
-        $url = 'http://'. $domain .'/.well-known/host-meta';
-        $content = $this->fetchURL($url);
-        if (empty($content)) {
-            common_log(LOG_DEBUG, 'Error fetching host-meta');
-            return false;
-        }
-        $result = XRD::parse($content);
-
-        // Ensure that the host == domain (spec may include signing later)
-        if ($result->host != $domain) {
-            return false;
-        }
-        
-        $links = array();
-        foreach ($result->links as $link) {
-            if ($link['rel'] == WEBFINGER_SERVICE_REL_VALUE) {
-                $links[] = $link;
-            }
-
-        }
-        return $links;
-    }
-
-    function getServiceDescription($template, $id)
-    {
-        $url = $this->applyTemplate($template, 'acct:' . $id);
-
-        $content = $this->fetchURL($url);
-
-        return XRD::parse($content);
-    }
-
-    function fetchURL($url)
-    {
-        try {
-            $client = new HTTPClient();
-            $response = $client->get($url);
-        } catch (HTTP_Request2_Exception $e) {
-            return false;
-        }
-
-        if ($response->getStatus() != 200) {
-            return false;
-        }
-
-        return $response->getBody();
-    }
-
-    function applyTemplate($template, $id)
-    {
-        $template = str_replace('{uri}', urlencode($id), $template);
-
-        return $template;
-    }
-
-    function getHostMeta($domain, $template) {
-        $xrd = new XRD();
-        $xrd->host = $domain;
-        $xrd->links[] = array('rel' => 'lrdd',
-                              'template' => $template,
-                              'title' => array('Resource Descriptor'));
-
-        return $xrd->toXML();
-    }
-}
-
-
diff --git a/plugins/OStatus/lib/XRD.php b/plugins/OStatus/lib/XRD.php
deleted file mode 100644 (file)
index 16d27f8..0000000
+++ /dev/null
@@ -1,183 +0,0 @@
-<?php
-/**
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * A sample module to show best practices for StatusNet plugins
- *
- * PHP version 5
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   StatusNet
- * @author    James Walker <james@status.net>
- * @copyright 2010 StatusNet, Inc.
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
- * @link      http://status.net/
- */
-
-
-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/lib/activity.php b/plugins/OStatus/lib/activity.php
new file mode 100644 (file)
index 0000000..048efda
--- /dev/null
@@ -0,0 +1,393 @@
+<?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 getLink($element)
+    {
+        $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
+
+        foreach ($links as $link) {
+
+            $rel = $link->getAttribute(self::REL);
+            $type = $link->getAttribute(self::TYPE);
+
+            if ($rel == 'alternate' && $type == 'text/html') {
+                return $link->getAttribute(self::HREF);
+            }
+        }
+
+        return null;
+    }
+}
+
+/**
+ * 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 $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->source = $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->_childContent($element, self::SOURCE);
+
+            $this->link = ActivityUtils::getLink($element);
+
+            // XXX: grab PoCo stuff
+        }
+    }
+
+    /**
+     * 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
+     */
+
+    private function _childContent($element, $tag, $namespace=Activity::ATOM)
+    {
+        $els = $element->getElementsByTagnameNS($namespace, $tag);
+
+        if (empty($els) || $els->length == 0) {
+            return null;
+        } else {
+            $el = $els->item(0);
+            return $el->textContent;
+        }
+    }
+}
+
+/**
+ * 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::getLink($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;
+    }
+
+    /**
+     * 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
+     */
+
+    private function _child($element, $tag, $namespace=self::SPEC)
+    {
+        $els = $element->getElementsByTagnameNS($namespace, $tag);
+
+        if (empty($els) || $els->length == 0) {
+            return null;
+        } else {
+            return $els->item(0);
+        }
+    }
+}
\ No newline at end of file
index 5dce95342e1cfe26672535fce90bf30f0fc7ff66..c895b6ce24afde9a48ae991530415cc457437045 100644 (file)
@@ -83,13 +83,17 @@ class FeedMunger
         $this->url = $url;
     }
     
-    function feedinfo()
+    function ostatusProfile()
     {
-        $feedinfo = new Feedinfo();
-        $feedinfo->feeduri = $this->url;
-        $feedinfo->homeuri = $this->feed->link;
-        $feedinfo->huburi = $this->getHubLink();
-        return $feedinfo;
+        $profile = new Ostatus_profile();
+        $profile->feeduri = $this->url;
+        $profile->homeuri = $this->feed->link;
+        $profile->huburi = $this->getHubLink();
+        $salmon = $this->getSalmonLink();
+        if ($salmon) {
+            $profile->salmonuri = $salmon;
+        }
+        return $profile;
     }
 
     function getAtomLink($item, $attribs=array())
@@ -155,6 +159,16 @@ class FeedMunger
         return $this->getAtomLink($this->feed, array('rel' => 'hub'));
     }
 
+    function getSalmonLink()
+    {
+        return $this->getAtomLink($this->feed, array('rel' => 'salmon'));
+    }
+
+    function getSelfLink()
+    {
+        return $this->getAtomLink($this->feed, array('rel' => 'self'));
+    }
+
     /**
      * Get an appropriate avatar image source URL, if available.
      * @return mixed string or false
@@ -209,6 +223,7 @@ class FeedMunger
             $notice->id = -1;
         } else {
             $notice = new Notice();
+            $notice->profile_id = $this->profileIdForEntry($index);
         }
 
         $link = $this->getAltLink($entry);
@@ -239,7 +254,22 @@ class FeedMunger
         return $notice;
     }
 
+    function profileIdForEntry($index=1)
+    {
+        // hack hack hack
+        // should get profile for this entry's author...
+        $remote = Ostatus_profile::staticGet('feeduri', $this->getSelfLink());
+        if ($feed) {
+            return $feed->profile_id;
+        } else {
+            throw new Exception("Can't find feed profile");
+        }
+    }
+
     /**
+     * Parse location given as a GeoRSS-simple point, if provided.
+     * http://www.georss.org/simple
+     *
      * @param feed item $entry
      * @return mixed Location or false
      */
@@ -249,7 +279,10 @@ class FeedMunger
         $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point');
         
         for ($i = 0; $i < $points->length; $i++) {
-            $point = trim($points->item(0)->textContent);
+            $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;
index 189ccbedf9fc26e25efd075ee048ef90e077fa1f..245a57f7200dac1e321d3aefe98661d9edaec724 100644 (file)
@@ -38,6 +38,7 @@ class HubDistribQueueHandler extends QueueHandler
         foreach ($notice->getGroups() as $group) {
             $this->pushGroup($notice, $group->group_id);
         }
+        return true;
     }
     
     function pushUser($notice)
@@ -48,14 +49,7 @@ class HubDistribQueueHandler extends QueueHandler
         $feed = common_local_url('ApiTimelineUser',
                                  array('id' => $notice->profile_id,
                                        'format' => 'atom'));
-        $sub = new HubSub();
-        $sub->topic = $feed;
-        if ($sub->find()) {
-            $atom = $this->userFeedForNotice($notice);
-            $this->pushFeeds($atom, $sub);
-        } else {
-            common_log(LOG_INFO, "No PuSH subscribers for $feed");
-        }
+        $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice);
     }
 
     function pushGroup($notice, $group_id)
@@ -63,19 +57,69 @@ class HubDistribQueueHandler extends QueueHandler
         $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()) {
-            common_log(LOG_INFO, "Building PuSH feed for $feed");
-            $atom = $this->groupFeedForNotice($group_id, $notice);
-            $this->pushFeeds($atom, $sub);
+            $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;
     }
 
-    
-    function pushFeeds($atom, $sub)
+    /**
+     * 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();
index cb44ad2c4edc55d298b2ae09060740d42eed5e52..0791c7e5db16c4fd186ba5158bddc93546172770 100644 (file)
@@ -43,7 +43,7 @@ class HubOutQueueHandler extends QueueHandler
             common_log(LOG_ERR, "Failed PuSH to $sub->callback for $sub->topic: " .
                                 $e->getMessage());
             // @fixme Reschedule a later delivery?
-            // Currently we have no way to do this other than 'send NOW'
+            return true;
         }
 
         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/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/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 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 8490fb58032b15a5b74f0d07da255dc062fe4e4b..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;
@@ -1482,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 82eb135316c30638c54bb091faf708853574f4a4..a2f10134283210534a36c9f08a13cc25f9fcf31e 100644 (file)
@@ -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] {
@@ -221,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;
 }
 
@@ -308,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);
index 44ae4953b72d5b555c7d9bbbc094eba9f9c4742d..e214047451efbd8c79a0f33c98a4f61bd3e86de7 100644 (file)
@@ -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] {
@@ -221,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;
 }
 
@@ -307,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);