]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch 'twitter-facebook-mods'
authorZach Copley <zach@status.net>
Wed, 3 Mar 2010 17:53:16 +0000 (09:53 -0800)
committerZach Copley <zach@status.net>
Wed, 3 Mar 2010 17:53:16 +0000 (09:53 -0800)
* twitter-facebook-mods:
  Make Facebook plugin look for API key and secret before doing anything
  Show global key and secret, if defined, in Twitter bridge admin panel
  Remove double word from Twitter bridge README
  - Have Twitter bridge check for a global key and secret if it can't
  stupid mistake... let's not talk about this.
  Updated some references to the long gnone "isEnclosure" function to the new "getEnclosure"
  Update Facebook plugin README with info about new admin panel
  Initial Facebook admin panel
  Some wording / spelling fixes
  - Make 'Sign in with Twitter' optional
  Remove un-needed config variable for enabling/disabling Twitter integration
  Initial Twitter bridge admin panel
  Upgrade XML output scrubbing to better deal with newline and a few other chars

68 files changed:
EVENTS.txt
actions/apidirectmessage.php
actions/apigrouplist.php
actions/apigroupmembership.php
actions/apitimelinefriends.php
actions/apitimelinegroup.php
actions/apitimelinehome.php
actions/apitimelinementions.php
actions/apitimelinepublic.php
actions/apitimelineuser.php
actions/deleteuser.php
actions/editgroup.php
actions/newnotice.php
actions/postnotice.php
actions/showgroup.php
actions/updateprofile.php
classes/Fave.php
classes/Inbox.php
classes/Notice.php
classes/Notice_inbox.php
classes/Notice_tag.php
classes/Profile.php
classes/Reply.php
classes/Subscription.php
classes/User.php
classes/User_group.php
db/08to09.sql
db/statusnet.sql
lib/action.php
lib/activity.php
lib/adminpanelaction.php
lib/apiaction.php
lib/common.php
lib/noticelist.php
lib/oauthstore.php
lib/omb.php
lib/util.php
locale/statusnet.po
locale/te/LC_MESSAGES/statusnet.po
plugins/Blacklist/BlacklistPlugin.php
plugins/Blacklist/blacklistadminpanel.php [new file with mode: 0644]
plugins/OStatus/OStatusPlugin.php
plugins/OStatus/actions/hostmeta.php
plugins/OStatus/actions/ostatusinit.php
plugins/OStatus/actions/ostatussub.php
plugins/OStatus/actions/pushhub.php
plugins/OStatus/actions/webfinger.php [deleted file]
plugins/OStatus/actions/xrd.php [new file with mode: 0644]
plugins/OStatus/classes/HubSub.php
plugins/OStatus/classes/Magicsig.php
plugins/OStatus/classes/Ostatus_profile.php
plugins/OStatus/lib/discovery.php [new file with mode: 0644]
plugins/OStatus/lib/magicenvelope.php
plugins/OStatus/lib/ostatusqueuehandler.php
plugins/OStatus/lib/salmon.php
plugins/OStatus/lib/salmonaction.php
plugins/OStatus/lib/salmonqueuehandler.php
plugins/OStatus/lib/webfinger.php [deleted file]
plugins/OStatus/lib/xrd.php
plugins/OStatus/locale/OStatus.po
plugins/OStatus/scripts/updateostatus.php [new file with mode: 0644]
plugins/RegisterThrottle/RegisterThrottlePlugin.php [new file with mode: 0644]
plugins/RegisterThrottle/Registration_ip.php [new file with mode: 0644]
plugins/SphinxSearch/sphinxsearch.php
scripts/createsim.php
scripts/update_po_templates.php
tests/ActivityParseTests.php
theme/base/css/display.css

index c387274c03f55fed99d400eb6b322ad8003eba58..a2b405acc8e07f01a829e93ed69478088929cfb9 100644 (file)
@@ -769,3 +769,20 @@ StartShowSubscriptionsContent: before showing the subscriptions content
 
 EndShowSubscriptionsContent: after showing the subscriptions content
 - $action: the current action
+
+StartDeleteUserForm: starting the data in the form for deleting a user
+- $action: action being shown
+- $user: user being deleted
+
+EndDeleteUserForm: Ending the data in the form for deleting a user
+- $action: action being shown
+- $user: user being deleted
+
+StartDeleteUser: handling the post for deleting a user
+- $action: action being shown
+- $user: user being deleted
+
+EndDeleteUser: handling the post for deleting a user
+- $action: action being shown
+- $user: user being deleted
+
index 5355acf8253805796cfa5411a857b8a5947d2310..53da9e0c68a297195c74c6b33eee1c8a5648539a 100644 (file)
@@ -182,11 +182,6 @@ class ApiDirectMessageAction extends ApiAuthAction
             $message->whereAdd('id > ' . $this->since_id);
         }
 
-        if (!empty($since)) {
-            $d = date('Y-m-d H:i:s', $this->since);
-            $message->whereAdd("created > '$d'");
-        }
-
         $message->orderBy('created DESC, id DESC');
         $message->limit((($this->page - 1) * $this->count), $this->count);
         $message->find();
index 605b3823261338bcf750b5c1a74ad9c5fae28983..98fdb0497a3c3bb5e59194d254d25d04abc0e901 100644 (file)
@@ -152,8 +152,7 @@ class ApiGroupListAction extends ApiBareAuthAction
             ($this->page - 1) * $this->count,
             $this->count,
             $this->since_id,
-            $this->max_id,
-            $this->since
+            $this->max_id
         );
 
         while ($group->fetch()) {
index 3c7c8e88350de063c5adc051947997ed554c34cc..9f72b527cfd7a43601fcf84de764cf330bc82e63 100644 (file)
@@ -125,8 +125,7 @@ class ApiGroupMembershipAction extends ApiPrivateAuthAction
             ($this->page - 1) * $this->count,
             $this->count,
             $this->since_id,
-            $this->max_id,
-            $this->since
+            $this->max_id
         );
 
         while ($profile->fetch()) {
index 2db76857e3c7615d43647f24ad27f7867a33fcd8..9ef3ace607eb5b9ef22ad96bd495ca887c413b1c 100644 (file)
@@ -202,11 +202,11 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction
         if (!empty($this->auth_user) && $this->auth_user->id == $this->user->id) {
             $notice = $this->user->ownFriendsTimeline(($this->page-1) * $this->count,
                                                       $this->count, $this->since_id,
-                                                      $this->max_id, $this->since);
+                                                      $this->max_id);
         } else {
             $notice = $this->user->friendsTimeline(($this->page-1) * $this->count,
                                                    $this->count, $this->since_id,
-                                                   $this->max_id, $this->since);
+                                                   $this->max_id);
         }
 
         while ($notice->fetch()) {
index 04456ffea4826456862de6de03b165f51f461919..d0af49844c64957dadd3cc0902d192051b9e8088 100644 (file)
@@ -204,8 +204,7 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction
             ($this->page-1) * $this->count,
             $this->count,
             $this->since_id,
-            $this->max_id,
-            $this->since
+            $this->max_id
         );
 
         while ($notice->fetch()) {
index 0c72f4020c47d77229bb798752bb482dbaac25c3..abd3877860371cd152b7cb7e34266948e5776a1e 100644 (file)
@@ -200,13 +200,13 @@ class ApiTimelineHomeAction extends ApiBareAuthAction
             $notice = $this->user->noticeInbox(
                 ($this->page-1) * $this->count,
                 $this->count, $this->since_id,
-                $this->max_id, $this->since
+                $this->max_id
             );
         } else {
             $notice = $this->user->noticesWithFriends(
                 ($this->page-1) * $this->count,
                 $this->count, $this->since_id,
-                $this->max_id, $this->since
+                $this->max_id
             );
         }
 
index a39c63346abfa2dc990c8a2c806fed835c1924b7..31627ab7bf36424ea6fa09539388265bb61f96b1 100644 (file)
@@ -189,7 +189,7 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction
 
         $notice = $this->user->getReplies(
             ($this->page - 1) * $this->count, $this->count,
-            $this->since_id, $this->max_id, $this->since
+            $this->since_id, $this->max_id
         );
 
         while ($notice->fetch()) {
index 1ff0fd26172892defc5d48c047fe9da50ffb4423..3e4dad690e83ee80684ef39544ca49b3f5b01e66 100644 (file)
@@ -75,10 +75,6 @@ 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;
     }
 
index b3ded97c0fc09152b985e62fa93c3fb3929e2627..94491946c21b5943f61a7c40bfafef2818c70cd8 100644 (file)
@@ -211,7 +211,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction
 
         $notice = $this->user->getNotices(
             ($this->page-1) * $this->count, $this->count,
-            $this->since_id, $this->max_id, $this->since
+            $this->since_id, $this->max_id
         );
 
         while ($notice->fetch()) {
index 32b703aa7f099fe3e79bf067b184ea1c52339183..c4f84fad2d82d03711b703891bd1045c8862b8ff 100644 (file)
@@ -131,18 +131,21 @@ class DeleteuserAction extends ProfileFormAction
         $this->elementStart('fieldset');
         $this->hidden('token', common_session_token());
         $this->element('legend', _('Delete user'));
-        $this->element('p', null,
-                       _('Are you sure you want to delete this user? '.
-                         'This will clear all data about the user from the '.
-                         'database, without a backup.'));
-        $this->element('input', array('id' => 'deleteuserto-' . $id,
-                                      'name' => 'profileid',
-                                      'type' => 'hidden',
-                                      'value' => $id));
-        foreach ($this->args as $k => $v) {
-            if (substr($k, 0, 9) == 'returnto-') {
-                $this->hidden($k, $v);
+        if (Event::handle('StartDeleteUserForm', array($this, $this->user))) {
+            $this->element('p', null,
+                           _('Are you sure you want to delete this user? '.
+                             'This will clear all data about the user from the '.
+                             'database, without a backup.'));
+            $this->element('input', array('id' => 'deleteuserto-' . $id,
+                                          'name' => 'profileid',
+                                          'type' => 'hidden',
+                                          'value' => $id));
+            foreach ($this->args as $k => $v) {
+                if (substr($k, 0, 9) == 'returnto-') {
+                    $this->hidden($k, $v);
+                }
             }
+            Event::handle('EndDeleteUserForm', array($this, $this->user));
         }
         $this->submit('form_action-no', _('No'), 'submit form_action-primary', 'no', _("Do not block this user"));
         $this->submit('form_action-yes', _('Yes'), 'submit form_action-secondary', 'yes', _('Delete this user'));
@@ -158,7 +161,9 @@ class DeleteuserAction extends ProfileFormAction
 
     function handlePost()
     {
-        $this->user->delete();
+        if (Event::handle('StartDeleteUser', array($this, $this->user))) {
+            $this->user->delete();
+            Event::handle('EndDeleteUser', array($this, $this->user));
+        }
     }
 }
-
index d486db0c0aa3ba2bace92761e0443e6a30978046..4b596cade95cb22871904d1b0ed34781fea56f30 100644 (file)
@@ -286,7 +286,7 @@ class EditgroupAction extends GroupDesignAction
         $group = Local_group::staticGet('nickname', $nickname);
 
         if (!empty($group) &&
-            $group->id != $this->group->id) {
+            $group->group_id != $this->group->id) {
             return true;
         }
 
index 78480ababb069849508550141ee4c5df9862b78c..ed0fa1b2b5768026e486196d0112a2c46af3f54e 100644 (file)
@@ -294,6 +294,9 @@ class NewnoticeAction extends Action
             if ($profile) {
                 $content = '@' . $profile->nickname . ' ';
             }
+        } else {
+            // @fixme most of these bits above aren't being passed on above
+            $inreplyto = null;
         }
 
         $notice_form = new NoticeForm($this, '', $content, null, $inreplyto);
index fb0670376651ed145b0ea8ea7673eb3f7e07d30a..b2f6f1bb95debe9a57e441bbdc07a96c8a37447e 100644 (file)
@@ -54,7 +54,10 @@ class PostnoticeAction extends Action
      */
     function prepare($argarray)
     {
+        StatusNet::setApi(true); // Send smaller error pages
+
         parent::prepare($argarray);
+
         try {
             $this->checkNotice();
         } catch (Exception $e) {
@@ -71,6 +74,14 @@ class PostnoticeAction extends Action
             $srv = new OMB_Service_Provider(null, omb_oauth_datastore(),
                                             omb_oauth_server());
             $srv->handlePostNotice();
+        } catch (OMB_RemoteServiceException $rse) {
+            $msg = $rse->getMessage();
+            if (preg_match('/Revoked accesstoken/', $msg) ||
+                preg_match('/No subscriber/', $msg)) {
+                $this->clientError($msg, 403);
+            } else {
+                $this->clientError($msg);
+            }
         } catch (Exception $e) {
             $this->serverError($e->getMessage());
             return;
index 0139ba157de75ca00496a695813d655931027ff5..4e1fcb6c7b1cb86b253870a3d50c1fb998054ea6 100644 (file)
@@ -125,14 +125,6 @@ class ShowgroupAction extends GroupDesignAction
         $local = Local_group::staticGet('nickname', $nickname);
 
         if (!$local) {
-            common_log(LOG_NOTICE, "Couldn't find local group for nickname '$nickname'");
-            $this->clientError(_('No such group.'), 404);
-            return false;
-        }
-
-        $this->group = User_group::staticGet('id', $local->group_id);
-
-        if (!$this->group) {
             $alias = Group_alias::staticGet('alias', $nickname);
             if ($alias) {
                 $args = array('id' => $alias->group_id);
@@ -142,11 +134,19 @@ class ShowgroupAction extends GroupDesignAction
                 common_redirect(common_local_url('groupbyid', $args), 301);
                 return false;
             } else {
+                common_log(LOG_NOTICE, "Couldn't find local group for nickname '$nickname'");
                 $this->clientError(_('No such group.'), 404);
                 return false;
             }
         }
 
+        $this->group = User_group::staticGet('id', $local->group_id);
+
+        if (!$this->group) {
+            $this->clientError(_('No such group.'), 404);
+            return false;
+        }
+
         common_set_returnto($this->selfUrl());
 
         return true;
index e416a6fa93dbd881ab152249d1abed50dfd2ff52..bae6108cced4e0647982e4b579705ff48ba938c1 100644 (file)
@@ -55,6 +55,8 @@ class UpdateprofileAction extends Action
      */
     function prepare($argarray)
     {
+        StatusNet::setApi(true); // Send smaller error pages
+
         parent::prepare($argarray);
         $license      = $_POST['omb_listenee_license'];
         $site_license = common_config('license', 'url');
@@ -75,6 +77,14 @@ class UpdateprofileAction extends Action
             $srv = new OMB_Service_Provider(null, omb_oauth_datastore(),
                                             omb_oauth_server());
             $srv->handleUpdateProfile();
+        } catch (OMB_RemoteServiceException $rse) {
+            $msg = $rse->getMessage();
+            if (preg_match('/Revoked accesstoken/', $msg) ||
+                preg_match('/No subscriber/', $msg)) {
+                $this->clientError($msg, 403);
+            } else {
+                $this->clientError($msg);
+            }
         } catch (Exception $e) {
             $this->serverError($e->getMessage());
             return;
index 0b6eec2bc40af2e2742332fcc1dae6883521e218..a04f15e9c4e63cb1ddc0a0dc606eaa00dc355ccc 100644 (file)
@@ -77,7 +77,7 @@ class Fave extends Memcached_DataObject
         return $ids;
     }
 
-    function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id, $since)
+    function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id)
     {
         $fav = new Fave();
         $qry = null;
@@ -100,10 +100,6 @@ class Fave extends Memcached_DataObject
             $qry .= 'AND notice_id <= ' . $max_id . ' ';
         }
 
-        if (!is_null($since)) {
-            $qry .= 'AND modified > \'' . date('Y-m-d H:i:s', $since) . '\' ';
-        }
-
         // NOTE: we sort by fave time, not by notice time!
 
         $qry .= 'ORDER BY modified DESC ';
index be62611a16504416675d474e6d1920d6d6080273..014ba3d82923efb70d07fe0b70137fba726ee99c 100644 (file)
@@ -137,7 +137,7 @@ class Inbox extends Memcached_DataObject
         }
     }
 
-    function stream($user_id, $offset, $limit, $since_id, $max_id, $since, $own=false)
+    function stream($user_id, $offset, $limit, $since_id, $max_id, $own=false)
     {
         $inbox = Inbox::staticGet('user_id', $user_id);
 
@@ -195,15 +195,15 @@ class Inbox extends Memcached_DataObject
      * @param int $limit
      * @param mixed $since_id return only notices after but not including this id
      * @param mixed $max_id return only notices up to and including this id
-     * @param mixed $since obsolete/ignored
      * @param mixed $own ignored?
      * @return array of Notice objects
      *
      * @todo consider repacking the inbox when this happens?
+     * @fixme reimplement $own if we need it?
      */
-    function streamNotices($user_id, $offset, $limit, $since_id, $max_id, $since, $own=false)
+    function streamNotices($user_id, $offset, $limit, $since_id, $max_id, $own=false)
     {
-        $ids = self::stream($user_id, $offset, self::MAX_NOTICES, $since_id, $max_id, $since, $own);
+        $ids = self::stream($user_id, $offset, self::MAX_NOTICES, $since_id, $max_id, $own);
 
         // Do a bulk lookup for the first $limit items
         // Fast path when nothing's deleted.
index ac4640534c48b15264ef8cc3fcd1206be144b4ac..22dcbcd741d33d18b8f4da6974d3036f0ee0821a 100644 (file)
@@ -282,12 +282,6 @@ class Notice extends Memcached_DataObject
 
         $notice->content = $final;
 
-        if (!empty($rendered)) {
-            $notice->rendered = $rendered;
-        } else {
-            $notice->rendered = common_render_content($final, $notice);
-        }
-
         $notice->source = $source;
         $notice->uri = $uri;
         $notice->url = $url;
@@ -315,6 +309,12 @@ class Notice extends Memcached_DataObject
             $notice->location_ns = $location_ns;
         }
 
+        if (!empty($rendered)) {
+            $notice->rendered = $rendered;
+        } else {
+            $notice->rendered = common_render_content($final, $notice);
+        }
+
         if (Event::handle('StartNoticeSave', array(&$notice))) {
 
             // XXX: some of these functions write to the DB
@@ -559,17 +559,17 @@ class Notice extends Memcached_DataObject
         }
     }
 
-    function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
+    function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0)
     {
         $ids = Notice::stream(array('Notice', '_publicStreamDirect'),
                               array(),
                               'public',
-                              $offset, $limit, $since_id, $max_id, $since);
+                              $offset, $limit, $since_id, $max_id);
 
         return Notice::getStreamByIds($ids);
     }
 
-    function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
+    function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0)
     {
         $notice = new Notice();
 
@@ -598,10 +598,6 @@ class Notice extends Memcached_DataObject
             $notice->whereAdd('id <= ' . $max_id);
         }
 
-        if (!is_null($since)) {
-            $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
-        }
-
         $ids = array();
 
         if ($notice->find()) {
@@ -616,17 +612,17 @@ class Notice extends Memcached_DataObject
         return $ids;
     }
 
-    function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
+    function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0)
     {
         $ids = Notice::stream(array('Notice', '_conversationStreamDirect'),
                               array($id),
                               'notice:conversation_ids:'.$id,
-                              $offset, $limit, $since_id, $max_id, $since);
+                              $offset, $limit, $since_id, $max_id);
 
         return Notice::getStreamByIds($ids);
     }
 
-    function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
+    function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0)
     {
         $notice = new Notice();
 
@@ -649,10 +645,6 @@ class Notice extends Memcached_DataObject
             $notice->whereAdd('id <= ' . $max_id);
         }
 
-        if (!is_null($since)) {
-            $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
-        }
-
         $ids = array();
 
         if ($notice->find()) {
@@ -944,6 +936,8 @@ class Notice extends Memcached_DataObject
                 $reply->profile_id = $user->id;
 
                 $id = $reply->insert();
+
+                self::blow('reply:stream:%d', $user->id);
             }
         }
 
@@ -971,7 +965,10 @@ class Notice extends Memcached_DataObject
 
         $sender = Profile::staticGet($this->profile_id);
 
-        $mentions = common_find_mentions($this->profile_id, $this->content);
+        // @todo ideally this parser information would only
+        // be calculated once.
+
+        $mentions = common_find_mentions($this->content, $this);
 
         $replied = array();
 
@@ -1265,16 +1262,16 @@ class Notice extends Memcached_DataObject
         }
     }
 
-    function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
+    function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0)
     {
         $cache = common_memcache();
 
         if (empty($cache) ||
-            $since_id != 0 || $max_id != 0 || (!is_null($since) && $since > 0) ||
+            $since_id != 0 || $max_id != 0 ||
             is_null($limit) ||
             ($offset + $limit) > NOTICE_CACHE_WINDOW) {
             return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id,
-                                                                      $max_id, $since)));
+                                                                      $max_id)));
         }
 
         $idkey = common_cache_key($cachekey);
index c27dcdfd616079359df0f4db998f7c09ca285537..47ed6b22db628a8baaed7bd797c027ff0d5531fa 100644 (file)
@@ -49,12 +49,12 @@ class Notice_inbox extends Memcached_DataObject
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
 
-    function stream($user_id, $offset, $limit, $since_id, $max_id, $since, $own=false)
+    function stream($user_id, $offset, $limit, $since_id, $max_id, $own=false)
     {
         throw new Exception('Notice_inbox no longer used; use Inbox');
     }
 
-    function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id, $since)
+    function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id)
     {
         throw new Exception('Notice_inbox no longer used; use Inbox');
     }
index 4fd76e8ea85e8c04c9119b370bf3c89a462d7dec..a5d0716a7136d6b00c073b0679f7afe273340eb0 100644 (file)
@@ -46,7 +46,7 @@ class Notice_tag extends Memcached_DataObject
         return Notice::getStreamByIds($ids);
     }
 
-    function _streamDirect($tag, $offset, $limit, $since_id, $max_id, $since)
+    function _streamDirect($tag, $offset, $limit, $since_id, $max_id)
     {
         $nt = new Notice_tag();
 
@@ -63,10 +63,6 @@ class Notice_tag extends Memcached_DataObject
             $nt->whereAdd('notice_id < ' . $max_id);
         }
 
-        if (!is_null($since)) {
-            $nt->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
-        }
-
         $nt->orderBy('notice_id DESC');
 
         if (!is_null($offset)) {
index 78223b34a1956d0e16901e949810cbc3c367f6ba..470ef33207da5e7e9a49cb9b1f3768512c0d86d2 100644 (file)
@@ -163,27 +163,27 @@ class Profile extends Memcached_DataObject
         return null;
     }
 
-    function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0, $since=null)
+    function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
     {
         $ids = Notice::stream(array($this, '_streamTaggedDirect'),
                               array($tag),
                               'profile:notice_ids_tagged:' . $this->id . ':' . $tag,
-                              $offset, $limit, $since_id, $max_id, $since);
+                              $offset, $limit, $since_id, $max_id);
         return Notice::getStreamByIds($ids);
     }
 
-    function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0, $since=null)
+    function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
     {
         // XXX: I'm not sure this is going to be any faster. It probably isn't.
         $ids = Notice::stream(array($this, '_streamDirect'),
                               array(),
                               'profile:notice_ids:' . $this->id,
-                              $offset, $limit, $since_id, $max_id, $since);
+                              $offset, $limit, $since_id, $max_id);
 
         return Notice::getStreamByIds($ids);
     }
 
-    function _streamTaggedDirect($tag, $offset, $limit, $since_id, $max_id, $since)
+    function _streamTaggedDirect($tag, $offset, $limit, $since_id, $max_id)
     {
         // XXX It would be nice to do this without a join
 
@@ -202,10 +202,6 @@ class Profile extends Memcached_DataObject
             $query .= " and id < $max_id";
         }
 
-        if (!is_null($since)) {
-            $query .= " and created > '" . date('Y-m-d H:i:s', $since) . "'";
-        }
-
         $query .= ' order by id DESC';
 
         if (!is_null($offset)) {
@@ -223,7 +219,7 @@ class Profile extends Memcached_DataObject
         return $ids;
     }
 
-    function _streamDirect($offset, $limit, $since_id, $max_id, $since = null)
+    function _streamDirect($offset, $limit, $since_id, $max_id)
     {
         $notice = new Notice();
 
@@ -240,10 +236,6 @@ class Profile extends Memcached_DataObject
             $notice->whereAdd('id <= ' . $max_id);
         }
 
-        if (!is_null($since)) {
-            $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
-        }
-
         $notice->orderBy('id DESC');
 
         if (!is_null($offset)) {
index 49b1e05e517e1bd34bb7645cead3148990f27c0b..659e04c9253934ad82dad90b0cd2ce4d198b32f5 100644 (file)
@@ -22,16 +22,16 @@ class Reply extends Memcached_DataObject
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
 
-    function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0, $since=null)
+    function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
     {
         $ids = Notice::stream(array('Reply', '_streamDirect'),
                               array($user_id),
                               'reply:stream:' . $user_id,
-                              $offset, $limit, $since_id, $max_id, $since);
+                              $offset, $limit, $since_id, $max_id);
         return $ids;
     }
 
-    function _streamDirect($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0, $since=null)
+    function _streamDirect($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
     {
         $reply = new Reply();
         $reply->profile_id = $user_id;
@@ -44,10 +44,6 @@ class Reply extends Memcached_DataObject
             $reply->whereAdd('notice_id < ' . $max_id);
         }
 
-        if (!is_null($since)) {
-            $reply->whereAdd('modified > \'' . date('Y-m-d H:i:s', $since) . '\'');
-        }
-
         $reply->orderBy('notice_id DESC');
 
         if (!is_null($offset)) {
index d6fb3fcbdda82f17d70dcce6692868dfed6f1840..9cef2df1ad7c222d407fc1c3177fd4fdc1f1fa44 100644 (file)
@@ -172,6 +172,28 @@ class Subscription extends Memcached_DataObject
 
             assert(!empty($sub));
 
+            // @todo: move this block to EndSubscribe handler for
+            // OMB plugin when it exists.
+
+            if (!empty($sub->token)) {
+
+                $token = new Token();
+
+                $token->tok    = $sub->token;
+
+                if ($token->find(true)) {
+
+                    $result = $token->delete();
+
+                    if (!$result) {
+                        common_log_db_error($token, 'DELETE', __FILE__);
+                        throw new Exception(_('Couldn\'t delete subscription OMB token.'));
+                    }
+                } else {
+                    common_log(LOG_ERR, "Couldn't find credentials with token {$token->tok}");
+                }
+            }
+
             $result = $sub->delete();
 
             if (!$result) {
index 10b1f486513a4bb5b38ecc5c48d4f8e371c5b684..57d76731b8a288f8d45bc1ba15dab42f48130125 100644 (file)
@@ -456,28 +456,28 @@ class User extends Memcached_DataObject
         return $user;
     }
 
-    function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
+    function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
     {
-        $ids = Reply::stream($this->id, $offset, $limit, $since_id, $before_id, $since);
+        $ids = Reply::stream($this->id, $offset, $limit, $since_id, $before_id);
         return Notice::getStreamByIds($ids);
     }
 
-    function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) {
+    function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) {
         $profile = $this->getProfile();
         if (!$profile) {
             return null;
         } else {
-            return $profile->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id, $since);
+            return $profile->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id);
         }
     }
 
-    function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
+    function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
     {
         $profile = $this->getProfile();
         if (!$profile) {
             return null;
         } else {
-            return $profile->getNotices($offset, $limit, $since_id, $before_id, $since);
+            return $profile->getNotices($offset, $limit, $since_id, $before_id);
         }
     }
 
@@ -487,24 +487,24 @@ class User extends Memcached_DataObject
         return Notice::getStreamByIds($ids);
     }
 
-    function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
+    function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
     {
-        return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, $since, false);
+        return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, false);
     }
 
-    function noticeInbox($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
+    function noticeInbox($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
     {
-        return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, $since, true);
+        return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, true);
     }
 
-    function friendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
+    function friendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
     {
-        return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, $since, false);
+        return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, false);
     }
 
-    function ownFriendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
+    function ownFriendsTimeline($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
     {
-        return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, $since, true);
+        return Inbox::streamNotices($this->id, $offset, $limit, $since_id, $before_id, true);
     }
 
     function blowFavesCache()
@@ -789,7 +789,7 @@ class User extends Memcached_DataObject
         return Notice::getStreamByIds($ids);
     }
 
-    function _repeatedByMeDirect($offset, $limit, $since_id, $max_id, $since)
+    function _repeatedByMeDirect($offset, $limit, $since_id, $max_id)
     {
         $notice = new Notice();
 
@@ -813,10 +813,6 @@ class User extends Memcached_DataObject
             $notice->whereAdd('id <= ' . $max_id);
         }
 
-        if (!is_null($since)) {
-            $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
-        }
-
         $ids = array();
 
         if ($notice->find()) {
@@ -836,12 +832,12 @@ class User extends Memcached_DataObject
         $ids = Notice::stream(array($this, '_repeatsOfMeDirect'),
                               array(),
                               'user:repeats_of_me:'.$this->id,
-                              $offset, $limit, $since_id, $max_id, null);
+                              $offset, $limit, $since_id, $max_id);
 
         return Notice::getStreamByIds($ids);
     }
 
-    function _repeatsOfMeDirect($offset, $limit, $since_id, $max_id, $since)
+    function _repeatsOfMeDirect($offset, $limit, $since_id, $max_id)
     {
         $qry =
           'SELECT DISTINCT original.id AS id ' .
@@ -856,10 +852,6 @@ class User extends Memcached_DataObject
             $qry .= 'AND original.id <= ' . $max_id . ' ';
         }
 
-        if (!is_null($since)) {
-            $qry .= 'AND original.modified > \'' . date('Y-m-d H:i:s', $since) . '\' ';
-        }
-
         // NOTE: we sort by fave time, not by notice time!
 
         $qry .= 'ORDER BY original.id DESC ';
index 7240e27037b32c91a6b6ba37a01e566fbb93b850..64fe024b34de06deeb19495aa4c429bb7f7f1502 100644 (file)
@@ -91,7 +91,7 @@ class User_group extends Memcached_DataObject
         return Notice::getStreamByIds($ids);
     }
 
-    function _streamDirect($offset, $limit, $since_id, $max_id, $since)
+    function _streamDirect($offset, $limit, $since_id, $max_id)
     {
         $inbox = new Group_inbox();
 
@@ -108,10 +108,6 @@ class User_group extends Memcached_DataObject
             $inbox->whereAdd('notice_id <= ' . $max_id);
         }
 
-        if (!is_null($since)) {
-            $inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
-        }
-
         $inbox->orderBy('notice_id DESC');
 
         if (!is_null($offset)) {
index b10e47dbcb67f769f1577c046c9222e0e772c600..f30572154197c96d922c545af69a6c256d495eaa 100644 (file)
@@ -110,3 +110,8 @@ insert into queue_item_new (frame,transport,created,claimed)
 alter table queue_item rename to queue_item_old;
 alter table queue_item_new rename to queue_item;
 
+alter table file_to_post
+    add index post_id_idx (post_id);
+
+alter table group_inbox
+    add index group_inbox_notice_id_idx (notice_id);
index 4158f0167db2a9f9def64f29ac8fff1fef574382..3f95948e1ed5ede5d9cde05511f78fb49f657021 100644 (file)
@@ -458,7 +458,8 @@ create table group_inbox (
     created datetime not null comment 'date the notice was created',
 
     constraint primary key (group_id, notice_id),
-    index group_inbox_created_idx (created)
+    index group_inbox_created_idx (created),
+    index group_inbox_notice_id_idx (notice_id)
 
 ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
 
@@ -523,7 +524,8 @@ create table file_to_post (
     post_id integer comment 'id of the notice it belongs to' references notice (id),
     modified timestamp comment 'date this record was modified',
 
-    constraint primary key (file_id, post_id)
+    constraint primary key (file_id, post_id),
+    index post_id_idx (post_id)
 
 ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
 
index 94b1fa5358b5667e4cdd23b9f28c52081546abac..0918c68582b31cc2c2ff7e9b2d07c6f2c0420a34 100644 (file)
@@ -974,7 +974,7 @@ class Action extends HTMLOutputter // lawsuit
 
         if (is_null($arg)) {
             return $def;
-        } else if (in_array($arg, array('true', 'yes', '1'))) {
+        } else if (in_array($arg, array('true', 'yes', '1', 'on'))) {
             return true;
         } else if (in_array($arg, array('false', 'no', '0'))) {
             return false;
index b2015321386ae454df53ce32587c3d26f8fe2e0c..7926d05697e667998d27d0ea89d823dff0861a3e 100644 (file)
@@ -344,16 +344,18 @@ class ActivityUtils
 
     static function getLink(DOMNode $element, $rel, $type=null)
     {
-        $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
+        $els = $element->childNodes;
 
-        foreach ($links as $link) {
+        foreach ($els as $link) {
+            if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) {
 
-            $linkRel = $link->getAttribute(self::REL);
-            $linkType = $link->getAttribute(self::TYPE);
+                $linkRel = $link->getAttribute(self::REL);
+                $linkType = $link->getAttribute(self::TYPE);
 
-            if ($linkRel == $rel &&
-                (is_null($type) || $linkType == $type)) {
-                return $link->getAttribute(self::HREF);
+                if ($linkRel == $rel &&
+                    (is_null($type) || $linkType == $type)) {
+                    return $link->getAttribute(self::HREF);
+                }
             }
         }
 
@@ -362,17 +364,19 @@ class ActivityUtils
 
     static function getLinks(DOMNode $element, $rel, $type=null)
     {
-        $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
+        $els = $element->childNodes;
         $out = array();
 
-        foreach ($links as $link) {
+        foreach ($els as $link) {
+            if ($link->localName == self::LINK && $link->namespaceURI == self::ATOM) {
 
-            $linkRel = $link->getAttribute(self::REL);
-            $linkType = $link->getAttribute(self::TYPE);
+                $linkRel = $link->getAttribute(self::REL);
+                $linkType = $link->getAttribute(self::TYPE);
 
-            if ($linkRel == $rel &&
-                (is_null($type) || $linkType == $type)) {
-                $out[] = $link;
+                if ($linkRel == $rel &&
+                    (is_null($type) || $linkType == $type)) {
+                    $out[] = $link;
+                }
             }
         }
 
index f05627b317cd47ffbfc9eff4cbf3988301a20a89..536d97cdf5d440329a4a0a16681289840020b026 100644 (file)
@@ -103,7 +103,7 @@ class AdminPanelAction extends Action
 
         $name = mb_substr($name, 0, -10);
 
-        if (!in_array($name, common_config('admin', 'panels'))) {
+        if (!self::canAdmin($name)) {
             $this->clientError(_('Changes to that panel are not allowed.'), 403);
             return false;
         }
@@ -262,6 +262,17 @@ class AdminPanelAction extends Action
 
         return $result;
     }
+
+    function canAdmin($name)
+    {
+        $isOK = false;
+
+        if (Event::handle('AdminPanelCheck', array($name, &$isOK))) {
+            $isOK = in_array($name, common_config('admin', 'panels'));
+        }
+
+        return $isOK;
+    }
 }
 
 /**
@@ -307,32 +318,32 @@ class AdminPanelNav extends Widget
 
         if (Event::handle('StartAdminPanelNav', array($this))) {
 
-            if ($this->canAdmin('site')) {
+            if (AdminPanelAction::canAdmin('site')) {
                 $this->out->menuItem(common_local_url('siteadminpanel'), _('Site'),
                                      _('Basic site configuration'), $action_name == 'siteadminpanel', 'nav_site_admin_panel');
             }
 
-            if ($this->canAdmin('design')) {
+            if (AdminPanelAction::canAdmin('design')) {
                 $this->out->menuItem(common_local_url('designadminpanel'), _('Design'),
                                      _('Design configuration'), $action_name == 'designadminpanel', 'nav_design_admin_panel');
             }
 
-            if ($this->canAdmin('user')) {
+            if (AdminPanelAction::canAdmin('user')) {
                 $this->out->menuItem(common_local_url('useradminpanel'), _('User'),
                                      _('User configuration'), $action_name == 'useradminpanel', 'nav_design_admin_panel');
             }
 
-            if ($this->canAdmin('access')) {
+            if (AdminPanelAction::canAdmin('access')) {
                 $this->out->menuItem(common_local_url('accessadminpanel'), _('Access'),
                                      _('Access configuration'), $action_name == 'accessadminpanel', 'nav_design_admin_panel');
             }
 
-            if ($this->canAdmin('paths')) {
+            if (AdminPanelAction::canAdmin('paths')) {
                 $this->out->menuItem(common_local_url('pathsadminpanel'), _('Paths'),
                                     _('Paths configuration'), $action_name == 'pathsadminpanel', 'nav_design_admin_panel');
             }
 
-            if ($this->canAdmin('sessions')) {
+            if (AdminPanelAction::canAdmin('sessions')) {
                 $this->out->menuItem(common_local_url('sessionsadminpanel'), _('Sessions'),
                                      _('Sessions configuration'), $action_name == 'sessionsadminpanel', 'nav_design_admin_panel');
             }
@@ -342,8 +353,4 @@ class AdminPanelNav extends Widget
         $this->action->elementEnd('ul');
     }
 
-    function canAdmin($name)
-    {
-        return in_array($name, common_config('admin', 'panels'));
-    }
 }
index 9bfca3b66e49b643c403714c02744dd4ca0363fd..eef0ba637d20a52ef38c5bfcb2c3b2d520c05cbd 100644 (file)
@@ -63,7 +63,6 @@ class ApiAction extends Action
     var $count     = null;
     var $max_id    = null;
     var $since_id  = null;
-    var $since     = null;
 
     var $access    = self::READ_ONLY;  // read (default) or read-write
 
@@ -85,7 +84,10 @@ class ApiAction extends Action
         $this->count    = (int)$this->arg('count', 20);
         $this->max_id   = (int)$this->arg('max_id', 0);
         $this->since_id = (int)$this->arg('since_id', 0);
-        $this->since    = $this->arg('since');
+
+        if ($this->arg('since')) {
+            $this->clientError(_("since parameter is disabled for performance; use since_id"), 403);
+        }
 
         return true;
     }
@@ -1326,8 +1328,6 @@ class ApiAction extends Action
             case 'max_id':
                 $max_id = (int)$this->args['max_id'];
                 return ($max_id < 1) ? 0 : $max_id;
-            case 'since':
-                return strtotime($this->args['since']);
             default:
                 return parent::arg($key, $def);
             }
index 2dbe3b3c531ba481060854e7a7ad0f0d66b8ac65..546f6bbe4beecc327f25b3b6cf407fd7d1bb48fd 100644 (file)
@@ -22,7 +22,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
 //exit with 200 response, if this is checking fancy from the installer
 if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') {  exit; }
 
-define('STATUSNET_VERSION', '0.9.0beta6');
+define('STATUSNET_VERSION', '0.9.0beta6+bugfix1');
 define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
 
 define('STATUSNET_CODENAME', 'Stand');
index 28a563d875aff8c2e7ce621ba204ada667f9ee77..88a9252414325ba8d38cb2f0f327770ff186133a 100644 (file)
@@ -540,22 +540,40 @@ class NoticeListItem extends Widget
     function showContext()
     {
         $hasConversation = false;
-        if( !empty($this->notice->conversation)
-            && $this->notice->conversation != $this->notice->id){
-            $hasConversation = true;
-        }else{
-            $conversation = Notice::conversationStream($this->notice->id, 1, 1);
-            if($conversation->N > 0){
+        if (!empty($this->notice->conversation)) {
+            $conversation = Notice::conversationStream(
+                $this->notice->conversation,
+                1,
+                1
+            );
+            if ($conversation->N > 0) {
                 $hasConversation = true;
             }
         }
-        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,
-                                           'class' => 'response'),
-                                _('in context'));
+        if ($hasConversation) {
+            $conv = Conversation::staticGet(
+                'id',
+                $this->notice->conversation
+            );
+            $convurl = $conv->uri;
+            if (!empty($convurl)) {
+                $this->out->text(' ');
+                $this->out->element(
+                    'a',
+                    array(
+                    'href' => $convurl.'#notice-'.$this->notice->id,
+                    'class' => 'response'),
+                    _('in context')
+                );
+            } else {
+                $msg = sprintf(
+                    "Couldn't find Conversation ID %d to make 'in context'"
+                    . "link for Notice ID %d",
+                    $this->notice->conversation,
+                    $this->notice->id
+                );
+                common_log(LOG_WARNING, $msg);
+            }
         }
     }
 
index eabe37f9fa4a944d76f99c91c9a57de69c6c0f23..a6a6de7505c946bbdd0a24b7760dea3ff8abb940 100644 (file)
@@ -390,7 +390,7 @@ class StatusNetOAuthDataStore extends OAuthDataStore
         $sub->subscribed = $user->id;
 
         if (!$sub->find(true)) {
-            return 0;
+            return array();
         }
 
         /* Since we do not use OMB_Service_Provider’s action methods, there
index 17132a594f6ec20ea2e079270d9a791895297f2a..db60fa0ef2d0a122a65805cffe1154713df02658 100644 (file)
@@ -77,7 +77,7 @@ function omb_broadcast_notice($notice)
     /* Get remote users subscribed to this profile. */
     $rp = new Remote_profile();
 
-    $rp->query('SELECT postnoticeurl, token, secret ' .
+    $rp->query('SELECT remote_profile.*, secret, token ' .
                'FROM subscription JOIN remote_profile ' .
                'ON subscription.subscriber = remote_profile.id ' .
                'WHERE subscription.subscribed = ' . $notice->profile_id . ' ');
@@ -93,7 +93,8 @@ function omb_broadcast_notice($notice)
 
         /* Post notice. */
         $service = new StatusNet_OMB_Service_Consumer(
-                     array(OMB_ENDPOINT_POSTNOTICE => $rp->postnoticeurl));
+                     array(OMB_ENDPOINT_POSTNOTICE => $rp->postnoticeurl),
+                                                      $rp->uri);
         try {
             $service->setToken($rp->token, $rp->secret);
             $service->postNotice($omb_notice);
@@ -125,7 +126,7 @@ function omb_broadcast_profile($profile)
     /* Get remote users subscribed to this profile. */
     $rp = new Remote_profile();
 
-    $rp->query('SELECT updateprofileurl, token, secret ' .
+    $rp->query('SELECT remote_profile.*, secret, token ' .
                'FROM subscription JOIN remote_profile ' .
                'ON subscription.subscriber = remote_profile.id ' .
                'WHERE subscription.subscribed = ' . $profile->id . ' ');
@@ -141,7 +142,8 @@ function omb_broadcast_profile($profile)
 
         /* Update profile. */
         $service = new StatusNet_OMB_Service_Consumer(
-                     array(OMB_ENDPOINT_UPDATEPROFILE => $rp->updateprofileurl));
+                     array(OMB_ENDPOINT_UPDATEPROFILE => $rp->updateprofileurl),
+                                                      $rp->uri);
         try {
             $service->setToken($rp->token, $rp->secret);
             $service->updateProfile($omb_profile);
@@ -159,13 +161,14 @@ function omb_broadcast_profile($profile)
 }
 
 class StatusNet_OMB_Service_Consumer extends OMB_Service_Consumer {
-    public function __construct($urls)
+    public function __construct($urls, $listener_uri=null)
     {
         $this->services       = $urls;
         $this->datastore      = omb_oauth_datastore();
         $this->oauth_consumer = omb_oauth_consumer();
         $this->fetcher        = Auth_Yadis_Yadis::getHTTPFetcher();
         $this->fetcher->timeout = intval(common_config('omb', 'timeout'));
+        $this->listener_uri   = $listener_uri;
     }
 
 }
index 77a332b5ecdd8386fcb48fc68d1657b7e34b9929..add1b0ae67e4ec5512713244627e2d5a4deac2d2 100644 (file)
@@ -426,14 +426,14 @@ function common_render_content($text, $notice)
 {
     $r = common_render_text($text);
     $id = $notice->profile_id;
-    $r = common_linkify_mentions($id, $r);
+    $r = common_linkify_mentions($r, $notice);
     $r = preg_replace('/(^|[\s\.\,\:\;]+)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r);
     return $r;
 }
 
-function common_linkify_mentions($profile_id, $text)
+function common_linkify_mentions($text, $notice)
 {
-    $mentions = common_find_mentions($profile_id, $text);
+    $mentions = common_find_mentions($text, $notice);
 
     // We need to go through in reverse order by position,
     // so our positions stay valid despite our fudging with the
@@ -487,11 +487,11 @@ function common_linkify_mention($mention)
     return $output;
 }
 
-function common_find_mentions($profile_id, $text)
+function common_find_mentions($text, $notice)
 {
     $mentions = array();
 
-    $sender = Profile::staticGet('id', $profile_id);
+    $sender = Profile::staticGet('id', $notice->profile_id);
 
     if (empty($sender)) {
         return $mentions;
@@ -499,6 +499,30 @@ function common_find_mentions($profile_id, $text)
 
     if (Event::handle('StartFindMentions', array($sender, $text, &$mentions))) {
 
+        // Get the context of the original notice, if any
+
+        $originalAuthor   = null;
+        $originalNotice   = null;
+        $originalMentions = array();
+
+        // Is it a reply?
+
+        if (!empty($notice) && !empty($notice->reply_to)) {
+            $originalNotice = Notice::staticGet('id', $notice->reply_to);
+            if (!empty($originalNotice)) {
+                $originalAuthor = Profile::staticGet('id', $originalNotice->profile_id);
+
+                $ids = $originalNotice->getReplies();
+
+                foreach ($ids as $id) {
+                    $repliedTo = Profile::staticGet('id', $id);
+                    if (!empty($repliedTo)) {
+                        $originalMentions[$repliedTo->nickname] = $repliedTo;
+                    }
+                }
+            }
+        }
+
         preg_match_all('/^T ([A-Z0-9]{1,64}) /',
                        $text,
                        $tmatches,
@@ -514,7 +538,22 @@ function common_find_mentions($profile_id, $text)
         foreach ($matches as $match) {
 
             $nickname = common_canonical_nickname($match[0]);
-            $mentioned = common_relative_profile($sender, $nickname);
+
+            // Try to get a profile for this nickname.
+            // Start with conversation context, then go to
+            // sender context.
+
+            if (!empty($originalAuthor) && $originalAuthor->nickname == $nickname) {
+
+                $mentioned = $originalAuthor;
+
+            } else if (!empty($originalMentions) &&
+                       array_key_exists($nickname, $originalMentions)) {
+
+                $mentioned = $originalMentions[$nickname];
+            } else {
+                $mentioned = common_relative_profile($sender, $nickname);
+            }
 
             if (!empty($mentioned)) {
 
@@ -869,7 +908,7 @@ function common_relative_profile($sender, $nickname, $dt=null)
     return null;
 }
 
-function common_local_url($action, $args=null, $params=null, $fragment=null)
+function common_local_url($action, $args=null, $params=null, $fragment=null, $addSession=true)
 {
     $r = Router::get();
     $path = $r->build($action, $args, $params, $fragment);
@@ -877,12 +916,12 @@ function common_local_url($action, $args=null, $params=null, $fragment=null)
     $ssl = common_is_sensitive($action);
 
     if (common_config('site','fancy')) {
-        $url = common_path(mb_substr($path, 1), $ssl);
+        $url = common_path(mb_substr($path, 1), $ssl, $addSession);
     } else {
         if (mb_strpos($path, '/index.php') === 0) {
-            $url = common_path(mb_substr($path, 1), $ssl);
+            $url = common_path(mb_substr($path, 1), $ssl, $addSession);
         } else {
-            $url = common_path('index.php'.$path, $ssl);
+            $url = common_path('index.php'.$path, $ssl, $addSession);
         }
     }
     return $url;
@@ -901,7 +940,7 @@ function common_is_sensitive($action)
     return $ssl;
 }
 
-function common_path($relative, $ssl=false)
+function common_path($relative, $ssl=false, $addSession=true)
 {
     $pathpart = (common_config('site', 'path')) ? common_config('site', 'path')."/" : '';
 
@@ -925,7 +964,9 @@ function common_path($relative, $ssl=false)
         }
     }
 
-    $relative = common_inject_session($relative, $serverpart);
+    if ($addSession) {
+        $relative = common_inject_session($relative, $serverpart);
+    }
 
     return $proto.'://'.$serverpart.'/'.$pathpart.$relative;
 }
@@ -1147,14 +1188,15 @@ function common_broadcast_profile(Profile $profile)
 
 function common_profile_url($nickname)
 {
-    return common_local_url('showstream', array('nickname' => $nickname));
+    return common_local_url('showstream', array('nickname' => $nickname),
+                            null, null, false);
 }
 
 // Should make up a reasonable root URL
 
 function common_root_url($ssl=false)
 {
-    $url = common_path('', $ssl);
+    $url = common_path('', $ssl, false);
     $i = strpos($url, '?');
     if ($i !== false) {
         $url = substr($url, 0, $i);
@@ -1439,7 +1481,8 @@ function common_remove_magic_from_request()
 
 function common_user_uri(&$user)
 {
-    return common_local_url('userbyid', array('id' => $user->id));
+    return common_local_url('userbyid', array('id' => $user->id),
+                            null, null, false);
 }
 
 function common_notice_uri(&$notice)
index cf44e2d3c960111c3679c3283fb8a86b6278abe0..8e143449781eea68b849d4c9f543784e0f42e516 100644 (file)
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2010-02-24 23:49+0000\n"
+"POT-Creation-Date: 2010-03-01 14:08+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,7 +17,7 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
 
-#: actions/accessadminpanel.php:54 lib/adminpanelaction.php:326
+#: actions/accessadminpanel.php:54 lib/adminpanelaction.php:337
 msgid "Access"
 msgstr ""
 
@@ -88,14 +88,15 @@ msgstr ""
 #: actions/apitimelinehome.php:79 actions/apitimelinementions.php:79
 #: actions/apitimelineuser.php:81 actions/avatarbynickname.php:75
 #: actions/favoritesrss.php:74 actions/foaf.php:40 actions/foaf.php:58
-#: actions/microsummary.php:62 actions/newmessage.php:116 actions/otp.php:76
-#: actions/remotesubscribe.php:145 actions/remotesubscribe.php:154
-#: actions/replies.php:73 actions/repliesrss.php:38 actions/rsd.php:116
-#: actions/showfavorites.php:105 actions/userbyid.php:74
-#: actions/usergroups.php:91 actions/userrss.php:38 actions/xrds.php:71
-#: lib/command.php:163 lib/command.php:302 lib/command.php:355
-#: lib/command.php:401 lib/command.php:462 lib/command.php:518
-#: lib/galleryaction.php:59 lib/mailbox.php:82 lib/profileaction.php:77
+#: actions/hcard.php:67 actions/microsummary.php:62 actions/newmessage.php:116
+#: actions/otp.php:76 actions/remotesubscribe.php:145
+#: actions/remotesubscribe.php:154 actions/replies.php:73
+#: actions/repliesrss.php:38 actions/rsd.php:116 actions/showfavorites.php:105
+#: actions/userbyid.php:74 actions/usergroups.php:91 actions/userrss.php:38
+#: actions/xrds.php:71 lib/command.php:163 lib/command.php:302
+#: lib/command.php:355 lib/command.php:401 lib/command.php:462
+#: lib/command.php:518 lib/galleryaction.php:59 lib/mailbox.php:82
+#: lib/profileaction.php:77
 msgid "No such user."
 msgstr ""
 
@@ -171,20 +172,20 @@ msgstr ""
 #: actions/apiaccountverifycredentials.php:70 actions/apidirectmessage.php:156
 #: actions/apifavoritecreate.php:99 actions/apifavoritedestroy.php:100
 #: actions/apifriendshipscreate.php:100 actions/apifriendshipsdestroy.php:100
-#: actions/apifriendshipsshow.php:128 actions/apigroupcreate.php:136
+#: actions/apifriendshipsshow.php:128 actions/apigroupcreate.php:138
 #: actions/apigroupismember.php:114 actions/apigroupjoin.php:155
 #: actions/apigroupleave.php:141 actions/apigrouplist.php:132
 #: actions/apigrouplistall.php:120 actions/apigroupmembership.php:106
-#: actions/apigroupshow.php:115 actions/apihelptest.php:88
+#: actions/apigroupshow.php:105 actions/apihelptest.php:88
 #: actions/apistatusesdestroy.php:102 actions/apistatusesretweets.php:112
-#: actions/apistatusesshow.php:108 actions/apistatusnetconfig.php:137
+#: actions/apistatusesshow.php:108 actions/apistatusnetconfig.php:131
 #: actions/apistatusnetversion.php:93 actions/apisubscriptions.php:111
 #: actions/apitimelinefavorites.php:183 actions/apitimelinefriends.php:187
-#: actions/apitimelinegroup.php:195 actions/apitimelinehome.php:184
+#: actions/apitimelinegroup.php:185 actions/apitimelinehome.php:184
 #: actions/apitimelinementions.php:175 actions/apitimelinepublic.php:152
 #: actions/apitimelineretweetedtome.php:121
 #: actions/apitimelineretweetsofme.php:152 actions/apitimelinetag.php:166
-#: actions/apitimelineuser.php:207 actions/apiusershow.php:101
+#: actions/apitimelineuser.php:196 actions/apiusershow.php:101
 msgid "API method not found."
 msgstr ""
 
@@ -216,8 +217,9 @@ msgstr ""
 #: actions/apiaccountupdateprofilebackgroundimage.php:194
 #: actions/apiaccountupdateprofilecolors.php:185
 #: actions/apiaccountupdateprofileimage.php:130 actions/apiusershow.php:108
-#: actions/avatarbynickname.php:80 actions/foaf.php:65 actions/replies.php:80
-#: actions/usergroups.php:98 lib/galleryaction.php:66 lib/profileaction.php:84
+#: actions/avatarbynickname.php:80 actions/foaf.php:65 actions/hcard.php:74
+#: actions/replies.php:80 actions/usergroups.php:98 lib/galleryaction.php:66
+#: lib/profileaction.php:84
 msgid "User has no profile."
 msgstr ""
 
@@ -241,7 +243,7 @@ msgstr ""
 #: actions/apiaccountupdateprofilebackgroundimage.php:146
 #: actions/apiaccountupdateprofilecolors.php:164
 #: actions/apiaccountupdateprofilecolors.php:174
-#: actions/groupdesignsettings.php:287 actions/groupdesignsettings.php:297
+#: actions/groupdesignsettings.php:290 actions/groupdesignsettings.php:300
 #: actions/userdesignsettings.php:210 actions/userdesignsettings.php:220
 #: actions/userdesignsettings.php:263 actions/userdesignsettings.php:273
 msgid "Unable to save your design settings."
@@ -351,87 +353,87 @@ msgstr ""
 msgid "Could not find target user."
 msgstr ""
 
-#: actions/apigroupcreate.php:164 actions/editgroup.php:182
+#: actions/apigroupcreate.php:166 actions/editgroup.php:186
 #: actions/newgroup.php:126 actions/profilesettings.php:215
 #: actions/register.php:205
 msgid "Nickname must have only lowercase letters and numbers and no spaces."
 msgstr ""
 
-#: actions/apigroupcreate.php:173 actions/editgroup.php:186
+#: actions/apigroupcreate.php:175 actions/editgroup.php:190
 #: actions/newgroup.php:130 actions/profilesettings.php:238
 #: actions/register.php:208
 msgid "Nickname already in use. Try another one."
 msgstr ""
 
-#: actions/apigroupcreate.php:180 actions/editgroup.php:189
+#: actions/apigroupcreate.php:182 actions/editgroup.php:193
 #: actions/newgroup.php:133 actions/profilesettings.php:218
 #: actions/register.php:210
 msgid "Not a valid nickname."
 msgstr ""
 
-#: actions/apigroupcreate.php:196 actions/editapplication.php:215
-#: actions/editgroup.php:195 actions/newapplication.php:203
+#: actions/apigroupcreate.php:198 actions/editapplication.php:215
+#: actions/editgroup.php:199 actions/newapplication.php:203
 #: actions/newgroup.php:139 actions/profilesettings.php:222
 #: actions/register.php:217
 msgid "Homepage is not a valid URL."
 msgstr ""
 
-#: actions/apigroupcreate.php:205 actions/editgroup.php:198
+#: actions/apigroupcreate.php:207 actions/editgroup.php:202
 #: actions/newgroup.php:142 actions/profilesettings.php:225
 #: actions/register.php:220
 msgid "Full name is too long (max 255 chars)."
 msgstr ""
 
-#: actions/apigroupcreate.php:213 actions/editapplication.php:190
+#: actions/apigroupcreate.php:215 actions/editapplication.php:190
 #: actions/newapplication.php:172
 #, php-format
 msgid "Description is too long (max %d chars)."
 msgstr ""
 
-#: actions/apigroupcreate.php:224 actions/editgroup.php:204
+#: actions/apigroupcreate.php:226 actions/editgroup.php:208
 #: actions/newgroup.php:148 actions/profilesettings.php:232
 #: actions/register.php:227
 msgid "Location is too long (max 255 chars)."
 msgstr ""
 
-#: actions/apigroupcreate.php:243 actions/editgroup.php:215
+#: actions/apigroupcreate.php:245 actions/editgroup.php:219
 #: actions/newgroup.php:159
 #, php-format
 msgid "Too many aliases! Maximum %d."
 msgstr ""
 
-#: actions/apigroupcreate.php:264 actions/editgroup.php:224
+#: actions/apigroupcreate.php:266 actions/editgroup.php:228
 #: actions/newgroup.php:168
 #, php-format
 msgid "Invalid alias: \"%s\""
 msgstr ""
 
-#: actions/apigroupcreate.php:273 actions/editgroup.php:228
+#: actions/apigroupcreate.php:275 actions/editgroup.php:232
 #: actions/newgroup.php:172
 #, php-format
 msgid "Alias \"%s\" already in use. Try another one."
 msgstr ""
 
-#: actions/apigroupcreate.php:286 actions/editgroup.php:234
+#: actions/apigroupcreate.php:288 actions/editgroup.php:238
 #: actions/newgroup.php:178
 msgid "Alias can't be the same as nickname."
 msgstr ""
 
 #: actions/apigroupismember.php:95 actions/apigroupjoin.php:104
 #: actions/apigroupleave.php:104 actions/apigroupmembership.php:91
-#: actions/apigroupshow.php:82 actions/apitimelinegroup.php:91
+#: actions/apigroupshow.php:90 actions/apitimelinegroup.php:91
 msgid "Group not found!"
 msgstr ""
 
-#: actions/apigroupjoin.php:110 actions/joingroup.php:90
+#: actions/apigroupjoin.php:110 actions/joingroup.php:100
 msgid "You are already a member of that group."
 msgstr ""
 
-#: actions/apigroupjoin.php:119 actions/joingroup.php:95 lib/command.php:221
+#: actions/apigroupjoin.php:119 actions/joingroup.php:105 lib/command.php:221
 msgid "You have been blocked from that group by the admin."
 msgstr ""
 
-#: actions/apigroupjoin.php:138 actions/joingroup.php:124
+#: actions/apigroupjoin.php:138 actions/joingroup.php:134
 #, php-format
 msgid "Could not join user %1$s to group %2$s."
 msgstr ""
@@ -440,7 +442,7 @@ msgstr ""
 msgid "You are not a member of this group."
 msgstr ""
 
-#: actions/apigroupleave.php:124 actions/leavegroup.php:119
+#: actions/apigroupleave.php:124 actions/leavegroup.php:129
 #, php-format
 msgid "Could not remove user %1$s from group %2$s."
 msgstr ""
@@ -471,7 +473,7 @@ msgstr ""
 #: actions/apioauthauthorize.php:123 actions/avatarsettings.php:268
 #: actions/deletenotice.php:157 actions/disfavor.php:74
 #: actions/emailsettings.php:238 actions/favor.php:75 actions/geocode.php:54
-#: actions/groupblock.php:66 actions/grouplogo.php:309
+#: actions/groupblock.php:66 actions/grouplogo.php:312
 #: actions/groupunblock.php:66 actions/imsettings.php:206
 #: actions/invite.php:56 actions/login.php:115 actions/makeadmin.php:66
 #: actions/newmessage.php:135 actions/newnotice.php:103 actions/nudge.php:80
@@ -491,11 +493,11 @@ msgid "Invalid nickname / password!"
 msgstr ""
 
 #: actions/apioauthauthorize.php:159
-msgid "Database error deleting OAuth application user."
+msgid "DB error deleting OAuth app user."
 msgstr ""
 
 #: actions/apioauthauthorize.php:185
-msgid "Database error inserting OAuth application user."
+msgid "DB error inserting OAuth app user."
 msgstr ""
 
 #: actions/apioauthauthorize.php:214
@@ -512,7 +514,7 @@ msgstr ""
 
 #: actions/apioauthauthorize.php:232 actions/avatarsettings.php:281
 #: actions/designadminpanel.php:103 actions/editapplication.php:139
-#: actions/emailsettings.php:256 actions/grouplogo.php:319
+#: actions/emailsettings.php:256 actions/grouplogo.php:322
 #: actions/imsettings.php:220 actions/newapplication.php:121
 #: actions/oauthconnectionssettings.php:147 actions/recoverpassword.php:44
 #: actions/smssettings.php:248 lib/designsettings.php:304
@@ -541,7 +543,7 @@ msgstr ""
 
 #: actions/apioauthauthorize.php:313 actions/login.php:230
 #: actions/profilesettings.php:106 actions/register.php:424
-#: actions/showgroup.php:236 actions/tagother.php:94
+#: actions/showgroup.php:244 actions/tagother.php:94
 #: actions/userauthorization.php:145 lib/groupeditform.php:152
 #: lib/userprofile.php:131
 msgid "Nickname"
@@ -623,12 +625,12 @@ msgid "%1$s updates favorited by %2$s / %2$s."
 msgstr ""
 
 #: actions/apitimelinegroup.php:109 actions/apitimelineuser.php:118
-#: actions/grouprss.php:131 actions/userrss.php:90
+#: actions/grouprss.php:138 actions/userrss.php:90
 #, php-format
 msgid "%s timeline"
 msgstr ""
 
-#: actions/apitimelinegroup.php:114 actions/apitimelineuser.php:126
+#: actions/apitimelinegroup.php:112 actions/apitimelineuser.php:124
 #: actions/userrss.php:92
 #, php-format
 msgid "Updates from %1$s on %2$s!"
@@ -685,8 +687,7 @@ msgstr ""
 #: actions/avatarbynickname.php:59 actions/blockedfromgroup.php:73
 #: actions/editgroup.php:84 actions/groupdesignsettings.php:84
 #: actions/grouplogo.php:86 actions/groupmembers.php:76
-#: actions/grouprss.php:91 actions/joingroup.php:76 actions/leavegroup.php:76
-#: actions/showgroup.php:121
+#: actions/grouprss.php:91 actions/showgroup.php:121
 msgid "No nickname."
 msgstr ""
 
@@ -698,7 +699,7 @@ msgstr ""
 msgid "Invalid size."
 msgstr ""
 
-#: actions/avatarsettings.php:67 actions/showgroup.php:221
+#: actions/avatarsettings.php:67 actions/showgroup.php:229
 #: lib/accountsettingsaction.php:112
 msgid "Avatar"
 msgstr ""
@@ -715,30 +716,30 @@ msgid "User without matching profile"
 msgstr ""
 
 #: actions/avatarsettings.php:119 actions/avatarsettings.php:197
-#: actions/grouplogo.php:251
+#: actions/grouplogo.php:254
 msgid "Avatar settings"
 msgstr ""
 
 #: actions/avatarsettings.php:127 actions/avatarsettings.php:205
-#: actions/grouplogo.php:199 actions/grouplogo.php:259
+#: actions/grouplogo.php:202 actions/grouplogo.php:262
 msgid "Original"
 msgstr ""
 
 #: actions/avatarsettings.php:142 actions/avatarsettings.php:217
-#: actions/grouplogo.php:210 actions/grouplogo.php:271
+#: actions/grouplogo.php:213 actions/grouplogo.php:274
 msgid "Preview"
 msgstr ""
 
 #: actions/avatarsettings.php:149 actions/showapplication.php:252
-#: lib/deleteuserform.php:66 lib/noticelist.php:637
+#: lib/deleteuserform.php:66 lib/noticelist.php:655
 msgid "Delete"
 msgstr ""
 
-#: actions/avatarsettings.php:166 actions/grouplogo.php:233
+#: actions/avatarsettings.php:166 actions/grouplogo.php:236
 msgid "Upload"
 msgstr ""
 
-#: actions/avatarsettings.php:231 actions/grouplogo.php:286
+#: actions/avatarsettings.php:231 actions/grouplogo.php:289
 msgid "Crop"
 msgstr ""
 
@@ -746,7 +747,7 @@ msgstr ""
 msgid "Pick a square area of the image to be your avatar"
 msgstr ""
 
-#: actions/avatarsettings.php:343 actions/grouplogo.php:377
+#: actions/avatarsettings.php:343 actions/grouplogo.php:380
 msgid "Lost our file data."
 msgstr ""
 
@@ -778,22 +779,22 @@ msgid ""
 msgstr ""
 
 #: actions/block.php:143 actions/deleteapplication.php:153
-#: actions/deletenotice.php:145 actions/deleteuser.php:147
+#: actions/deletenotice.php:145 actions/deleteuser.php:150
 #: actions/groupblock.php:178
 msgid "No"
 msgstr ""
 
-#: actions/block.php:143 actions/deleteuser.php:147
+#: actions/block.php:143 actions/deleteuser.php:150
 msgid "Do not block this user"
 msgstr ""
 
 #: actions/block.php:144 actions/deleteapplication.php:158
-#: actions/deletenotice.php:146 actions/deleteuser.php:148
+#: actions/deletenotice.php:146 actions/deleteuser.php:151
 #: actions/groupblock.php:179 lib/repeatform.php:132
 msgid "Yes"
 msgstr ""
 
-#: actions/block.php:144 actions/groupmembers.php:348 lib/blockform.php:80
+#: actions/block.php:144 actions/groupmembers.php:355 lib/blockform.php:80
 msgid "Block this user"
 msgstr ""
 
@@ -801,39 +802,43 @@ msgstr ""
 msgid "Failed to save block information."
 msgstr ""
 
-#: actions/blockedfromgroup.php:80 actions/editgroup.php:96
-#: actions/foafgroup.php:44 actions/foafgroup.php:62 actions/groupblock.php:86
-#: actions/groupbyid.php:83 actions/groupdesignsettings.php:97
-#: actions/grouplogo.php:99 actions/groupmembers.php:83
-#: actions/grouprss.php:98 actions/groupunblock.php:86
-#: actions/joingroup.php:83 actions/leavegroup.php:83 actions/makeadmin.php:86
-#: actions/showgroup.php:137 lib/command.php:212 lib/command.php:260
+#: actions/blockedfromgroup.php:80 actions/blockedfromgroup.php:87
+#: actions/editgroup.php:100 actions/foafgroup.php:44 actions/foafgroup.php:62
+#: actions/foafgroup.php:69 actions/groupblock.php:86 actions/groupbyid.php:83
+#: actions/groupdesignsettings.php:100 actions/grouplogo.php:102
+#: actions/groupmembers.php:83 actions/groupmembers.php:90
+#: actions/grouprss.php:98 actions/grouprss.php:105
+#: actions/groupunblock.php:86 actions/joingroup.php:82
+#: actions/joingroup.php:93 actions/leavegroup.php:82
+#: actions/leavegroup.php:93 actions/makeadmin.php:86
+#: actions/showgroup.php:138 actions/showgroup.php:146 lib/command.php:212
+#: lib/command.php:260
 msgid "No such group."
 msgstr ""
 
-#: actions/blockedfromgroup.php:90
+#: actions/blockedfromgroup.php:97
 #, php-format
 msgid "%s blocked profiles"
 msgstr ""
 
-#: actions/blockedfromgroup.php:93
+#: actions/blockedfromgroup.php:100
 #, php-format
 msgid "%1$s blocked profiles, page %2$d"
 msgstr ""
 
-#: actions/blockedfromgroup.php:108
+#: actions/blockedfromgroup.php:115
 msgid "A list of the users blocked from joining this group."
 msgstr ""
 
-#: actions/blockedfromgroup.php:281
+#: actions/blockedfromgroup.php:288
 msgid "Unblock user from group"
 msgstr ""
 
-#: actions/blockedfromgroup.php:313 lib/unblockform.php:69
+#: actions/blockedfromgroup.php:320 lib/unblockform.php:69
 msgid "Unblock"
 msgstr ""
 
-#: actions/blockedfromgroup.php:313 lib/unblockform.php:80
+#: actions/blockedfromgroup.php:320 lib/unblockform.php:80
 msgid "Unblock this user"
 msgstr ""
 
@@ -876,7 +881,7 @@ msgid "Couldn't delete email confirmation."
 msgstr ""
 
 #: actions/confirmaddress.php:144
-msgid "Confirm address"
+msgid "Confirm Address"
 msgstr ""
 
 #: actions/confirmaddress.php:159
@@ -963,7 +968,7 @@ msgstr ""
 msgid "Do not delete this notice"
 msgstr ""
 
-#: actions/deletenotice.php:146 lib/noticelist.php:637
+#: actions/deletenotice.php:146 lib/noticelist.php:655
 msgid "Delete this notice"
 msgstr ""
 
@@ -979,18 +984,18 @@ msgstr ""
 msgid "Delete user"
 msgstr ""
 
-#: actions/deleteuser.php:135
+#: actions/deleteuser.php:136
 msgid ""
 "Are you sure you want to delete this user? This will clear all data about "
 "the user from the database, without a backup."
 msgstr ""
 
-#: actions/deleteuser.php:148 lib/deleteuserform.php:77
+#: actions/deleteuser.php:151 lib/deleteuserform.php:77
 msgid "Delete this user"
 msgstr ""
 
 #: actions/designadminpanel.php:62 lib/accountsettingsaction.php:124
-#: lib/adminpanelaction.php:316 lib/groupnav.php:119
+#: lib/adminpanelaction.php:327 lib/groupnav.php:119
 msgid "Design"
 msgstr ""
 
@@ -1182,29 +1187,29 @@ msgstr ""
 msgid "You must be logged in to create a group."
 msgstr ""
 
-#: actions/editgroup.php:103 actions/editgroup.php:168
-#: actions/groupdesignsettings.php:104 actions/grouplogo.php:106
+#: actions/editgroup.php:107 actions/editgroup.php:172
+#: actions/groupdesignsettings.php:107 actions/grouplogo.php:109
 msgid "You must be an admin to edit the group."
 msgstr ""
 
-#: actions/editgroup.php:154
+#: actions/editgroup.php:158
 msgid "Use this form to edit the group."
 msgstr ""
 
-#: actions/editgroup.php:201 actions/newgroup.php:145
+#: actions/editgroup.php:205 actions/newgroup.php:145
 #, php-format
 msgid "description is too long (max %d chars)."
 msgstr ""
 
-#: actions/editgroup.php:253
+#: actions/editgroup.php:258
 msgid "Could not update group."
 msgstr ""
 
-#: actions/editgroup.php:259 classes/User_group.php:433
+#: actions/editgroup.php:264 classes/User_group.php:478
 msgid "Could not create aliases."
 msgstr ""
 
-#: actions/editgroup.php:269
+#: actions/editgroup.php:280
 msgid "Options saved."
 msgstr ""
 
@@ -1533,7 +1538,7 @@ msgstr ""
 msgid "User is not a member of group."
 msgstr ""
 
-#: actions/groupblock.php:136 actions/groupmembers.php:316
+#: actions/groupblock.php:136 actions/groupmembers.php:323
 msgid "Block user from group"
 msgstr ""
 
@@ -1565,86 +1570,86 @@ msgstr ""
 msgid "You must be logged in to edit a group."
 msgstr ""
 
-#: actions/groupdesignsettings.php:141
+#: actions/groupdesignsettings.php:144
 msgid "Group design"
 msgstr ""
 
-#: actions/groupdesignsettings.php:152
+#: actions/groupdesignsettings.php:155
 msgid ""
 "Customize the way your group looks with a background image and a colour "
 "palette of your choice."
 msgstr ""
 
-#: actions/groupdesignsettings.php:263 actions/userdesignsettings.php:186
+#: actions/groupdesignsettings.php:266 actions/userdesignsettings.php:186
 #: lib/designsettings.php:391 lib/designsettings.php:413
 msgid "Couldn't update your design."
 msgstr ""
 
-#: actions/groupdesignsettings.php:308 actions/userdesignsettings.php:231
+#: actions/groupdesignsettings.php:311 actions/userdesignsettings.php:231
 msgid "Design preferences saved."
 msgstr ""
 
-#: actions/grouplogo.php:139 actions/grouplogo.php:192
+#: actions/grouplogo.php:142 actions/grouplogo.php:195
 msgid "Group logo"
 msgstr ""
 
-#: actions/grouplogo.php:150
+#: actions/grouplogo.php:153
 #, php-format
 msgid ""
 "You can upload a logo image for your group. The maximum file size is %s."
 msgstr ""
 
-#: actions/grouplogo.php:178
+#: actions/grouplogo.php:181
 msgid "User without matching profile."
 msgstr ""
 
-#: actions/grouplogo.php:362
+#: actions/grouplogo.php:365
 msgid "Pick a square area of the image to be the logo."
 msgstr ""
 
-#: actions/grouplogo.php:396
+#: actions/grouplogo.php:399
 msgid "Logo updated."
 msgstr ""
 
-#: actions/grouplogo.php:398
+#: actions/grouplogo.php:401
 msgid "Failed updating logo."
 msgstr ""
 
-#: actions/groupmembers.php:93 lib/groupnav.php:92
+#: actions/groupmembers.php:100 lib/groupnav.php:92
 #, php-format
 msgid "%s group members"
 msgstr ""
 
-#: actions/groupmembers.php:96
+#: actions/groupmembers.php:103
 #, php-format
 msgid "%1$s group members, page %2$d"
 msgstr ""
 
-#: actions/groupmembers.php:111
+#: actions/groupmembers.php:118
 msgid "A list of the users in this group."
 msgstr ""
 
-#: actions/groupmembers.php:175 lib/action.php:448 lib/groupnav.php:107
+#: actions/groupmembers.php:182 lib/action.php:448 lib/groupnav.php:107
 msgid "Admin"
 msgstr ""
 
-#: actions/groupmembers.php:348 lib/blockform.php:69
+#: actions/groupmembers.php:355 lib/blockform.php:69
 msgid "Block"
 msgstr ""
 
-#: actions/groupmembers.php:443
+#: actions/groupmembers.php:450
 msgid "Make user an admin of the group"
 msgstr ""
 
-#: actions/groupmembers.php:475
+#: actions/groupmembers.php:482
 msgid "Make Admin"
 msgstr ""
 
-#: actions/groupmembers.php:475
+#: actions/groupmembers.php:482
 msgid "Make this user an admin"
 msgstr ""
 
-#: actions/grouprss.php:133
+#: actions/grouprss.php:140
 #, php-format
 msgid "Updates from members of %1$s on %2$s!"
 msgstr ""
@@ -1924,7 +1929,11 @@ msgstr ""
 msgid "You must be logged in to join a group."
 msgstr ""
 
-#: actions/joingroup.php:131
+#: actions/joingroup.php:88 actions/leavegroup.php:88
+msgid "No nickname or ID."
+msgstr ""
+
+#: actions/joingroup.php:141
 #, php-format
 msgid "%1$s joined group %2$s"
 msgstr ""
@@ -1933,11 +1942,11 @@ msgstr ""
 msgid "You must be logged in to leave a group."
 msgstr ""
 
-#: actions/leavegroup.php:90 lib/command.php:265
+#: actions/leavegroup.php:100 lib/command.php:265
 msgid "You are not a member of that group."
 msgstr ""
 
-#: actions/leavegroup.php:127
+#: actions/leavegroup.php:137
 #, php-format
 msgid "%1$s left group %2$s"
 msgstr ""
@@ -2194,8 +2203,8 @@ msgstr ""
 msgid "Only "
 msgstr ""
 
-#: actions/oembed.php:181 actions/oembed.php:200 lib/api.php:1040
-#: lib/api.php:1068 lib/api.php:1177
+#: actions/oembed.php:181 actions/oembed.php:200 lib/apiaction.php:1039
+#: lib/apiaction.php:1067 lib/apiaction.php:1176
 msgid "Not a supported data format."
 msgstr ""
 
@@ -2208,7 +2217,7 @@ msgid "Notice Search"
 msgstr ""
 
 #: actions/othersettings.php:60
-msgid "Other settings"
+msgid "Other Settings"
 msgstr ""
 
 #: actions/othersettings.php:71
@@ -2334,7 +2343,7 @@ msgstr ""
 msgid "Password saved."
 msgstr ""
 
-#: actions/pathsadminpanel.php:59 lib/adminpanelaction.php:331
+#: actions/pathsadminpanel.php:59 lib/adminpanelaction.php:342
 msgid "Paths"
 msgstr ""
 
@@ -2367,7 +2376,7 @@ msgid "Invalid SSL server. The maximum length is 255 characters."
 msgstr ""
 
 #: actions/pathsadminpanel.php:234 actions/siteadminpanel.php:58
-#: lib/adminpanelaction.php:311
+#: lib/adminpanelaction.php:322
 msgid "Site"
 msgstr ""
 
@@ -2535,7 +2544,7 @@ msgid "1-64 lowercase letters or numbers, no punctuation or spaces"
 msgstr ""
 
 #: actions/profilesettings.php:111 actions/register.php:448
-#: actions/showgroup.php:247 actions/tagother.php:104
+#: actions/showgroup.php:255 actions/tagother.php:104
 #: lib/groupeditform.php:157 lib/userprofile.php:149
 msgid "Full name"
 msgstr ""
@@ -2563,7 +2572,7 @@ msgid "Bio"
 msgstr ""
 
 #: actions/profilesettings.php:132 actions/register.php:471
-#: actions/showgroup.php:256 actions/tagother.php:112
+#: actions/showgroup.php:264 actions/tagother.php:112
 #: actions/userauthorization.php:166 lib/groupeditform.php:177
 #: lib/userprofile.php:164
 msgid "Location"
@@ -3032,7 +3041,7 @@ msgstr ""
 msgid "You already repeated that notice."
 msgstr ""
 
-#: actions/repeat.php:114 lib/noticelist.php:656
+#: actions/repeat.php:114 lib/noticelist.php:674
 msgid "Repeated"
 msgstr ""
 
@@ -3105,7 +3114,7 @@ msgid "User is already sandboxed."
 msgstr ""
 
 #: actions/sessionsadminpanel.php:54 actions/sessionsadminpanel.php:170
-#: lib/adminpanelaction.php:336
+#: lib/adminpanelaction.php:347
 msgid "Sessions"
 msgstr ""
 
@@ -3160,7 +3169,7 @@ msgstr ""
 msgid "Description"
 msgstr ""
 
-#: actions/showapplication.php:192 actions/showgroup.php:429
+#: actions/showapplication.php:192 actions/showgroup.php:437
 #: lib/profileaction.php:174
 msgid "Statistics"
 msgstr ""
@@ -3271,67 +3280,67 @@ msgstr ""
 msgid "%1$s group, page %2$d"
 msgstr ""
 
-#: actions/showgroup.php:218
+#: actions/showgroup.php:226
 msgid "Group profile"
 msgstr ""
 
-#: actions/showgroup.php:263 actions/tagother.php:118
+#: actions/showgroup.php:271 actions/tagother.php:118
 #: actions/userauthorization.php:175 lib/userprofile.php:177
 msgid "URL"
 msgstr ""
 
-#: actions/showgroup.php:274 actions/tagother.php:128
+#: actions/showgroup.php:282 actions/tagother.php:128
 #: actions/userauthorization.php:187 lib/userprofile.php:194
 msgid "Note"
 msgstr ""
 
-#: actions/showgroup.php:284 lib/groupeditform.php:184
+#: actions/showgroup.php:292 lib/groupeditform.php:184
 msgid "Aliases"
 msgstr ""
 
-#: actions/showgroup.php:293
+#: actions/showgroup.php:301
 msgid "Group actions"
 msgstr ""
 
-#: actions/showgroup.php:328
+#: actions/showgroup.php:336
 #, php-format
 msgid "Notice feed for %s group (RSS 1.0)"
 msgstr ""
 
-#: actions/showgroup.php:334
+#: actions/showgroup.php:342
 #, php-format
 msgid "Notice feed for %s group (RSS 2.0)"
 msgstr ""
 
-#: actions/showgroup.php:340
+#: actions/showgroup.php:348
 #, php-format
 msgid "Notice feed for %s group (Atom)"
 msgstr ""
 
-#: actions/showgroup.php:345
+#: actions/showgroup.php:353
 #, php-format
 msgid "FOAF for %s group"
 msgstr ""
 
-#: actions/showgroup.php:381 actions/showgroup.php:438 lib/groupnav.php:91
+#: actions/showgroup.php:389 actions/showgroup.php:446 lib/groupnav.php:91
 msgid "Members"
 msgstr ""
 
-#: actions/showgroup.php:386 lib/profileaction.php:117
+#: actions/showgroup.php:394 lib/profileaction.php:117
 #: lib/profileaction.php:148 lib/profileaction.php:236 lib/section.php:95
 #: lib/subscriptionlist.php:126 lib/tagcloudsection.php:71
 msgid "(None)"
 msgstr ""
 
-#: actions/showgroup.php:392
+#: actions/showgroup.php:400
 msgid "All members"
 msgstr ""
 
-#: actions/showgroup.php:432
+#: actions/showgroup.php:440
 msgid "Created"
 msgstr ""
 
-#: actions/showgroup.php:448
+#: actions/showgroup.php:456
 #, php-format
 msgid ""
 "**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en."
@@ -3341,7 +3350,7 @@ msgid ""
 "of this group and many more! ([Read more](%%%%doc.help%%%%))"
 msgstr ""
 
-#: actions/showgroup.php:454
+#: actions/showgroup.php:462
 #, php-format
 msgid ""
 "**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en."
@@ -3350,7 +3359,7 @@ msgid ""
 "their life and interests. "
 msgstr ""
 
-#: actions/showgroup.php:482
+#: actions/showgroup.php:490
 msgid "Admins"
 msgstr ""
 
@@ -3861,7 +3870,7 @@ msgstr ""
 msgid "No such tag."
 msgstr ""
 
-#: actions/twitapitrends.php:87
+#: actions/twitapitrends.php:85
 msgid "API method under construction."
 msgstr ""
 
@@ -3891,7 +3900,7 @@ msgid ""
 "Listenee stream license ‘%1$s’ is not compatible with site license ‘%2$s’."
 msgstr ""
 
-#: actions/useradminpanel.php:58 lib/adminpanelaction.php:321
+#: actions/useradminpanel.php:58 lib/adminpanelaction.php:332
 #: lib/personalgroupnav.php:115
 msgid "User"
 msgstr ""
@@ -4164,6 +4173,10 @@ msgstr ""
 msgid "Group leave failed."
 msgstr ""
 
+#: classes/Local_group.php:41
+msgid "Could not update local group."
+msgstr ""
+
 #: classes/Login_token.php:76
 #, php-format
 msgid "Could not create login token for %s"
@@ -4181,43 +4194,43 @@ msgstr ""
 msgid "Could not update message with new URI."
 msgstr ""
 
-#: classes/Notice.php:157
+#: classes/Notice.php:172
 #, php-format
 msgid "DB error inserting hashtag: %s"
 msgstr ""
 
-#: classes/Notice.php:222
+#: classes/Notice.php:239
 msgid "Problem saving notice. Too long."
 msgstr ""
 
-#: classes/Notice.php:226
+#: classes/Notice.php:243
 msgid "Problem saving notice. Unknown user."
 msgstr ""
 
-#: classes/Notice.php:231
+#: classes/Notice.php:248
 msgid ""
 "Too many notices too fast; take a breather and post again in a few minutes."
 msgstr ""
 
-#: classes/Notice.php:237
+#: classes/Notice.php:254
 msgid ""
 "Too many duplicate messages too quickly; take a breather and post again in a "
 "few minutes."
 msgstr ""
 
-#: classes/Notice.php:243
+#: classes/Notice.php:260
 msgid "You are banned from posting notices on this site."
 msgstr ""
 
-#: classes/Notice.php:309 classes/Notice.php:335
+#: classes/Notice.php:326 classes/Notice.php:352
 msgid "Problem saving notice."
 msgstr ""
 
-#: classes/Notice.php:882
+#: classes/Notice.php:911
 msgid "Problem saving group inbox."
 msgstr ""
 
-#: classes/Notice.php:1407
+#: classes/Notice.php:1442
 #, php-format
 msgid "RT @%1$s %2$s"
 msgstr ""
@@ -4242,7 +4255,7 @@ msgstr ""
 msgid "Couldn't delete self-subscription."
 msgstr ""
 
-#: classes/Subscription.php:179 lib/subs.php:69
+#: classes/Subscription.php:179
 msgid "Couldn't delete subscription."
 msgstr ""
 
@@ -4251,14 +4264,22 @@ msgstr ""
 msgid "Welcome to %1$s, @%2$s!"
 msgstr ""
 
-#: classes/User_group.php:423
+#: classes/User_group.php:462
 msgid "Could not create group."
 msgstr ""
 
-#: classes/User_group.php:452
+#: classes/User_group.php:471
+msgid "Could not set group uri."
+msgstr ""
+
+#: classes/User_group.php:492
 msgid "Could not set group membership."
 msgstr ""
 
+#: classes/User_group.php:506
+msgid "Could not save local group info."
+msgstr ""
+
 #: lib/accountsettingsaction.php:108
 msgid "Change your profile settings"
 msgstr ""
@@ -4471,15 +4492,15 @@ msgstr ""
 msgid "Before"
 msgstr ""
 
-#: lib/activity.php:382
+#: lib/activity.php:449
 msgid "Can't handle remote content yet."
 msgstr ""
 
-#: lib/activity.php:410
+#: lib/activity.php:477
 msgid "Can't handle embedded XML content yet."
 msgstr ""
 
-#: lib/activity.php:414
+#: lib/activity.php:481
 msgid "Can't handle embedded Base64 content yet."
 msgstr ""
 
@@ -4503,35 +4524,35 @@ msgstr ""
 msgid "Unable to delete design setting."
 msgstr ""
 
-#: lib/adminpanelaction.php:312
+#: lib/adminpanelaction.php:323
 msgid "Basic site configuration"
 msgstr ""
 
-#: lib/adminpanelaction.php:317
+#: lib/adminpanelaction.php:328
 msgid "Design configuration"
 msgstr ""
 
-#: lib/adminpanelaction.php:322
+#: lib/adminpanelaction.php:333
 msgid "User configuration"
 msgstr ""
 
-#: lib/adminpanelaction.php:327
+#: lib/adminpanelaction.php:338
 msgid "Access configuration"
 msgstr ""
 
-#: lib/adminpanelaction.php:332
+#: lib/adminpanelaction.php:343
 msgid "Paths configuration"
 msgstr ""
 
-#: lib/adminpanelaction.php:337
+#: lib/adminpanelaction.php:348
 msgid "Sessions configuration"
 msgstr ""
 
-#: lib/apiauth.php:95
+#: lib/apiauth.php:94
 msgid "API resource requires read-write access, but you only have read access."
 msgstr ""
 
-#: lib/apiauth.php:273
+#: lib/apiauth.php:272
 #, php-format
 msgid "Failed API auth attempt, nickname = %1$s, proxy = %2$s, ip = %3$s"
 msgstr ""
@@ -4621,11 +4642,11 @@ msgstr ""
 msgid "Tags for this attachment"
 msgstr ""
 
-#: lib/authenticationplugin.php:218 lib/authenticationplugin.php:223
+#: lib/authenticationplugin.php:182 lib/authenticationplugin.php:187
 msgid "Password changing failed"
 msgstr ""
 
-#: lib/authenticationplugin.php:233
+#: lib/authenticationplugin.php:197
 msgid "Password changing is not allowed"
 msgstr ""
 
@@ -4782,7 +4803,7 @@ msgstr ""
 msgid "Subscribed to %s"
 msgstr ""
 
-#: lib/command.php:582 lib/command.php:685
+#: lib/command.php:582
 msgid "Specify the name of the user to unsubscribe from"
 msgstr ""
 
@@ -4820,42 +4841,37 @@ msgstr ""
 msgid "This link is useable only once, and is good for only 2 minutes: %s"
 msgstr ""
 
-#: lib/command.php:692
-#, php-format
-msgid "Unsubscribed  %s"
-msgstr ""
-
-#: lib/command.php:709
+#: lib/command.php:681
 msgid "You are not subscribed to anyone."
 msgstr ""
 
-#: lib/command.php:711
+#: lib/command.php:683
 msgid "You are subscribed to this person:"
 msgid_plural "You are subscribed to these people:"
 msgstr[0] ""
 msgstr[1] ""
 
-#: lib/command.php:731
+#: lib/command.php:703
 msgid "No one is subscribed to you."
 msgstr ""
 
-#: lib/command.php:733
+#: lib/command.php:705
 msgid "This person is subscribed to you:"
 msgid_plural "These people are subscribed to you:"
 msgstr[0] ""
 msgstr[1] ""
 
-#: lib/command.php:753
+#: lib/command.php:725
 msgid "You are not a member of any groups."
 msgstr ""
 
-#: lib/command.php:755
+#: lib/command.php:727
 msgid "You are a member of this group:"
 msgid_plural "You are a member of these groups:"
 msgstr[0] ""
 msgstr[1] ""
 
-#: lib/command.php:769
+#: lib/command.php:741
 msgid ""
 "Commands:\n"
 "on - turn on notifications\n"
@@ -4869,7 +4885,6 @@ msgid ""
 "d <nickname> <text> - direct message to user\n"
 "get <nickname> - get last notice from user\n"
 "whois <nickname> - get profile info on user\n"
-"lose <nickname> - force user to stop following you\n"
 "fav <nickname> - add user's last notice as a 'fave'\n"
 "fav #<notice_id> - add notice with the given id as a 'fave'\n"
 "repeat #<notice_id> - repeat a notice with a given id\n"
@@ -5460,23 +5475,23 @@ msgstr ""
 msgid "at"
 msgstr ""
 
-#: lib/noticelist.php:558
+#: lib/noticelist.php:566
 msgid "in context"
 msgstr ""
 
-#: lib/noticelist.php:583
+#: lib/noticelist.php:601
 msgid "Repeated by"
 msgstr ""
 
-#: lib/noticelist.php:610
+#: lib/noticelist.php:628
 msgid "Reply to this notice"
 msgstr ""
 
-#: lib/noticelist.php:611
+#: lib/noticelist.php:629
 msgid "Reply"
 msgstr ""
 
-#: lib/noticelist.php:655
+#: lib/noticelist.php:673
 msgid "Notice repeated"
 msgstr ""
 
@@ -5613,7 +5628,7 @@ msgstr ""
 msgid "Repeat this notice"
 msgstr ""
 
-#: lib/router.php:665
+#: lib/router.php:668
 msgid "No single user defined for single-user mode."
 msgstr ""
 
@@ -5754,47 +5769,47 @@ msgstr ""
 msgid "Moderate"
 msgstr ""
 
-#: lib/util.php:952
+#: lib/util.php:1000
 msgid "a few seconds ago"
 msgstr ""
 
-#: lib/util.php:954
+#: lib/util.php:1002
 msgid "about a minute ago"
 msgstr ""
 
-#: lib/util.php:956
+#: lib/util.php:1004
 #, php-format
 msgid "about %d minutes ago"
 msgstr ""
 
-#: lib/util.php:958
+#: lib/util.php:1006
 msgid "about an hour ago"
 msgstr ""
 
-#: lib/util.php:960
+#: lib/util.php:1008
 #, php-format
 msgid "about %d hours ago"
 msgstr ""
 
-#: lib/util.php:962
+#: lib/util.php:1010
 msgid "about a day ago"
 msgstr ""
 
-#: lib/util.php:964
+#: lib/util.php:1012
 #, php-format
 msgid "about %d days ago"
 msgstr ""
 
-#: lib/util.php:966
+#: lib/util.php:1014
 msgid "about a month ago"
 msgstr ""
 
-#: lib/util.php:968
+#: lib/util.php:1016
 #, php-format
 msgid "about %d months ago"
 msgstr ""
 
-#: lib/util.php:970
+#: lib/util.php:1018
 msgid "about a year ago"
 msgstr ""
 
index 37a2582b3056126ffbd568da64956b4ef4e8fbe7..09ede3d86cc03fd821fcd3939bde9c6946158da9 100644 (file)
@@ -5341,7 +5341,7 @@ msgstr ""
 "%7$s.\n"
 "\n"
 "----\n"
-"మీ ఈమెయిలు చిరునామాని లేదా గమనింపుల ఎంపికలను %8$s వద్ద మార్చుకోండి"
+"మీ ఈమెయిలు చిరునామాని లేదా గమనింపుల ఎంపికలను %8$s వద్ద మార్చుకోండి\n"
 
 #: lib/mail.php:258
 #, php-format
index 84a2cb6168e7752d2ea2489917c06ec6c927cb4a..fb8f7306f51e4a4747e670294fbd2a136ebcd975 100644 (file)
@@ -22,7 +22,7 @@
  * @category  Action
  * @package   StatusNet
  * @author    Evan Prodromou <evan@status.net>
- * @copyright 2009 StatusNet Inc.
+ * @copyright 2010 StatusNet Inc.
  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  * @link      http://status.net/
  */
@@ -47,6 +47,55 @@ class BlacklistPlugin extends Plugin
 
     public $nicknames = array();
     public $urls      = array();
+    public $canAdmin  = true;
+
+    private $_nicknamePatterns = array();
+    private $_urlPatterns      = array();
+
+    /**
+     * Initialize the plugin
+     *
+     * @return void
+     */
+
+    function initialize()
+    {
+        $confNicknames = $this->_configArray('blacklist', 'nicknames');
+
+        $this->_nicknamePatterns = array_merge($this->nicknames,
+                                               $confNicknames);
+
+        $confURLs = $this->_configArray('blacklist', 'urls');
+
+        $this->_urlPatterns = array_merge($this->urls,
+                                          $confURLs);
+    }
+
+    /**
+     * Retrieve an array from configuration
+     *
+     * Carefully checks a section.
+     *
+     * @param string $section Configuration section
+     * @param string $setting Configuration setting
+     *
+     * @return array configuration values
+     */
+
+    function _configArray($section, $setting)
+    {
+        $config = common_config($section, $setting);
+
+        if (empty($config)) {
+            return array();
+        } else if (is_array($config)) {
+            return $config;
+        } else if (is_string($config)) {
+            return explode("\r\n", $config);
+        } else {
+            throw new Exception("Unknown data type for config $section + $setting");
+        }
+    }
 
     /**
      * Hook registration to prevent blacklisted homepages or nicknames
@@ -173,7 +222,8 @@ class BlacklistPlugin extends Plugin
 
     private function _checkUrl($url)
     {
-        foreach ($this->urls as $pattern) {
+        foreach ($this->_urlPatterns as $pattern) {
+            common_debug("Checking $url against $pattern");
             if (preg_match("/$pattern/", $url)) {
                 return false;
             }
@@ -194,7 +244,8 @@ class BlacklistPlugin extends Plugin
 
     private function _checkNickname($nickname)
     {
-        foreach ($this->nicknames as $pattern) {
+        foreach ($this->_nicknamePatterns as $pattern) {
+            common_debug("Checking $nickname against $pattern");
             if (preg_match("/$pattern/", $nickname)) {
                 return false;
             }
@@ -203,14 +254,191 @@ class BlacklistPlugin extends Plugin
         return true;
     }
 
+    /**
+     * Add our actions to the URL router
+     *
+     * @param Net_URL_Mapper $m URL mapper for this hit
+     *
+     * @return boolean hook return
+     */
+
+    function onRouterInitialized($m)
+    {
+        $m->connect('admin/blacklist', array('action' => 'blacklistadminpanel'));
+        return true;
+    }
+
+    /**
+     * Auto-load our classes if called
+     *
+     * @param string $cls Class to load
+     *
+     * @return boolean hook return
+     */
+
+    function onAutoload($cls)
+    {
+        switch (strtolower($cls))
+        {
+        case 'blacklistadminpanelaction':
+            $base = strtolower(mb_substr($cls, 0, -6));
+            include_once INSTALLDIR.'/plugins/Blacklist/'.$base.'.php';
+            return false;
+        default:
+            return true;
+        }
+    }
+
+    /**
+     * Plugin version data
+     *
+     * @param array &$versions array of version blocks
+     *
+     * @return boolean hook value
+     */
+
     function onPluginVersion(&$versions)
     {
         $versions[] = array('name' => 'Blacklist',
                             'version' => self::VERSION,
                             'author' => 'Evan Prodromou',
-                            'homepage' => 'http://status.net/wiki/Plugin:Blacklist',
+                            'homepage' =>
+                            'http://status.net/wiki/Plugin:Blacklist',
                             'description' =>
-                            _m('Keep a blacklist of forbidden nickname and URL patterns.'));
+                            _m('Keep a blacklist of forbidden nickname '.
+                               'and URL patterns.'));
+        return true;
+    }
+
+    /**
+     * Determines if our admin panel can be shown
+     *
+     * @param string  $name  name of the admin panel
+     * @param boolean &$isOK result
+     *
+     * @return boolean hook value
+     */
+
+    function onAdminPanelCheck($name, &$isOK)
+    {
+        if ($name == 'blacklist') {
+            $isOK = $this->canAdmin;
+            return false;
+        }
+
         return true;
     }
+
+    /**
+     * Add our tab to the admin panel
+     *
+     * @param Widget $nav Admin panel nav
+     *
+     * @return boolean hook value
+     */
+
+    function onEndAdminPanelNav($nav)
+    {
+        if (AdminPanelAction::canAdmin('blacklist')) {
+
+            $action_name = $nav->action->trimmed('action');
+
+            $nav->out->menuItem(common_local_url('blacklistadminpanel'),
+                                _('Blacklist'),
+                                _('Blacklist configuration'),
+                                $action_name == 'blacklistadminpanel',
+                                'nav_blacklist_admin_panel');
+        }
+
+        return true;
+    }
+
+    function onEndDeleteUserForm($action, $user)
+    {
+        $cur = common_current_user();
+
+        if (empty($cur) || !$cur->hasRight(Right::CONFIGURESITE)) {
+            return;
+        }
+
+        $profile = $user->getProfile();
+
+        if (empty($profile)) {
+            return;
+        }
+
+        $action->elementStart('ul', 'form_data');
+        $action->elementStart('li');
+        $this->checkboxAndText($action,
+                               'blacklistnickname',
+                               _('Add this nickname pattern to blacklist'),
+                               'blacklistnicknamepattern',
+                               $this->patternizeNickname($user->nickname));
+        $action->elementEnd('li');
+
+        if (!empty($profile->homepage)) {
+            $action->elementStart('li');
+            $this->checkboxAndText($action,
+                                   'blacklisthomepage',
+                                   _('Add this homepage pattern to blacklist'),
+                                   'blacklisthomepagepattern',
+                                   $this->patternizeHomepage($profile->homepage));
+            $action->elementEnd('li');
+        }
+
+        $action->elementEnd('ul');
+    }
+
+    function onEndDeleteUser($action, $user)
+    {
+        common_debug("Action args: " . print_r($action->args, true));
+
+        if ($action->boolean('blacklisthomepage')) {
+            $pattern = $action->trimmed('blacklisthomepagepattern');
+            $confURLs = $this->_configArray('blacklist', 'urls');
+            $confURLs[] = $pattern;
+            Config::save('blacklist', 'urls', implode("\r\n", $confURLs));
+        }
+
+        if ($action->boolean('blacklistnickname')) {
+            $pattern = $action->trimmed('blacklistnicknamepattern');
+            $confNicknames = $this->_configArray('blacklist', 'nicknames');
+            $confNicknames[] = $pattern;
+            Config::save('blacklist', 'nicknames', implode("\r\n", $confNicknames));
+        }
+
+        return true;
+    }
+
+    function checkboxAndText($action, $checkID, $label, $textID, $value)
+    {
+        $action->element('input', array('name' => $checkID,
+                                        'type' => 'checkbox',
+                                        'class' => 'checkbox',
+                                        'id' => $checkID));
+
+        $action->text(' ');
+
+        $action->element('label', array('class' => 'checkbox',
+                                        'for' => $checkID),
+                         $label);
+
+        $action->text(' ');
+
+        $action->element('input', array('name' => $textID,
+                                        'type' => 'text',
+                                        'id' => $textID,
+                                        'value' => $value));
+    }
+
+    function patternizeNickname($nickname)
+    {
+        return $nickname;
+    }
+
+    function patternizeHomepage($homepage)
+    {
+        $hostname = parse_url($homepage, PHP_URL_HOST);
+        return $hostname;
+    }
 }
diff --git a/plugins/Blacklist/blacklistadminpanel.php b/plugins/Blacklist/blacklistadminpanel.php
new file mode 100644 (file)
index 0000000..98d0708
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Blacklist administration panel
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Settings
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Administer blacklist
+ *
+ * @category Admin
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link     http://status.net/
+ */
+
+class BlacklistadminpanelAction extends AdminPanelAction
+{
+    /**
+     * title of the admin panel
+     *
+     * @return string title
+     */
+
+    function title()
+    {
+        return _('Blacklist');
+    }
+
+    /**
+     * Panel instructions
+     *
+     * @return string instructions
+     */
+
+    function getInstructions()
+    {
+        return _('Blacklisted URLs and nicknames');
+    }
+
+    /**
+     * Show the actual form
+     *
+     * @return void
+     *
+     * @see BlacklistAdminPanelForm
+     */
+
+    function showForm()
+    {
+        $form = new BlacklistAdminPanelForm($this);
+        $form->show();
+        return;
+    }
+
+    /**
+     * Save the form settings
+     *
+     * @return void
+     */
+
+    function saveSettings()
+    {
+        static $settings = array(
+                'blacklist' => array('nicknames', 'urls'),
+        );
+
+        $values = array();
+
+        foreach ($settings as $section => $parts) {
+            foreach ($parts as $setting) {
+                $values[$section][$setting] = $this->trimmed("$section-$setting");
+            }
+        }
+
+        // This throws an exception on validation errors
+
+        $this->validate($values);
+
+        // assert(all values are valid);
+
+        $config = new Config();
+
+        $config->query('BEGIN');
+
+        foreach ($settings as $section => $parts) {
+            foreach ($parts as $setting) {
+                Config::save($section, $setting, $values[$section][$setting]);
+            }
+        }
+
+        $config->query('COMMIT');
+
+        return;
+    }
+
+    /**
+     * Validate the values
+     *
+     * @param array &$values 2d array of values to check
+     *
+     * @return boolean success flag
+     */
+
+    function validate(&$values)
+    {
+        return true;
+    }
+}
+
+/**
+ * Admin panel form for blacklist panel
+ *
+ * @category Admin
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link     http://status.net/
+ */
+
+class BlacklistAdminPanelForm extends Form
+{
+    /**
+     * ID of the form
+     *
+     * @return string ID
+     */
+
+    function id()
+    {
+        return 'blacklistadminpanel';
+    }
+
+    /**
+     * Class of the form
+     *
+     * @return string class
+     */
+
+    function formClass()
+    {
+        return 'form_settings';
+    }
+
+    /**
+     * Action we post to
+     *
+     * @return string action URL
+     */
+
+    function action()
+    {
+        return common_local_url('blacklistadminpanel');
+    }
+
+    /**
+     * Show the form controls
+     *
+     * @return void
+     */
+
+    function formData()
+    {
+        $this->out->elementStart('ul', 'form_data');
+
+        $this->out->elementStart('li');
+        $this->out->textarea('blacklist-nicknames', _m('Nicknames'),
+                             common_config('blacklist', 'nicknames'),
+                             _('Patterns of nicknames to block, one per line'));
+        $this->out->elementEnd('li');
+
+        $this->out->elementStart('li');
+        $this->out->textarea('blacklist-urls', _m('URLs'),
+                             common_config('blacklist', 'urls'),
+                             _('Patterns of URLs to block, one per line'));
+        $this->out->elementEnd('li');
+
+        $this->out->elementEnd('ul');
+    }
+
+    /**
+     * Buttons for submitting
+     *
+     * @return void
+     */
+
+    function formActions()
+    {
+        $this->out->submit('submit',
+                           _('Save'),
+                           'submit',
+                           null,
+                           _('Save site settings'));
+    }
+}
index 7f75b7b2b4aff75995e0cf2adc764827bc05f02d..4ffbba45b96c093574c1a2809fdb88037fba938b 100644 (file)
@@ -43,8 +43,8 @@ class OStatusPlugin extends Plugin
         // Discovery actions
         $m->connect('.well-known/host-meta',
                     array('action' => 'hostmeta'));
-        $m->connect('main/webfinger',
-                    array('action' => 'webfinger'));
+        $m->connect('main/xrd',
+                    array('action' => 'xrd'));
         $m->connect('main/ostatus',
                     array('action' => 'ostatusinit'));
         $m->connect('main/ostatus?nickname=:nickname',
@@ -102,6 +102,20 @@ class OStatusPlugin extends Plugin
         return true;
     }
 
+    /**
+     * Add a link header for LRDD Discovery
+     */
+    function onStartShowHTML($action)
+    {
+        if ($action instanceof ShowstreamAction) {
+            $acct = 'acct:'. $action->profile->nickname .'@'. common_config('site', 'server');
+            $url = common_local_url('xrd');
+            $url.= '?uri='. $acct;
+            
+            header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="application/xrd+xml"');
+        }
+    }
+    
     /**
      * Set up a PuSH hub link to our internal link for canonical timeline
      * Atom feeds for users and groups.
@@ -135,7 +149,8 @@ class OStatusPlugin extends Plugin
 
             // Also, we'll add in the salmon link
             $salmon = common_local_url($salmonAction, array('id' => $id));
-            $feed->addLink($salmon, array('rel' => 'salmon'));
+            $feed->addLink($salmon, array('rel' => Salmon::NS_REPLIES));
+            $feed->addLink($salmon, array('rel' => Salmon::NS_MENTIONS));
         }
 
         return true;
@@ -207,31 +222,62 @@ class OStatusPlugin extends Plugin
     }
 
     /**
-     *
+     * Find any explicit remote mentions. Accepted forms:
+     *   Webfinger: @user@example.com
+     *   Profile link: @example.com/mublog/user
+     * @param Profile $sender (os user?)
+     * @param string $text input markup text
+     * @param array &$mention in/out param: set of found mentions
+     * @return boolean hook return value
      */
 
     function onEndFindMentions($sender, $text, &$mentions)
     {
-        preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/',
+        preg_match_all('!(?:^|\s+)
+                        @(                                # Webfinger:
+                          (?:\w+\.)*\w+                   #   user
+                          @                               #   @
+                          (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ #   domain
+                         |                                # Profile:
+                          (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ #   domain
+                          (?:/\w+)+                       #   /path1(/path2...)
+                         )!x',
                        $text,
                        $wmatches,
                        PREG_OFFSET_CAPTURE);
 
         foreach ($wmatches[1] as $wmatch) {
-
-            $webfinger = $wmatch[0];
-
-            $this->log(LOG_INFO, "Checking Webfinger for address '$webfinger'");
-
-            $oprofile = Ostatus_profile::ensureWebfinger($webfinger);
+            $target = $wmatch[0];
+            $oprofile = null;
+
+            if (strpos($target, '/') === false) {
+                $this->log(LOG_INFO, "Checking Webfinger for address '$target'");
+                try {
+                    $oprofile = Ostatus_profile::ensureWebfinger($target);
+                } catch (Exception $e) {
+                    $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
+                }
+            } else {
+                $schemes = array('https', 'http');
+                foreach ($schemes as $scheme) {
+                    $url = "$scheme://$target";
+                    $this->log(LOG_INFO, "Checking profile address '$url'");
+                    try {
+                        $oprofile = Ostatus_profile::ensureProfile($url);
+                        if ($oprofile) {
+                            continue;
+                        }
+                    } catch (Exception $e) {
+                        $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage());
+                    }
+                }
+            }
 
             if (empty($oprofile)) {
-
-                $this->log(LOG_INFO, "No Ostatus_profile found for address '$webfinger'");
-
+                $this->log(LOG_INFO, "No Ostatus_profile found for address '$target'");
             } else {
 
-                $this->log(LOG_INFO, "Ostatus_profile found for address '$webfinger'");
+                $this->log(LOG_INFO, "Ostatus_profile found for address '$target'");
 
                 if ($oprofile->isGroup()) {
                     continue;
@@ -246,7 +292,7 @@ class OStatusPlugin extends Plugin
                     }
                 }
                 $mentions[] = array('mentioned' => array($profile),
-                                    'text' => $wmatch[0],
+                                    'text' => $target,
                                     'position' => $pos,
                                     'url' => $profile->profileurl);
             }
@@ -400,7 +446,7 @@ class OStatusPlugin extends Plugin
         $act->actor   = ActivityObject::fromProfile($subscriber);
         $act->object  = ActivityObject::fromProfile($other);
 
-        $oprofile->notifyActivity($act);
+        $oprofile->notifyActivity($act, $subscriber);
 
         return true;
     }
@@ -448,7 +494,7 @@ class OStatusPlugin extends Plugin
         $act->actor   = ActivityObject::fromProfile($profile);
         $act->object  = ActivityObject::fromProfile($other);
 
-        $oprofile->notifyActivity($act);
+        $oprofile->notifyActivity($act, $profile);
 
         return true;
     }
@@ -490,7 +536,7 @@ class OStatusPlugin extends Plugin
                                     $member->getBestName(),
                                     $oprofile->getBestName());
 
-            if ($oprofile->notifyActivity($act)) {
+            if ($oprofile->notifyActivity($act, $member)) {
                 return true;
             } else {
                 $oprofile->garbageCollect();
@@ -540,7 +586,7 @@ class OStatusPlugin extends Plugin
                                     $member->getBestName(),
                                     $oprofile->getBestName());
 
-            $oprofile->notifyActivity($act);
+            $oprofile->notifyActivity($act, $member);
         }
     }
 
@@ -583,7 +629,7 @@ class OStatusPlugin extends Plugin
         $act->actor   = ActivityObject::fromProfile($profile);
         $act->object  = ActivityObject::fromNotice($notice);
 
-        $oprofile->notifyActivity($act);
+        $oprofile->notifyActivity($act, $profile);
 
         return true;
     }
@@ -627,7 +673,7 @@ class OStatusPlugin extends Plugin
         $act->actor   = ActivityObject::fromProfile($profile);
         $act->object  = ActivityObject::fromNotice($notice);
 
-        $oprofile->notifyActivity($act);
+        $oprofile->notifyActivity($act, $profile);
 
         return true;
     }
@@ -644,7 +690,7 @@ class OStatusPlugin extends Plugin
 
     function onStartUserGroupHomeUrl($group, &$url)
     {
-        return $this->onStartUserGroupPermalink($group, &$url);
+        return $this->onStartUserGroupPermalink($group, $url);
     }
 
     function onStartUserGroupPermalink($group, &$url)
@@ -716,7 +762,7 @@ class OStatusPlugin extends Plugin
         $act->object  = $act->actor;
 
         while ($oprofile->fetch()) {
-            $oprofile->notifyDeferred($act);
+            $oprofile->notifyDeferred($act, $profile);
         }
 
         return true;
index 850b8a0fe89d47bab373327abab797db13dbc5b9..3d00b98ae0d359687380fbdbe5cf4c27e1466244 100644 (file)
@@ -31,12 +31,18 @@ class HostMetaAction extends Action
     {
         parent::handle();
 
-        $w = new Webfinger();
-
-
         $domain = common_config('site', 'server');
-        $url = common_local_url('webfinger');
+        $url = common_local_url('xrd');
         $url.= '?uri={uri}';
-        print $w->getHostMeta($domain, $url);
+
+        $xrd = new XRD();
+        
+        $xrd = new XRD();
+        $xrd->host = $domain;
+        $xrd->links[] = array('rel' => Discovery::LRDD_REL,
+                              'template' => $url,
+                              'title' => array('Resource Descriptor'));
+
+        print $xrd->toXML();
     }
 }
index 3f2f6368f6038b94092e8fc0090fd8cfa4c5e18f..8ba8dcdcc7514ae18dd84fcd02f9f9e51fce7260 100644 (file)
@@ -131,9 +131,9 @@ class OStatusInitAction extends Action
 
     function connectWebfinger($acct)
     {
-        $w = new Webfinger;
+        $disco = new Discovery;
 
-        $result = $w->lookup($acct);
+        $result = $disco->lookup($acct);
         if (!$result) {
             $this->clientError(_m("Couldn't look up OStatus account profile."));
         }
@@ -144,7 +144,7 @@ class OStatusInitAction extends Action
                 $user = User::staticGet('nickname', $this->nickname);
                 $target_profile = common_local_url('userbyid', array('id' => $user->id));
 
-                $url = $w->applyTemplate($link['template'], $target_profile);
+                $url = Discovery::applyTemplate($link['template'], $target_profile);
                 common_log(LOG_INFO, "Sending remote subscriber $acct to $url");
                 common_redirect($url, 303);
             }
index aae22f868a315582098b70df06c7f99e8b09521c..f45e6a8d1abae4bc3633a5e86fdb55ddb2d3429b 100644 (file)
@@ -332,6 +332,7 @@ class OStatusSubAction extends Action
         if ($this->oprofile->isGroup()) {
             $group = $this->oprofile->localGroup();
             if ($user->isMember($group)) {
+                // TRANS: OStatus remote group subscription dialog error.
                 $this->showForm(_m('Already a member!'));
                 return;
             }
@@ -341,18 +342,22 @@ class OStatusSubAction extends Action
                     Event::handle('EndJoinGroup', array($group, $user));
                     $this->successGroup();
                 } else {
+                    // TRANS: OStatus remote group subscription dialog error.
                     $this->showForm(_m('Remote group join failed!'));
                 }
             } else {
+                // TRANS: OStatus remote group subscription dialog error.
                 $this->showForm(_m('Remote group join aborted!'));
             }
         } else {
             $local = $this->oprofile->localProfile();
             if ($user->isSubscribed($local)) {
+                // TRANS: OStatus remote subscription dialog error.
                 $this->showForm(_m('Already subscribed!'));
             } elseif ($this->oprofile->subscribeLocalToRemote($user)) {
                 $this->successUser();
             } else {
+                // TRANS: OStatus remote subscription dialog error.
                 $this->showForm(_m('Remote subscription failed!'));
             }
         }
@@ -450,6 +455,7 @@ class OStatusSubAction extends Action
 
     function title()
     {
+        // TRANS: Page title for OStatus remote subscription form
         return _m('Authorize subscription');
     }
 
index f33690bc4999acc89c1974913ea95a59d9d130ef..842d65e7d29d9eac29518249560aa98c3e37a7e7 100644 (file)
@@ -104,7 +104,7 @@ class PushHubAction extends Action
             throw new ClientException("Invalid hub.secret $secret; must be under 200 bytes.");
         }
 
-        $sub = HubSub::staticGet($sub->topic, $sub->callback);
+        $sub = HubSub::staticGet($topic, $callback);
         if (!$sub) {
             // Creating a new one!
             $sub = new HubSub();
diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/webfinger.php
deleted file mode 100644 (file)
index e292cce..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2010, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package OStatusPlugin
- * @maintainer James Walker <james@status.net>
- */
-
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-
-class WebfingerAction extends Action
-{
-
-    public $uri;
-
-    function prepare($args)
-    {
-        parent::prepare($args);
-
-        $this->uri = $this->trimmed('uri');
-
-        return true;
-    }
-
-    function handle()
-    {
-        $acct = Webfinger::normalize($this->uri);
-
-        $xrd = new XRD();
-
-        list($nick, $domain) = explode('@', urldecode($acct));
-        $nick = common_canonical_nickname($nick);
-
-        $this->user = User::staticGet('nickname', $nick);
-        if (!$this->user) {
-            $this->clientError(_('No such user.'), 404);
-            return false;
-        }
-
-        $xrd->subject = $this->uri;
-        $xrd->alias[] = common_profile_url($nick);
-        $xrd->links[] = array('rel' => Webfinger::PROFILEPAGE,
-                              'type' => 'text/html',
-                              'href' => common_profile_url($nick));
-
-        $xrd->links[] = array('rel' => Webfinger::UPDATESFROM,
-                              'href' => common_local_url('ApiTimelineUser',
-                                                         array('id' => $this->user->id,
-                                                               'format' => 'atom')),
-                              'type' => 'application/atom+xml');
-
-        // hCard
-        $xrd->links[] = array('rel' => Webfinger::HCARD,
-                              'type' => 'text/html',
-                              'href' => common_local_url('hcard', array('nickname' => $nick)));
-
-        // XFN
-        $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11',
-                              'type' => 'text/html',
-                              'href' => common_profile_url($nick));
-        // FOAF
-        $xrd->links[] = array('rel' => 'describedby',
-                              'type' => 'application/rdf+xml',
-                              'href' => common_local_url('foaf',
-                                                         array('nickname' => $nick)));
-
-        $salmon_url = common_local_url('salmon',
-                                       array('id' => $this->user->id));
-
-        $xrd->links[] = array('rel' => 'salmon',
-                              'href' => $salmon_url);
-
-        // Get this user's keypair
-        $magickey = Magicsig::staticGet('user_id', $this->user->id);
-        if (!$magickey) {
-            // No keypair yet, let's generate one.
-            $magickey = new Magicsig();
-            $magickey->generate();
-        }
-
-        $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL,
-                              'href' => 'data:application/magic-public-key;'. $magickey->keypair);
-
-        // TODO - finalize where the redirect should go on the publisher
-        $url = common_local_url('ostatussub') . '?profile={uri}';
-        $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe',
-                              'template' => $url );
-
-        header('Content-type: text/xml');
-        print $xrd->toXML();
-    }
-
-}
diff --git a/plugins/OStatus/actions/xrd.php b/plugins/OStatus/actions/xrd.php
new file mode 100644 (file)
index 0000000..f574b60
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class XrdAction extends Action
+{
+
+    public $uri;
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $this->uri = $this->trimmed('uri');
+
+        return true;
+    }
+
+    function handle()
+    {
+        $acct = Discovery::normalize($this->uri);
+
+        $xrd = new XRD();
+
+        list($nick, $domain) = explode('@', substr(urldecode($acct), 5));
+        $nick = common_canonical_nickname($nick);
+
+        $this->user = User::staticGet('nickname', $nick);
+        if (!$this->user) {
+            $this->clientError(_('No such user.'), 404);
+            return false;
+        }
+
+        $xrd->subject = $this->uri;
+        $xrd->alias[] = common_profile_url($nick);
+        $xrd->links[] = array('rel' => Discovery::PROFILEPAGE,
+                              'type' => 'text/html',
+                              'href' => common_profile_url($nick));
+
+        $xrd->links[] = array('rel' => Discovery::UPDATESFROM,
+                              'href' => common_local_url('ApiTimelineUser',
+                                                         array('id' => $this->user->id,
+                                                               'format' => 'atom')),
+                              'type' => 'application/atom+xml');
+
+        // hCard
+        $xrd->links[] = array('rel' => Discovery::HCARD,
+                              'type' => 'text/html',
+                              'href' => common_local_url('hcard', array('nickname' => $nick)));
+
+        // XFN
+        $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11',
+                              'type' => 'text/html',
+                              'href' => common_profile_url($nick));
+        // FOAF
+        $xrd->links[] = array('rel' => 'describedby',
+                              'type' => 'application/rdf+xml',
+                              'href' => common_local_url('foaf',
+                                                         array('nickname' => $nick)));
+
+        // Salmon
+        $salmon_url = common_local_url('usersalmon',
+                                       array('id' => $this->user->id));
+
+        $xrd->links[] = array('rel' => Salmon::NS_REPLIES,
+                              'href' => $salmon_url);
+
+        $xrd->links[] = array('rel' => Salmon::NS_MENTIONS,
+                              'href' => $salmon_url);
+
+        // Get this user's keypair
+        $magickey = Magicsig::staticGet('user_id', $this->user->id);
+        if (!$magickey) {
+            // No keypair yet, let's generate one.
+            $magickey = new Magicsig();
+            $magickey->generate($this->user->id);
+        }
+
+        $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL,
+                              'href' => 'data:application/magic-public-key;'. $magickey->toString(false));
+
+        // TODO - finalize where the redirect should go on the publisher
+        $url = common_local_url('ostatussub') . '?profile={uri}';
+        $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe',
+                              'template' => $url );
+
+        header('Content-type: text/xml');
+        print $xrd->toXML();
+    }
+
+}
index 1ac181feeb946db8097f0f0954892f3658ea1b0b..3120a70f9fb39dd978c14482c59b79ed90abbbc4 100644 (file)
@@ -99,7 +99,7 @@ class HubSub extends Memcached_DataObject
         return array_keys($this->keyTypes());
     }
 
-    function sequenceKeys()
+    function sequenceKey()
     {
         return array(false, false, false);
     }
@@ -260,9 +260,15 @@ class HubSub extends Memcached_DataObject
             $retries = intval(common_config('ostatus', 'hub_retries'));
         }
 
-        $data = array('sub' => clone($this),
+        // We dare not clone() as when the clone is discarded it'll
+        // destroy the result data for the parent query.
+        // @fixme use clone() again when it's safe to copy an
+        // individual item from a multi-item query again.
+        $sub = HubSub::staticGet($this->topic, $this->callback);
+        $data = array('sub' => $sub,
                       'atom' => $atom,
                       'retries' => $retries);
+        common_log(LOG_INFO, "Queuing PuSH: $this->topic to $this->callback");
         $qm = QueueManager::get();
         $qm->enqueue($data, 'hubout');
     }
index 681aec184d0ec8672cf01342826a52abb4b54ac5..5a46aeeb6e5f79b8b6ce59fab283685b51521d42 100644 (file)
@@ -49,7 +49,12 @@ class Magicsig extends Memcached_DataObject
     
     public /*static*/ function staticGet($k, $v=null)
     {
-        return parent::staticGet(__CLASS__, $k, $v);
+        $obj =  parent::staticGet(__CLASS__, $k, $v);
+        if (!empty($obj)) {
+            return Magicsig::fromString($obj->keypair);
+        }
+
+        return $obj;
     }
 
 
@@ -83,6 +88,10 @@ class Magicsig extends Memcached_DataObject
         return array('user_id' => 'K');
     }
 
+    function sequenceKey() {
+        return array(false, false, false);
+    }
+
     function insert()
     {
         $this->keypair = $this->toString();
@@ -90,7 +99,7 @@ class Magicsig extends Memcached_DataObject
         return parent::insert();
     }
 
-    public function generate($key_length = 512)
+    public function generate($user_id, $key_length = 512)
     {
         PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
 
@@ -101,6 +110,7 @@ class Magicsig extends Memcached_DataObject
         $this->_rsa = new Crypt_RSA($params);
         PEAR::popErrorHandling();
 
+        $this->user_id = $user_id;
         $this->insert();
     }
 
@@ -136,8 +146,10 @@ class Magicsig extends Memcached_DataObject
         
         $mod = base64_url_decode($matches[1]);
         $exp = base64_url_decode($matches[2]);
-        if ($matches[4]) {
+        if (!empty($matches[4])) {
             $private_exp = base64_url_decode($matches[4]);
+        } else {
+            $private_exp = false;
         }
 
         $params['public_key'] = new Crypt_RSA_KEY($mod, $exp, 'public');
@@ -171,14 +183,15 @@ class Magicsig extends Memcached_DataObject
         switch ($this->alg) {
 
         case 'RSA-SHA256':
-            return 'sha256';
+            return 'magicsig_sha256';
         }
 
     }
     
     public function sign($bytes)
     {
-        $sig = $this->_rsa->createSign($bytes, null, 'sha256');
+        $hash = $this->getHash();
+        $sig = $this->_rsa->createSign($bytes, null, $hash);
         if ($this->_rsa->isError()) {
             $error = $this->_rsa->getLastError();
             common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
@@ -190,7 +203,8 @@ class Magicsig extends Memcached_DataObject
 
     public function verify($signed_bytes, $signature)
     {
-        $result =  $this->_rsa->validateSign($signed_bytes, $signature, null, 'sha256');
+        $hash = $this->getHash();
+        $result =  $this->_rsa->validateSign($signed_bytes, $signature, null, $hash);
         if ($this->_rsa->isError()) {
             $error = $this->keypair->getLastError();
             common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
@@ -203,7 +217,7 @@ class Magicsig extends Memcached_DataObject
 
 // Define a sha256 function for hashing
 // (Crypt_RSA should really be updated to use hash() )
-function sha256($bytes)
+function magicsig_sha256($bytes)
 {
     return hash('sha256', $bytes);
 }
index 75b4bef41b59e9a0b0b2505dceecaddae25ddec6..a33e95d932c02f7faea0c4f7ee3f56eb9ac788d6 100644 (file)
@@ -150,27 +150,7 @@ class Ostatus_profile extends Memcached_DataObject
     function asActivityObject()
     {
         if ($this->isGroup()) {
-            $object = new ActivityObject();
-            $object->type = 'http://activitystrea.ms/schema/1.0/group';
-            $object->id = $this->uri;
-            $self = $this->localGroup();
-
-            // @fixme put a standard getAvatar() interface on groups too
-            if ($self->homepage_logo) {
-                $object->avatar = $self->homepage_logo;
-                $map = array('png' => 'image/png',
-                             'jpg' => 'image/jpeg',
-                             'jpeg' => 'image/jpeg',
-                             'gif' => 'image/gif');
-                $extension = pathinfo(parse_url($object->avatar, PHP_URL_PATH), PATHINFO_EXTENSION);
-                if (isset($map[$extension])) {
-                    // @fixme this ain't used/saved yet
-                    $object->avatarType = $map[$extension];
-                }
-            }
-
-            $object->link = $this->uri; // @fixme accurate?
-            return $object;
+            return ActivityObject::fromGroup($this->localGroup());
         } else {
             return ActivityObject::fromProfile($this->localProfile());
         }
@@ -189,57 +169,13 @@ class Ostatus_profile extends Memcached_DataObject
      */
     function asActivityNoun($element)
     {
-        $xs = new XMLStringer(true);
-        $avatarHref = Avatar::defaultImage(AVATAR_PROFILE_SIZE);
-        $avatarType = 'image/png';
         if ($this->isGroup()) {
-            $type = 'http://activitystrea.ms/schema/1.0/group';
-            $self = $this->localGroup();
-
-            // @fixme put a standard getAvatar() interface on groups too
-            if ($self->homepage_logo) {
-                $avatarHref = $self->homepage_logo;
-                $map = array('png' => 'image/png',
-                             'jpg' => 'image/jpeg',
-                             'jpeg' => 'image/jpeg',
-                             'gif' => 'image/gif');
-                $extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION);
-                if (isset($map[$extension])) {
-                    $avatarType = $map[$extension];
-                }
-            }
+            $noun = ActivityObject::fromGroup($this->localGroup());
+            return $noun->asString('activity:' . $element);
         } else {
-            $type = 'http://activitystrea.ms/schema/1.0/person';
-            $self = $this->localProfile();
-            $avatar = $self->getAvatar(AVATAR_PROFILE_SIZE);
-            if ($avatar) {
-                  $avatarHref = $avatar->url;
-                  $avatarType = $avatar->mediatype;
-            }
+            $noun = ActivityObject::fromProfile($this->localProfile());
+            return $noun->asString('activity:' . $element);
         }
-        $xs->elementStart('activity:' . $element);
-        $xs->element(
-            'activity:object-type',
-            null,
-            $type
-        );
-        $xs->element(
-            'id',
-            null,
-            $this->uri); // ?
-        $xs->element('title', null, $self->getBestName());
-
-        $xs->element(
-            'link', array(
-                'type' => $avatarType,
-                'href' => $avatarHref
-            ),
-            ''
-        );
-
-        $xs->elementEnd('activity:' . $element);
-
-        return $xs->getString();
     }
 
     /**
@@ -421,7 +357,7 @@ class Ostatus_profile extends Memcached_DataObject
             common_log(LOG_INFO, "Posting to Salmon endpoint $this->salmonuri: $xml");
 
             $salmon = new Salmon(); // ?
-            return $salmon->post($this->salmonuri, $xml);
+            return $salmon->post($this->salmonuri, $xml, $actor);
         }
         return false;
     }
@@ -433,11 +369,11 @@ class Ostatus_profile extends Memcached_DataObject
      * @param mixed $entry XML string, Notice, or Activity
      * @return boolean success
      */
-    public function notifyActivity($entry)
+    public function notifyActivity($entry, $actor)
     {
         if ($this->salmonuri) {
             $salmon = new Salmon();
-            return $salmon->post($this->salmonuri, $this->notifyPrepXml($entry));
+            return $salmon->post($this->salmonuri, $this->notifyPrepXml($entry), $actor);
         }
 
         return false;
@@ -450,11 +386,12 @@ class Ostatus_profile extends Memcached_DataObject
      * @param mixed $entry XML string, Notice, or Activity
      * @return boolean success
      */
-    public function notifyDeferred($entry)
+    public function notifyDeferred($entry, $actor)
     {
         if ($this->salmonuri) {
             $data = array('salmonuri' => $this->salmonuri,
-                          'entry' => $this->notifyPrepXml($entry));
+                          'entry' => $this->notifyPrepXml($entry),
+                          'actor' => $actor->id);
 
             $qm = QueueManager::get();
             return $qm->enqueue($data, 'salmon');
@@ -486,36 +423,6 @@ class Ostatus_profile extends Memcached_DataObject
         }
     }
 
-    function atomFeed($actor)
-    {
-        $feed = new Atom10Feed();
-        // @fixme should these be set up somewhere else?
-        $feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
-        $feed->addNamespace('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_local_url('ApiTimelineUser',
-                                        array('id' => $actor->id,
-                                              'type' => 'atom')),
-                       array('rel' => 'self',
-                             'type' => 'application/atom+xml'));
-
-        $feed->addLink(common_local_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,
@@ -791,11 +698,18 @@ class Ostatus_profile extends Memcached_DataObject
     {
         // Get the canonical feed URI and check it
         $discover = new FeedDiscovery();
-        $feeduri = $discover->discoverFromURL($profile_uri);
+        if (isset($hints['feedurl'])) {
+            $feeduri = $hints['feedurl'];
+            $feeduri = $discover->discoverFromFeedURL($feeduri);
+        } else {
+            $feeduri = $discover->discoverFromURL($profile_uri);
+            $hints['feedurl'] = $feeduri;
+        }
 
-        //$feedsub = FeedSub::ensureFeed($feeduri, $discover->feed);
         $huburi = $discover->getAtomLink('hub');
-        $salmonuri = $discover->getAtomLink('salmon');
+        $hints['hub'] = $huburi;
+        $salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES);
+        $hints['salmon'] = $salmonuri;
 
         if (!$huburi) {
             // We can only deal with folks with a PuSH hub
@@ -810,7 +724,7 @@ class Ostatus_profile extends Memcached_DataObject
 
         if (!empty($subject)) {
             $subjObject = new ActivityObject($subject);
-            return self::ensureActivityObjectProfile($subjObject, $feeduri, $salmonuri, $hints);
+            return self::ensureActivityObjectProfile($subjObject, $hints);
         }
 
         // Otherwise, try the feed author
@@ -819,7 +733,7 @@ class Ostatus_profile extends Memcached_DataObject
 
         if (!empty($author)) {
             $authorObject = new ActivityObject($author);
-            return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri, $hints);
+            return self::ensureActivityObjectProfile($authorObject, $hints);
         }
 
         // Sheesh. Not a very nice feed! Let's try fingerpoken in the
@@ -835,7 +749,7 @@ class Ostatus_profile extends Memcached_DataObject
 
             if (!empty($actor)) {
                 $actorObject = new ActivityObject($actor);
-                return self::ensureActivityObjectProfile($actorObject, $feeduri, $salmonuri, $hints);
+                return self::ensureActivityObjectProfile($actorObject, $hints);
 
             }
 
@@ -843,7 +757,7 @@ class Ostatus_profile extends Memcached_DataObject
 
             if (!empty($author)) {
                 $authorObject = new ActivityObject($author);
-                return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri, $hints);
+                return self::ensureActivityObjectProfile($authorObject, $hints);
             }
         }
 
@@ -988,18 +902,18 @@ class Ostatus_profile extends Memcached_DataObject
      * @return Ostatus_profile
      */
 
-    public static function ensureActorProfile($activity, $feeduri=null, $salmonuri=null)
+    public static function ensureActorProfile($activity, $hints=array())
     {
-        return self::ensureActivityObjectProfile($activity->actor, $feeduri, $salmonuri);
+        return self::ensureActivityObjectProfile($activity->actor, $hints);
     }
 
-    public static function ensureActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array())
+    public static function ensureActivityObjectProfile($object, $hints=array())
     {
         $profile = self::getActivityObjectProfile($object);
         if ($profile) {
             $profile->updateFromActivityObject($object, $hints);
         } else {
-            $profile = self::createActivityObjectProfile($object, $feeduri, $salmonuri, $hints);
+            $profile = self::createActivityObjectProfile($object, $hints);
         }
         return $profile;
     }
@@ -1045,58 +959,55 @@ class Ostatus_profile extends Memcached_DataObject
      * @fixme validate stuff somewhere
      */
 
-    protected static function createActorProfile($activity, $feeduri=null, $salmonuri=null)
-    {
-        $actor = $activity->actor;
-
-        self::createActivityObjectProfile($actor, $feeduri, $salmonuri);
-    }
-
     /**
      * Create local ostatus_profile and profile/user_group entries for
      * the provided remote user or group.
      *
      * @param ActivityObject $object
-     * @param string $feeduri
-     * @param string $salmonuri
      * @param array $hints
      *
-     * @fixme fold $feeduri/$salmonuri into $hints
      * @return Ostatus_profile
      */
-    protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array())
+    protected static function createActivityObjectProfile($object, $hints=array())
     {
-        $homeuri  = $object->id;
+        $homeuri = $object->id;
+        $discover = false;
 
         if (!$homeuri) {
             common_log(LOG_DEBUG, __METHOD__ . " empty actor profile URI: " . var_export($activity, true));
             throw new ServerException("No profile URI");
         }
 
-        if (empty($feeduri)) {
-            if (array_key_exists('feedurl', $hints)) {
-                $feeduri = $hints['feedurl'];
-            }
+        if (array_key_exists('feedurl', $hints)) {
+            $feeduri = $hints['feedurl'];
+        } else {
+            $discover = new FeedDiscovery();
+            $feeduri = $discover->discoverFromURL($homeuri);
         }
 
-        if (empty($salmonuri)) {
-            if (array_key_exists('salmon', $hints)) {
-                $salmonuri = $hints['salmon'];
+        if (array_key_exists('salmon', $hints)) {
+            $salmonuri = $hints['salmon'];
+        } else {
+            if (!$discover) {
+                $discover = new FeedDiscovery();
+                $discover->discoverFromFeedURL($hints['feedurl']);
             }
+            $salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES);
         }
 
-        if (!$feeduri || !$salmonuri) {
-            // Get the canonical feed URI and check it
-            $discover = new FeedDiscovery();
-            $feeduri = $discover->discoverFromURL($homeuri);
-
+        if (array_key_exists('hub', $hints)) {
+            $huburi = $hints['hub'];
+        } else {
+            if (!$discover) {
+                $discover = new FeedDiscovery();
+                $discover->discoverFromFeedURL($hints['feedurl']);
+            }
             $huburi = $discover->getAtomLink('hub');
-            $salmonuri = $discover->getAtomLink('salmon');
+        }
 
-            if (!$huburi) {
-                // We can only deal with folks with a PuSH hub
-                throw new FeedSubNoHubException();
-            }
+        if (!$huburi) {
+            // We can only deal with folks with a PuSH hub
+            throw new FeedSubNoHubException();
         }
 
         $oprofile = new Ostatus_profile();
@@ -1234,7 +1145,7 @@ class Ostatus_profile extends Memcached_DataObject
 
         if (!empty($poco)) {
             $url = $poco->getPrimaryURL();
-            if ($url->type == 'homepage') {
+            if ($url && $url->type == 'homepage') {
                 $homepage = $url->value;
             }
         }
@@ -1375,27 +1286,27 @@ class Ostatus_profile extends Memcached_DataObject
 
         // Now, try some discovery
 
-        $wf = new Webfinger();
-
-        $result = $wf->lookup($addr);
+        $disco = new Discovery();
 
-        if (!$result) {
+        try {
+            $result = $disco->lookup($addr);
+        } catch (Exception $e) {
             self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), null);
             return null;
         }
 
         foreach ($result->links as $link) {
             switch ($link['rel']) {
-            case Webfinger::PROFILEPAGE:
+            case Discovery::PROFILEPAGE:
                 $profileUrl = $link['href'];
                 break;
-            case 'salmon':
+            case Salmon::NS_REPLIES:
                 $salmonEndpoint = $link['href'];
                 break;
-            case Webfinger::UPDATESFROM:
+            case Discovery::UPDATESFROM:
                 $feedUrl = $link['href'];
                 break;
-            case Webfinger::HCARD:
+            case Discovery::HCARD:
                 $hcardUrl = $link['href'];
                 break;
             default:
diff --git a/plugins/OStatus/lib/discovery.php b/plugins/OStatus/lib/discovery.php
new file mode 100644 (file)
index 0000000..f8449b3
--- /dev/null
@@ -0,0 +1,310 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+/**
+ * This class implements LRDD-based service discovery based on the "Hammer Draft"
+ * (including webfinger)
+ *
+ * @see http://groups.google.com/group/webfinger/browse_thread/thread/9f3d93a479e91bbf
+ */
+class Discovery
+{
+
+    const LRDD_REL = 'lrdd';
+    const PROFILEPAGE = 'http://webfinger.net/rel/profile-page';
+    const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
+    const HCARD = 'http://microformats.org/profile/hcard';
+    
+    public $methods = array();
+
+    public function __construct()
+    {
+        $this->registerMethod('Discovery_LRDD_Host_Meta');
+        $this->registerMethod('Discovery_LRDD_Link_Header');
+        $this->registerMethod('Discovery_LRDD_Link_HTML');
+    }
+
+
+    public function registerMethod($class)
+    {
+        $this->methods[] = $class;
+    }
+    
+    /**
+     * Given a "user id" make sure it's normalized to either a webfinger
+     * acct: uri or a profile HTTP URL.
+     */
+    public static function normalize($user_id)
+    {
+        if (substr($user_id, 0, 5) == 'http:' ||
+            substr($user_id, 0, 6) == 'https:' ||
+            substr($user_id, 0, 5) == 'acct:') {
+            return $user_id;
+        }
+
+        if (strpos($user_id, '@') !== FALSE) {
+            return 'acct:' . $user_id;
+        }
+
+        return 'http://' . $user_id;
+    }
+
+    public static function isWebfinger($user_id)
+    {
+        $uri = Discovery::normalize($user_id);
+        
+        return (substr($uri, 0, 5) == 'acct:');
+    }
+
+    /**
+     * This implements the actual lookup procedure
+     */
+    public function lookup($id)
+    {
+        // Normalize the incoming $id to make sure we have a uri
+        $uri = $this->normalize($id);
+
+        foreach ($this->methods as $class) {
+            $links = call_user_func(array($class, 'discover'), $uri);
+            if ($link = Discovery::getService($links, Discovery::LRDD_REL)) {
+                // Load the LRDD XRD
+                if (!empty($link['template'])) {
+                    $xrd_uri = Discovery::applyTemplate($link['template'], $uri);
+                } else {
+                    $xrd_uri = $link['href'];
+                }
+                
+                $xrd = $this->fetchXrd($xrd_uri);
+                if ($xrd) {
+                    return $xrd;
+                }
+            }
+        }
+
+        throw new Exception('Unable to find services for '. $id);
+    }
+
+    public static function getService($links, $service) {
+        if (!is_array($links)) {
+            return false;
+        }
+        
+        foreach ($links as $link) {
+            if ($link['rel'] == $service) {
+                return $link;
+            }
+        }
+    }
+    
+
+    public static function applyTemplate($template, $id)
+    {
+        $template = str_replace('{uri}', urlencode($id), $template);
+
+        return $template;
+    }
+
+    
+    public static function fetchXrd($url)
+    {
+        try {
+            $client = new HTTPClient();
+            $response = $client->get($url);
+        } catch (HTTP_Request2_Exception $e) {
+            return false;
+        }
+
+        if ($response->getStatus() != 200) {
+            return false;
+        }
+
+        return XRD::parse($response->getBody());
+    }
+}
+
+interface Discovery_LRDD
+{
+    public function discover($uri);
+}
+
+class Discovery_LRDD_Host_Meta implements Discovery_LRDD
+{
+    public function discover($uri)
+    {
+        if (!Discovery::isWebfinger($uri)) {
+            return false;
+        }
+
+        // We have a webfinger acct: - start with host-meta
+        list($name, $domain) = explode('@', $uri);
+        $url = 'http://'. $domain .'/.well-known/host-meta';
+
+        $xrd = Discovery::fetchXrd($url);
+
+        if ($xrd) {
+            if ($xrd->host != $domain) {
+                return false;
+            }
+            
+            return $xrd->links;
+        }
+    }
+}
+
+class Discovery_LRDD_Link_Header implements Discovery_LRDD
+{
+    public function discover($uri)
+    {
+        try {
+            $client = new HTTPClient();
+            $response = $client->get($uri);
+        } catch (HTTP_Request2_Exception $e) {
+            return false;
+        }
+             
+        if ($response->getStatus() != 200) {
+            return false;
+        }
+
+        $link_header = $response->getHeader('Link');
+        if (!$link_header) {
+            //            return false;
+        }
+        
+        return Discovery_LRDD_Link_Header::parseHeader($link_header);
+    }
+
+    protected static function parseHeader($header)
+    {
+        preg_match('/^<[^>]+>/', $header, $uri_reference);
+        //if (empty($uri_reference)) return;
+
+        $links = array();
+        
+        $link_uri = trim($uri_reference[0], '<>');
+        $link_rel = array();
+        $link_type = null;
+        
+        // remove uri-reference from header
+        $header = substr($header, strlen($uri_reference[0]));
+        
+        // parse link-params
+        $params = explode(';', $header);
+        
+        foreach ($params as $param) {
+            if (empty($param)) continue;
+            list($param_name, $param_value) = explode('=', $param, 2);
+            $param_name = trim($param_name);
+            $param_value = preg_replace('(^"|"$)', '', trim($param_value));
+            
+            // for now we only care about 'rel' and 'type' link params
+            // TODO do something with the other links-params
+            switch ($param_name) {
+            case 'rel':
+                $link_rel = trim($param_value);
+                break;
+                
+            case 'type':
+                $link_type = trim($param_value);
+            }
+        }
+
+        $links[] =  array(
+            'href' => $link_uri,
+            'rel' => $link_rel,
+            'type' => $link_type);
+
+        return $links;
+    }
+}
+
+class Discovery_LRDD_Link_HTML implements Discovery_LRDD
+{
+    public function discover($uri)
+    {
+        try {
+            $client = new HTTPClient();
+            $response = $client->get($uri);
+        } catch (HTTP_Request2_Exception $e) {
+            return false;
+        }
+
+        if ($response->getStatus() != 200) {
+            return false;
+        }
+
+        return Discovery_LRDD_Link_HTML::parse($response->getBody());
+    }
+
+
+    public function parse($html)
+    {
+        $links = array();
+        
+        preg_match('/<head(\s[^>]*)?>(.*?)<\/head>/is', $html, $head_matches);
+        $head_html = $head_matches[2];
+        
+        preg_match_all('/<link\s[^>]*>/i', $head_html, $link_matches);
+        
+        foreach ($link_matches[0] as $link_html) {
+            $link_url = null;
+            $link_rel = null;
+            $link_type = null;
+            
+            preg_match('/\srel=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $rel_matches);
+            if ( isset($rel_matches[3]) ) {
+                $link_rel = $rel_matches[3];
+            } else if ( isset($rel_matches[1]) ) {
+                $link_rel = $rel_matches[1];
+            }
+            
+            preg_match('/\shref=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $href_matches);
+            if ( isset($href_matches[3]) ) {
+                $link_uri = $href_matches[3];
+            } else if ( isset($href_matches[1]) ) {
+                $link_uri = $href_matches[1];
+            }
+            
+            preg_match('/\stype=(("|\')([^\\2]*?)\\2|[^"\'\s]+)/i', $link_html, $type_matches);
+            if ( isset($type_matches[3]) ) {
+                $link_type = $type_matches[3];
+            } else if ( isset($type_matches[1]) ) {
+                $link_type = $type_matches[1];
+            }
+            
+            $links[] = array(
+                'href' => $link_url,
+                'rel' => $link_rel,
+                'type' => $link_type,
+            );
+        }
+        
+        return $links;
+    }
+}
index 81f4609c5cd94a138ab795381486c967f0e00913..230d81ba1f0cf02cce340ad7284711e355c6ee80 100644 (file)
@@ -50,19 +50,26 @@ class MagicEnvelope
 
     public function getKeyPair($signer_uri)
     {
-        return 'RSA.79_L2gq-TD72Nsb5yGS0r9stLLpJZF5AHXyxzWmQmlqKl276LEJEs8CppcerLcR90MbYQUwt-SX9slx40Yq3vA==.AQAB.AR-jo5KMfSISmDAT2iMs2_vNFgWRjl5rbJVvA0SpGIEWyPdCGxlPtCbTexp8-0ZEIe8a4SyjatBECH5hxgMTpw==';
-    }
-
-
-    public function signMessage($text, $mimetype, $signer_uri)
-    {
-        $signer_uri = $this->normalizeUser($signer_uri);
+        $disco = new Discovery();
 
-        if (!$this->checkAuthor($text, $signer_uri)) {
+        try {
+            $xrd = $disco->lookup($signer_uri);
+        } catch (Exception $e) {
             return false;
         }
+        if ($xrd->links) {
+            if ($link = Discovery::getService($xrd->links, Magicsig::PUBLICKEYREL)) {
+                list($type, $keypair) = explode(';', $link['href']);
+                return $keypair;
+            }
+        }
+        throw new Exception('Unable to locate signer public key');
+    }
+
 
-        $signature_alg = Magicsig::fromString($this->getKeyPair($signer_uri));
+    public function signMessage($text, $mimetype, $keypair)
+    {
+        $signature_alg = Magicsig::fromString($keypair);
         $armored_text = base64_encode($text);
 
         return array(
@@ -76,6 +83,28 @@ class MagicEnvelope
             
     }
 
+    public function toXML($env) {
+        $dom = new DOMDocument();
+
+        $envelope = $dom->createElementNS(MagicEnvelope::NS, 'me:env');
+        $envelope->setAttribute('xmlns:me', MagicEnvelope::NS);
+        $data = $dom->createElementNS(MagicEnvelope::NS, 'me:data', $env['data']);
+        $data->setAttribute('type', $env['data_type']);
+        $envelope->appendChild($data);
+        $enc = $dom->createElementNS(MagicEnvelope::NS, 'me:encoding', $env['encoding']);
+        $envelope->appendChild($enc);
+        $alg = $dom->createElementNS(MagicEnvelope::NS, 'me:alg', $env['alg']);
+        $envelope->appendChild($alg);
+        $sig = $dom->createElementNS(MagicEnvelope::NS, 'me:sig', $env['sig']);
+        $envelope->appendChild($sig);
+
+        $dom->appendChild($envelope);
+        
+        
+        return $dom->saveXML();
+    }
+
+    
     public function unfold($env)
     {
         $dom = new DOMDocument();
index 0da85600fb99c39d07fa3f9d2abf0663f7e9fea3..6ca31c485c3346919410f09fb768feeab3cc792f 100644 (file)
@@ -87,7 +87,7 @@ class OStatusQueueHandler extends QueueHandler
             // remote user or group.
             // @fixme as an optimization we can skip this if the
             // remote profile is subscribed to the author.
-            $oprofile->notifyDeferred($this->notice);
+            $oprofile->notifyDeferred($this->notice, $this->user);
         }
     }
 
index b5f178cc6a2e4cec2f191dad33d96ed39aa9a4ad..3d3341bc63fafeef24cc011f6e7296c8b09a5947 100644 (file)
  */
 class Salmon
 {
+
+    const NS_REPLIES = "http://salmon-protocol.org/ns/salmon-replies";
+
+    const NS_MENTIONS = "http://salmon-protocol.org/ns/salmon-mention";
+    
     /**
      * Sign and post the given Atom entry as a Salmon message.
      *
@@ -37,17 +42,20 @@ class Salmon
      * @param string $xml
      * @return boolean success
      */
-    public function post($endpoint_uri, $xml)
+    public function post($endpoint_uri, $xml, $actor)
     {
         if (empty($endpoint_uri)) {
             return false;
         }
 
-        if (!common_config('ostatus', 'skip_signatures')) {
-            $xml = $this->createMagicEnv($xml);
+        try {
+            $xml = $this->createMagicEnv($xml, $actor);
+        } catch (Exception $e) {
+            common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage());
+            return false;
         }
 
-        $headers = array('Content-Type: application/atom+xml');
+        $headers = array('Content-Type: application/magic-envelope+xml');
 
         try {
             $client = new HTTPClient();
@@ -65,24 +73,37 @@ class Salmon
         return true;
     }
 
-    public function createMagicEnv($text)
+    public function createMagicEnv($text, $actor)
     {
         $magic_env = new MagicEnvelope();
 
-        // TODO: Should probably be getting the signer uri as an argument?
-        $signer_uri = $magic_env->getAuthor($text);
-
-        $env = $magic_env->signMessage($text, 'application/atom+xml', $signer_uri);
+        $user = User::staticGet('id', $actor->id);
+        if ($user->id) {
+            // Use local key
+            $magickey = Magicsig::staticGet('user_id', $user->id);
+            if (!$magickey) {
+                // No keypair yet, let's generate one.
+                $magickey = new Magicsig();
+                $magickey->generate($user->id);
+            } 
+        } else {
+            throw new Exception("Salmon invalid actor for signing");
+        }
 
-        return $magic_env->unfold($env);
+        try {
+            $env = $magic_env->signMessage($text, 'application/atom+xml', $magickey->toString());
+        } catch (Exception $e) {
+            return $text;
+        }
+        return $magic_env->toXML($env);
     }
 
 
-    public function verifyMagicEnv($dom)
+    public function verifyMagicEnv($text)
     {
         $magic_env = new MagicEnvelope();
         
-        $env = $magic_env->fromDom($dom);
+        $env = $magic_env->parse($text);
 
         return $magic_env->verify($env);
     }
index a03169101bf3248c117390b4ac51de36ec1b323e..fa9dc3b1daf11cb9fe759fa271a9f087aef09a84 100644 (file)
@@ -41,29 +41,32 @@ class SalmonAction extends Action
             $this->clientError(_m('This method requires a POST.'));
         }
 
-        if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
-            $this->clientError(_m('Salmon requires application/atom+xml'));
+        if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/magic-envelope+xml') {
+            $this->clientError(_m('Salmon requires application/magic-envelope+xml'));
         }
 
         $xml = file_get_contents('php://input');
 
-        $dom = DOMDocument::loadXML($xml);
 
+        // Check the signature
+        $salmon = new Salmon;
+        if (!$salmon->verifyMagicEnv($xml)) {
+            common_log(LOG_DEBUG, "Salmon signature verification failed.");
+            $this->clientError(_m('Salmon signature verification failed.'));
+        } else {
+            $magic_env = new MagicEnvelope();
+            $env = $magic_env->parse($xml);
+            $xml = $magic_env->unfold($env);
+        }
+        
+
+        $dom = DOMDocument::loadXML($xml);
         if ($dom->documentElement->namespaceURI != Activity::ATOM ||
             $dom->documentElement->localName != 'entry') {
             common_log(LOG_DEBUG, "Got invalid Salmon post: $xml");
             $this->clientError(_m('Salmon post must be an Atom entry.'));
         }
 
-        // Check the signature
-        $salmon = new Salmon;
-        if (!common_config('ostatus', 'skip_signatures')) {
-            if (!$salmon->verifyMagicEnv($dom)) {
-                common_log(LOG_DEBUG, "Salmon signature verification failed.");
-                $this->clientError(_m('Salmon signature verification failed.'));
-            }
-        }
-
         $this->act = new Activity($dom->documentElement);
         return true;
     }
index aa97018dc9ac0c4fb048b3af3d144c40f5ef51eb..7eeb5f8e9c1f54988e19d5dcc5c566b2fddc7264 100644 (file)
@@ -35,8 +35,10 @@ class SalmonQueueHandler extends QueueHandler
         assert(is_string($data['salmonuri']));
         assert(is_string($data['entry']));
 
+        $actor = Profile::staticGet($data['actor']);
+        
         $salmon = new Salmon();
-        $salmon->post($data['salmonuri'], $data['entry']);
+        $salmon->post($data['salmonuri'], $data['entry'], $actor);
 
         // @fixme detect failure and attempt to resend
         return true;
diff --git a/plugins/OStatus/lib/webfinger.php b/plugins/OStatus/lib/webfinger.php
deleted file mode 100644 (file)
index 4b777c9..0000000
+++ /dev/null
@@ -1,164 +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
-{
-    const PROFILEPAGE = 'http://webfinger.net/rel/profile-page';
-    const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
-    const HCARD       = 'http://microformats.org/profile/hcard';
-
-    /**
-     * 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);
-
-        if (!$content) {
-            return false;
-        }
-
-        return XRD::parse($content);
-    }
-
-    function fetchURL($url)
-    {
-        try {
-            $c = Cache::instance();
-            $content = $c->get('webfinger:url:'.$url);
-            if ($content !== false) {
-                return $content;
-            }
-            $client = new HTTPClient();
-            $response = $client->get($url);
-        } catch (HTTP_Request2_Exception $e) {
-            return false;
-        }
-
-        if ($response->getStatus() != 200) {
-            return false;
-        }
-
-        $body = $response->getBody();
-
-        $c->set('webfinger:url:'.$url, $body);
-
-        return $body;
-    }
-
-    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();
-    }
-}
-
index 16d27f8eb7f66072d24f1f5b98ba8606d85c9563..85df26c54cb752605c9babb4bccd4ef65f690085 100644 (file)
@@ -53,17 +53,22 @@ class XRD
         $xrd = new XRD();
 
         $dom = new DOMDocument();
-        $dom->loadXML($xml);
+        if (!$dom->loadXML($xml)) {
+            throw new Exception("Invalid XML");
+        }
         $xrd_element = $dom->getElementsByTagName('XRD')->item(0);
 
         // Check for host-meta host
-        $host = $xrd_element->getElementsByTagName('Host')->item(0)->nodeValue;
+        $host = $xrd_element->getElementsByTagName('Host')->item(0);
         if ($host) {
-            $xrd->host = $host;
+            $xrd->host = $host->nodeValue;
         }
 
         // Loop through other elements
         foreach ($xrd_element->childNodes as $node) {
+            if (!($node instanceof DOMElement)) {
+                continue;
+            }
             switch ($node->tagName) {
             case 'Expires':
                 $xrd->expires = $node->nodeValue;
@@ -156,20 +161,20 @@ class XRD
     function saveLink($doc, $link)
     {
         $link_element = $doc->createElement('Link');
-        if ($link['rel']) {
+        if (!empty($link['rel'])) {
             $link_element->setAttribute('rel', $link['rel']);
         }
-        if ($link['type']) {
+        if (!empty($link['type'])) {
             $link_element->setAttribute('type', $link['type']);
         }
-        if ($link['href']) {
+        if (!empty($link['href'])) {
             $link_element->setAttribute('href', $link['href']);
         }
-        if ($link['template']) {
+        if (!empty($link['template'])) {
             $link_element->setAttribute('template', $link['template']);
         }
 
-        if (is_array($link['title'])) {
+        if (!empty($link['title']) && is_array($link['title'])) {
             foreach($link['title'] as $title) {
                 $title = $doc->createElement('Title', $title);
                 $link_element->appendChild($title);
index dedc018e3fbd7200d7e2a07f427a86fc6ce23b7f..ee19cf3dbd8e6a14fa392900d4d72906ad1540f6 100644 (file)
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-12-07 20:38-0800\n"
+"POT-Creation-Date: 2010-03-01 14:08-0800\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -16,89 +16,297 @@ msgstr ""
 "Content-Type: text/plain; charset=CHARSET\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: tests/gettext-speedtest.php:57 FeedSubPlugin.php:76
-msgid "Feeds"
+#: actions/groupsalmon.php:51
+msgid "Can't accept remote posts for a remote group."
+msgstr ""
+
+#: actions/groupsalmon.php:123
+msgid "Can't read profile to set up group membership."
 msgstr ""
 
-#: FeedSubPlugin.php:77
-msgid "Feed subscription options"
+#: actions/groupsalmon.php:126 actions/groupsalmon.php:169
+msgid "Groups can't join groups."
 msgstr ""
 
-#: feedmunger.php:215
+#: actions/groupsalmon.php:153
 #, php-format
-msgid "New post: \"%1$s\" %2$s"
+msgid "Could not join remote user %1$s to group %2$s."
 msgstr ""
 
-#: actions/feedsubsettings.php:41
-msgid "Feed subscriptions"
+#: actions/groupsalmon.php:166
+msgid "Can't read profile to cancel group membership."
 msgstr ""
 
-#: actions/feedsubsettings.php:52
-msgid ""
-"You can subscribe to feeds from other sites; updates will appear in your "
-"personal timeline."
+#: actions/groupsalmon.php:182
+#, php-format
+msgid "Could not remove remote user %1$s from group %2$s."
+msgstr ""
+
+#: actions/ostatusinit.php:40
+msgid "You can use the local subscription!"
+msgstr ""
+
+#: actions/ostatusinit.php:61
+msgid "There was a problem with your session token. Try again, please."
+msgstr ""
+
+#: actions/ostatusinit.php:79 actions/ostatussub.php:439
+msgid "Subscribe to user"
+msgstr ""
+
+#: actions/ostatusinit.php:97
+#, php-format
+msgid "Subscribe to %s"
 msgstr ""
 
-#: actions/feedsubsettings.php:96
+#: actions/ostatusinit.php:102
+msgid "User nickname"
+msgstr ""
+
+#: actions/ostatusinit.php:103
+msgid "Nickname of the user you want to follow"
+msgstr ""
+
+#: actions/ostatusinit.php:106
+msgid "Profile Account"
+msgstr ""
+
+#: actions/ostatusinit.php:107
+msgid "Your account id (i.e. user@identi.ca)"
+msgstr ""
+
+#: actions/ostatusinit.php:110 actions/ostatussub.php:115
+#: OStatusPlugin.php:205
 msgid "Subscribe"
 msgstr ""
 
-#: actions/feedsubsettings.php:98
+#: actions/ostatusinit.php:128
+msgid "Must provide a remote profile."
+msgstr ""
+
+#: actions/ostatusinit.php:138
+msgid "Couldn't look up OStatus account profile."
+msgstr ""
+
+#: actions/ostatusinit.php:153
+msgid "Couldn't confirm remote profile address."
+msgstr ""
+
+#: actions/ostatusinit.php:171
+msgid "OStatus Connect"
+msgstr ""
+
+#: actions/ostatussub.php:68
+msgid "Address or profile URL"
+msgstr ""
+
+#: actions/ostatussub.php:70
+msgid "Enter the profile URL of a PubSubHubbub-enabled feed"
+msgstr ""
+
+#: actions/ostatussub.php:74
 msgid "Continue"
 msgstr ""
 
-#: actions/feedsubsettings.php:151
-msgid "Empty feed URL!"
+#: actions/ostatussub.php:112 OStatusPlugin.php:503
+msgid "Join"
+msgstr ""
+
+#: actions/ostatussub.php:113
+msgid "Join this group"
+msgstr ""
+
+#: actions/ostatussub.php:116
+msgid "Subscribe to this user"
+msgstr ""
+
+#: actions/ostatussub.php:137
+msgid "You are already subscribed to this user."
+msgstr ""
+
+#: actions/ostatussub.php:165
+msgid "You are already a member of this group."
 msgstr ""
 
-#: actions/feedsubsettings.php:161
+#: actions/ostatussub.php:286
+msgid "Empty remote profile URL!"
+msgstr ""
+
+#: actions/ostatussub.php:297
+msgid "Invalid address format."
+msgstr ""
+
+#: actions/ostatussub.php:302
 msgid "Invalid URL or could not reach server."
 msgstr ""
 
-#: actions/feedsubsettings.php:164
+#: actions/ostatussub.php:304
 msgid "Cannot read feed; server returned error."
 msgstr ""
 
-#: actions/feedsubsettings.php:167
+#: actions/ostatussub.php:306
 msgid "Cannot read feed; server returned an empty page."
 msgstr ""
 
-#: actions/feedsubsettings.php:170
+#: actions/ostatussub.php:308
 msgid "Bad HTML, could not find feed link."
 msgstr ""
 
-#: actions/feedsubsettings.php:173
+#: actions/ostatussub.php:310
 msgid "Could not find a feed linked from this URL."
 msgstr ""
 
-#: actions/feedsubsettings.php:176
+#: actions/ostatussub.php:312
 msgid "Not a recognized feed type."
 msgstr ""
 
-#: actions/feedsubsettings.php:180
-msgid "Bad feed URL."
+#: actions/ostatussub.php:315
+#, php-format
+msgid "Bad feed URL: %s %s"
+msgstr ""
+
+#. TRANS: OStatus remote group subscription dialog error.
+#: actions/ostatussub.php:336
+msgid "Already a member!"
 msgstr ""
 
-#: actions/feedsubsettings.php:188
-msgid "Feed is not PuSH-enabled; cannot subscribe."
+#. TRANS: OStatus remote group subscription dialog error.
+#: actions/ostatussub.php:346
+msgid "Remote group join failed!"
 msgstr ""
 
-#: actions/feedsubsettings.php:208
-msgid "Feed subscription failed! Bad response from hub."
+#. TRANS: OStatus remote group subscription dialog error.
+#: actions/ostatussub.php:350
+msgid "Remote group join aborted!"
 msgstr ""
 
-#: actions/feedsubsettings.php:218
+#. TRANS: OStatus remote subscription dialog error.
+#: actions/ostatussub.php:356
 msgid "Already subscribed!"
 msgstr ""
 
-#: actions/feedsubsettings.php:220
-msgid "Feed subscribed!"
+#. TRANS: OStatus remote subscription dialog error.
+#: actions/ostatussub.php:361
+msgid "Remote subscription failed!"
 msgstr ""
 
-#: actions/feedsubsettings.php:222
-msgid "Feed subscription failed!"
+#. TRANS: Page title for OStatus remote subscription form
+#: actions/ostatussub.php:459
+msgid "Authorize subscription"
+msgstr ""
+
+#: actions/ostatussub.php:470
+msgid ""
+"You can subscribe to users from other supported sites. Paste their address "
+"or profile URI below:"
+msgstr ""
+
+#: classes/Ostatus_profile.php:789
+#, php-format
+msgid "Tried to update avatar for unsaved remote profile %s"
 msgstr ""
 
-#: actions/feedsubsettings.php:231
-msgid "Previewing feed:"
+#: classes/Ostatus_profile.php:797
+#, php-format
+msgid "Unable to fetch avatar from %s"
+msgstr ""
+
+#: lib/salmonaction.php:41
+msgid "This method requires a POST."
+msgstr ""
+
+#: lib/salmonaction.php:45
+msgid "Salmon requires application/magic-envelope+xml"
+msgstr ""
+
+#: lib/salmonaction.php:55
+msgid "Salmon signature verification failed."
+msgstr ""
+
+#: lib/salmonaction.php:66
+msgid "Salmon post must be an Atom entry."
+msgstr ""
+
+#: lib/salmonaction.php:114
+msgid "Unrecognized activity type."
+msgstr ""
+
+#: lib/salmonaction.php:122
+msgid "This target doesn't understand posts."
+msgstr ""
+
+#: lib/salmonaction.php:127
+msgid "This target doesn't understand follows."
+msgstr ""
+
+#: lib/salmonaction.php:132
+msgid "This target doesn't understand unfollows."
+msgstr ""
+
+#: lib/salmonaction.php:137
+msgid "This target doesn't understand favorites."
+msgstr ""
+
+#: lib/salmonaction.php:142
+msgid "This target doesn't understand unfavorites."
+msgstr ""
+
+#: lib/salmonaction.php:147
+msgid "This target doesn't understand share events."
+msgstr ""
+
+#: lib/salmonaction.php:152
+msgid "This target doesn't understand joins."
+msgstr ""
+
+#: lib/salmonaction.php:157
+msgid "This target doesn't understand leave events."
+msgstr ""
+
+#: OStatusPlugin.php:319
+#, php-format
+msgid "Sent from %s via OStatus"
+msgstr ""
+
+#: OStatusPlugin.php:371
+msgid "Could not set up remote subscription."
+msgstr ""
+
+#: OStatusPlugin.php:487
+msgid "Could not set up remote group membership."
+msgstr ""
+
+#: OStatusPlugin.php:504
+#, php-format
+msgid "%s has joined group %s."
+msgstr ""
+
+#: OStatusPlugin.php:512
+msgid "Failed joining remote group."
+msgstr ""
+
+#: OStatusPlugin.php:553
+msgid "Leave"
+msgstr ""
+
+#: OStatusPlugin.php:554
+#, php-format
+msgid "%s has left group %s."
+msgstr ""
+
+#: OStatusPlugin.php:685
+msgid "Subscribe to remote user"
+msgstr ""
+
+#: OStatusPlugin.php:726
+msgid "Profile update"
+msgstr ""
+
+#: OStatusPlugin.php:727
+#, php-format
+msgid "%s has updated their profile page."
+msgstr ""
+
+#: tests/gettext-speedtest.php:57
+msgid "Feeds"
 msgstr ""
diff --git a/plugins/OStatus/scripts/updateostatus.php b/plugins/OStatus/scripts/updateostatus.php
new file mode 100644 (file)
index 0000000..d553a7d
--- /dev/null
@@ -0,0 +1,127 @@
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+
+$shortoptions = 'i:n:a';
+$longoptions = array('id=', 'nickname=', 'all');
+
+$helptext = <<<END_OF_UPDATEOSTATUS_HELP
+updateostatus.php [options]
+update the OMB subscriptions of a user to use OStatus if possible
+
+  -i --id       ID of user to update
+  -n --nickname nickname of the user to update
+  -a --all      update all
+
+END_OF_UPDATEOSTATUS_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+try {
+    $user = null;
+
+    if (have_option('i', 'id')) {
+        $id = get_option_value('i', 'id');
+        $user = User::staticGet('id', $id);
+        if (empty($user)) {
+            throw new Exception("Can't find user with id '$id'.");
+        }
+        updateProfileURL($user);
+    } else if (have_option('n', 'nickname')) {
+        $nickname = get_option_value('n', 'nickname');
+        $user = User::staticGet('nickname', $nickname);
+        if (empty($user)) {
+            throw new Exception("Can't find user with nickname '$nickname'");
+        }
+        updateProfileURL($user);
+    } else if (have_option('a', 'all')) {
+        $user = new User();
+        if ($user->find()) {
+            while ($user->fetch()) {
+                updateOStatus($user);
+            }
+        }
+    } else {
+        show_help();
+        exit(1);
+    }
+} catch (Exception $e) {
+    print $e->getMessage()."\n";
+    exit(1);
+}
+
+function updateOStatus($user)
+{
+    if (!have_option('q', 'quiet')) {
+        echo "{$user->nickname}...";
+    }
+
+    $up = $user->getProfile();
+
+    $sp = $user->getSubscriptions();
+
+    $rps = array();
+
+    while ($sp->fetch()) {
+        $remote = Remote_profile::staticGet('id', $sp->id);
+
+        if (!empty($remote)) {
+            $rps[] = clone($sp);
+        }
+    }
+
+    if (!have_option('q', 'quiet')) {
+        echo count($rps) . "\n";
+    }
+
+    foreach ($rps as $rp) {
+        try {
+            if (!have_option('q', 'quiet')) {
+                echo "Checking {$rp->nickname}...";
+            }
+
+            $op = Ostatus_profile::ensureProfile($rp->profileurl);
+
+            if (empty($op)) {
+                echo "can't convert.\n";
+                continue;
+            } else {
+                if (!have_option('q', 'quiet')) {
+                    echo "Converting...";
+                }
+                Subscription::cancel($up, $rp);
+                Subscription::start($up, $op->localProfile());
+                if (!have_option('q', 'quiet')) {
+                    echo "done.\n";
+                }
+            }
+
+        } catch (Exception $e) {
+            if (!have_option('q', 'quiet')) {
+                echo "fail.\n";
+            }
+            continue;
+            common_log(LOG_WARNING, "Couldn't convert OMB subscription (" . $up->nickname . ", " . $rp->nickname .
+                       ") to OStatus: " . $e->getMessage());
+            continue;
+        }
+    }
+}
diff --git a/plugins/RegisterThrottle/RegisterThrottlePlugin.php b/plugins/RegisterThrottle/RegisterThrottlePlugin.php
new file mode 100644 (file)
index 0000000..05709b7
--- /dev/null
@@ -0,0 +1,249 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Throttle registration by IP address
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Spam
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Throttle registration by IP address
+ *
+ * We a) record IP address of registrants and b) throttle registrations.
+ *
+ * @category  Spam
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+class RegisterThrottlePlugin extends Plugin
+{
+    /**
+     * Array of time spans in seconds to limits.
+     *
+     * Default is 3 registrations per hour, 5 per day, 10 per week.
+     */
+
+    public $regLimits = array(604800 => 10, // per week
+                              86400 => 5, // per day
+                              3600 => 3); // per hour
+
+    /**
+     * Database schema setup
+     *
+     * We store user registrations in a table registration_ip.
+     *
+     * @return boolean hook value; true means continue processing, false means stop.
+     */
+
+    function onCheckSchema()
+    {
+        $schema = Schema::get();
+
+        // For storing user-submitted flags on profiles
+
+        $schema->ensureTable('registration_ip',
+                             array(new ColumnDef('user_id', 'integer', null,
+                                                 false, 'PRI'),
+                                   new ColumnDef('ipaddress', 'varchar', 15, false, 'MUL'),
+                                   new ColumnDef('created', 'timestamp', null, false, 'MUL')));
+
+        return true;
+    }
+
+    /**
+     * Load related modules when needed
+     *
+     * @param string $cls Name of the class to be loaded
+     *
+     * @return boolean hook value; true means continue processing, false means stop.
+     */
+
+    function onAutoload($cls)
+    {
+        $dir = dirname(__FILE__);
+
+        switch ($cls)
+        {
+        case 'Registration_ip':
+            include_once $dir . '/'.$cls.'.php';
+            return false;
+        default:
+            return true;
+        }
+    }
+
+    /**
+     * Called when someone tries to register.
+     *
+     * We check the IP here to determine if it goes over any of our
+     * configured limits.
+     *
+     * @param Action $action Action that is being executed
+     *
+     * @return boolean hook value
+     *
+     */
+
+    function onStartRegistrationTry($action)
+    {
+        $ipaddress = $this->_getIpAddress();
+
+        if (empty($ipaddress)) {
+            throw new ServerException(_m('Cannot find IP address.'));
+        }
+
+        foreach ($this->regLimits as $seconds => $limit) {
+
+            $this->debug("Checking $seconds ($limit)");
+
+            $reg = $this->_getNthReg($ipaddress, $limit);
+
+            if (!empty($reg)) {
+                $this->debug("Got a {$limit}th registration.");
+                $regtime = strtotime($reg->created);
+                $now     = time();
+                $this->debug("Comparing {$regtime} to {$now}");
+                if ($now - $regtime < $seconds) {
+                    throw new Exception(_("Too many registrations. Take a break and try again later."));
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Called after someone registers.
+     *
+     * We record the successful registration and IP address.
+     *
+     * @param Action $action Action that is being executed
+     *
+     * @return boolean hook value
+     *
+     */
+
+    function onEndRegistrationTry($action)
+    {
+        $ipaddress = $this->_getIpAddress();
+
+        if (empty($ipaddress)) {
+            throw new ServerException(_m('Cannot find IP address.'));
+        }
+
+        $user = common_current_user();
+
+        if (empty($user)) {
+            throw new ServerException(_m('Cannot find user after successful registration.'));
+        }
+
+        $reg = new Registration_ip();
+
+        $reg->user_id   = $user->id;
+        $reg->ipaddress = $ipaddress;
+
+        $result = $reg->insert();
+
+        if (!$result) {
+            common_log_db_error($reg, 'INSERT', __FILE__);
+            // @todo throw an exception?
+        }
+
+        return true;
+    }
+
+    /**
+     * Check the version of the plugin.
+     *
+     * @param array &$versions Version array.
+     *
+     * @return boolean hook value
+     */
+
+    function onPluginVersion(&$versions)
+    {
+        $versions[] = array('name' => 'RegisterThrottle',
+                            'version' => STATUSNET_VERSION,
+                            'author' => 'Evan Prodromou',
+                            'homepage' => 'http://status.net/wiki/Plugin:RegisterThrottle',
+                            'description' =>
+                            _m('Throttles excessive registration from a single IP.'));
+        return true;
+    }
+
+    /**
+     * Gets the current IP address.
+     *
+     * @return string IP address or null if not found.
+     */
+
+    private function _getIpAddress()
+    {
+        $keys = array('HTTP_X_FORWARDED_FOR',
+                      'CLIENT-IP',
+                      'REMOTE_ADDR');
+
+        foreach ($keys as $k) {
+            if (!empty($_SERVER[$k])) {
+                return $_SERVER[$k];
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets the Nth registration with the given IP address.
+     *
+     * @param string  $ipaddress Address to key on
+     * @param integer $n         Nth address
+     *
+     * @return Registration_ip nth registration or null if not found.
+     */
+
+    private function _getNthReg($ipaddress, $n)
+    {
+        $reg = new Registration_ip();
+
+        $reg->ipaddress = $ipaddress;
+
+        $reg->orderBy('created DESC');
+        $reg->limit($n - 1, 1);
+
+        if ($reg->find(true)) {
+            return $reg;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/plugins/RegisterThrottle/Registration_ip.php b/plugins/RegisterThrottle/Registration_ip.php
new file mode 100644 (file)
index 0000000..7e61d08
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+/**
+ * Data class for storing IP addresses of new registrants.
+ *
+ * PHP version 5
+ *
+ * @category Data
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+require_once INSTALLDIR . '/classes/Memcached_DataObject.php';
+
+/**
+ * Data class for storing IP addresses of new registrants.
+ *
+ * @category Spam
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ */
+
+class Registration_ip extends Memcached_DataObject
+{
+    public $__table = 'registration_ip';     // table name
+    public $user_id;                         // int(4)  primary_key not_null
+    public $ipaddress;                       // varchar(15)
+    public $created;                         // timestamp
+
+    /**
+     * Get an instance by key
+     *
+     * @param string $k Key to use to lookup (usually 'user_id' for this class)
+     * @param mixed  $v Value to lookup
+     *
+     * @return User_greeting_count object found, or null for no hits
+     *
+     */
+
+    function staticGet($k, $v=null)
+    {
+        return Memcached_DataObject::staticGet('Registration_ip', $k, $v);
+    }
+
+    /**
+     * return table definition for DB_DataObject
+     *
+     * @return array array of column definitions
+     */
+
+    function table()
+    {
+        return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+                     'ipaddress' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+                     'created' => DB_DATAOBJECT_MYSQLTIMESTAMP + DB_DATAOBJECT_NOTNULL);
+    }
+
+    /**
+     * return key definitions for DB_DataObject
+     *
+     * DB_DataObject needs to know about keys that the table has; this function
+     * defines them.
+     *
+     * @return array key definitions
+     */
+
+    function keys()
+    {
+        return array('user_id' => 'K');
+    }
+
+    /**
+     * return key definitions for Memcached_DataObject
+     *
+     * Our caching system uses the same key definitions, but uses a different
+     * method to get them.
+     *
+     * @return array key definitions
+     */
+
+    function keyTypes()
+    {
+        return $this->keys();
+    }
+
+    /**
+     * Magic formula for non-autoincrementing integer primary keys
+     *
+     * If a table has a single integer column as its primary key, DB_DataObject
+     * assumes that the column is auto-incrementing and makes a sequence table
+     * to do this incrementation. Since we don't need this for our class, we
+     * overload this method and return the magic formula that DB_DataObject needs.
+     *
+     * @return array magic three-false array that stops auto-incrementing.
+     */
+
+    function sequenceKey()
+    {
+        return array(false, false, false);
+    }
+}
index 71f3308281163b9338efc660c3f156e986e61bd1..654b9c9d5beada8e83664c7a3a3138310e50466f 100644 (file)
@@ -75,7 +75,7 @@ class SphinxSearch extends SearchEngine
     {
         if ('chron' === $mode) {
             $this->sphinx->SetSortMode(SPH_SORT_ATTR_DESC, 'created_ts');
-            return $this->target->orderBy('created desc');
+            return $this->target->orderBy('id desc');
         }
     }
 
index 882d744564dbb4c4398cf9741fe7d6eafc90d739..e0b5fc906be8c0e9fc79bb50d20496f8aa786cd7 100644 (file)
@@ -119,6 +119,9 @@ function newSub($i)
 
 function main($usercount, $noticeavg, $subsavg, $tagmax)
 {
+    global $config;
+    $config['site']['dupelimit'] = -1;
+
     $n = 1;
 
     newUser(0);
index 61a6ac78331d543f66af381255961b103ee9f15a..0bfa62a22e79d2c47a8bfb84094a6d3cb4c3bf5e 100755 (executable)
@@ -36,7 +36,11 @@ xgettext \
     --default-domain=$domain \
     --output=locale/$domain.po \
     --language=PHP \
-    --keyword="_m:1" \
+    --add-comments=TRANS \
+    --keyword="_m:1,1t" \
+    --keyword="_m:1c,2,2t" \
+    --keyword="_m:1,2,3t" \
+    --keyword="_m:1c,2,3,4t" \
     --keyword="pgettext:1c,2" \
     --keyword="npgettext:1c,2,3" \
     actions/*.php \
@@ -62,6 +66,7 @@ xgettext \
     --default-domain=$domain \
     --output=locale/$domain.po \
     --language=PHP \
+    --add-comments=TRANS \
     --keyword='' \
     --keyword="_m:1,1t" \
     --keyword="_m:1c,2,2t" \
index d1d8717343576a9b2c6272f1d426b4415a95c332..7bf9cec7c453d57e048b15ec2671ba31c602faf2 100644 (file)
@@ -121,10 +121,14 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase
         $this->assertEquals($act->actor->title, 'Test User');
         $this->assertEquals($act->actor->id, 'http://example.net/mysite/user/3');
         $this->assertEquals($act->actor->link, 'http://example.net/mysite/testuser');
+
+        $avatars = $act->actor->avatarLinks;
+
         $this->assertEquals(
-            $act->actor->avatar,
-            'http://example.net/mysite/avatar/3-96-20100224004207.jpeg'
+                $avatars[0]->url,
+                'http://example.net/mysite/avatar/3-96-20100224004207.jpeg'
         );
+
         $this->assertEquals($act->actor->displayName, 'Test User');
 
         $poco = $act->actor->poco;
index 52f97f6b12b64378be02d92685f87afe49b744e3..f32c57ea451758aa48fa0017817deb74e00cf784 100644 (file)
@@ -799,8 +799,8 @@ list-style-type:none;
 display:inline;
 }
 .entity_tags li {
-float:left;
-margin-right:11px;
+display:inline;
+margin-right:7px;
 }
 
 .aside .section {