]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch 'rewrites-master/type-hints-asserts' into rewrites-nightly/type-hints... rewrites-nightly/type-hints-asserts
authorRoland Haeder <roland@mxchange.org>
Fri, 27 Mar 2015 21:24:37 +0000 (22:24 +0100)
committerRoland Haeder <roland@mxchange.org>
Fri, 27 Mar 2015 21:24:37 +0000 (22:24 +0100)
Conflicts:
classes/Notice.php
lib/docfile.php
lib/framework.php

Signed-off-by: Roland Haeder <roland@mxchange.org>
1  2 
actions/invite.php
actions/replies.php
classes/Notice.php
lib/docfile.php
lib/framework.php
plugins/Favorite/classes/Fave.php

diff --combined actions/invite.php
index c3464efb5407ccbbf8bacadbe6c158f815c5dc60,b6e0ba8dd17dffb23151548219844316c90f29d1..0deb7a76ee2d997a5dd66236fc5fc1774e31e1cc
@@@ -74,6 -74,7 +74,7 @@@ class InviteAction extends Actio
              $bestname = $profile->getBestName();
              $sitename = common_config('site', 'name');
              $personal = $this->trimmed('personal');
+             $language = $this->trimmed('language');
  
              $addresses = explode("\n", $this->trimmed('addresses'));
              foreach ($addresses as $email) {
                          $this->already[] = $other;
                      } else {
                          try {
 -                            Subscription::start($profile, $other);
 +                            Subscription::ensureStart($profile, $other);
                              $this->subbed[] = $other;
                          } catch (Exception $e) {
                              // subscription failed, but keep working
                  } catch (NoSuchUserException $e) {
                      // If email was not known, let's send an invite!
                      $this->sent[] = $email;
-                     $this->sendInvitation($email, $user, $personal);
+                     $this->sendInvitation($email, $user, $personal, $language);
                  }
              }
  
          }
      }
  
-     function sendInvitation($email, $user, $personal)
+     function sendInvitation($email, $user, $personal, $language)
      {
          $profile = $user->getProfile();
          $bestname = $profile->getBestName();
          $title = (empty($personal)) ? 'invite' : 'invitepersonal';
  
          // @todo FIXME: i18n issue.
-         $inviteTemplate = DocFile::forTitle($title, DocFile::mailPaths());
+         $inviteTemplate = DocFile::forTitle($title, DocFile::mailPaths(), $language);
  
          $body = $inviteTemplate->toHTML(array('inviter' => $bestname,
                                                'inviterurl' => $profile->profileurl,
diff --combined actions/replies.php
index 5bf2f8edc429bb4829f0966c82053fccb7576045,11d9c7c706cc29a777a326fa5b3e96ece2799e56..ce65fef7a9fbe497aef387b80c1507da0b2d067e
   * @link      http://status.net/
   */
  
 -if (!defined('STATUSNET') && !defined('LACONICA')) {
 -    exit(1);
 -}
 -
 -require_once INSTALLDIR.'/lib/personalgroupnav.php';
 -require_once INSTALLDIR.'/lib/noticelist.php';
 -require_once INSTALLDIR.'/lib/feedlist.php';
 +if (!defined('GNUSOCIAL')) { exit(1); }
  
  /**
   * List of replies
   * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
   * @link     http://status.net/
   */
 -class RepliesAction extends Action
 +class RepliesAction extends ManagedAction
  {
      var $page = null;
      var $notice;
  
 -    /**
 -     * Prepare the object
 -     *
 -     * Check the input values and initialize the object.
 -     * Shows an error page on bad input.
 -     *
 -     * @param array $args $_REQUEST data
 -     *
 -     * @return boolean success flag
 -     */
 -    function prepare(array $args=array())
 +    protected function doPreparation()
      {
 -        parent::prepare($args);
 -
          $nickname = common_canonical_nickname($this->arg('nickname'));
  
          $this->user = User::getKV('nickname', $nickname);
  
 -        if (!$this->user) {
 +        if (!$this->user instanceof User) {
              // TRANS: Client error displayed when trying to reply to a non-exsting user.
              $this->clientError(_('No such user.'));
          }
  
 -        $profile = $this->user->getProfile();
 +        $this->target = $this->user->getProfile();
  
 -        if (!$profile) {
 +        if (!$this->target instanceof Profile) {
              // TRANS: Error message displayed when referring to a user without a profile.
              $this->serverError(_('User has no profile.'));
          }
  
 -        $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
 +        $this->page = $this->int('page') ?: 1;
  
          common_set_returnto($this->selfUrl());
  
 -        $stream = new ReplyNoticeStream($this->user->id,
 -                                        Profile::current());
 +        $stream = new ReplyNoticeStream($this->target->getID(), $this->scoped);
  
          $this->notice = $stream->getNotices(($this->page-1) * NOTICES_PER_PAGE,
                                              NOTICES_PER_PAGE + 1);
  
 -        if($this->page > 1 && $this->notice->N == 0){
 +        if ($this->page > 1 && $this->notice->N == 0) {
              // TRANS: Client error when page not found (404)
              $this->clientError(_('No such page.'), 404);
          }
- <<<<<<< HEAD
--
--        return true;
--    }
--
--    /**
--     * Handle a request
--     *
--     * Just show the page. All args already handled.
--     *
--     * @param array $args $_REQUEST data
--     *
--     * @return void
--     */
--    function handle(array $args=array())
--    {
--        parent::handle($args);
--        $this->showPage();
- =======
- >>>>>>> 1442ca16b410d327d7ec6269944144dfa075ff17
      }
  
      /**
          if ($this->page == 1) {
              // TRANS: Title for first page of replies for a user.
              // TRANS: %s is a user nickname.
 -            return sprintf(_("Replies to %s"), $this->user->nickname);
 +            return sprintf(_("Replies to %s"), $this->target->getNickname());
          } else {
              // TRANS: Title for all but the first page of replies for a user.
              // TRANS: %1$s is a user nickname, %2$d is a page number.
              return sprintf(_('Replies to %1$s, page %2$d'),
 -                           $this->user->nickname,
 +                           $this->target->getNickname(),
                             $this->page);
          }
      }
          return array(new Feed(Feed::JSON,
                                common_local_url('ApiTimelineMentions',
                                                 array(
 -                                                    'id' => $this->user->nickname,
 +                                                    'id' => $this->target->getNickname(),
                                                      'format' => 'as')),
                                // TRANS: Link for feed with replies for a user.
                                // TRANS: %s is a user nickname.
                                        $this->user->nickname)),
                       new Feed(Feed::RSS1,
                                common_local_url('repliesrss',
 -                                               array('nickname' => $this->user->nickname)),
 +                                               array('nickname' => $this->target->getNickname())),
                                // TRANS: Link for feed with replies for a user.
                                // TRANS: %s is a user nickname.
                                sprintf(_('Replies feed for %s (RSS 1.0)'),
 -                                      $this->user->nickname)),
 +                                      $this->target->getNickname())),
                       new Feed(Feed::RSS2,
                                common_local_url('ApiTimelineMentions',
                                                 array(
 -                                                    'id' => $this->user->nickname,
 +                                                    'id' => $this->target->getNickname(),
                                                      'format' => 'rss')),
                                // TRANS: Link for feed with replies for a user.
                                // TRANS: %s is a user nickname.
                                sprintf(_('Replies feed for %s (RSS 2.0)'),
 -                                      $this->user->nickname)),
 +                                      $this->target->getNickname())),
                       new Feed(Feed::ATOM,
                                common_local_url('ApiTimelineMentions',
                                                 array(
 -                                                    'id' => $this->user->nickname,
 +                                                    'id' => $this->target->getNickname(),
                                                      'format' => 'atom')),
                                // TRANS: Link for feed with replies for a user.
                                // TRANS: %s is a user nickname.
                                sprintf(_('Replies feed for %s (Atom)'),
 -                                    $this->user->nickname)));
 +                                    $this->target->getNickname())));
      }
  
 -    /**
 -     * Show the content
 -     *
 -     * A list of notices that are replies to the user, plus pagination.
 -     *
 -     * @return void
 -     */
      function showContent()
      {
          $nl = new PrimaryNoticeList($this->notice, $this, array('show_n'=>NOTICES_PER_PAGE));
  
          $this->pagination($this->page > 1, $cnt > NOTICES_PER_PAGE,
                            $this->page, 'replies',
 -                          array('nickname' => $this->user->nickname));
 +                          array('nickname' => $this->target->getNickname()));
      }
  
      function showEmptyListMessage()
      {
          // TRANS: Empty list message for page with replies for a user.
 -        // TRANS: %1$s and %s$s are the user nickname.
 -        $message = sprintf(_('This is the timeline showing replies to %1$s but %2$s hasn\'t received a notice to them yet.'),
 -                           $this->user->nickname,
 -                           $this->user->nickname) . ' ';
 +        // TRANS: %1$s is the user nickname.
 +        $message = sprintf(_('This is the timeline showing replies to %1$s but no notices have arrived yet.'), $this->target->getNickname());
 +        $message .= ' ';    // Spacing between this sentence and the next.
  
          if (common_logged_in()) {
 -            $current_user = common_current_user();
 -            if ($this->user->id === $current_user->id) {
 +            if ($this->target->getID() === $this->scoped->getID()) {
                  // TRANS: Empty list message for page with replies for a user for the logged in user.
                  // TRANS: This message contains a Markdown link in the form [link text](link).
                  $message .= _('You can engage other users in a conversation, subscribe to more people or [join groups](%%action.groups%%).');
              } else {
                  // TRANS: Empty list message for page with replies for a user for all logged in users but the user themselves.
 -                // TRANS: %1$s, %2$s and %3$s are a user nickname. This message contains a Markdown link in the form [link text](link).
 -                $message .= sprintf(_('You can try to [nudge %1$s](../%2$s) or [post something to them](%%%%action.newnotice%%%%?status_textarea=%3$s).'), $this->user->nickname, $this->user->nickname, '@' . $this->user->nickname);
 +                // TRANS: %1$s is a user nickname and %2$s is the same but with a prepended '@' character. This message contains a Markdown link in the form [link text](link).
 +                $message .= sprintf(_('You can try to [nudge %1$s](../%1$s) or [post something to them](%%%%action.newnotice%%%%?content=%2$s).'), $this->target->getNickname(), '@' . $this->target->getNickname());
              }
 -        }
 -        else {
 +        } else {
              // TRANS: Empty list message for page with replies for a user for not logged in users.
              // TRANS: %1$s is a user nickname. This message contains a Markdown link in the form [link text](link).
 -            $message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to them.'), $this->user->nickname);
 +            $message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to them.'), $this->target->getNickname());
          }
  
          $this->elementStart('div', 'guide');
          $this->elementEnd('div');
      }
  
 +<<<<<<< HEAD
      function isReadOnly(array $args=array())
 +=======
 +    public function isReadOnly($args)
 +>>>>>>> 1442ca16b410d327d7ec6269944144dfa075ff17
      {
          return true;
      }
diff --combined classes/Notice.php
index 65190da8906e5da3169d4f3645ccc76fa8d230fa,02b8ce3549f3b79ca4fb6715e6c762a997f87f0e..5c18f03500f1de2625ad9b57ad67a484d1faf3b3
@@@ -55,10 -55,10 +55,10 @@@ class Notice extends Managed_DataObjec
      public $__table = 'notice';                          // table name
      public $id;                              // int(4)  primary_key not_null
      public $profile_id;                      // int(4)  multiple_key not_null
 -    public $uri;                             // varchar(255)  unique_key
 +    public $uri;                             // varchar(191)  unique_key   not 255 because utf8mb4 takes more space
      public $content;                         // text
      public $rendered;                        // text
 -    public $url;                             // varchar(255)
 +    public $url;                             // varchar(191)   not 255 because utf8mb4 takes more space
      public $created;                         // datetime  multiple_key not_null default_0000-00-00%2000%3A00%3A00
      public $modified;                        // timestamp   not_null default_CURRENT_TIMESTAMP
      public $reply_to;                        // int(4)
@@@ -70,8 -70,8 +70,8 @@@
      public $location_id;                     // int(4)
      public $location_ns;                     // int(4)
      public $repeat_of;                       // int(4)
 -    public $verb;                            // varchar(255)
 -    public $object_type;                     // varchar(255)
 +    public $verb;                            // varchar(191)   not 255 because utf8mb4 takes more space
 +    public $object_type;                     // varchar(191)   not 255 because utf8mb4 takes more space
      public $scope;                           // int(4)
  
      /* the code above is auto generated do not remove the tag below */
              'fields' => array(
                  'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
                  'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'who made the update'),
 -                'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'),
 +                'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'),
                  'content' => array('type' => 'text', 'description' => 'update content', 'collate' => 'utf8_general_ci'),
                  'rendered' => array('type' => 'text', 'description' => 'HTML version of the content'),
 -                'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'),
 +                'url' => array('type' => 'varchar', 'length' => 191, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'),
                  'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
                  'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
                  'reply_to' => array('type' => 'int', 'description' => 'notice replied to (usually a guess)'),
@@@ -98,8 -98,8 +98,8 @@@
                  'location_id' => array('type' => 'int', 'description' => 'location id if possible'),
                  'location_ns' => array('type' => 'int', 'description' => 'namespace for location'),
                  'repeat_of' => array('type' => 'int', 'description' => 'notice this is a repeat of'),
 -                'object_type' => array('type' => 'varchar', 'length' => 255, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'),
 -                'verb' => array('type' => 'varchar', 'length' => 255, 'description' => 'URI representing activity streams verb', 'default' => 'http://activitystrea.ms/schema/1.0/post'),
 +                'object_type' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'),
 +                'verb' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams verb', 'default' => 'http://activitystrea.ms/schema/1.0/post'),
                  'scope' => array('type' => 'int',
                                   'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = followers; null = default'),
              ),
  
          return $def;
      }
 -      
 +
      /* Notice types */
      const LOCAL_PUBLIC    =  1;
      const REMOTE          =  0;
      const FOLLOWER_SCOPE  = 8;
  
      protected $_profile = array();
 -    
 +
      /**
       * Will always return a profile, if anything fails it will
       * (through _setProfile) throw a NoProfileException.
          }
          return $this->_profile[$this->profile_id];
      }
 -    
 +
      public function _setProfile(Profile $profile=null)
      {
          if (!$profile instanceof Profile) {
          }
          return $title;
      }
 -    
 +
      public function getContent()
      {
          return $this->content;
          return $notice;
      }
  
 +    public static function getById($id)
 +    {
 +        $notice = new Notice();
 +        $notice->id = $id;
 +        if (!$notice->find(true)) {
 +            throw new NoResultException($notice);
 +        }
 +        return $notice;
 +    }
 +
      /**
       * Extract #hashtags from this notice's content and save them to the database.
       */
                  throw new ClientException(_('You cannot repeat your own notice.'));
              }
  
-             if ($repeat->scope != Notice::SITE_SCOPE &&
-                 $repeat->scope != Notice::PUBLIC_SCOPE) {
+             if ($repeat->isPrivateScope()) {
                  // TRANS: Client error displayed when trying to repeat a non-public notice.
                  throw new ClientException(_('Cannot repeat a private notice.'), 403);
              }
                  $notice->insert();  // throws exception on failure
                  // If it's not part of a conversation, it's
                  // the beginning of a new conversation.
 -                if (empty($notice->conversation)) { 
 +                if (empty($notice->conversation)) {
                      $orig = clone($notice);
                      // $act->context->conversation will be null if it was not provided
                      $conv = Conversation::create($notice, $options['conversation']);
                            'distribute' => true);
  
          // options will have default values when nothing has been supplied
 -        $options = array_merge($defaults, $options); 
 +        $options = array_merge($defaults, $options);
          foreach (array_keys($defaults) as $key) {
              // Only convert the keynames we specify ourselves from 'defaults' array into variables
              $$key = $options[$key];
              // Prepare inbox delivery, may be queued to background.
              $stored->distribute();
          }
 -        
 +
          return $stored;
      }
  
          }
  
          $args = func_get_args();
 -
          $format = array_shift($args);
 -
          $keyPart = vsprintf($format, $args);
 -
          $cacheKey = Cache::key($keyPart);
 -        
          $c->delete($cacheKey);
  
          // delete the "last" stream, too, if this notice is
      }
  
        protected $_attachments = array();
 -      
 +
      function attachments() {
                if (isset($this->_attachments[$this->id])) {
              return $this->_attachments[$this->id];
          }
 -              
 +
          $f2ps = File_to_post::listGet('post_id', array($this->id));
 -              
                $ids = array();
 -              
                foreach ($f2ps[$this->id] as $f2p) {
 -            $ids[] = $f2p->file_id;    
 +            $ids[] = $f2p->file_id;
          }
 -              
 -              $files = File::multiGet('id', $ids);
  
 +              $files = File::multiGet('id', $ids);
                $this->_attachments[$this->id] = $files->fetchAll();
 -              
          return $this->_attachments[$this->id];
      }
  
              $root->free();
              return $root;
          }
 -        
 +
          if (is_null($profile)) {
              $keypart = sprintf('notice:conversation_root:%d:null', $this->id);
          } else {
                                 $this->id,
                                 $profile->id);
          }
 -            
 +
          $root = self::cacheGet($keypart);
  
          if ($root !== false && $root->inScope($profile)) {
      function getReplyProfiles()
      {
          $ids = $this->getReplies();
 -        
 +
          $profiles = Profile::multiGet('id', $ids);
 -        
 +
          return $profiles->fetchAll();
      }
  
       *
       * @return array of Group objects
       */
 -    
 +
      protected $_groups = array();
 -    
 +
      function getGroups()
      {
          // Don't save groups for repeats
          if (!empty($this->repeat_of)) {
              return array();
          }
 -        
 +
          if (isset($this->_groups[$this->id])) {
              return $this->_groups[$this->id];
          }
 -        
 +
          $gis = Group_inbox::listGet('notice_id', array($this->id));
  
          $ids = array();
  
 -              foreach ($gis[$this->id] as $gi)
 -              {
 +              foreach ($gis[$this->id] as $gi) {
                    $ids[] = $gi->group_id;
                }
 -              
 +
                $groups = User_group::multiGet('id', $ids);
                $this->_setGroups($groups->fetchAll());
                return $this->_groups[$this->id];
              // Unfortunately this is likely to lose tags or URLs
              // at the end of long notices.
              $content = mb_substr($content, 0, $maxlen - 4) . ' ...';
 -        }     
 +        }
  
  
          // Scope is same as this one's
       */
      public function getTags()
      {
+         // Check default scope (non-private notices)
+         $inScope = (!$this->isPrivateScope());
+         // Get current user
+         $user = common_current_user();
+         // Is the general scope check okay and the user in logged in?
+         /* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . ']: inScope=' . intval($inScope) . ',user[]=' . gettype($user));
+         if (($inScope === TRUE) && ($user instanceof User)) {
+             // Get profile from it
+             $profile = $user->getProfile();
+             /* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . ']: inScope=' . intval($inScope) . ',profile[]=' . gettype($profile));
+             /*
+              * Check scope, else a privacy leaks happens this way:
+              *
+              * 1) Bob and Alice follow each other and write private notices
+              *    (this->scope=2) to each other.
+              * 2) Bob uses tags in his private notice to alice (which she can
+              *    read from him).
+              * 3) Alice adds that notice (with tags) to her favorites
+              *    ("faving") it.
+              * 4) The tags from Bob's private notice becomes visible in Alice's
+              *    profile.
+              *
+              * This has the simple background that the scope is not being
+              * re-checked. This has to be done here at this point because given
+              * above scenario is a privacy leak as the tags may be *really*
+              * private (nobody else shall see them) such as initmate words or
+              * very political words.
+              */
+             $inScope = $this->inScope($profile);
+             /* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . ']: inScope=' . intval($inScope) . ' - After inScope() has been called.');
+         }
          $tags = array();
  
          $keypart = sprintf('notice:tags:%d', $this->id);
          } else {
              $tag = new Notice_tag();
              $tag->notice_id = $this->id;
-             if ($tag->find()) {
+             // Check scope for privacy-leak protection (see some lines above why)
+             if (($inScope === TRUE) && ($tag->find())) {
                  while ($tag->fetch()) {
                      $tags[] = $tag->tag;
                  }
              $scope = self::defaultScope();
          }
  
 -        // If there's no scope, anyone (even anon) is in scope.
 -
 -        if ($scope == 0) { // Not private
 -
 +        if ($scope == 0 && !$this->getProfile()->isPrivateStream()) { // Not scoping, so it is public.
              return !$this->isHiddenSpam($profile);
 +        }
  
 -        } else { // Private, somehow
 -
 -            // If there's scope, anon cannot be in scope
 +        // If there's scope, anon cannot be in scope
 +        if (empty($profile)) {
 +            return false;
 +        }
  
 -            if (empty($profile)) {
 -                return false;
 -            }
 +        // Author is always in scope
 +        if ($this->profile_id == $profile->id) {
 +            return true;
 +        }
  
 -            // Author is always in scope
 +        // Only for users on this site
 +        if (($scope & Notice::SITE_SCOPE) && !$profile->isLocal()) {
 +            return false;
 +        }
  
 -            if ($this->profile_id == $profile->id) {
 -                return true;
 -            }
 +        // Only for users mentioned in the notice
 +        if ($scope & Notice::ADDRESSEE_SCOPE) {
  
 -            // Only for users on this site
 +            $reply = Reply::pkeyGet(array('notice_id' => $this->id,
 +                                         'profile_id' => $profile->id));
  
 -            if (($scope & Notice::SITE_SCOPE) && !$profile->isLocal()) {
 +            if (!$reply instanceof Reply) {
                  return false;
              }
 +        }
  
 -            // Only for users mentioned in the notice
 -
 -            if ($scope & Notice::ADDRESSEE_SCOPE) {
 -
 -                $reply = Reply::pkeyGet(array('notice_id' => $this->id,
 -                                             'profile_id' => $profile->id));
 -
 -                if (!$reply instanceof Reply) {
 -                    return false;
 -                }
 -            }
 -
 -            // Only for members of the given group
 -
 -            if ($scope & Notice::GROUP_SCOPE) {
 +        // Only for members of the given group
 +        if ($scope & Notice::GROUP_SCOPE) {
  
 -                // XXX: just query for the single membership
 +            // XXX: just query for the single membership
  
 -                $groups = $this->getGroups();
 -
 -                $foundOne = false;
 +            $groups = $this->getGroups();
  
 -                foreach ($groups as $group) {
 -                    if ($profile->isMember($group)) {
 -                        $foundOne = true;
 -                        break;
 -                    }
 -                }
 +            $foundOne = false;
  
 -                if (!$foundOne) {
 -                    return false;
 +            foreach ($groups as $group) {
 +                if ($profile->isMember($group)) {
 +                    $foundOne = true;
 +                    break;
                  }
              }
  
 -            // Only for followers of the author
 -
 -            $author = null;
 +            if (!$foundOne) {
 +                return false;
 +            }
 +        }
  
 -            if ($scope & Notice::FOLLOWER_SCOPE) {
 +        if ($scope & Notice::FOLLOWER_SCOPE || $this->getProfile()->isPrivateStream()) {
  
 -                try {
 -                    $author = $this->getProfile();
 -                } catch (Exception $e) {
 -                    return false;
 -                }
 -        
 -                if (!Subscription::exists($profile, $author)) {
 -                    return false;
 -                }
 +            if (!Subscription::exists($profile, $this->getProfile())) {
 +                return false;
              }
 -
 -            return !$this->isHiddenSpam($profile);
          }
 +
 +        return !$this->isHiddenSpam($profile);
      }
  
      function isHiddenSpam(Profile $profile=null) {
          $skip = array('_profile', '_groups', '_attachments', '_faves', '_replies', '_repeats');
          return array_diff($vars, $skip);
      }
 -    
 +
      static function defaultScope()
      {
        $scope = common_config('notice', 'defaultscope');
        static function fillProfiles(array $notices)
        {
                $map = self::getProfiles($notices);
 -              
                foreach ($notices as $entry=>$notice) {
              try {
                        if (array_key_exists($notice->profile_id, $map)) {
                  unset($notices[$entry]);
              }
                }
 -              
 +
                return array_values($map);
        }
  
                foreach ($notices as $notice) {
                        $ids[] = $notice->profile_id;
                }
 -              
                $ids = array_unique($ids);
 -              
 -              return Profile::pivotGet('id', $ids); 
 +              return Profile::pivotGet('id', $ids);
        }
  
        static function fillGroups(array &$notices)
        {
          $ids = self::_idsOf($notices);
 -              
          $gis = Group_inbox::listGet('notice_id', $ids);
 -              
          $gids = array();
  
 -              foreach ($gis as $id => $gi)
 -              {
 +              foreach ($gis as $id => $gi) {
                    foreach ($gi as $g)
                    {
                        $gids[] = $g->group_id;
                    }
                }
 -              
 +
                $gids = array_unique($gids);
 -              
                $group = User_group::pivotGet('id', $gids);
 -              
                foreach ($notices as $notice)
                {
                        $grps = array();
      static function fillAttachments(array &$notices)
      {
          $ids = self::_idsOf($notices);
 -
          $f2pMap = File_to_post::listGet('post_id', $ids);
 -              
                $fileIds = array();
 -              
                foreach ($f2pMap as $noticeId => $f2ps) {
              foreach ($f2ps as $f2p) {
 -                $fileIds[] = $f2p->file_id;    
 +                $fileIds[] = $f2p->file_id;
              }
          }
  
          $fileIds = array_unique($fileIds);
 -
                $fileMap = File::pivotGet('id', $fileIds);
 -
                foreach ($notices as $notice)
                {
                        $files = array();
              $notice->_setRepeats($repeats);
          }
      }
+     /**
+      * Checks whether this notice is in "private scope" (non-public notice)
+      *
+      * @return $isPrivate Whether this notice is private
+      */
+     public function isPrivateScope ()
+     {
+         return ($this->scope != Notice::SITE_SCOPE &&
+                 $this->scope != Notice::PUBLIC_SCOPE);
+     }
  }
diff --combined lib/docfile.php
index 316aebb8726304430f411ff8489174d3f9eab660,2e2afbd014de511f6c3acb75fa04eee8e56e9006..583f7ebc9b3190353763ca2439f9d42f1815511c
@@@ -55,12 -55,8 +55,8 @@@ class DocFil
          $this->filename = $filename;
      }
  
-     static function forTitle($title, $paths)
+     static function forTitle($title, array $paths, $language=null)
      {
-         if (!is_array($paths)) {
-             $paths = array($paths);
-         }
          $filename = null;
  
          if (Event::handle('StartDocFileForTitle', array($title, &$paths, &$filename))) {
@@@ -80,7 -76,7 +76,7 @@@
                  }
  
                  if (!empty($lang) || !empty($def)) {
-                     $filename = self::negotiateLanguage($lang, $def);
+                     $filename = self::negotiateLanguage($lang, $def, $language);
                      break;
                  }
              }
          }
      }
  
-     function toHTML(array $args=null)
+     function toHTML(array $args=array())
      {
-         if (is_null($args)) {
-             $args = array();
-         }
          if (empty($this->contents)) {
              $this->contents = file_get_contents($this->filename);
          }
          $paths = array(INSTALLDIR.'/local/doc-src/',
                         INSTALLDIR.'/doc-src/');
  
 -        $site = StatusNet::currentSite();
 +        $site = GNUsocial::currentSite();
-         
          if (!empty($site)) {
              array_unshift($paths, INSTALLDIR.'/local/doc-src/'.$site.'/');
          }
          $paths = array(INSTALLDIR.'/local/mail-src/',
                         INSTALLDIR.'/mail-src/');
  
 -        $site = StatusNet::currentSite();
 +        $site = GNUsocial::currentSite();
-         
          if (!empty($site)) {
              array_unshift($paths, INSTALLDIR.'/local/mail-src/'.$site.'/');
          }
          return $paths;
      }
  
-     static function negotiateLanguage($filenames, $defaultFilename=null)
+     private static function negotiateLanguage(array $filenames, $defaultFilename=null, $language = null)
      {
-         // XXX: do this better
+         // Default is current language
          $langcode = common_language();
  
+         // Is a language set?
+         if (!empty($language)) {
+             // And is it valid?
+             if (common_valid_language($language)) {
+                 // Use this as language (e.g. from form)
+                 $langcode = strval($language);
+             }
+         }
          foreach ($filenames as $filename) {
              if (preg_match('/\.'.$langcode.'$/', $filename)) {
                  return $filename;
diff --combined lib/framework.php
index 6fe6028f6e229699a1cfbf1152b51a1cf3ccf1ac,b7fd44512c58fa850c5a42f14f9da9aa83c7cf30..c534bebd04d4d90928b184a950aea8ee2bc7487d
@@@ -22,12 -22,12 +22,12 @@@ if (!defined('GNUSOCIAL')) { exit(1); 
  define('GNUSOCIAL_ENGINE', 'GNU social');
  define('GNUSOCIAL_ENGINE_URL', 'https://www.gnu.org/software/social/');
  
 -define('GNUSOCIAL_BASE_VERSION', '1.1.3');
 -define('GNUSOCIAL_LIFECYCLE', 'release'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
 +define('GNUSOCIAL_BASE_VERSION', '1.2.0');
 +define('GNUSOCIAL_LIFECYCLE', 'dev'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
  
  define('GNUSOCIAL_VERSION', GNUSOCIAL_BASE_VERSION . '-' . GNUSOCIAL_LIFECYCLE);
  
- define('GNUSOCIAL_CODENAME', 'Not decided yet');
+ define('GNUSOCIAL_CODENAME', 'Only a fixed bug is a good bug.');
  
  define('AVATAR_PROFILE_SIZE', 96);
  define('AVATAR_STREAM_SIZE', 48);
@@@ -97,12 -97,12 +97,12 @@@ require_once(INSTALLDIR.'/lib/plugin.ph
  
  function addPlugin($name, array $attrs=array())
  {
 -    return StatusNet::addPlugin($name, $attrs);
 +    return GNUsocial::addPlugin($name, $attrs);
  }
  
  function _have_config()
  {
 -    return StatusNet::haveConfig();
 +    return GNUsocial::haveConfig();
  }
  
  function common_get_temp_dir()
index 48ed3ba93bcb3d4a8accae430e4c054e2a0ef9f2,f6c64dac0e9529f69d8111f7599d597a130bb39e..4869d02d8595eab2a529b53b10b4cccdb364185d
@@@ -8,7 -8,7 +8,7 @@@ class Fave extends Managed_DataObjec
      public $__table = 'fave';                            // table name
      public $notice_id;                       // int(4)  primary_key not_null
      public $user_id;                         // int(4)  primary_key not_null
 -    public $uri;                             // varchar(255)
 +    public $uri;                             // varchar(191)   not 255 because utf8mb4 takes more space   not 255 because utf8mb4 takes more space
      public $created;                         // datetime  multiple_key not_null
      public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
  
@@@ -18,7 -18,7 +18,7 @@@
              'fields' => array(
                  'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'notice that is the favorite'),
                  'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user who likes this notice'),
 -                'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'),
 +                'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'),
                  'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
                  'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
              ),
       * @throws Exception on failure
       */
      static function addNew(Profile $actor, Notice $target) {
 +        if (self::existsForProfile($target, $actor)) {
 +            // TRANS: Client error displayed when trying to mark a notice as favorite that already is a favorite.
 +            throw new AlreadyFulfilledException(_('You have already favorited this!'));
 +        }
 +
          $act = new Activity();
          $act->type    = ActivityObject::ACTIVITY;
          $act->verb    = ActivityVerb::FAVORITE;
          return $stored;
      }
  
 +    public function removeEntry(Profile $actor, Notice $target)
 +    {
 +        $fave            = new Fave();
 +        $fave->user_id   = $actor->getID();
 +        $fave->notice_id = $target->getID();
 +        if (!$fave->find(true)) {
 +            // TRANS: Client error displayed when trying to remove a 'favor' when there is none in the first place.
 +            throw new AlreadyFulfilledException(_('This is already not favorited.'));
 +        }
 +
 +        $result = $fave->delete();
 +        if ($result === false) {
 +            common_log_db_error($fave, 'DELETE', __FILE__);
 +            // TRANS: Server error displayed when removing a favorite from the database fails.
 +            throw new ServerException(_('Could not delete favorite.'));
 +        }
 +
 +        Fave::blowCacheForProfileId($actor->getID());
 +        Fave::blowCacheForNoticeId($target->getID());
 +    }
 +
      // exception throwing takeover!
      public function insert()
      {
       *
       * @return array Array of Fave objects
       */
-     static public function byNotice($notice)
+     static public function byNotice(Notice $notice)
      {
          if (!isset(self::$_faves[$notice->id])) {
              self::fillFaves(array($notice->id));