]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.9.x' of gitorious.org:statusnet/mainline into 1.0.x
authorBrion Vibber <brion@pobox.com>
Mon, 4 Oct 2010 19:54:36 +0000 (12:54 -0700)
committerBrion Vibber <brion@pobox.com>
Mon, 4 Oct 2010 19:54:36 +0000 (12:54 -0700)
Conflicts:
actions/hostmeta.php
actions/imsettings.php
classes/User.php
lib/adminpanelaction.php
lib/channel.php
lib/default.php
lib/router.php
lib/util.php

32 files changed:
1  2 
EVENTS.txt
actions/apiaccountupdatedeliverydevice.php
actions/shownotice.php
classes/File_redirection.php
classes/Memcached_DataObject.php
classes/Notice.php
classes/Profile.php
classes/User.php
lib/adminpanelaction.php
lib/apiaction.php
lib/cache.php
lib/channel.php
lib/command.php
lib/common.php
lib/connectsettingsaction.php
lib/default.php
lib/htmloutputter.php
lib/router.php
lib/util.php
lib/xrd.php
plugins/BitlyUrl/BitlyUrlPlugin.php
plugins/CasAuthentication/caslogin.php
plugins/ClientSideShorten/ClientSideShortenPlugin.php
plugins/Geonames/GeonamesPlugin.php
plugins/Imap/imapmanager.php
plugins/LdapCommon/LdapCommon.php
plugins/LilUrl/LilUrlPlugin.php
plugins/Minify/MinifyPlugin.php
plugins/Minify/minify.php
plugins/OStatus/OStatusPlugin.php
plugins/OpenID/OpenIDPlugin.php
plugins/Recaptcha/RecaptchaPlugin.php

diff --combined EVENTS.txt
index 789c985ae3c2272dd4d343d48e92161044db97cd,24964161737e3845740620729a46b060ea75d5c6..58189b9c3f2681e9927a1512328d4ece0ae6abb4
@@@ -258,10 -258,28 +258,28 @@@ EndShowExportData: just after showing t
  - $action: action object being shown
  
  StartShowNoticeItem: just before showing the notice item
- - $action: action object being shown
+ - $item: The NoticeListItem object being shown
  
  EndShowNoticeItem: just after showing the notice item
- - $action: action object being shown
+ - $item: the NoticeListItem object being shown
+ StartShowNoticeInfo: just before showing notice info
+ - $item: The NoticeListItem object being shown
+ EndShowNoticeInfo: just after showing notice info
+ - $item: The NoticeListItem object being shown
+ StartShowNoticeOptions: just before showing notice options like fave, repeat, etc.
+ - $item: the NoticeListItem object being shown
+ EndShowNoticeOptions: just after showing notice options like fave, repeat, etc.
+ - $item: the NoticeListItem object being shown
+ StartShowFaveForm: just before showing the fave form
+ - $item: the NoticeListItem object being shown
+ EndShowFaveForm: just after showing the fave form
+ - $item: the NoticeListItem object being shown
  
  StartShowPageNotice: just before showing the page notice (instructions or error)
  - $action: action object being shown
@@@ -551,12 -569,6 +569,12 @@@ EndPublicXRDS: End XRDS output (right b
  - $action: the current action
  - &$xrdsoutputter - XRDSOutputter object to write to
  
 +StartHostMetaLinks: Start /.well-known/host-meta links
 +- &links: array containing the links elements to be written
 +
 +EndHostMetaLinks: End /.well-known/host-meta links
 +- &links: array containing the links elements to be written
 +
  StartCheckPassword: Check a username/password
  - $nickname: The nickname to check
  - $password: The password to check
@@@ -722,24 -734,6 +740,24 @@@ StartShowContentLicense: Showing the de
  EndShowContentLicense: Showing the default license for content
  - $action: the current action
  
 +GetImTransports: Get IM transports that are available
 +- &$transports: append your transport to this array like so: $transports[transportName]=array('display'=>display)
 +
 +NormalizeImScreenname: Normalize an IM screenname
 +- $transport: transport the screenname is on
 +- &$screenname: screenname to be normalized
 +
 +ValidateImScreenname: Validate an IM screenname
 +- $transport: transport the screenname is on
 +- $screenname: screenname to be validated
 +- $valid: is the screenname valid?
 +
 +SendImConfirmationCode: Send a confirmation code to confirm a user owns an IM screenname
 +- $transport: transport the screenname exists on
 +- $screenname: screenname being confirmed
 +- $code: confirmation code for confirmation URL
 +- $user: user requesting the confirmation
 +
  StartUserRegister: When a new user is being registered
  - &$profile: new profile data (no ID)
  - &$user: new user account (no ID or URI)
@@@ -780,6 -774,22 +798,22 @@@ EndDisfavorNotice: After saving a notic
  - $profile: profile of the person faving (can be remote!)
  - $notice: notice being faved
  
+ StartFavorNoticeForm: starting the data in the form for favoring a notice
+ - $FavorForm: the favor form being shown
+ - $notice: notice being favored
+ EndFavorNoticeForm: Ending the data in the form for favoring a notice
+ - $FavorForm: the favor form being shown
+ - $notice: notice being favored
+ StartDisFavorNoticeForm: starting the data in the form for disfavoring a notice
+ - $DisfavorForm: the disfavor form being shown
+ - $notice: notice being difavored
+ EndDisFavorNoticeForm: Ending the data in the form for disfavoring a notice
+ - $DisfavorForm: the disfavor form being shown
+ - $notice: notice being disfavored
  StartFindMentions: start finding mentions in a block of text
  - $sender: sender profile
  - $text: plain text version of the notice
@@@ -1110,3 -1120,19 +1144,19 @@@ StartDeleteOwnNotice: when a user start
  EndDeleteOwnNotice: when a user has deleted their own notice
  - $user: the user doing the delete
  - $notice: the notice being deleted
+ StartShowFeedLinkList: before showing the feed list in the sidebar
+ - $action: action being executed
+ - $feeds: list of feeds to show
+ EndShowFeedLinkList: after showing the feed list in the sidebar
+ - $action: action being executed
+ - $feeds: list of feeds shown
+ StartShowFeedLink: before showing an individual feed item
+ - $action: action being executed
+ - $feed: feed to show
+ EndShowFeedLink: after showing an individual feed
+ - $action: action being executed
+ - $feed: feed to show
index e25b9a954d1ada5e1fea82d9fa83a66ab8dfa92a,2d903cb46087fe51bee5f0afac8d59849e4ac256..e732e23560d6b323e0c1af295e2981e35b2e9dee
@@@ -83,6 -83,7 +83,7 @@@ class ApiAccountUpdateDeliveryDeviceAct
  
          if ($_SERVER['REQUEST_METHOD'] != 'POST') {
              $this->clientError(
+                 // TRANS: Client error message. POST is a HTTP command. It should not be translated.
                  _('This method requires a POST.'),
                  400, $this->format
              );
          if (strtolower($this->device) == 'sms') {
              $this->user->smsnotify = true;
          } elseif (strtolower($this->device) == 'im') {
 -            $this->user->jabbernotify = true;
 +            //TODO IM is pluginized now, so what should we do?
 +            //Enable notifications for all IM plugins?
 +            //For now, don't do anything
 +            //$this->user->jabbernotify = true;
          } elseif (strtolower($this->device == 'none')) {
              $this->user->smsnotify    = false;
 -            $this->user->jabbernotify = false;
 +            //TODO IM is pluginized now, so what should we do?
 +            //Disable notifications for all IM plugins?
 +            //For now, don't do anything
 +            //$this->user->jabbernotify = false;
          }
  
          $result = $this->user->update($original);
diff --combined actions/shownotice.php
index 77ba2ce9fda913ba217803b9bc9cd105e54bc155,005335e3b4b281831bb2a01dada39c9733392d37..1161de86368857f72efabb5b8268abecb5f8bca8
@@@ -151,6 -151,7 +151,7 @@@ class ShownoticeAction extends OwnerDes
            strtotime($this->avatar->modified) : 0;
  
          return 'W/"' . implode(':', array($this->arg('action'),
+                                           common_user_cache_hash(),
                                            common_language(),
                                            $this->notice->id,
                                            strtotime($this->notice->created),
                                           'content' => $id->toString()));
          }
  
 -        if ($user->jabbermicroid && $user->jabber && $this->notice->uri) {
 -            $id = new Microid('xmpp:', $user->jabber,
 -                              $this->notice->uri);
 -            $this->element('meta', array('name' => 'microid',
 -                                         'content' => $id->toString()));
 -        }
          $this->element('link',array('rel'=>'alternate',
              'type'=>'application/json+oembed',
              'href'=>common_local_url(
                  array(),
                  array('format'=>'xml','url'=>$this->notice->uri)),
              'title'=>'oEmbed'),null);
+         // Extras to aid in sharing notices to Facebook
+         $avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
+         $avatarUrl = ($avatar) ?
+                      $avatar->displayUrl() :
+                      Avatar::defaultImage($avatar_size);
+         $this->element('meta', array('property' => 'og:image',
+                                      'content' => $avatarUrl));
+         $this->element('meta', array('property' => 'og:description',
+                                      'content' => $this->notice->content));
      }
  }
  
@@@ -307,10 -324,14 +318,14 @@@ class SingleNoticeItem extends NoticeLi
      function show()
      {
          $this->showStart();
-         $this->showNotice();
-         $this->showNoticeAttachments();
-         $this->showNoticeInfo();
-         $this->showNoticeOptions();
+         if (Event::handle('StartShowNoticeItem', array($this))) {
+             $this->showNotice();
+             $this->showNoticeAttachments();
+             $this->showNoticeInfo();
+             $this->showNoticeOptions();
+             Event::handle('EndShowNoticeItem', array($this));
+         }
          $this->showEnd();
      }
  
index 6a86197d9378d7abc358815ff32ac62bef1f313a,68fed77e8bb3eef6402fc0b737676c193cf3b420..92f0125a406dca56accd2439e9aef3f4f57bf14f
@@@ -176,52 -176,22 +176,52 @@@ class File_redirection extends Memcache
       * @param string $long_url
       * @return string
       */
 -    function makeShort($long_url) {
  
 +    function makeShort($long_url)
 +    {
          $canon = File_redirection::_canonUrl($long_url);
  
          $short_url = File_redirection::_userMakeShort($canon);
  
          // Did we get one? Is it shorter?
 -        if (!empty($short_url) && mb_strlen($short_url) < mb_strlen($long_url)) {
 +
 +        if (!empty($short_url)) {
 +            return $short_url;
 +        } else {
 +            return $long_url;
 +        }
 +    }
 +
 +    /**
 +     * Shorten a URL with the current user's configured shortening
 +     * options, if applicable.
 +     *
 +     * If it cannot be shortened or the "short" URL is longer than the
 +     * original, the original is returned.
 +     *
 +     * If the referenced item has not been seen before, embedding data
 +     * may be saved.
 +     *
 +     * @param string $long_url
 +     * @return string
 +     */
 +
 +    function forceShort($long_url)
 +    {
 +        $canon = File_redirection::_canonUrl($long_url);
 +
 +        $short_url = File_redirection::_userMakeShort($canon, true);
 +
 +        // Did we get one? Is it shorter?
 +        if (!empty($short_url)) {
              return $short_url;
          } else {
              return $long_url;
          }
      }
  
 -    function _userMakeShort($long_url) {
 -        $short_url = common_shorten_url($long_url);
 +    function _userMakeShort($long_url, $force = false) {
 +        $short_url = common_shorten_url($long_url, $force);
          if (!empty($short_url) && $short_url != $long_url) {
              $short_url = (string)$short_url;
              // store it
          $file_redir->insert();
      }
  }
index 6feb59c34188f6f0de81a62948791f23d06d13ae,ccfd886a1d3dc3ce9ce8de594aa1c5287f5d8634..8ffb46cc52d5da0e4a4a2b43cf453f3748858da6
@@@ -124,7 -124,7 +124,7 @@@ class Memcached_DataObject extends Safe
      }
  
      static function memcache() {
 -        return common_memcache();
 +        return Cache::instance();
      }
  
      static function cacheKey($cls, $k, $v) {
                  str_replace("\n", " ", $e->getTraceAsString()));
          }
          $vstr = self::valueString($v);
 -        return common_cache_key(strtolower($cls).':'.$k.':'.$vstr);
 +        return Cache::key(strtolower($cls).':'.$k.':'.$vstr);
      }
  
      static function getcached($cls, $k, $v) {
                         str_replace("\n", " ", $e->getTraceAsString()));
              return false;
          } else {
-               $keys = $this->_allCacheKeys();
+             $keys = $this->_allCacheKeys();
  
-               foreach ($keys as $key) {
-                   $c->set($key, $this);
-               }
+             foreach ($keys as $key) {
+                 $c->set($key, $this);
+             }
          }
      }
  
              $inst->query($qry);
              return $inst;
          }
 -        $key_part = common_keyize($cls).':'.md5($qry);
 -        $ckey = common_cache_key($key_part);
 +        $key_part = Cache::keyize($cls).':'.md5($qry);
 +        $ckey = Cache::key($key_part);
          $stored = $c->get($ckey);
  
          if ($stored !== false) {
  
          $keyPart = vsprintf($format, $args);
  
 -        $cacheKey = common_cache_key($keyPart);
 +        $cacheKey = Cache::key($keyPart);
  
          return $c->delete($cacheKey);
      }
              return false;
          }
  
 -        $cacheKey = common_cache_key($keyPart);
 +        $cacheKey = Cache::key($keyPart);
  
          return $c->get($cacheKey);
      }
              return false;
          }
  
 -        $cacheKey = common_cache_key($keyPart);
 +        $cacheKey = Cache::key($keyPart);
  
          return $c->set($cacheKey, $value, $flag, $expiry);
      }
          return $vstr;
      }
  }
diff --combined classes/Notice.php
index 4c6efd3eb72e98191c7d1253069ae6055193afcb,e268544b5089a619d76cb31316a207f1d18a11ae..a8ec0529f26b419a9a25f85d92681ba213ddd857
@@@ -593,7 -593,7 +593,7 @@@ class Notice extends Memcached_DataObje
  
      function getStreamByIds($ids)
      {
 -        $cache = common_memcache();
 +        $cache = Cache::instance();
  
          if (!empty($cache)) {
              $notices = array();
                  1,
                  1
              );
              if ($conversation->N > 0) {
                  return true;
              }
      }
  
      /**
-      * @param $groups array of Group *objects*
-      * @param $recipients array of profile *ids*
+      * Pull up a full list of local recipients who will be getting
+      * this notice in their inbox. Results will be cached, so don't
+      * change the input data wily-nilly!
+      *
+      * @param array $groups optional list of Group objects;
+      *              if left empty, will be loaded from group_inbox records
+      * @param array $recipient optional list of reply profile ids
+      *              if left empty, will be loaded from reply records
+      * @return array associating recipient user IDs with an inbox source constant
       */
      function whoGets($groups=null, $recipients=null)
      {
          $c = self::memcache();
  
          if (!empty($c)) {
 -            $ni = $c->get(common_cache_key('notice:who_gets:'.$this->id));
 +            $ni = $c->get(Cache::key('notice:who_gets:'.$this->id));
              if ($ni !== false) {
                  return $ni;
              }
              $ni[$id] = NOTICE_INBOX_SOURCE_SUB;
          }
  
-         $profile = $this->getProfile();
          foreach ($groups as $group) {
              $users = $group->getUserMembers();
              foreach ($users as $id) {
                  if (!array_key_exists($id, $ni)) {
-                     $user = User::staticGet('id', $id);
-                     if (!$user->hasBlocked($profile)) {
-                         $ni[$id] = NOTICE_INBOX_SOURCE_GROUP;
-                     }
+                     $ni[$id] = NOTICE_INBOX_SOURCE_GROUP;
                  }
              }
          }
  
          foreach ($recipients as $recipient) {
              if (!array_key_exists($recipient, $ni)) {
-                 $recipientUser = User::staticGet('id', $recipient);
-                 if (!empty($recipientUser)) {
-                     $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY;
-                 }
+                 $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY;
+             }
+         }
+         // Exclude any deleted, non-local, or blocking recipients.
+         $profile = $this->getProfile();
+         foreach ($ni as $id => $source) {
+             $user = User::staticGet('id', $id);
+             if (empty($user) || $user->hasBlocked($profile)) {
+                 unset($ni[$id]);
              }
          }
  
          if (!empty($c)) {
              // XXX: pack this data better
 -            $c->set(common_cache_key('notice:who_gets:'.$this->id), $ni);
 +            $c->set(Cache::key('notice:who_gets:'.$this->id), $ni);
          }
  
          return $ni;
          return $groups;
      }
  
+     function asActivity()
+     {
+         $profile = $this->getProfile();
+         $act = new Activity();
+         $act->actor     = ActivityObject::fromProfile($profile);
+         $act->verb      = ActivityVerb::POST;
+         $act->objects[] = ActivityObject::fromNotice($this);
+         $act->time    = strtotime($this->created);
+         $act->link    = $this->bestUrl();
+         $act->content = common_xml_safe_str($this->rendered);
+         $act->id      = $this->uri;
+         $act->title   = common_xml_safe_str($this->content);
+         $ctx = new ActivityContext();
+         if (!empty($this->reply_to)) {
+             $reply = Notice::staticGet('id', $this->reply_to);
+             if (!empty($reply)) {
+                 $ctx->replyToID  = $reply->uri;
+                 $ctx->replyToUrl = $reply->bestUrl();
+             }
+         }
+         $ctx->location = $this->getLocation();
+         $conv = null;
+         if (!empty($this->conversation)) {
+             $conv = Conversation::staticGet('id', $this->conversation);
+             if (!empty($conv)) {
+                 $ctx->conversation = $conv->uri;
+             }
+         }
+         $reply_ids = $this->getReplies();
+         foreach ($reply_ids as $id) {
+             $profile = Profile::staticGet('id', $id);
+             if (!empty($profile)) {
+                 $ctx->attention[] = $profile->getUri();
+             }
+         }
+         $groups = $this->getGroups();
+         foreach ($groups as $group) {
+             $ctx->attention[] = $group->uri;
+         }
+         $act->context = $ctx;
+         return $act;
+     }
      // This has gotten way too long. Needs to be sliced up into functional bits
      // or ideally exported to a utility class.
  
          }
  
          if (Event::handle('StartActivitySource', array(&$this, &$xs))) {
              if ($source) {
                  $atom_feed = $profile->getAtomFeed();
  
                  if (!empty($atom_feed)) {
                      $xs->elementStart('source');
  
                      // XXX: we should store the actual feed ID
  
      function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0)
      {
 -        $cache = common_memcache();
 +        $cache = Cache::instance();
  
          if (empty($cache) ||
              $since_id != 0 || $max_id != 0 ||
                                                                        $max_id)));
          }
  
 -        $idkey = common_cache_key($cachekey);
 +        $idkey = Cache::key($cachekey);
  
          $idstr = $cache->get($idkey);
  
  
      function repeatStream($limit=100)
      {
 -        $cache = common_memcache();
 +        $cache = Cache::instance();
  
          if (empty($cache)) {
              $ids = $this->_repeatStreamDirect($limit);
          } else {
 -            $idstr = $cache->get(common_cache_key('notice:repeats:'.$this->id));
 +            $idstr = $cache->get(Cache::key('notice:repeats:'.$this->id));
              if ($idstr !== false) {
                  $ids = explode(',', $idstr);
              } else {
                  $ids = $this->_repeatStreamDirect(100);
 -                $cache->set(common_cache_key('notice:repeats:'.$this->id), implode(',', $ids));
 +                $cache->set(Cache::key('notice:repeats:'.$this->id), implode(',', $ids));
              }
              if ($limit < 100) {
                  // We do a max of 100, so slice down to limit
          $options = array();
  
          if (!empty($location_id) && !empty($location_ns)) {
              $options['location_id'] = $location_id;
              $options['location_ns'] = $location_ns;
  
              }
  
          } else if (!empty($lat) && !empty($lon)) {
              $options['lat'] = $lat;
              $options['lon'] = $lon;
  
                  $options['location_ns'] = $location->location_ns;
              }
          } else if (!empty($profile)) {
              if (isset($profile->lat) && isset($profile->lon)) {
                  $options['lat'] = $profile->lat;
                  $options['lon'] = $profile->lon;
  
          if ($tag->find()) {
              while ($tag->fetch()) {
 -                self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, common_keyize($tag->tag));
 -                self::blow('profile:notice_ids_tagged:%d:%s;last', $this->profile_id, common_keyize($tag->tag));
 -                self::blow('notice_tag:notice_ids:%s', common_keyize($tag->tag));
 -                self::blow('notice_tag:notice_ids:%s;last', common_keyize($tag->tag));
 +                self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, Cache::keyize($tag->tag));
 +                self::blow('profile:notice_ids_tagged:%d:%s;last', $this->profile_id, Cache::keyize($tag->tag));
 +                self::blow('notice_tag:notice_ids:%s', Cache::keyize($tag->tag));
 +                self::blow('notice_tag:notice_ids:%s;last', Cache::keyize($tag->tag));
                  $tag->delete();
              }
          }
      {
          // We always insert for the author so they don't
          // have to wait
+         Event::handle('StartNoticeDistribute', array($this));
  
          $user = User::staticGet('id', $this->profile_id);
          if (!empty($user)) {
diff --combined classes/Profile.php
index 230b3aa3a08683f14add72fd22d6eebe1f3766eb,3844077e629371adc7677131af778f10dbb12ca1..1d130c44adcac8a2ef08a1aca6194ac66ee3ad0c
@@@ -103,7 -103,6 +103,6 @@@ class Profile extends Memcached_DataObj
          foreach (array(AVATAR_PROFILE_SIZE, AVATAR_STREAM_SIZE, AVATAR_MINI_SIZE) as $size) {
              # We don't do a scaled one if original is our scaled size
              if (!($avatar->width == $size && $avatar->height == $size)) {
                  $scaled_filename = $imagefile->resize($size);
  
                  //$scaled = DB_DataObject::factory('avatar');
  
      function subscriptionCount()
      {
 -        $c = common_memcache();
 +        $c = Cache::instance();
  
          if (!empty($c)) {
 -            $cnt = $c->get(common_cache_key('profile:subscription_count:'.$this->id));
 +            $cnt = $c->get(Cache::key('profile:subscription_count:'.$this->id));
              if (is_integer($cnt)) {
                  return (int) $cnt;
              }
          $cnt = ($cnt > 0) ? $cnt - 1 : $cnt;
  
          if (!empty($c)) {
 -            $c->set(common_cache_key('profile:subscription_count:'.$this->id), $cnt);
 +            $c->set(Cache::key('profile:subscription_count:'.$this->id), $cnt);
          }
  
          return $cnt;
  
      function subscriberCount()
      {
 -        $c = common_memcache();
 +        $c = Cache::instance();
          if (!empty($c)) {
 -            $cnt = $c->get(common_cache_key('profile:subscriber_count:'.$this->id));
 +            $cnt = $c->get(Cache::key('profile:subscriber_count:'.$this->id));
              if (is_integer($cnt)) {
                  return (int) $cnt;
              }
          $cnt = (int) $sub->count('distinct subscriber');
  
          if (!empty($c)) {
 -            $c->set(common_cache_key('profile:subscriber_count:'.$this->id), $cnt);
 +            $c->set(Cache::key('profile:subscriber_count:'.$this->id), $cnt);
          }
  
          return $cnt;
      }
  
 -        $cache = common_memcache();
+     function hasFave($notice)
+     {
++        $cache = Cache::instance();
+         // XXX: Kind of a hack.
+         if (!empty($cache)) {
+             // This is the stream of favorite notices, in rev chron
+             // order. This forces it into cache.
+             $ids = Fave::stream($this->id, 0, NOTICE_CACHE_WINDOW);
+             // If it's in the list, then it's a fave
+             if (in_array($notice->id, $ids)) {
+                 return true;
+             }
+             // If we're not past the end of the cache window,
+             // then the cache has all available faves, so this one
+             // is not a fave.
+             if (count($ids) < NOTICE_CACHE_WINDOW) {
+                 return false;
+             }
+             // Otherwise, cache doesn't have all faves;
+             // fall through to the default
+         }
+         $fave = Fave::pkeyGet(array('user_id' => $this->id,
+                                     'notice_id' => $notice->id));
+         return ((is_null($fave)) ? false : true);
+     }
      function faveCount()
      {
 -        $c = common_memcache();
 +        $c = Cache::instance();
          if (!empty($c)) {
 -            $cnt = $c->get(common_cache_key('profile:fave_count:'.$this->id));
 +            $cnt = $c->get(Cache::key('profile:fave_count:'.$this->id));
              if (is_integer($cnt)) {
                  return (int) $cnt;
              }
          $cnt = (int) $faves->count('distinct notice_id');
  
          if (!empty($c)) {
 -            $c->set(common_cache_key('profile:fave_count:'.$this->id), $cnt);
 +            $c->set(Cache::key('profile:fave_count:'.$this->id), $cnt);
          }
  
          return $cnt;
  
      function noticeCount()
      {
 -        $c = common_memcache();
 +        $c = Cache::instance();
  
          if (!empty($c)) {
 -            $cnt = $c->get(common_cache_key('profile:notice_count:'.$this->id));
 +            $cnt = $c->get(Cache::key('profile:notice_count:'.$this->id));
              if (is_integer($cnt)) {
                  return (int) $cnt;
              }
          $cnt = (int) $notices->count('distinct id');
  
          if (!empty($c)) {
 -            $c->set(common_cache_key('profile:notice_count:'.$this->id), $cnt);
 +            $c->set(Cache::key('profile:notice_count:'.$this->id), $cnt);
          }
  
          return $cnt;
      }
  
+     function blowFavesCache()
+     {
+         $cache = common_memcache();
+         if ($cache) {
+             // Faves don't happen chronologically, so we need to blow
+             // ;last cache, too
+             $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id));
+             $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id.';last'));
+             $cache->delete(common_cache_key('fave:ids_by_user_own:'.$this->id));
+             $cache->delete(common_cache_key('fave:ids_by_user_own:'.$this->id.';last'));
+         }
+         $this->blowFaveCount();
+     }
      function blowSubscriberCount()
      {
 -        $c = common_memcache();
 +        $c = Cache::instance();
          if (!empty($c)) {
 -            $c->delete(common_cache_key('profile:subscriber_count:'.$this->id));
 +            $c->delete(Cache::key('profile:subscriber_count:'.$this->id));
          }
      }
  
      function blowSubscriptionCount()
      {
 -        $c = common_memcache();
 +        $c = Cache::instance();
          if (!empty($c)) {
 -            $c->delete(common_cache_key('profile:subscription_count:'.$this->id));
 +            $c->delete(Cache::key('profile:subscription_count:'.$this->id));
          }
      }
  
      function blowFaveCount()
      {
 -        $c = common_memcache();
 +        $c = Cache::instance();
          if (!empty($c)) {
 -            $c->delete(common_cache_key('profile:fave_count:'.$this->id));
 +            $c->delete(Cache::key('profile:fave_count:'.$this->id));
          }
      }
  
      function blowNoticeCount()
      {
 -        $c = common_memcache();
 +        $c = Cache::instance();
          if (!empty($c)) {
 -            $c->delete(common_cache_key('profile:notice_count:'.$this->id));
 +            $c->delete(Cache::key('profile:notice_count:'.$this->id));
          }
      }
  
       * @param $right string Name of the right, usually a constant in class Right
       * @return boolean whether the user has the right in question
       */
      function hasRight($right)
      {
          $result = false;
          if ($this->hasRole(Profile_role::DELETED)) {
              return false;
          }
          if (Event::handle('UserRightsCheck', array($this, $right, &$result))) {
              switch ($right)
              {
diff --combined classes/User.php
index c15ddc9dc5550ad2e6ae7f89ef84203ebc97e688,e784fd9e9a7278ec8c49955bcdeb5ad94588facc..259df7e2c3eb4b74b120df4a1a4413a5d9fdbd66
@@@ -48,6 -48,11 +48,6 @@@ class User extends Memcached_DataObjec
      public $language;                        // varchar(50)
      public $timezone;                        // varchar(50)
      public $emailpost;                       // tinyint(1)   default_1
 -    public $jabber;                          // varchar(255)  unique_key
 -    public $jabbernotify;                    // tinyint(1)
 -    public $jabberreplies;                   // tinyint(1)
 -    public $jabbermicroid;                   // tinyint(1)   default_1
 -    public $updatefrompresence;              // tinyint(1)
      public $sms;                             // varchar(64)  unique_key
      public $carrier;                         // int(4)
      public $smsnotify;                       // tinyint(1)
@@@ -88,7 -93,7 +88,7 @@@
      {
          $this->_connect();
          $parts = array();
 -        foreach (array('nickname', 'email', 'jabber', 'incomingemail', 'sms', 'carrier', 'smsemail', 'language', 'timezone') as $k) {
 +        foreach (array('nickname', 'email', 'incomingemail', 'sms', 'carrier', 'smsemail', 'language', 'timezone') as $k) {
              if (strcmp($this->$k, $orig->$k) != 0) {
                  $parts[] = $k . ' = ' . $this->_quote($this->$k);
              }
  
          $user->inboxed = 1;
  
+         // Set default-on options here, otherwise they'll be disabled
+         // initially for sites using caching, since the initial encache
+         // doesn't know about the defaults in the database.
+         $user->emailnotifysub = 1;
+         $user->emailnotifyfav = 1;
+         $user->emailnotifynudge = 1;
+         $user->emailnotifymsg = 1;
+         $user->emailnotifyattn = 1;
+         $user->emailmicroid = 1;
+         $user->emailpost = 1;
+         $user->jabbermicroid = 1;
+         $user->viewdesigns = 1;
          $user->created = common_sql_now();
  
          if (Event::handle('StartUserRegister', array(&$user, &$profile))) {
              }
  
              $user->id = $id;
-             $user->uri = common_user_uri($user);
+             if (!empty($uri)) {
+                 $user->uri = $uri;
+             } else {
+                 $user->uri = common_user_uri($user);
+             }
              if (!empty($password)) { // may not have a password for OpenID users
                  $user->password = common_munge_password($password, $id);
              }
  
      function hasFave($notice)
      {
-         $cache = Cache::instance();
-         // XXX: Kind of a hack.
-         if ($cache) {
-             // This is the stream of favorite notices, in rev chron
-             // order. This forces it into cache.
-             $ids = Fave::stream($this->id, 0, NOTICE_CACHE_WINDOW);
-             // If it's in the list, then it's a fave
-             if (in_array($notice->id, $ids)) {
-                 return true;
-             }
-             // If we're not past the end of the cache window,
-             // then the cache has all available faves, so this one
-             // is not a fave.
-             if (count($ids) < NOTICE_CACHE_WINDOW) {
-                 return false;
-             }
-             // Otherwise, cache doesn't have all faves;
-             // fall through to the default
-         }
-         $fave = Fave::pkeyGet(array('user_id' => $this->id,
-                                     'notice_id' => $notice->id));
-         return ((is_null($fave)) ? false : true);
+         $profile = $this->getProfile();
+         return $profile->hasFave($notice);
      }
  
      function mutuallySubscribed($other)
  
      function blowFavesCache()
      {
-         $cache = Cache::instance();
-         if ($cache) {
-             // Faves don't happen chronologically, so we need to blow
-             // ;last cache, too
-             $cache->delete(Cache::key('fave:ids_by_user:'.$this->id));
-             $cache->delete(Cache::key('fave:ids_by_user:'.$this->id.';last'));
-             $cache->delete(Cache::key('fave:ids_by_user_own:'.$this->id));
-             $cache->delete(Cache::key('fave:ids_by_user_own:'.$this->id.';last'));
-         }
          $profile = $this->getProfile();
-         $profile->blowFaveCount();
+         $profile->blowFavesCache();
      }
  
      function getSelfTags()
          if (Subscription::exists($other, $self)) {
              Subscription::cancel($other, $self);
          }
+         if (Subscription::exists($self, $other)) {
+             Subscription::cancel($self, $other);
+         }
  
          $block->query('COMMIT');
  
diff --combined lib/adminpanelaction.php
index 9e0b2d041b422ea9c5c8ba220f50c1c1e952a47b,fae9f4fa57a83a9b4a4f6542862013209c82be58..8dd16e9d0ce6a2b1d1e8b2abdd6f712257c1561c
@@@ -44,7 -44,6 +44,6 @@@ if (!defined('STATUSNET')) 
   *
   * @todo Find some commonalities with SettingsAction and combine
   */
  class AdminPanelAction extends Action
  {
      var $success = true;
@@@ -61,7 -60,6 +60,6 @@@
       *
       * @return boolean success flag
       */
      function prepare($args)
      {
          parent::prepare($args);
       *
       * @return void
       */
      function handle($args)
      {
          if ($_SERVER['REQUEST_METHOD'] == 'POST') {
       * @return void
       * @see AdminPanelNav
       */
      function showLocalNav()
      {
          $nav = new AdminPanelNav($this);
       *
       * @return void.
       */
      function showContent()
      {
          $this->showForm();
       *
       * @return void
       */
      function showPageNotice()
      {
          if ($this->msg) {
       *
       * @return void
       */
      function showForm()
      {
          // TRANS: Client error message.
       *
       * @return void
       */
      function getInstructions()
      {
          return '';
       *
       * @return void
       */
      function saveSettings()
      {
          // TRANS: Client error message
       *
       * @return mixed $result false if something didn't work
       */
      function deleteSetting($section, $setting)
      {
          $config = new Config();
   *
   * @see      Widget
   */
  class AdminPanelNav extends Widget
  {
      var $action = null;
       *
       * @param Action $action current action, used for output
       */
      function __construct($action=null)
      {
          parent::__construct($action);
       *
       * @return void
       */
      function show()
      {
          $action_name = $this->action->trimmed('action');
                                       $menu_title, $action_name == 'snapshotadminpanel', 'nav_snapshot_admin_panel');
              }
  
+             if (AdminPanelAction::canAdmin('license')) {
+                 // TRANS: Menu item title/tooltip
+                 $menu_title = _('Set site license');
+                 // TRANS: Menu item for site administration
+                 $this->out->menuItem(common_local_url('licenseadminpanel'), _('License'),
+                                      $menu_title, $action_name == 'licenseadminpanel', 'nav_license_admin_panel');
+             }
 +            if (AdminPanelAction::canAdmin('plugins')) {
 +                // TRANS: Menu item title/tooltip
 +                $menu_title = _('Plugins configuration');
 +                // TRANS: Menu item for site administration
 +                $this->out->menuItem(common_local_url('pluginsadminpanel'), _('Plugins'),
 +                                     $menu_title, $action_name == 'pluginsadminpanel', 'nav_design_admin_panel');
 +            }
 +
              Event::handle('EndAdminPanelNav', array($this));
          }
          $this->action->elementEnd('ul');
      }
  }
diff --combined lib/apiaction.php
index b4252db95aaf389c3f160aa84ac6e99866775f98,0ebf88282a5f074ff070d1e78744f8b3b0f624f6..d8249055a492d30c980e4aad3ed7106b6d2c79db
@@@ -98,8 -98,6 +98,8 @@@ if (!defined('STATUSNET')) 
      exit(1);
  }
  
 +class ApiValidationException extends Exception { }
 +
  /**
   * Contains most of the Twitter-compatible API output functions.
   *
   * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
   * @link     http://status.net/
   */
  class ApiAction extends Action
  {
      const READ_ONLY  = 1;
       *
       * @return boolean false if user doesn't exist
       */
      function prepare($args)
      {
          StatusNet::setApi(true); // reduce exception reports to aid in debugging
       *
       * @return void
       */
      function handle($args)
      {
          header('Access-Control-Allow-Origin: *');
          $entry = array();
  
          if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
              $profile = $notice->getProfile();
  
              // We trim() to avoid extraneous whitespace in the output
          $notifications = false;
  
          if ($source->isSubscribed($target)) {
              $sub = Subscription::pkeyGet(array('subscriber' =>
                  $source->id, 'subscribed' => $target->id));
  
  
      function showXmlTimeline($notice)
      {
          $this->initDocument('xml');
          $this->elementStart('statuses', array('type' => 'array',
                                                'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
  
      function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
      {
          $this->initDocument('rss');
  
          $this->element('title', null, $title);
  
      function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
      {
          $this->initDocument('atom');
  
          $this->element('title', null, $title);
          }
  
          $this->endDocument('atom');
      }
  
      function showRssGroups($group, $title, $link, $subtitle)
      {
          $this->initDocument('rss');
  
          $this->element('title', null, $title);
  
      function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
      {
          $this->initDocument('atom');
  
          $this->element('title', null, common_xml_safe_str($title));
  
      function showJsonTimeline($notice)
      {
          $this->initDocument('json');
  
          $statuses = array();
  
      function showJsonGroups($group)
      {
          $this->initDocument('json');
  
          $groups = array();
  
      function showTwitterXmlUsers($user)
      {
          $this->initDocument('xml');
          $this->elementStart('users', array('type' => 'array',
                                             'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
  
      function showJsonUsers($user)
      {
          $this->initDocument('json');
  
          $users = array();
              $this->endXML();
              break;
          case 'json':
              // Check for JSONP callback
              if (isset($this->callback)) {
                  print ')';
      function getTargetUser($id)
      {
          if (empty($id)) {
              // Twitter supports these other ways of passing the user ID
              if (is_numeric($this->arg('id'))) {
                  return User::staticGet($this->arg('id'));
       */
      function arg($key, $def=null)
      {
          // XXX: Do even more input validation/scrubbing?
  
          if (array_key_exists($key, $this->args)) {
  
          return $uri;
      }
  }
diff --combined lib/cache.php
index 17cc5f0472546b73ef8bd52a895a324aeacc03a8,ea0ff769d1992c4b9e16690777bd104311d531ce..3d78c79adb2ba5f34003c3f69c60ce893a3b3d3f
@@@ -41,7 -41,6 +41,6 @@@
   * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
   * @link      http://status.net/
   */
  class Cache
  {
      var $_items   = array();
@@@ -56,7 -55,6 +55,6 @@@
       *
       * @return Cache cache object
       */
      static function instance()
      {
          if (is_null(self::$_inst)) {
       *
       * @return string full key
       */
      static function key($extra)
      {
          $base_key = common_config('cache', 'base');
  
          if (empty($base_key)) {
 -            $base_key = common_keyize(common_config('site', 'name'));
 +            $base_key = self::keyize(common_config('site', 'name'));
          }
  
          return 'statusnet:' . $base_key . ':' . $extra;
@@@ -98,7 -95,6 +95,6 @@@
       *
       * @return string keyized string
       */
      static function keyize($str)
      {
          $str = strtolower($str);
       *
       * @return string retrieved value or null if unfound
       */
      function get($key)
      {
          $value = false;
       *
       * @return boolean success flag
       */
      function set($key, $value, $flag=null, $expiry=null)
      {
          $success = false;
       *
       * @return boolean success flag
       */
      function delete($key)
      {
          $success = false;
       *
       * @return boolean success flag
       */
      function reconnect()
      {
          $success = false;
diff --combined lib/channel.php
index 5b38a4b6aeed8792867fa8309400734dc03b4f10,fbc2e8697c803e7c9aa6ebd373560c869436893d..ae9b2d214f602e8b68a35feeedeaa4bf2655f37f
@@@ -19,6 -19,9 +19,9 @@@
  
  if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
  
+ /**
+  * @todo Needs documentation.
+  */
  class Channel
  {
      function on($user)
@@@ -66,6 -69,62 +69,6 @@@ class CLIChannel extends Channe
      }
  }
  
 -class XMPPChannel extends Channel
 -{
 -    var $conn = null;
 -
 -    function source()
 -    {
 -        return 'xmpp';
 -    }
 -
 -    function __construct($conn)
 -    {
 -        $this->conn = $conn;
 -    }
 -
 -    function on($user)
 -    {
 -        return $this->set_notify($user, 1);
 -    }
 -
 -    function off($user)
 -    {
 -        return $this->set_notify($user, 0);
 -    }
 -
 -    function output($user, $text)
 -    {
 -        $text = '['.common_config('site', 'name') . '] ' . $text;
 -        jabber_send_message($user->jabber, $text);
 -    }
 -
 -    function error($user, $text)
 -    {
 -        $text = '['.common_config('site', 'name') . '] ' . $text;
 -        jabber_send_message($user->jabber, $text);
 -    }
 -
 -    function set_notify(&$user, $notify)
 -    {
 -        $orig = clone($user);
 -        $user->jabbernotify = $notify;
 -        $result = $user->update($orig);
 -        if (!$result) {
 -            $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
 -            common_log(LOG_ERR,
 -                       'Could not set notify flag to ' . $notify .
 -                       ' for user ' . common_log_objstring($user) .
 -                       ': ' . $last_error->message);
 -            return false;
 -        } else {
 -            common_log(LOG_INFO,
 -                       'User ' . $user->nickname . ' set notify flag to ' . $notify);
 -            return true;
 -        }
 -    }
 -}
 -
  class WebChannel extends Channel
  {
      var $out = null;
          #      depending on what command was run
          $this->out->startHTML();
          $this->out->elementStart('head');
+         // TRANS: Title for command results.
          $this->out->element('title', null, _('Command results'));
          $this->out->elementEnd('head');
          $this->out->elementStart('body');
@@@ -117,6 -177,7 +121,7 @@@ class AjaxWebChannel extends WebChanne
      {
          $this->out->startHTML('text/xml;charset=utf-8');
          $this->out->elementStart('head');
+         // TRANS: Title for command results.
          $this->out->element('title', null, _('Command results'));
          $this->out->elementEnd('head');
          $this->out->elementStart('body');
      {
          $this->out->startHTML('text/xml;charset=utf-8');
          $this->out->elementStart('head');
-         $this->out->element('title', null, _('Ajax Error'));
+         // TRANS: Title for command results.
+         $this->out->element('title', null, _('AJAX error'));
          $this->out->elementEnd('head');
          $this->out->elementStart('body');
          $this->out->element('p', array('id' => 'error'), $text);
  
  class MailChannel extends Channel
  {
      var $addr = null;
  
      function source()
  
      function on($user)
      {
 -        return $this->set_notify($user, 1);
 +        return $this->setNotify($user, 1);
      }
  
      function off($user)
      {
 -        return $this->set_notify($user, 0);
 +        return $this->setNotify($user, 0);
      }
  
      function output($user, $text)
      {
          $headers['From'] = $user->incomingemail;
          $headers['To'] = $this->addr;
  
+         // TRANS: E-mail subject when a command has completed.
          $headers['Subject'] = _('Command complete');
  
          return mail_send(array($this->addr), $headers, $text);
  
      function error($user, $text)
      {
          $headers['From'] = $user->incomingemail;
          $headers['To'] = $this->addr;
  
+         // TRANS: E-mail subject when a command has failed.
          $headers['Subject'] = _('Command failed');
  
          return mail_send(array($this->addr), $headers, $text);
      }
  
 -    function set_notify($user, $value)
 +    function setNotify($user, $value)
      {
          $orig = clone($user);
          $user->smsnotify = $value;
diff --combined lib/command.php
index 90a321ad323385831ec3cefeb657de6d76a0daa9,658262a0908fe6f2c92f0e9f5fc80cb7b7da5a2f..efe917fb11895d03a2cf443008f02c7a0753a245
@@@ -23,7 -23,6 +23,6 @@@ require_once(INSTALLDIR.'/lib/channel.p
  
  class Command
  {
      var $user = null;
  
      function __construct($user=null)
@@@ -49,7 -48,6 +48,6 @@@
          }
      }
  
      /**
       * Override this with the meat!
       *
@@@ -313,7 -311,6 +311,6 @@@ class FavCommand extends Comman
          // TRANS: Text shown when a notice has been marked as favourite successfully.
          $channel->output($this->user, _('Notice marked as fave.'));
      }
  }
  
  class JoinCommand extends Command
                                                $cur->nickname,
                                                $group->nickname));
      }
  }
  class DropCommand extends Command
  {
      var $other = null;
                                                $cur->nickname,
                                                $group->nickname));
      }
  }
  
  class WhoisCommand extends Command
@@@ -471,6 -467,7 +467,7 @@@ class MessageCommand extends Comman
                  throw $e;
              }
              // TRANS: Command exception text shown when trying to send a direct message to a remote user (a user not registered at the current server).
+             // TRANS: %s is a remote profile.
              throw new CommandException(sprintf(_('%s is a remote profile; you can only send direct messages to users on the same server.'), $this->other));
          }
  
@@@ -611,7 -608,6 +608,6 @@@ class ReplyCommand extends Comman
  
  class GetCommand extends Command
  {
      var $other = null;
  
      function __construct($user, $other)
  
  class SubCommand extends Command
  {
      var $other = null;
  
      function __construct($user, $other)
  
  class UnsubCommand extends Command
  {
      var $other = null;
  
      function __construct($user, $other)
  class OffCommand extends Command
  {
      var $other = null;
      function __construct($user, $other=null)
      {
          parent::__construct($user);
      }
      function handle($channel)
      {
 -        if ($other) {
 +        if ($this->other) {
              // TRANS: Error text shown when issuing the command "off" with a setting which has not yet been implemented.
              $channel->error($this->user, _("Command not yet implemented."));
          } else {
@@@ -745,7 -740,7 +740,7 @@@ class OnCommand extends Comman
  
      function handle($channel)
      {
 -        if ($other) {
 +        if ($this->other) {
              // TRANS: Error text shown when issuing the command "on" with a setting which has not yet been implemented.
              $channel->error($this->user, _("Command not yet implemented."));
          } else {
@@@ -831,7 -826,7 +826,7 @@@ class SubscriptionsCommand extends Comm
              $out=_('You are not subscribed to anyone.');
          }else{
              // TRANS: Text shown after requesting other users a user is subscribed to.
-             // TRANS: This message support plural forms. This message is followed by a
+             // TRANS: This message supports plural forms. This message is followed by a
              // TRANS: hard coded space and a comma separated list of subscribed users.
              $out = ngettext('You are subscribed to this person:',
                  'You are subscribed to these people:',
@@@ -858,7 -853,7 +853,7 @@@ class SubscribersCommand extends Comman
              $out=_('No one is subscribed to you.');
          }else{
              // TRANS: Text shown after requesting other users that are subscribed to a user (followers).
-             // TRANS: This message support plural forms. This message is followed by a
+             // TRANS: This message supports plural forms. This message is followed by a
              // TRANS: hard coded space and a comma separated list of subscribing users.
              $out = ngettext('This person is subscribed to you:',
                  'These people are subscribed to you:',
@@@ -885,7 -880,7 +880,7 @@@ class GroupsCommand extends Comman
              $out=_('You are not a member of any groups.');
          }else{
              // TRANS: Text shown after requesting groups a user is subscribed to.
-             // TRANS: This message support plural forms. This message is followed by a
+             // TRANS: This message supports plural forms. This message is followed by a
              // TRANS: hard coded space and a comma separated list of subscribed groups.
              $out = ngettext('You are a member of this group:',
                  'You are a member of these groups:',
@@@ -900,8 -895,8 +895,8 @@@ class HelpCommand extends Comman
  {
      function handle($channel)
      {
-       // TRANS: Help text for commands.
          $channel->output($this->user,
+                          // TRANS: Help text for commands. Do not translate the command names themselves; they are fixed strings.
                           _("Commands:\n".
                             "on - turn on notifications\n".
                             "off - turn off notifications\n".
diff --combined lib/common.php
index 0a0f5c6317e4cb90d3ca72b7204e8fa2638aa1c6,236f2d68a744c77be457e79b57e5d3113ed3e3ae..2a11ab722ddc0d71d3bf0107a6faebc842e909e7
@@@ -71,7 -71,6 +71,7 @@@ if (!function_exists('dl')) 
  # global configuration object
  
  require_once('PEAR.php');
 +require_once('PEAR/Exception.php');
  require_once('DB/DataObject.php');
  require_once('DB/DataObject/Cast.php'); # for dates
  
@@@ -128,33 -127,22 +128,39 @@@ require_once INSTALLDIR.'/lib/subs.php'
  require_once INSTALLDIR.'/lib/clientexception.php';
  require_once INSTALLDIR.'/lib/serverexception.php';
  
 +
 +//set PEAR error handling to use regular PHP exceptions
 +function PEAR_ErrorToPEAR_Exception($err)
 +{
 +    //DB_DataObject throws error when an empty set would be returned
 +    //That behavior is weird, and not how the rest of StatusNet works.
 +    //So just ignore those errors.
 +    if ($err->getCode() == DB_DATAOBJECT_ERROR_NODATA) {
 +        return;
 +    }
 +    if ($err->getCode()) {
 +        throw new PEAR_Exception($err->getMessage(), $err->getCode());
 +    }
 +    throw new PEAR_Exception($err->getMessage());
 +}
 +PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'PEAR_ErrorToPEAR_Exception');
 +
  try {
      StatusNet::init(@$server, @$path, @$conffile);
  } catch (NoConfigException $e) {
      // XXX: Throw a conniption if database not installed
      // XXX: Find a way to use htmlwriter for this instead of handcoded markup
+     // TRANS: Error message displayed when no configuration file was found for a StatusNet installation.
      echo '<p>'. _('No configuration file found. ') .'</p>';
-     echo '<p>'. _('I looked for configuration files in the following places: ') .'<br/> ';
-     echo implode($e->configFiles, '<br/>');
+     // TRANS: Error message displayed when no configuration file was found for a StatusNet installation.
+     // TRANS: Is followed by a list of directories (separated by HTML breaks).
+     echo '<p>'. _('I looked for configuration files in the following places: ') .'<br /> ';
+     echo implode($e->configFiles, '<br />');
+     // TRANS: Error message displayed when no configuration file was found for a StatusNet installation.
      echo '<p>'. _('You may wish to run the installer to fix this.') .'</p>';
+     // @todo FIXME Link should be in a para?
+     // TRANS: Error message displayed when no configuration file was found for a StatusNet installation.
+     // TRANS: The text is link text that leads to the installer page.
      echo '<a href="install.php">'. _('Go to the installer.') .'</a>';
      exit;
  }
index 5d62fc56b3e9e868a2ff6f6d1dfa9d4a87e72588,bb2e86176aa3ec44e2c1e19e6278ba1503f9839a..c2e759f0f38e7cd92b11305c3814a716861f6a21
@@@ -44,7 -44,6 +44,6 @@@ require_once INSTALLDIR.'/lib/settingsa
   *
   * @see      Widget
   */
  class ConnectSettingsAction extends SettingsAction
  {
      /**
@@@ -54,7 -53,6 +53,6 @@@
       *
       * @return void
       */
      function showLocalNav()
      {
          $menu = new ConnectSettingsNav($this);
@@@ -73,7 -71,6 +71,6 @@@
   *
   * @see      HTMLOutputter
   */
  class ConnectSettingsNav extends Widget
  {
      var $action = null;
@@@ -83,7 -80,6 +80,6 @@@
       *
       * @param Action $action current action, used for output
       */
      function __construct($action=null)
      {
          parent::__construct($action);
@@@ -95,7 -91,6 +91,6 @@@
       *
       * @return void
       */
      function show()
      {
          $action_name = $this->action->trimmed('action');
  
              # action => array('prompt', 'title')
              $menu = array();
 -            if (common_config('xmpp', 'enabled')) {
 +            $transports = array();
 +            Event::handle('GetImTransports', array(&$transports));
 +            if ($transports) {
                  $menu['imsettings'] =
-                   array(_('IM'),
+                   // TRANS: Menu item for Instant Messaging settings.
+                   array(_m('MENU','IM'),
+                         // TRANS: Tooltip for Instant Messaging menu item.
                          _('Updates by instant messenger (IM)'));
              }
              if (common_config('sms', 'enabled')) {
                  $menu['smssettings'] =
-                   array(_('SMS'),
+                   // TRANS: Menu item for Short Message Service settings.
+                   array(_m('MENU','SMS'),
+                         // TRANS: Tooltip for Short Message Service menu item.
                          _('Updates by SMS'));
              }
-             
              $menu['oauthconnectionssettings'] = array(
-                 _('Connections'),
+                 // TRANS: Menu item for OAth connection settings.
+                 _m('MENU','Connections'),
+                 // TRANS: Tooltip for connected applications (Connections through OAth) menu item.
                  _('Authorized connected applications')
              );
  
  
          $this->action->elementEnd('ul');
      }
  }
diff --combined lib/default.php
index 76e4e44cf2b17a43e57c86a68eae916f6353a98b,45e35e83d380c8326e1008ccf2a6fb59fee1fa1f..79b54fc3bddb8e216a2efd8a62d2de847fc2d1c5
@@@ -297,12 -297,11 +297,13 @@@ $default 
                                   'OStatus' => null,
                                   'WikiHashtags' => null,
                                   'RSSCloud' => null,
 +                                 'ClientSideShorten' => null,
                                   'OpenID' => null),
+               'locale_path' => false, // Set to a path to use *instead of* each plugin's own locale subdirectories
                ),
 +        'pluginlist' => array(),
          'admin' =>
-         array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions', 'sitenotice', 'plugins')),
 -        array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions', 'sitenotice', 'license')),
++        array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions', 'sitenotice', 'license', 'plugins')),
          'singleuser' =>
          array('enabled' => false,
                'nickname' => null),
          array('subscribers' => true,
                'members' => true,
                'peopletag' => true),
 +        'url' =>
 +        array('shortener' => 'ur1.ca',
 +              'maxlength' => 25,
 +              'maxnoticelength' => -1),
          'http' => // HTTP client settings when contacting other sites
          array('ssl_cafile' => false, // To enable SSL cert validation, point to a CA bundle (eg '/usr/lib/ssl/certs/ca-certificates.crt')
                'curl' => false, // Use CURL backend for HTTP fetches if available. (If not, PHP's socket streams will be used.)
diff --combined lib/htmloutputter.php
index 7eccd6cc0e56cddd9ed745fc20cd2e35b17bf733,44b02960467aa0762dfa8b9663e0d4989b138946..9780dc42432b87b4d04e3ced4643660e313b9652
@@@ -177,7 -177,7 +177,7 @@@ class HTMLOutputter extends XMLOutputte
          $attrs = array('name' => $id,
                         'type' => 'text',
                         'id' => $id);
 -        if ($value) {
 +        if (!is_null($value)) { // value can be 0 or ''
              $attrs['value'] = $value;
          }
          $this->element('input', $attrs);
       * @param string $label text of the button
       * @param string $cls   class of the button, default 'submit'
       * @param string $name  name, if different than ID
+      * @param string $title  title text for the submit button
       *
       * @return void
       *
diff --combined lib/router.php
index 86dd116c8d49d8a49db73b70a3a3a96dedb66624,00b2993734fe90a647b34f639ab603c51f256fce..3bbb4a044e5009c22a2ab8d00b8a92703788f4d9
@@@ -151,8 -151,6 +151,8 @@@ class Route
  
              $m->connect('main/xrds',
                          array('action' => 'publicxrds'));
 +            $m->connect('.well-known/host-meta',
 +                        array('action' => 'hostmeta'));
  
              // these take a code
  
              // Social graph
  
              $m->connect('api/friends/ids/:id.:format',
-                         array('action' => 'apiuserfriends',
+                         array('action' => 'ApiUserFriends',
                                'ids_only' => true));
  
              $m->connect('api/followers/ids/:id.:format',
-                         array('action' => 'apiuserfollowers',
+                         array('action' => 'ApiUserFollowers',
                                'ids_only' => true));
  
              $m->connect('api/friends/ids.:format',
-                         array('action' => 'apiuserfriends',
+                         array('action' => 'ApiUserFriends',
                                'ids_only' => true));
  
              $m->connect('api/followers/ids.:format',
-                         array('action' => 'apiuserfollowers',
+                         array('action' => 'ApiUserFollowers',
                                'ids_only' => true));
  
              // account
              $m->connect('api/statusnet/groups/create.:format',
                          array('action' => 'ApiGroupCreate',
                                'format' => '(xml|json)'));
 +
 +            $m->connect('api/statusnet/groups/update/:id.:format',
 +                        array('action' => 'ApiGroupProfileUpdate',
 +                              'id' => '[a-zA-Z0-9]+',
 +                              'format' => '(xml|json)'));
 +
              // Tags
              $m->connect('api/statusnet/tags/timeline/:tag.:format',
                          array('action' => 'ApiTimelineTag',
              $m->connect('api/trends.json', array('action' => 'ApiTrends'));
  
              $m->connect('api/oauth/request_token',
-                         array('action' => 'apioauthrequesttoken'));
+                         array('action' => 'ApiOauthRequestToken'));
  
              $m->connect('api/oauth/access_token',
-                         array('action' => 'apioauthaccesstoken'));
+                         array('action' => 'ApiOauthAccessToken'));
  
              $m->connect('api/oauth/authorize',
-                         array('action' => 'apioauthauthorize'));
+                         array('action' => 'ApiOauthAuthorize'));
  
              // Admin
  
              $m->connect('admin/sessions', array('action' => 'sessionsadminpanel'));
              $m->connect('admin/sitenotice', array('action' => 'sitenoticeadminpanel'));
              $m->connect('admin/snapshot', array('action' => 'snapshotadminpanel'));
 -
+             $m->connect('admin/license', array('action' => 'licenseadminpanel'));
 +            $m->connect('admin/plugins', array('action' => 'pluginsadminpanel'));
 +            $m->connect('admin/plugins/enable/:plugin',
 +                        array('action' => 'pluginenable'),
 +                        array('plugin' => '[A-Za-z0-9_]+'));
 +            $m->connect('admin/plugins/disable/:plugin',
 +                        array('action' => 'plugindisable'),
 +                        array('plugin' => '[A-Za-z0-9_]+'));
  
              $m->connect('getfile/:filename',
                          array('action' => 'getfile'),
diff --combined lib/util.php
index 20c9144d4729a73a47efb6440b50b6b6f61b00d3,dc853f657b1e53f76b00d162cb22a7dd49fc7ab2..b20ed8225f4a991f2e9842fddda8843734ae4b97
  
  /* XXX: break up into separate modules (HTTP, user, files) */
  
- // Show a server error
+ /**
+  * Show a server error.
+  */
  function common_server_error($msg, $code=500)
  {
      $err = new ServerErrorAction($msg, $code);
      $err->showPage();
  }
  
- // Show a user error
+ /**
+  * Show a user error.
+  */
  function common_user_error($msg, $code=400)
  {
      $err = new ClientErrorAction($msg, $code);
@@@ -142,6 -145,7 +145,6 @@@ function common_switch_locale($language
      textdomain("statusnet");
  }
  
 -
  function common_timezone()
  {
      if (common_logged_in()) {
      return common_config('site', 'timezone');
  }
  
 +function common_valid_language($lang)
 +{
 +    if ($lang) {
 +        // Validate -- we don't want to end up with a bogus code
 +        // left over from some old junk.
 +        foreach (common_config('site', 'languages') as $code => $info) {
 +            if ($info['lang'] == $lang) {
 +                return true;
 +            }
 +        }
 +    }
 +    return false;
 +}
 +
  function common_language()
  {
 +    // Allow ?uselang=xx override, very useful for debugging
 +    // and helping translators check usage and context.
 +    if (isset($_GET['uselang'])) {
 +        $uselang = strval($_GET['uselang']);
 +        if (common_valid_language($uselang)) {
 +            return $uselang;
 +        }
 +    }
 +
      // If there is a user logged in and they've set a language preference
      // then return that one...
      if (_have_config() && common_logged_in()) {
          $user = common_current_user();
 -        $user_language = $user->language;
 -
 -        if ($user->language) {
 -            // Validate -- we don't want to end up with a bogus code
 -            // left over from some old junk.
 -            foreach (common_config('site', 'languages') as $code => $info) {
 -                if ($info['lang'] == $user_language) {
 -                    return $user_language;
 -                }
 -            }
 +
 +        if (common_valid_language($user->language)) {
 +            return $user->language;
          }
      }
  
      // Finally, if none of the above worked, use the site's default...
      return common_config('site', 'language');
  }
- // salted, hashed passwords are stored in the DB
  
+ /**
+  * Salted, hashed passwords are stored in the DB.
+  */
  function common_munge_password($password, $id)
  {
      if (is_object($id) || is_object($password)) {
      return md5($password . $id);
  }
  
- // check if a username exists and has matching password
+ /**
+  * Check if a username exists and has matching password.
+  */
  function common_check_user($nickname, $password)
  {
      // empty nickname always unacceptable
      return $authenticatedUser;
  }
  
- // is the current user logged in?
+ /**
+  * Is the current user logged in?
+  */
  function common_logged_in()
  {
      return (!is_null(common_current_user()));
@@@ -289,12 -282,10 +297,10 @@@ function common_ensure_session(
  // 3) null to clear
  
  // Initialize to false; set to null if none found
  $_cur = false;
  
  function common_set_user($user)
  {
      global $_cur;
  
      if (is_null($user) && common_have_session()) {
@@@ -380,7 -371,6 +386,6 @@@ function common_rememberme($user=null
  
  function common_remembered_user()
  {
      $user = null;
  
      $packed = isset($_COOKIE[REMEMBERME]) ? $_COOKIE[REMEMBERME] : null;
      return $user;
  }
  
- // must be called with a valid user!
+ /**
+  * must be called with a valid user!
+  */
  function common_forgetme()
  {
      common_set_cookie(REMEMBERME, '', 0);
  }
  
- // who is the current user?
+ /**
+  * Who is the current user?
+  */
  function common_current_user()
  {
      global $_cur;
      return $_cur;
  }
  
- // Logins that are 'remembered' aren't 'real' -- they're subject to
- // cookie-stealing. So, we don't let them do certain things. New reg,
- // OpenID, and password logins _are_ real.
+ /**
+  * Logins that are 'remembered' aren't 'real' -- they're subject to
+  * cookie-stealing. So, we don't let them do certain things. New reg,
+  * OpenID, and password logins _are_ real.
+  */
  function common_real_login($real=true)
  {
      common_ensure_session();
@@@ -500,6 -494,29 +509,29 @@@ function common_is_real_login(
      return common_logged_in() && $_SESSION['real_login'];
  }
  
+ /**
+  * Get a hash portion for HTTP caching Etags and such including
+  * info on the current user's session. If login/logout state changes,
+  * or we've changed accounts, or we've renamed the current user,
+  * we'll get a new hash value.
+  *
+  * This should not be considered secure information.
+  *
+  * @param User $user (optional; uses common_current_user() if left out)
+  * @return string
+  */
+ function common_user_cache_hash($user=false)
+ {
+     if ($user === false) {
+         $user = common_current_user();
+     }
+     if ($user) {
+         return crc32($user->id . ':' . $user->nickname);
+     } else {
+         return '0';
+     }
+ }
  // get canonical version of nickname for comparison
  function common_canonical_nickname($nickname)
  {
@@@ -591,9 -608,7 +623,7 @@@ function common_find_mentions($text, $n
      }
  
      if (Event::handle('StartFindMentions', array($sender, $text, &$mentions))) {
          // Get the context of the original notice, if any
          $originalAuthor   = null;
          $originalNotice   = null;
          $originalMentions = array();
          $matches = array_merge($tmatches[1], $atmatches[1]);
  
          foreach ($matches as $match) {
              $nickname = common_canonical_nickname($match[0]);
  
              // Try to get a profile for this nickname.
              // 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)) {
                  $user = User::staticGet('id', $mentioned->id);
  
                  if ($user) {
@@@ -891,21 -901,9 +916,21 @@@ function common_linkify($url) 
  
  function common_shorten_links($text, $always = false)
  {
 -    $maxLength = Notice::maxContent();
 -    if (!$always && ($maxLength == 0 || mb_strlen($text) <= $maxLength)) return $text;
 -    return common_replace_urls_callback($text, array('File_redirection', 'makeShort'));
 +    common_debug("common_shorten_links() called");
 +
 +    $user = common_current_user();
 +
 +    $maxLength = User_urlshortener_prefs::maxNoticeLength($user);
 +
 +    common_debug("maxLength = $maxLength");
 +
 +    if ($always || mb_strlen($text) > $maxLength) {
 +        common_debug("Forcing shortening");
 +        return common_replace_urls_callback($text, array('File_redirection', 'forceShort'));
 +    } else {
 +        common_debug("Not forcing shortening");
 +        return common_replace_urls_callback($text, array('File_redirection', 'makeShort'));
 +    }
  }
  
  function common_xml_safe_str($str)
@@@ -1130,30 -1128,30 +1155,30 @@@ function common_date_string($dt
          // TRANS: Used in notices to indicate when the notice was made compared to now.
          return _('about a minute ago');
      } else if ($diff < 3300) {
-         // XXX: should support plural.
+         $minutes = round($diff/60);
          // TRANS: Used in notices to indicate when the notice was made compared to now.
-         return sprintf(_('about %d minutes ago'), round($diff/60));
+         return sprintf( ngettext('about one minute ago', 'about %d minutes ago', $minutes), $minutes);
      } else if ($diff < 5400) {
          // TRANS: Used in notices to indicate when the notice was made compared to now.
          return _('about an hour ago');
      } else if ($diff < 22 * 3600) {
-         // XXX: should support plural.
+         $hours = round($diff/3600);
          // TRANS: Used in notices to indicate when the notice was made compared to now.
-         return sprintf(_('about %d hours ago'), round($diff/3600));
+         return sprintf( ngettext('about one hour ago', 'about %d hours ago', $hours), $hours);
      } else if ($diff < 37 * 3600) {
          // TRANS: Used in notices to indicate when the notice was made compared to now.
          return _('about a day ago');
      } else if ($diff < 24 * 24 * 3600) {
-         // XXX: should support plural.
+         $days = round($diff/(24*3600));
          // TRANS: Used in notices to indicate when the notice was made compared to now.
-         return sprintf(_('about %d days ago'), round($diff/(24*3600)));
+         return sprintf( ngettext('about one day ago', 'about %d days ago', $days), $days);
      } else if ($diff < 46 * 24 * 3600) {
          // TRANS: Used in notices to indicate when the notice was made compared to now.
          return _('about a month ago');
      } else if ($diff < 330 * 24 * 3600) {
-         // XXX: should support plural.
+         $months = round($diff/(30*24*3600));
          // TRANS: Used in notices to indicate when the notice was made compared to now.
-         return sprintf(_('about %d months ago'), round($diff/(30*24*3600)));
+         return sprintf( ngettext('about one month ago', 'about %d months ago',$months), $months);
      } else if ($diff < 480 * 24 * 3600) {
          // TRANS: Used in notices to indicate when the notice was made compared to now.
          return _('about a year ago');
@@@ -1251,8 -1249,14 +1276,8 @@@ function common_redirect($url, $code=30
      exit;
  }
  
 -function common_broadcast_notice($notice, $remote=false)
 -{
 -    // DO NOTHING!
 -}
 +// Stick the notice on the queue
  
 -/**
 - * Stick the notice on the queue.
 - */
  function common_enqueue_notice($notice)
  {
      static $localTransports = array('omb',
          $transports[] = 'plugin';
      }
  
 -    $xmpp = common_config('xmpp', 'enabled');
 -
 -    if ($xmpp) {
 -        $transports[] = 'jabber';
 -    }
 -
      // We can skip these for gatewayed notices.
      if ($notice->isLocal()) {
          $transports = array_merge($transports, $localTransports);
 -        if ($xmpp) {
 -            $transports[] = 'public';
 -        }
      }
  
      if (Event::handle('StartEnqueueNotice', array($notice, &$transports))) {
@@@ -1305,8 -1318,9 +1330,9 @@@ function common_profile_url($nickname
                              null, null, false);
  }
  
- // Should make up a reasonable root URL
+ /**
+  * Should make up a reasonable root URL
+  */
  function common_root_url($ssl=false)
  {
      $url = common_path('', $ssl, false);
      return $url;
  }
  
- // returns $bytes bytes of random data as a hexadecimal string
- // "good" here is a goal and not a guarantee
+ /**
+  * returns $bytes bytes of random data as a hexadecimal string
+  * "good" here is a goal and not a guarantee
+  */
  function common_good_rand($bytes)
  {
      // XXX: use random.org...?
@@@ -1465,7 -1480,12 +1492,12 @@@ function common_log_db_error(&$object, 
  {
      $objstr = common_log_objstring($object);
      $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
-     common_log(LOG_ERR, $last_error->message . '(' . $verb . ' on ' . $objstr . ')', $filename);
+     if (is_object($last_error)) {
+         $msg = $last_error->message;
+     } else {
+         $msg = 'Unknown error (' . var_export($last_error, true) . ')';
+     }
+     common_log(LOG_ERR, $msg . '(' . $verb . ' on ' . $objstr . ')', $filename);
  }
  
  function common_log_objstring(&$object)
@@@ -1793,6 -1813,21 +1825,6 @@@ function common_session_token(
      return $_SESSION['token'];
  }
  
 -function common_cache_key($extra)
 -{
 -    return Cache::key($extra);
 -}
 -
 -function common_keyize($str)
 -{
 -    return Cache::keyize($str);
 -}
 -
 -function common_memcache()
 -{
 -    return Cache::instance();
 -}
 -
  function common_license_terms($uri)
  {
      if(preg_match('/creativecommons.org\/licenses\/([^\/]+)/', $uri, $matches)) {
@@@ -1823,7 -1858,6 +1855,6 @@@ function common_compatible_license($fro
   */
  function common_database_tablename($tablename)
  {
    if(common_config('db','quote_identifiers')) {
        $tablename = '"'. $tablename .'"';
    }
  /**
   * Shorten a URL with the current user's configured shortening service,
   * or ur1.ca if configured, or not at all if no shortening is set up.
 - * Length is not considered.
   *
 - * @param string $long_url
 + * @param string  $long_url original URL
 + * @param boolean $force    Force shortening (used when notice is too long)
 + *
   * @return string may return the original URL if shortening failed
   *
   * @fixme provide a way to specify a particular shortener
   * @fixme provide a way to specify to use a given user's shortening preferences
   */
 -function common_shorten_url($long_url)
 +
 +function common_shorten_url($long_url, $force = false)
  {
 +    common_debug("Shortening URL '$long_url' (force = $force)");
 +
      $long_url = trim($long_url);
 +
      $user = common_current_user();
 -    if (empty($user)) {
 -        // common current user does not find a user when called from the XMPP daemon
 -        // therefore we'll set one here fix, so that XMPP given URLs may be shortened
 -        $shortenerName = 'ur1.ca';
 -    } else {
 -        $shortenerName = $user->urlshorteningservice;
 +
 +    $maxUrlLength = User_urlshortener_prefs::maxUrlLength($user);
 +    common_debug("maxUrlLength = $maxUrlLength");
 +
 +    // $force forces shortening even if it's not strictly needed
 +
 +    if (mb_strlen($long_url) < $maxUrlLength && !$force) {
 +        common_debug("Skipped shortening URL.");
 +        return $long_url;
      }
  
 -    if(Event::handle('StartShortenUrl', array($long_url,$shortenerName,&$shortenedUrl))){
 +    $shortenerName = User_urlshortener_prefs::urlShorteningService($user);
 +
 +    common_debug("Shortener name = '$shortenerName'");
 +
 +    if (Event::handle('StartShortenUrl', array($long_url, $shortenerName, &$shortenedUrl))) {
          //URL wasn't shortened, so return the long url
          return $long_url;
 -    }else{
 +    } else {
          //URL was shortened, so return the result
          return trim($shortenedUrl);
      }
diff --combined lib/xrd.php
index 145cd64cb4e92ddb42d5fd77b8e8b6a81c1ab562,0000000000000000000000000000000000000000..c8cffed9cde255fd14decbb0d68c2353fcfee748
mode 100644,000000..100644
--- /dev/null
@@@ -1,171 -1,0 +1,172 @@@
 +<?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/
 + */
 +
-             throw new Exception("Invalid XML.");
 +class XRD
 +{
 +    const XML_NS = 'http://www.w3.org/2000/xmlns/';
 +
 +    const XRD_NS = 'http://docs.oasis-open.org/ns/xri/xrd-1.0';
 +
 +    const HOST_META_NS = 'http://host-meta.net/xrd/1.0';
 +
 +    public $expires;
 +
 +    public $subject;
 +
 +    public $host;
 +
 +    public $alias = array();
 +
 +    public $types = array();
 +
 +    public $links = array();
 +
 +    public static function parse($xml)
 +    {
 +        $xrd = new XRD();
 +
 +        $dom = new DOMDocument();
 +
 +        // Don't spew XML warnings to output
 +        $old = error_reporting();
 +        error_reporting($old & ~E_WARNING);
 +        $ok = $dom->loadXML($xml);
 +        error_reporting($old);
 +
 +        if (!$ok) {
-             throw new Exception("Invalid XML, missing XRD root.");
++            // TRANS: Exception.
++            throw new Exception(_m('Invalid XML.'));
 +        }
 +        $xrd_element = $dom->getElementsByTagName('XRD')->item(0);
 +        if (!$xrd_element) {
++            // TRANS: Exception.
++            throw new Exception(_m('Invalid XML, missing XRD root.'));
 +        }
 +
 +        // Check for host-meta host
 +        $host = $xrd_element->getElementsByTagName('Host')->item(0);
 +        if ($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;
 +                break;
 +            case 'Subject':
 +                $xrd->subject = $node->nodeValue;
 +                break;
 +
 +            case 'Alias':
 +                $xrd->alias[] = $node->nodeValue;
 +                break;
 +
 +            case 'Link':
 +                $xrd->links[] = $xrd->parseLink($node);
 +                break;
 +
 +            case 'Type':
 +                $xrd->types[] = $xrd->parseType($node);
 +                break;
 +
 +            }
 +        }
 +        return $xrd;
 +    }
 +
 +    public function toXML()
 +    {
 +        $xs = new XMLStringer();
 +
 +        $xs->startXML();
 +        $xs->elementStart('XRD', array('xmlns' => XRD::XRD_NS));
 +
 +        if ($this->host) {
 +            $xs->element('hm:Host', array('xmlns:hm' => XRD::HOST_META_NS), $this->host);
 +        }
 +
 +        if ($this->expires) {
 +            $xs->element('Expires', null, $this->expires);
 +        }
 +
 +        if ($this->subject) {
 +            $xs->element('Subject', null, $this->subject);
 +        }
 +
 +        foreach ($this->alias as $alias) {
 +            $xs->element('Alias', null, $alias);
 +        }
 +
 +        foreach ($this->links as $link) {
 +            $titles = array();
 +            if (isset($link['title'])) {
 +                $titles = $link['title'];
 +                unset($link['title']);
 +            }
 +            $xs->elementStart('Link', $link);
 +            foreach ($titles as $title) {
 +                $xs->element('Title', null, $title);
 +            }
 +            $xs->elementEnd('Link');
 +        }
 +
 +        $xs->elementEnd('XRD');
 +
 +        return $xs->getString();
 +    }
 +
 +    function parseType($element)
 +    {
 +        return array();
 +    }
 +
 +    function parseLink($element)
 +    {
 +        $link = array();
 +        $link['rel'] = $element->getAttribute('rel');
 +        $link['type'] = $element->getAttribute('type');
 +        $link['href'] = $element->getAttribute('href');
 +        $link['template'] = $element->getAttribute('template');
 +        foreach ($element->childNodes as $node) {
 +            if ($node instanceof DOMElement) {
 +                switch($node->tagName) {
 +                case 'Title':
 +                    $link['title'][] = $node->nodeValue;
 +                }
 +            }
 +        }
 +
 +        return $link;
 +    }
 +}
index 38c821636107bc2f21350707d7603d16ec4f9f20,10d99b35886788f5ea4386b800f673d6f0761152..e1c8d3462eb9b40d69541bf87445b0a27e4262cf
@@@ -31,6 -31,8 +31,6 @@@ if (!defined('STATUSNET')) 
      exit(1);
  }
  
 -require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php';
 -
  class BitlyUrlPlugin extends UrlShortenerPlugin
  {
      public $serviceUrl;
@@@ -38,7 -40,7 +38,7 @@@
      function onInitializePlugin(){
          parent::onInitializePlugin();
          if(!isset($this->serviceUrl)){
-             throw new Exception("must specify a serviceUrl");
+             throw new Exception(_m("You must specify a serviceUrl."));
          }
      }
  
@@@ -61,4 -63,3 +61,3 @@@
          return true;
      }
  }
index be6967381069a7e3c27a2d55fdd60f7414b34b23,846774e7c671a88cd3c4fae15d88bdff1cc97c42..3301ce5824bffdaa80875c3833cce746191a5a63
@@@ -28,7 -28,7 +28,7 @@@ class CasloginAction extends Actio
              $this->clientError(_m('Already logged in.'));
          } else {
              global $casSettings;
 -            phpCAS::client(CAS_VERSION_2_0,$casSettings['server'],$casSettings['port'],$casSettings['path']);
 +            phpCAS::client(CAS_VERSION_2_0,$casSettings['server'],$casSettings['port'],$casSettings['path'],false);
              phpCAS::setNoCasServerValidation();
              phpCAS::handleLogoutRequests();
              phpCAS::forceAuthentication();
              $casTempPassword = common_good_rand(16);
              $user = common_check_user(phpCAS::getUser(), $casTempPassword);
              if (!$user) {
-                 $this->serverError(_('Incorrect username or password.'));
+                 $this->serverError(_m('Incorrect username or password.'));
                  return;
              }
  
              // success!
              if (!common_set_user($user)) {
-                 $this->serverError(_('Error setting user. You are probably not authorized.'));
+                 $this->serverError(_m('Error setting user. You are probably not authorized.'));
                  return;
              }
  
@@@ -69,7 -69,6 +69,6 @@@
              }
  
              common_redirect($url, 303);
          }
      }
  }
index 3da08e05da351c67372609ed5b7d891970f19bdd,27a3a56f72d2e3cd62db96aaf3d366a03acef5b2..65e27a0374988bdbf5f1dcd71ad7250259e54069
@@@ -51,10 -51,8 +51,10 @@@ class ClientSideShortenPlugin extends P
      }
  
      function onEndShowScripts($action){
 -        $action->inlineScript('var Notice_maxContent = ' . Notice::maxContent());
          if (common_logged_in()) {
 +            $user = common_current_user();
 +            $action->inlineScript('var maxNoticeLength = ' . User_urlshortener_prefs::maxNoticeLength($user));
 +            $action->inlineScript('var maxUrlLength = ' . User_urlshortener_prefs::maxUrlLength($user));
              $action->script('plugins/ClientSideShorten/shorten.js');
          }
      }
@@@ -73,9 -71,7 +73,7 @@@
                              'author' => 'Craig Andrews',
                              'homepage' => 'http://status.net/wiki/Plugin:ClientSideShorten',
                              'rawdescription' =>
-                             _m('ClientSideShorten causes the web interface\'s notice form to automatically shorten urls as they entered, and before the notice is submitted.'));
+                             _m('ClientSideShorten causes the web interface\'s notice form to automatically shorten URLs as they entered, and before the notice is submitted.'));
          return true;
      }
  }
index 0000000000000000000000000000000000000000,310641ce6027dfac967f17b1b625c2f57e6ea0c6..d88014bb8070b57d614063dcb7a091d815a3ab6d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,503 +1,503 @@@
 -        $c = common_memcache();
+ <?php
+ /**
+  * StatusNet, the distributed open-source microblogging tool
+  *
+  * Plugin to convert string locations to Geonames IDs and vice versa
+  *
+  * 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  Action
+  * @package   StatusNet
+  * @author    Evan Prodromou <evan@status.net>
+  * @copyright 2009 StatusNet Inc.
+  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+  * @link      http://status.net/
+  */
+ if (!defined('STATUSNET')) {
+     exit(1);
+ }
+ /**
+  * Plugin to convert string locations to Geonames IDs and vice versa
+  *
+  * This handles most of the events that Location class emits. It uses
+  * the geonames.org Web service to convert names like 'Montreal, Quebec, Canada'
+  * into IDs and lat/lon pairs.
+  *
+  * @category Plugin
+  * @package  StatusNet
+  * @author   Evan Prodromou <evan@status.net>
+  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+  * @link     http://status.net/
+  *
+  * @seeAlso  Location
+  */
+ class GeonamesPlugin extends Plugin
+ {
+     const LOCATION_NS = 1;
+     public $host     = 'ws.geonames.org';
+     public $username = null;
+     public $token    = null;
+     public $expiry   = 7776000; // 90-day expiry
+     public $timeout  = 2;       // Web service timeout in seconds.
+     public $timeoutWindow = 60; // Further lookups in this process will be disabled for N seconds after a timeout.
+     public $cachePrefix = null; // Optional shared memcache prefix override
+                                 // to share lookups between local instances.
+     protected $lastTimeout = null; // timestamp of last web service timeout
+     /**
+      * convert a name into a Location object
+      *
+      * @param string   $name      Name to convert
+      * @param string   $language  ISO code for anguage the name is in
+      * @param Location &$location Location object (may be null)
+      *
+      * @return boolean whether to continue (results in $location)
+      */
+     function onLocationFromName($name, $language, &$location)
+     {
+         $loc = $this->getCache(array('name' => $name,
+                                      'language' => $language));
+         if ($loc !== false) {
+             $location = $loc;
+             return false;
+         }
+         try {
+             $geonames = $this->getGeonames('search',
+                                            array('maxRows' => 1,
+                                                  'q' => $name,
+                                                  'lang' => $language,
+                                                  'type' => 'xml'));
+         } catch (Exception $e) {
+             $this->log(LOG_WARNING, "Error for $name: " . $e->getMessage());
+             return true;
+         }
+         if (count($geonames) == 0) {
+             // no results
+             $this->setCache(array('name' => $name,
+                                   'language' => $language),
+                             null);
+             return true;
+         }
+         $n = $geonames[0];
+         $location = new Location();
+         $location->lat              = $this->canonical($n->lat);
+         $location->lon              = $this->canonical($n->lng);
+         $location->names[$language] = (string)$n->name;
+         $location->location_id      = (string)$n->geonameId;
+         $location->location_ns      = self::LOCATION_NS;
+         $this->setCache(array('name' => $name,
+                               'language' => $language),
+                         $location);
+         // handled, don't continue processing!
+         return false;
+     }
+     /**
+      * convert an id into a Location object
+      *
+      * @param string   $id        Name to convert
+      * @param string   $ns        Name to convert
+      * @param string   $language  ISO code for language for results
+      * @param Location &$location Location object (may be null)
+      *
+      * @return boolean whether to continue (results in $location)
+      */
+     function onLocationFromId($id, $ns, $language, &$location)
+     {
+         if ($ns != self::LOCATION_NS) {
+             // It's not one of our IDs... keep processing
+             return true;
+         }
+         $loc = $this->getCache(array('id' => $id));
+         if ($loc !== false) {
+             $location = $loc;
+             return false;
+         }
+         try {
+             $geonames = $this->getGeonames('hierarchy',
+                                            array('geonameId' => $id,
+                                                  'lang' => $language));
+         } catch (Exception $e) {
+             $this->log(LOG_WARNING, "Error for ID $id: " . $e->getMessage());
+             return false;
+         }
+         $parts = array();
+         foreach ($geonames as $level) {
+             if (in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
+                 $parts[] = (string)$level->name;
+             }
+         }
+         $last = $geonames[count($geonames)-1];
+         if (!in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
+             $parts[] = (string)$last->name;
+         }
+         $location = new Location();
+         $location->location_id      = (string)$last->geonameId;
+         $location->location_ns      = self::LOCATION_NS;
+         $location->lat              = $this->canonical($last->lat);
+         $location->lon              = $this->canonical($last->lng);
+         $location->names[$language] = implode(', ', array_reverse($parts));
+         $this->setCache(array('id' => (string)$last->geonameId),
+                         $location);
+         // We're responsible for this namespace; nobody else
+         // can resolve it
+         return false;
+     }
+     /**
+      * convert a lat/lon pair into a Location object
+      *
+      * Given a lat/lon, we try to find a Location that's around
+      * it or nearby. We prefer populated places (cities, towns, villages).
+      *
+      * @param string   $lat       Latitude
+      * @param string   $lon       Longitude
+      * @param string   $language  ISO code for language for results
+      * @param Location &$location Location object (may be null)
+      *
+      * @return boolean whether to continue (results in $location)
+      */
+     function onLocationFromLatLon($lat, $lon, $language, &$location)
+     {
+         // Make sure they're canonical
+         $lat = $this->canonical($lat);
+         $lon = $this->canonical($lon);
+         $loc = $this->getCache(array('lat' => $lat,
+                                      'lon' => $lon));
+         if ($loc !== false) {
+             $location = $loc;
+             return false;
+         }
+         try {
+           $geonames = $this->getGeonames('findNearbyPlaceName',
+                                          array('lat' => $lat,
+                                                'lng' => $lon,
+                                                'lang' => $language));
+         } catch (Exception $e) {
+             $this->log(LOG_WARNING, "Error for coords $lat, $lon: " . $e->getMessage());
+             return true;
+         }
+         if (count($geonames) == 0) {
+             // no results
+             $this->setCache(array('lat' => $lat,
+                                   'lon' => $lon),
+                             null);
+             return true;
+         }
+         $n = $geonames[0];
+         $parts = array();
+         $location = new Location();
+         $parts[] = (string)$n->name;
+         if (!empty($n->adminName1)) {
+             $parts[] = (string)$n->adminName1;
+         }
+         if (!empty($n->countryName)) {
+             $parts[] = (string)$n->countryName;
+         }
+         $location->location_id = (string)$n->geonameId;
+         $location->location_ns = self::LOCATION_NS;
+         $location->lat         = $this->canonical($n->lat);
+         $location->lon         = $this->canonical($n->lng);
+         $location->names[$language] = implode(', ', $parts);
+         $this->setCache(array('lat' => $lat,
+                               'lon' => $lon),
+                         $location);
+         // Success! We handled it, so no further processing
+         return false;
+     }
+     /**
+      * Human-readable name for a location
+      *
+      * Given a location, we try to retrieve a human-readable name
+      * in the target language.
+      *
+      * @param Location $location Location to get the name for
+      * @param string   $language ISO code for language to find name in
+      * @param string   &$name    Place to put the name
+      *
+      * @return boolean whether to continue
+      */
+     function onLocationNameLanguage($location, $language, &$name)
+     {
+         if ($location->location_ns != self::LOCATION_NS) {
+             // It's not one of our IDs... keep processing
+             return true;
+         }
+         $id = $location->location_id;
+         $n = $this->getCache(array('id' => $id,
+                                    'language' => $language));
+         if ($n !== false) {
+             $name = $n;
+             return false;
+         }
+         try {
+             $geonames = $this->getGeonames('hierarchy',
+                                            array('geonameId' => $id,
+                                                  'lang' => $language));
+         } catch (Exception $e) {
+             $this->log(LOG_WARNING, "Error for ID $id: " . $e->getMessage());
+             return false;
+         }
+         if (count($geonames) == 0) {
+             $this->setCache(array('id' => $id,
+                                   'language' => $language),
+                             null);
+             return false;
+         }
+         $parts = array();
+         foreach ($geonames as $level) {
+             if (in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
+                 $parts[] = (string)$level->name;
+             }
+         }
+         $last = $geonames[count($geonames)-1];
+         if (!in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
+             $parts[] = (string)$last->name;
+         }
+         if (count($parts)) {
+             $name = implode(', ', array_reverse($parts));
+             $this->setCache(array('id' => $id,
+                                   'language' => $language),
+                             $name);
+         }
+         return false;
+     }
+     /**
+      * Human-readable URL for a location
+      *
+      * Given a location, we try to retrieve a geonames.org URL.
+      *
+      * @param Location $location Location to get the url for
+      * @param string   &$url     Place to put the url
+      *
+      * @return boolean whether to continue
+      */
+     function onLocationUrl($location, &$url)
+     {
+         if ($location->location_ns != self::LOCATION_NS) {
+             // It's not one of our IDs... keep processing
+             return true;
+         }
+         $url = 'http://www.geonames.org/' . $location->location_id;
+         // it's been filled, so don't process further.
+         return false;
+     }
+     /**
+      * Machine-readable name for a location
+      *
+      * Given a location, we try to retrieve a geonames.org URL.
+      *
+      * @param Location $location Location to get the url for
+      * @param string   &$url     Place to put the url
+      *
+      * @return boolean whether to continue
+      */
+     function onLocationRdfUrl($location, &$url)
+     {
+         if ($location->location_ns != self::LOCATION_NS) {
+             // It's not one of our IDs... keep processing
+             return true;
+         }
+         $url = 'http://sws.geonames.org/' . $location->location_id . '/';
+         // it's been filled, so don't process further.
+         return false;
+     }
+     function getCache($attrs)
+     {
 -        $c = common_memcache();
++        $c = Cache::instance();
+         if (empty($c)) {
+             return null;
+         }
+         $key = $this->cacheKey($attrs);
+         $value = $c->get($key);
+         return $value;
+     }
+     function setCache($attrs, $loc)
+     {
 -               common_keyize(implode(',', array_values($attrs)));
++        $c = Cache::instance();
+         if (empty($c)) {
+             return null;
+         }
+         $key = $this->cacheKey($attrs);
+         $result = $c->set($key, $loc, 0, time() + $this->expiry);
+         return $result;
+     }
+     function cacheKey($attrs)
+     {
+         $key = 'geonames:' .
+                implode(',', array_keys($attrs)) . ':'.
 -            return common_cache_key($key);
++               Cache::keyize(implode(',', array_values($attrs)));
+         if ($this->cachePrefix) {
+             return $this->cachePrefix . ':' . $key;
+         } else {
++            return Cache::key($key);
+         }
+     }
+     function wsUrl($method, $params)
+     {
+         if (!empty($this->username)) {
+             $params['username'] = $this->username;
+         }
+         if (!empty($this->token)) {
+             $params['token'] = $this->token;
+         }
+         $str = http_build_query($params, null, '&');
+         return 'http://'.$this->host.'/'.$method.'?'.$str;
+     }
+     function getGeonames($method, $params)
+     {
+         if ($this->lastTimeout && (time() - $this->lastTimeout < $this->timeoutWindow)) {
+             throw new Exception("skipping due to recent web service timeout");
+         }
+         $client = HTTPClient::start();
+         $client->setConfig('connect_timeout', $this->timeout);
+         $client->setConfig('timeout', $this->timeout);
+         try {
+             $result = $client->get($this->wsUrl($method, $params));
+         } catch (Exception $e) {
+             common_log(LOG_ERR, __METHOD__ . ": " . $e->getMessage());
+             $this->lastTimeout = time();
+             throw $e;
+         }
+         if (!$result->isOk()) {
+             throw new Exception("HTTP error code " . $result->getStatus());
+         }
+         $body = $result->getBody();
+         if (empty($body)) {
+             throw new Exception("Empty HTTP body in response");
+         }
+         // This will throw an exception if the XML is mal-formed
+         $document = new SimpleXMLElement($body);
+         // No children, usually no results
+         $children = $document->children();
+         if (count($children) == 0) {
+             return array();
+         }
+         if (isset($document->status)) {
+             throw new Exception("Error #".$document->status['value']." ('".$document->status['message']."')");
+         }
+         // Array of elements, >0 elements
+         return $document->geoname;
+     }
+     function onPluginVersion(&$versions)
+     {
+         $versions[] = array('name' => 'Geonames',
+                             'version' => STATUSNET_VERSION,
+                             'author' => 'Evan Prodromou',
+                             'homepage' => 'http://status.net/wiki/Plugin:Geonames',
+                             'rawdescription' =>
+                             _m('Uses <a href="http://geonames.org/">Geonames</a> service to get human-readable '.
+                                'names for locations based on user-provided lat/long pairs.'));
+         return true;
+     }
+     function canonical($coord)
+     {
+         $coord = rtrim($coord, "0");
+         $coord = rtrim($coord, ".");
+         return $coord;
+     }
+ }
index cfc08c1ee1fd0252ad324bd8bba8a1f3204a6504,e2f8c6d5431f829dd6153d03c95f04145669e5d3..c49c060921faafd8dbfc1798a7c9e3eedc0888f7
@@@ -44,7 -44,7 +44,7 @@@ class ImapManager extends IoManage
       */
      public static function get()
      {
-         throw new Exception('ImapManager should be created using it\'s constructor, not the static get method');
+         throw new Exception(_m('ImapManager should be created using its constructor, not the using the static get method.'));
      }
  
      /**
      }
  
      /**
 -     * Tell the i/o master we need one instance for each supporting site
 -     * being handled in this process.
 +     * Tell the i/o master we need one instance globally.
 +     * Since this is a plugin manager, the plugin class itself will
 +     * create one instance per site. This prevents the IoMaster from
 +     * making more instances.
       */
      public static function multiSite()
      {
 -        return IoManager::INSTANCE_PER_SITE;
 +        return IoManager::GLOBAL_SINGLE_ONLY;
      }
  
      /**
      {
          return $this->check_mailbox() > 0;
      }
-     
      function pollInterval()
      {
          return $this->plugin->poll_frequency;
      }
-     
      protected function connect()
      {
          $this->conn = imap_open($this->plugin->mailbox, $this->plugin->user, $this->plugin->password);
index 09ff54bad9fca7161f7b69103f6b6d6c4d8c979d,159b2d265a2c13c123f04c7e21620d01d310a672..579fe4b64b474fe649198170037e9c1ba1a6ad33
@@@ -60,13 -60,13 +60,13 @@@ class LdapCommo
          $this->ldap_config = $this->get_ldap_config();
  
          if(!isset($this->host)){
-             throw new Exception("must specify a host");
+             throw new Exception(_m("A host must be specified."));
          }
          if(!isset($this->basedn)){
-             throw new Exception("must specify a basedn");
+             throw new Exception(_m('"basedn" must be specified.'));
          }
          if(!isset($this->attributes['username'])){
-             throw new Exception("username attribute must be set.");
+             throw new Exception(_m('The username attribute must be set.'));
          }
      }
  
                  }
                  throw new Exception('Could not connect to LDAP server: '.$err->getMessage());
              }
 -            $c = common_memcache();
 +            $c = Cache::instance();
              if (!empty($c)) {
                  $cacheObj = new MemcacheSchemaCache(
                      array('c'=>$c,
 -                       'cacheKey' => common_cache_key('ldap_schema:' . $config_id)));
 +                       'cacheKey' => Cache::key('ldap_schema:' . $config_id)));
                  $ldap->registerSchemaCache($cacheObj);
              }
              self::$ldap_connections[$config_id] = $ldap;
                  $ldap = $this->get_ldap_connection($config);
  
                  $entry = $this->get_user($username,array(),$ldap);
-                 
                  $newCryptedPassword = $this->hashPassword($newpassword, $this->password_encoding);
                  if ($newCryptedPassword===false) {
                      return false;
       * @return string The hashed password.
       *
       */
-     function hashPassword( $passwordClear, $encodageType ) 
+     function hashPassword( $passwordClear, $encodageType )
      {
          $encodageType = strtolower( $encodageType );
          switch( $encodageType ) {
-             case 'crypt': 
-                 $cryptedPassword = '{CRYPT}' . crypt($passwordClear,$this->randomSalt(2)); 
+             case 'crypt':
+                 $cryptedPassword = '{CRYPT}' . crypt($passwordClear,$this->randomSalt(2));
                  break;
-                 
              case 'ext_des':
                  // extended des crypt. see OpenBSD crypt man page.
                  if ( ! defined( 'CRYPT_EXT_DES' ) || CRYPT_EXT_DES == 0 ) {return FALSE;} //Your system crypt library does not support extended DES encryption.
       * @param int $length The length of the salt string to generate.
       * @return string The generated salt string.
       */
-      
-     function randomSalt( $length ) 
+     function randomSalt( $length )
      {
          $possible = '0123456789'.
              'abcdefghijklmnopqrstuvwxyz'.
  
          return $str;
      }
  }
  
  class LdapInvalidCredentialsException extends Exception
  {
  }
index 06ea49ff57d542050531366bc2e3334226cc619e,bd98026fe821cb660a35b25eb02a1c185ea04b69..b63cc8a556c2f6bf837c1e896cb5d8a11a632ccd
@@@ -31,6 -31,8 +31,6 @@@ if (!defined('STATUSNET')) 
      exit(1);
  }
  
 -require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php';
 -
  class LilUrlPlugin extends UrlShortenerPlugin
  {
      public $serviceUrl;
@@@ -38,7 -40,7 +38,7 @@@
      function onInitializePlugin(){
          parent::onInitializePlugin();
          if(!isset($this->serviceUrl)){
-             throw new Exception("must specify a serviceUrl");
+             throw new Exception(_m('A serviceUrl must be specified.'));
          }
      }
  
@@@ -69,4 -71,3 +69,3 @@@
          return true;
      }
  }
index afe6edf5fd73f56ffd218d1ee4f5580289caa7b0,b37531165232be8e94e864ac794435925d96523d..cfed0779baf37f94a1fcb932967e47b2f2e7c333
@@@ -51,7 -51,6 +51,6 @@@ class MinifyPlugin extends Plugi
       *
       * @return boolean hook return
       */
      function onStartInitializeRouter($m)
      {
          $m->connect('main/min',
      function onStartInlineScriptElement($action,&$code,&$type)
      {
          if($this->minifyInlineJs && $type=='text/javascript'){
 -            $c = common_memcache();
 +            $c = Cache::instance();
              if (!empty($c)) {
 -                $cacheKey = common_cache_key(self::cacheKey . ':' . crc32($code));
 +                $cacheKey = Cache::key(self::cacheKey . ':' . crc32($code));
                  $out = $c->get($cacheKey);
              }
              if(empty($out)) {
      function onStartStyleElement($action,&$code,&$type,&$media)
      {
          if($this->minifyInlineCss && $type=='text/css'){
 -            $c = common_memcache();
 +            $c = Cache::instance();
              if (!empty($c)) {
 -                $cacheKey = common_cache_key(self::cacheKey . ':' . crc32($code));
 +                $cacheKey = Cache::key(self::cacheKey . ':' . crc32($code));
                  $out = $c->get($cacheKey);
              }
              if(empty($out)) {
                              'author' => 'Craig Andrews',
                              'homepage' => 'http://status.net/wiki/Plugin:Minify',
                              'rawdescription' =>
-                             _m('The Minify plugin minifies your CSS and Javascript, removing whitespace and comments.'));
+                             _m('The Minify plugin minifies StatusNet\'s CSS and JavaScript, removing whitespace and comments.'));
          return true;
      }
  }
index bac1df8e2d1414c186b53dfad1e141d083512330,9a59c4223c8ec1f613b995eefc01a66275a748ef..e012a40272c27d5eef0c89209ae7c4e101a06e8e
@@@ -46,11 -46,11 +46,11 @@@ class MinifyAction extends Actio
              if(file_exists($this->file)) {
                  return true;
              } else {
-                 $this->clientError(_('f parameter is not a valid path'),404);
+                 $this->clientError(_m('The parameter "f" is not a valid path.'),404);
                  return false;
              }
          }else{
-             $this->clientError(_('f parameter is required'),500);
+             $this->clientError(_m('The parameter "f" is required but missing.'),500);
              return false;
          }
      }
@@@ -74,9 -74,9 +74,9 @@@
      {
          parent::handle($args);
          
 -        $c = common_memcache();
 +        $c = Cache::instance();
          if (!empty($c)) {
 -            $cacheKey = common_cache_key(MinifyPlugin::cacheKey . ':' . $this->file . '?v=' . empty($this->v)?'':$this->v);
 +            $cacheKey = Cache::key(MinifyPlugin::cacheKey . ':' . $this->file . '?v=' . empty($this->v)?'':$this->v);
              $out = $c->get($cacheKey);
          }
          if(empty($out)) {
                  header('Content-Type: ' . self::TYPE_CSS);
                  break;
              default:
-                 $this->clientError(_('File type not supported'),500);
+                 $this->clientError(_m('File type not supported.'),500);
                  return false;
          }
          return $out;
      }
  }
index dd150995420717d891189490607cd1e9e33bcc3f,dcf1b36078d8f490d83d11b5e4c429df1b0a5d5d..601243b42580995fa6d52abd1875f3f9c525b835
@@@ -50,6 -50,8 +50,6 @@@ class OStatusPlugin extends Plugi
      function onRouterInitialized($m)
      {
          // Discovery actions
 -        $m->connect('.well-known/host-meta',
 -                    array('action' => 'hostmeta'));
          $m->connect('main/xrd',
                      array('action' => 'userxrd'));
          $m->connect('main/ownerxrd',
                                      array('nickname' => $profile->nickname));
              $output->element('a', array('href' => $url,
                                          'class' => 'entity_remote_subscribe'),
+                                 // TRANS: Link description for link to subscribe to a remote user.
                                  _m('Subscribe'));
  
              $output->elementEnd('li');
                                      array('group' => $group->nickname));
              $output->element('a', array('href' => $url,
                                          'class' => 'entity_remote_subscribe'),
+                                 // TRANS: Link description for link to join a remote group.
                                  _m('Join'));
          }
  
                  }
  
                  $url = $notice->url;
+                 // TRANSLATE: %s is a domain.
                  $title = sprintf(_m("Sent from %s via OStatus"), $domain);
                  return false;
              }
          }
  
          if (!$oprofile->subscribe()) {
+             // TRANS: Exception.
              throw new Exception(_m('Could not set up remote subscription.'));
          }
      }
              return true;
          }
  
-         $act = new Activity();
-         $act->verb = ActivityVerb::FOLLOW;
-         $act->id   = TagURI::mint('follow:%d:%d:%s',
-                                   $subscriber->id,
-                                   $other->id,
-                                   common_date_iso8601(time()));
-         $act->time    = time();
-         $act->title   = _("Follow");
-         // TRANS: Success message for subscribe to user attempt through OStatus.
-         // TRANS: %1$s is the subscriber name, %2$s is the subscribed user's name.
-         $act->content = sprintf(_("%1$s is now following %2$s."),
-                                $subscriber->getBestName(),
-                                $other->getBestName());
+         $sub = Subscription::pkeyGet(array('subscriber' => $subscriber->id,
+                                            'subscribed' => $other->id));
  
-         $act->actor   = ActivityObject::fromProfile($subscriber);
-         $act->object  = ActivityObject::fromProfile($other);
+         $act = $sub->asActivity();
  
          $oprofile->notifyActivity($act, $subscriber);
  
                                    common_date_iso8601(time()));
  
          $act->time    = time();
-         $act->title   = _("Unfollow");
+         $act->title   = _m('Unfollow');
          // TRANS: Success message for unsubscribe from user attempt through OStatus.
          // TRANS: %1$s is the unsubscriber's name, %2$s is the unsubscribed user's name.
-         $act->content = sprintf(_("%1$s stopped following %2$s."),
+         $act->content = sprintf(_m('%1$s stopped following %2$s.'),
                                 $profile->getBestName(),
                                 $other->getBestName());
  
                  throw new Exception(_m('Could not set up remote group membership.'));
              }
  
+             // NOTE: we don't use Group_member::asActivity() since that record
+             // has not yet been created.
              $member = Profile::staticGet($user->id);
  
              $act = new Activity();
                  return true;
              } else {
                  $oprofile->garbageCollect();
+                 // TRANS: Exception.
                  throw new Exception(_m("Failed joining remote group."));
              }
          }
       * @param Notice $notice being favored
       * @return hook return value
       */
      function onEndFavorNotice(Profile $profile, Notice $notice)
      {
          $user = User::staticGet('id', $profile->id);
              return true;
          }
  
-         $act = new Activity();
+         $fav = Fave::pkeyGet(array('user_id' => $user->id,
+                                    'notice_id' => $notice->id));
  
-         $act->verb = ActivityVerb::FAVORITE;
-         $act->id   = TagURI::mint('favor:%d:%d:%s',
-                                   $profile->id,
-                                   $notice->id,
-                                   common_date_iso8601(time()));
+         if (empty($fav)) {
+             // That's weird.
+             return true;
+         }
  
-         $act->time    = time();
-         $act->title   = _("Favor");
-         // TRANS: Success message for adding a favorite notice through OStatus.
-         // TRANS: %1$s is the favoring user's name, %2$s is URI to the favored notice.
-         $act->content = sprintf(_("%1$s marked notice %2$s as a favorite."),
-                                $profile->getBestName(),
-                                $notice->uri);
-         $act->actor   = ActivityObject::fromProfile($profile);
-         $act->object  = ActivityObject::fromNotice($notice);
+         $act = $fav->asActivity();
  
          $oprofile->notifyActivity($act, $profile);
  
                                    $notice->id,
                                    common_date_iso8601(time()));
          $act->time    = time();
-         $act->title   = _("Disfavor");
+         $act->title   = _m('Disfavor');
          // TRANS: Success message for remove a favorite notice through OStatus.
          // TRANS: %1$s is the unfavoring user's name, %2$s is URI to the no longer favored notice.
-         $act->content = sprintf(_("%1$s marked notice %2$s as no longer a favorite."),
+         $act->content = sprintf(_m('%1$s marked notice %2$s as no longer a favorite.'),
                                 $profile->getBestName(),
                                 $notice->uri);
  
              $action->elementStart('p', array('id' => 'entity_remote_subscribe',
                                               'class' => 'entity_subscribe'));
              $action->element('a', array('href' => common_local_url($target),
-                                         'class' => 'entity_remote_subscribe')
-                                 , _m('Remote')); // @todo: i18n: Add translator hint for this text.
+                                         'class' => 'entity_remote_subscribe'),
+                                 // TRANS: Link text for link to remote subscribe.
+                                 _m('Remote'));
              $action->elementEnd('p');
              $action->elementEnd('div');
          }
                                    $profile->id,
                                    common_date_iso8601(time()));
          $act->time    = time();
+         // TRANS: Title for activity.
          $act->title   = _m("Profile update");
          // TRANS: Ping text for remote profile update through OStatus.
          // TRANS: %s is user that updated their profile.
                                          array('nickname' => $profileUser->nickname));
                  $output->element('a', array('href' => $url,
                                              'class' => 'entity_remote_subscribe'),
-                                  _m('Subscribe')); // @todo: i18n: Add context.
+                                   // TRANS: Link text for a user to subscribe to an OStatus user.
+                                  _m('Subscribe'));
                  $output->elementEnd('li');
              }
          }
                              'version' => STATUSNET_VERSION,
                              'author' => 'Evan Prodromou, James Walker, Brion Vibber, Zach Copley',
                              'homepage' => 'http://status.net/wiki/Plugin:OStatus',
-                             'rawdescription' =>
-                             _m('Follow people across social networks that implement '.
-                                '<a href="http://ostatus.org/">OStatus</a>.')); // @todo i18n: Add translator hint.
+                             // TRANS: Plugin description.
+                             'rawdescription' => _m('Follow people across social networks that implement '.
+                                '<a href="http://ostatus.org/">OStatus</a>.'));
  
          return true;
      }
  
      /**
-      * Utility function to check if the given URL is a canonical group profile
+      * Utility function to check if the given URI is a canonical group profile
       * page, and if so return the ID number.
       *
       * @param string $url
       */
      public static function localGroupFromUrl($url)
      {
-         $template = common_local_url('groupbyid', array('id' => '31337'));
-         $template = preg_quote($template, '/');
-         $template = str_replace('31337', '(\d+)', $template);
-         if (preg_match("/$template/", $url, $matches)) {
-             return intval($matches[1]);
+         $group = User_group::staticGet('uri', $url);
+         if ($group) {
+             $local = Local_group::staticGet('id', $group->id);
+             if ($local) {
+                 return $group->id;
+             }
+         } else {
+             // To find local groups which haven't had their uri fields filled out...
+             // If the domain has changed since a subscriber got the URI, it'll
+             // be broken.
+             $template = common_local_url('groupbyid', array('id' => '31337'));
+             $template = preg_quote($template, '/');
+             $template = str_replace('31337', '(\d+)', $template);
+             if (preg_match("/$template/", $url, $matches)) {
+                 return intval($matches[1]);
+             }
          }
          return false;
      }
  
          return true;
      }
 +
 +    function onStartHostMetaLinks(&$links) {
 +        $url = common_local_url('userxrd');
 +        $url.= '?uri={uri}';
 +        $links[] = array('rel' => Discovery::LRDD_REL,
 +                              'template' => $url,
 +                              'title' => array('Resource Descriptor'));
 +    }
  }
index e58440fc1051ca5c2658f0d5e8b114e510306923,a033a50109087cd887487987cac5ba51bbdbf99a..9c32074520ec366c517e5edde476f9b75554e365
@@@ -102,9 -102,14 +102,14 @@@ class OpenIDPlugin extends Plugi
      function onStartConnectPath(&$path, &$defaults, &$rules, &$result)
      {
          if (common_config('site', 'openidonly')) {
-             static $block = array('main/login',
-                                   'main/register',
-                                   'main/recoverpassword',
+             // Note that we should not remove the login and register
+             // actions. Lots of auth-related things link to them,
+             // such as when visiting a private site without a session
+             // or revalidating a remembered login for admin work.
+             //
+             // We take those two over with redirects to ourselves
+             // over in onArgsInitialize().
+             static $block = array('main/recoverpassword',
                                    'settings/password');
  
              if (in_array($path, $block)) {
          return true;
      }
  
 +    /**
 +     * Add OpenID information to the Account Management Control Document
 +     * Event supplied by the Account Manager plugin
 +     *
 +     * @param array &$amcd Array that expresses the AMCD
 +     *
 +     * @return boolean hook value
 +     */
 +
 +    function onEndAccountManagementControlDocument(&$amcd)
 +    {
 +        $amcd['auth-methods']['openid'] = array(
 +            'connect' => array(
 +                'method' => 'POST',
 +                'path' => common_local_url('openidlogin'),
 +                'params' => array(
 +                    'identity' => 'openid_url'
 +                )
 +            )
 +        );
 +    }
 +
      /**
       * Add our version information to output
       *
index b7a0e92c7f4500a38bcab5c6d6c177e9320ddd8b,0c46a33e0b0cea702326c436fce4283bf8525cad..08557cbd8481b8cb77c77cf6936c2ec3b0828c36
@@@ -2,7 -2,7 +2,7 @@@
  /**
   * StatusNet, the distributed open-source microblogging tool
   *
-  * Plugin to show reCaptcha when a user registers 
+  * Plugin to show reCaptcha when a user registers
   *
   * PHP version 5
   *
@@@ -41,8 -41,7 +41,8 @@@ class RecaptchaPlugin extends Plugi
      var $failed;
      var $ssl;
  
 -    function onInitializePlugin(){
 +    function onInitializePlugin()
 +    {
          if(!isset($this->private_key)) {
              common_log(LOG_ERR, 'Recaptcha: Must specify private_key in config.php');
          }
@@@ -51,8 -50,7 +51,8 @@@
          }
      }
  
 -    function checkssl(){
 +    function checkssl()
 +    {
          if(common_config('site', 'ssl') === 'sometimes' || common_config('site', 'ssl') === 'always') {
              return true;
          }
      function onEndRegistrationFormData($action)
      {
          $action->elementStart('li');
-         $action->raw('<label for="recaptcha">Captcha</label>');
+         $action->raw('<label for="recaptcha">'._m('Captcha').'</label>');
  
          // AJAX API will fill this div out.
          // We're calling that instead of the regular one so we stay compatible
          // with application/xml+xhtml output as for mobile.
          $action->element('div', array('id' => 'recaptcha'));
          $action->elementEnd('li');
-         
          $action->recaptchaPluginNeedsOutput = true;
          return true;
      }
@@@ -85,7 -83,7 +85,7 @@@
                  $url = "http://api.recaptcha.net/js/recaptcha_ajax.js";
              }
              $action->script($url);
-             
              // And when we're ready, fill out the captcha!
              $key = json_encode($this->public_key);
              $action->inlinescript("\$(function(){Recaptcha.create($key, 'recaptcha');});");
  
          if (!$resp->is_valid) {
              if($this->display_errors) {
-                 $action->showForm ("(reCAPTCHA error: " . $resp->error . ")");
+                 $action->showForm(sprintf(_("(reCAPTCHA error: %s)", $resp->error)));
              }
              $action->showForm(_m("Captcha does not match!"));
              return false;
                                 'captcha to the registration page.'));
          return true;
      }
 -}
 +}