Merge "API: Handle "special" options in action=options"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 4 Feb 2014 18:43:40 +0000 (18:43 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 4 Feb 2014 18:43:40 +0000 (18:43 +0000)
1  2 
includes/Preferences.php
includes/User.php
includes/api/ApiOptions.php
tests/phpunit/includes/api/ApiOptionsTest.php

diff --combined includes/Preferences.php
@@@ -62,6 -62,13 +62,13 @@@ class Preferences 
                'emailaddress',
        );
  
+       /**
+        * @return array
+        */
+       static function getSaveBlacklist() {
+               return self::$saveBlacklist;
+       }
        /**
         * @throws MWException
         * @param $user User
                }
  
                // show a preview of the old signature first
 -              $oldsigWikiText = $wgParser->preSaveTransform( "~~~", $context->getTitle(), $user, ParserOptions::newFromContext( $context ) );
 +              $oldsigWikiText = $wgParser->preSaveTransform(
 +                      '~~~',
 +                      $context->getTitle(),
 +                      $user,
 +                      ParserOptions::newFromContext( $context )
 +              );
                $oldsigHTML = $context->getOutput()->parseInline( $oldsigWikiText, true, true );
                $defaultPreferences['oldsig'] = array(
                        'type' => 'info',
                $defaultPreferences['fancysig'] = array(
                        'type' => 'toggle',
                        'label-message' => 'tog-fancysig',
 -                      'help-message' => 'prefs-help-signature', // show general help about signature at the bottom of the section
 +                      // show general help about signature at the bottom of the section
 +                      'help-message' => 'prefs-help-signature',
                        'section' => 'personal/signature'
                );
  
                        $minDiff = $tz[1];
                        $tzSetting = sprintf( '%+03d:%02d', floor( $minDiff / 60 ), abs( $minDiff ) % 60 );
                } elseif ( count( $tz ) > 1 && $tz[0] == 'ZoneInfo' &&
 -                      !in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) ) )
 -              {
 +                      !in_array( $tzOffset, HTMLFormField::flattenOptions( $tzOptions ) )
 +              {
                        # Timezone offset can vary with DST
                        $userTZ = timezone_open( $tz[2] );
                        if ( $userTZ !== false ) {
                }
  
                $defaultPreferences['stubthreshold'] = array(
 -                      'type' => 'selectorother',
 +                      'type' => 'select',
                        'section' => 'rendering/advancedrendering',
                        'options' => $stubThresholdOptions,
                        'size' => 20,
                        'label-raw' => $context->msg( 'stub-threshold' )->text(), // Raw HTML message. Yay?
                );
  
 -              if ( $wgAllowUserCssPrefs ) {
 -                      $defaultPreferences['showtoc'] = array(
 -                              'type' => 'toggle',
 -                              'section' => 'rendering/advancedrendering',
 -                              'label-message' => 'tog-showtoc',
 -                      );
 -              }
 -              $defaultPreferences['nocache'] = array(
 -                      'type' => 'toggle',
 -                      'label-message' => 'tog-nocache',
 -                      'section' => 'rendering/advancedrendering',
 -              );
                $defaultPreferences['showhiddencats'] = array(
                        'type' => 'toggle',
                        'section' => 'rendering/advancedrendering',
                        'label-message' => 'tog-showhiddencats'
                );
  
 -              if ( $wgAllowUserCssPrefs ) {
 -                      $defaultPreferences['justify'] = array(
 -                              'type' => 'toggle',
 -                              'section' => 'rendering/advancedrendering',
 -                              'label-message' => 'tog-justify',
 -                      );
 -              }
 -
                $defaultPreferences['numberheadings'] = array(
                        'type' => 'toggle',
                        'section' => 'rendering/advancedrendering',
                global $wgAllowUserCssPrefs;
  
                ## Editing #####################################
 -              if ( $wgAllowUserCssPrefs ) {
 -                      $defaultPreferences['editsection'] = array(
 -                              'type' => 'toggle',
 -                              'section' => 'editing/advancedediting',
 -                              'label-message' => 'tog-editsection',
 -                      );
 -              }
                $defaultPreferences['editsectiononrightclick'] = array(
                        'type' => 'toggle',
                        'section' => 'editing/advancedediting',
        static function searchPreferences( $user, IContextSource $context, &$defaultPreferences ) {
                global $wgContLang, $wgVectorUseSimpleSearch;
  
 -              ## Search #####################################
 -              $defaultPreferences['searchlimit'] = array(
 -                      'type' => 'int',
 -                      'label-message' => 'resultsperpage',
 -                      'section' => 'searchoptions/displaysearchoptions',
 -                      'min' => 0,
 -              );
 -
                if ( $wgVectorUseSimpleSearch ) {
                        $defaultPreferences['vector-simplesearch'] = array(
                                'type' => 'toggle',
                        );
                }
  
 -              $defaultPreferences['disablesuggest'] = array(
 -                      'type' => 'toggle',
 -                      'label-message' => 'mwsuggest-disable',
 -                      'section' => 'searchoptions/displaysearchoptions',
 -              );
 -
                $defaultPreferences['searcheverything'] = array(
                        'type' => 'toggle',
                        'label-message' => 'searcheverything-enable',
                                $linkTools[] = Linker::link( $jsPage, $context->msg( 'prefs-custom-js' )->escaped() );
                        }
  
 -                      $display = $sn . ' ' . $context->msg( 'parentheses', $context->getLanguage()->pipeList( $linkTools ) )->text();
 +                      $display = $sn . ' ' . $context->msg(
 +                              'parentheses',
 +                              $context->getLanguage()->pipeList( $linkTools )
 +                      )->text();
                        $ret[$display] = $skinkey;
                }
  
                                $form->msg( 'badsiglength' )->numParams( $wgMaxSigChars )->text() );
                } elseif ( isset( $alldata['fancysig'] ) &&
                                $alldata['fancysig'] &&
 -                              false === $wgParser->validateSig( $signature ) ) {
 -                      return Xml::element( 'span', array( 'class' => 'error' ), $form->msg( 'badsig' )->text() );
 +                              $wgParser->validateSig( $signature ) === false
 +              ) {
 +                      return Xml::element(
 +                              'span',
 +                              array( 'class' => 'error' ),
 +                              $form->msg( 'badsig' )->text()
 +                      );
                } else {
                        return true;
                }
         * @param array $remove array of items to remove
         * @return HtmlForm
         */
 -      static function getFormObject( $user, IContextSource $context, $formClass = 'PreferencesForm', array $remove = array() ) {
 +      static function getFormObject(
 +              $user,
 +              IContextSource $context,
 +              $formClass = 'PreferencesForm',
 +              array $remove = array()
 +      ) {
                $formDescriptor = Preferences::getPreferences( $user, $context );
                if ( count( $remove ) ) {
                        $removeKeys = array_flip( $remove );
                $timestamp = MWTimestamp::getLocalInstance();
                // Check that $wgLocalTZoffset is the same as the local time zone offset
                if ( $wgLocalTZoffset == $timestamp->format( 'Z' ) / 60 ) {
 -                      $server_tz_msg = $context->msg( 'timezoneuseserverdefault', $timestamp->getTimezone()->getName() )->text();
 +                      $server_tz_msg = $context->msg(
 +                              'timezoneuseserverdefault',
 +                              $timestamp->getTimezone()->getName()
 +                      )->text();
                } else {
 -                      $tzstring = sprintf( '%+03d:%02d', floor( $wgLocalTZoffset / 60 ), abs( $wgLocalTZoffset ) % 60 );
 +                      $tzstring = sprintf(
 +                              '%+03d:%02d',
 +                              floor( $wgLocalTZoffset / 60 ),
 +                              abs( $wgLocalTZoffset ) % 60
 +                      );
                        $server_tz_msg = $context->msg( 'timezoneuseserverdefault', $tzstring )->text();
                }
                $opt[$server_tz_msg] = "System|$wgLocalTZoffset";
                        # If users have saved a value for a preference which has subsequently been disabled
                        # via $wgHiddenPrefs, we don't want to destroy that setting in case the preference
                        # is subsequently re-enabled
 -                      # TODO: maintenance script to actually delete these
                        foreach ( $wgHiddenPrefs as $pref ) {
                                # If the user has not set a non-default value here, the default will be returned
                                # and subsequently discarded
                                $user->setOption( $key, $value );
                        }
  
 +                      wfRunHooks( 'PreferencesFormPreSave', array( $formData, $form, $user, &$result ) );
                        $user->saveSettings();
                }
  
diff --combined includes/User.php
@@@ -465,7 -465,7 +465,7 @@@ class User 
         * user_name and user_real_name are not provided because the whole row
         * will be loaded once more from the database when accessing them.
         *
 -       * @param array $row A row from the user table
 +       * @param stdClass $row A row from the user table
         * @param array $data Further data to load into the object (see User::loadFromRow for valid keys)
         * @return User
         */
        /**
         * Initialize this object from a row from the user table.
         *
 -       * @param array $row Row from the user table to load.
 +       * @param stdClass $row Row from the user table to load.
         * @param array $data Further user data to load into the object
         *
         *      user_groups             Array with groups out of the user_groups table
  
                // Proxy blocking
                if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
 -                      && !in_array( $ip, $wgProxyWhitelist ) )
 -              {
 +                      && !in_array( $ip, $wgProxyWhitelist )
 +              {
                        // Local list
                        if ( self::isLocallyBlockedProxy( $ip ) ) {
                                $block = new Block;
                $blocked = $this->isBlocked( $bFromSlave );
                $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
                // If a user's name is suppressed, they cannot make edits anywhere
 -              if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
 -                $title->getNamespace() == NS_USER_TALK ) {
 +              if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName()
 +                      && $title->getNamespace() == NS_USER_TALK ) {
                        $blocked = false;
                        wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
                }
        /**
         * Set the password for a password reminder or new account email
         *
 -       * @param string $str New password to set
 +       * @param $str New password to set or null to set an invalid
 +       *  password hash meaning that the user will not be able to use it
         * @param bool $throttle If true, reset the throttle timestamp to the present
         */
        public function setNewpassword( $str, $throttle = true ) {
                $this->load();
 -              $this->mNewpassword = self::crypt( $str );
 -              if ( $throttle ) {
 -                      $this->mNewpassTime = wfTimestampNow();
 +
 +              if ( $str === null ) {
 +                      $this->mNewpassword = '';
 +                      $this->mNewpassTime = null;
 +              } else {
 +                      $this->mNewpassword = self::crypt( $str );
 +                      if ( $throttle ) {
 +                              $this->mNewpassTime = wfTimestampNow();
 +                      }
                }
        }
  
         * - 'registered-checkmatrix' - as above, using the 'checkmatrix' type.
         * - 'userjs' - preferences with names starting with 'userjs-', intended to
         *              be used by user scripts.
+        * - 'special' - "preferences" that are not accessible via User::getOptions
+        *               or User::setOptions.
         * - 'unused' - preferences about which MediaWiki doesn't know anything.
         *              These are usually legacy options, removed in newer versions.
         *
                        'registered-multiselect',
                        'registered-checkmatrix',
                        'userjs',
+                       'special',
                        'unused'
                );
        }
                $prefs = Preferences::getPreferences( $this, $context );
                $mapping = array();
  
+               // Pull out the "special" options, so they don't get converted as
+               // multiselect or checkmatrix.
+               $specialOptions = array_fill_keys( Preferences::getSaveBlacklist(), true );
+               foreach ( $specialOptions as $name => $value ) {
+                       unset( $prefs[$name] );
+               }
                // Multiselect and checkmatrix options are stored in the database with
                // one key per option, each having a boolean value. Extract those keys.
                $multiselectOptions = array();
                                $mapping[$key] = 'registered-multiselect';
                        } elseif ( isset( $checkmatrixOptions[$key] ) ) {
                                $mapping[$key] = 'registered-checkmatrix';
+                       } elseif ( isset( $specialOptions[$key] ) ) {
+                               $mapping[$key] = 'special';
                        } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
                                $mapping[$key] = 'userjs';
                        } else {
         * the next change of the page if it's watched etc.
         * @note If the user doesn't have 'editmywatchlist', this will do nothing.
         * @param $title Title of the article to look at
 +       * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
         */
 -      public function clearNotification( &$title ) {
 +      public function clearNotification( &$title, $oldid = 0 ) {
                global $wgUseEnotif, $wgShowUpdatedMarker;
  
                // Do nothing if the database is locked to writes
                        return;
                }
  
 -              if ( $title->getNamespace() == NS_USER_TALK &&
 -                      $title->getText() == $this->getName() ) {
 -                      if ( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) ) {
 +              // If we're working on user's talk page, we should update the talk page message indicator
 +              if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
 +                      if ( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this, $oldid ) ) ) {
                                return;
                        }
 -                      $this->setNewtalk( false );
 +
 +                      $nextid = $oldid ? $title->getNextRevisionID( $oldid ) : null;
 +
 +                      if ( !$oldid || !$nextid ) {
 +                              // If we're looking at the latest revision, we should definitely clear it
 +                              $this->setNewtalk( false );
 +                      } else {
 +                              // Otherwise we should update its revision, if it's present
 +                              if ( $this->getNewtalk() ) {
 +                                      // Naturally the other one won't clear by itself
 +                                      $this->setNewtalk( false );
 +                                      $this->setNewtalk( true, Revision::newFromId( $nextid ) );
 +                              }
 +                      }
                }
  
                if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
                // and when it does have to be executed, it can be on a slave
                // If this is the user's newtalk page, we always update the timestamp
                $force = '';
 -              if ( $title->getNamespace() == NS_USER_TALK &&
 -                      $title->getText() == $this->getName() )
 -              {
 +              if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
                        $force = 'force';
                }
  
 -              $this->getWatchedItem( $title )->resetNotificationTimestamp( $force );
 +              $this->getWatchedItem( $title )->resetNotificationTimestamp( $force, $oldid );
        }
  
        /**
                if ( $id != 0 ) {
                        $dbw = wfGetDB( DB_MASTER );
                        $dbw->update( 'watchlist',
 -                              array( /* SET */
 -                                      'wl_notificationtimestamp' => null
 -                              ), array( /* WHERE */
 -                                      'wl_user' => $id
 -                              ), __METHOD__
 +                              array( /* SET */ 'wl_notificationtimestamp' => null ),
 +                              array( /* WHERE */ 'wl_user' => $id ),
 +                              __METHOD__
                        );
 -              #       We also need to clear here the "you have new message" notification for the own user_talk page
 -              #       This is cleared one page view later in Article::viewUpdates();
 +                      // We also need to clear here the "you have new message" notification for the own user_talk page;
 +                      // it's cleared one page view later in WikiPage::doViewUpdates().
                }
        }
  
                return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
        }
  
 -      /**
 -       * Generate a string which will be different for any combination of
 -       * user options which would produce different parser output.
 -       * This will be used as part of the hash key for the parser cache,
 -       * so users with the same options can share the same cached data
 -       * safely.
 -       *
 -       * Extensions which require it should install 'PageRenderingHash' hook,
 -       * which will give them a chance to modify this key based on their own
 -       * settings.
 -       *
 -       * @deprecated since 1.17 use the ParserOptions object to get the relevant options
 -       * @return string Page rendering hash
 -       */
 -      public function getPageRenderingHash() {
 -              wfDeprecated( __METHOD__, '1.17' );
 -
 -              global $wgRenderHashAppend, $wgLang, $wgContLang;
 -              if ( $this->mHash ) {
 -                      return $this->mHash;
 -              }
 -
 -              // stubthreshold is only included below for completeness,
 -              // since it disables the parser cache, its value will always
 -              // be 0 when this function is called by parsercache.
 -
 -              $confstr = $this->getOption( 'math' );
 -              $confstr .= '!' . $this->getStubThreshold();
 -              $confstr .= '!' . ( $this->getOption( 'numberheadings' ) ? '1' : '' );
 -              $confstr .= '!' . $wgLang->getCode();
 -              $confstr .= '!' . $this->getOption( 'thumbsize' );
 -              // add in language specific options, if any
 -              $extra = $wgContLang->getExtraHashOptions();
 -              $confstr .= $extra;
 -
 -              // Since the skin could be overloading link(), it should be
 -              // included here but in practice, none of our skins do that.
 -
 -              $confstr .= $wgRenderHashAppend;
 -
 -              // Give a chance for extensions to modify the hash, if they have
 -              // extra options or other effects on the parser cache.
 -              wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
 -
 -              // Make it a valid memcached key fragment
 -              $confstr = str_replace( ' ', '_', $confstr );
 -              $this->mHash = $confstr;
 -              return $confstr;
 -      }
 -
        /**
         * Get whether the user is explicitly blocked from account creation.
         * @return bool|Block
                        // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
                        // Check for this with iconv
                        $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
 -                      if ( $cp1252Password != $password &&
 -                              self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) )
 -                      {
 +                      if ( $cp1252Password != $password
 +                              && self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId )
 +                      {
                                return true;
                        }
                }
         */
        public function sendMail( $subject, $body, $from = null, $replyto = null ) {
                if ( is_null( $from ) ) {
 -                      global $wgPasswordSender, $wgPasswordSenderName;
 -                      $sender = new MailAddress( $wgPasswordSender, $wgPasswordSenderName );
 +                      global $wgPasswordSender;
 +                      $sender = new MailAddress( $wgPasswordSender,
 +                              wfMessage( 'emailsender' )->inContentLanguage()->text() );
                } else {
                        $sender = new MailAddress( $from );
                }
@@@ -31,6 -31,7 +31,6 @@@
   * @ingroup API
   */
  class ApiOptions extends ApiBase {
 -
        /**
         * Changes preferences of the current user.
         */
@@@ -53,7 -54,7 +53,7 @@@
                }
  
                if ( $params['reset'] ) {
 -                      $user->resetOptions( $params['resetkinds'] );
 +                      $user->resetOptions( $params['resetkinds'], $this->getContext() );
                        $changed = true;
                }
  
@@@ -98,6 -99,9 +98,9 @@@
                                                $validation = true;
                                        }
                                        break;
+                               case 'special':
+                                       $validation = "cannot be set by this module";
+                                       break;
                                case 'unused':
                                default:
                                        $validation = "not a valid preference";
                        'token' => 'An options token previously obtained through the action=tokens',
                        'reset' => 'Resets preferences to the site defaults',
                        'resetkinds' => 'List of types of options to reset when the "reset" option is set',
 -                      'change' => 'List of changes, formatted name=value (e.g. skin=vector), value cannot contain pipe characters. If no value is given (not even an equals sign), e.g., optionname|otheroption|..., the option will be reset to its default value',
 +                      'change' => 'List of changes, formatted name=value (e.g. skin=vector), ' .
 +                              'value cannot contain pipe characters. If no value is given (not ' .
 +                              'even an equals sign), e.g., optionname|otheroption|..., the ' .
 +                              'option will be reset to its default value',
                        'optionname' => 'A name of a option which should have an optionvalue set',
 -                      'optionvalue' => 'A value of the option specified by the optionname, can contain pipe characters',
 +                      'optionvalue' => 'A value of the option specified by the optionname, ' .
 +                              'can contain pipe characters',
                );
        }
  
                return array(
                        'Change preferences of the current user',
                        'Only options which are registered in core or in one of installed extensions,',
 -                      'or as options with keys prefixed with \'userjs-\' (intended to be used by user scripts), can be set.'
 +                      'or as options with keys prefixed with \'userjs-\' (intended to be used by user',
 +                      'scripts), can be set.'
                );
        }
  
                return array(
                        'api.php?action=options&reset=&token=123ABC',
                        'api.php?action=options&change=skin=vector|hideminor=1&token=123ABC',
 -                      'api.php?action=options&reset=&change=skin=monobook&optionname=nickname&optionvalue=[[User:Beau|Beau]]%20([[User_talk:Beau|talk]])&token=123ABC',
 +                      'api.php?action=options&reset=&change=skin=monobook&optionname=nickname&' .
 +                              'optionvalue=[[User:Beau|Beau]]%20([[User_talk:Beau|talk]])&token=123ABC',
                );
        }
  }
@@@ -4,18 -4,10 +4,18 @@@
   * @group API
   * @group Database
   * @group medium
 + *
 + * @covers ApiOptions
   */
  class ApiOptionsTest extends MediaWikiLangTestCase {
  
 -      private $mTested, $mUserMock, $mContext, $mSession;
 +      /** @var PHPUnit_Framework_MockObject_MockObject */
 +      private $mUserMock;
 +      /** @var ApiOptions */
 +      private $mTested;
 +      private $mSession;
 +      /** @var DerivativeContext */
 +      private $mContext;
  
        private $mOldGetPreferencesHooks = false;
  
                return true;
        }
  
 +      /**
 +       * @param IContextSource $context
 +       * @param array|null $options
 +       *
 +       * @return array
 +       */
        public function getOptionKinds( IContextSource $context, $options = null ) {
                // Match with above.
                $kinds = array(
                        'testmultiselect-opt2' => 'registered-multiselect',
                        'testmultiselect-opt3' => 'registered-multiselect',
                        'testmultiselect-opt4' => 'registered-multiselect',
+                       'special' => 'special',
                );
  
                if ( $options === null ) {
                $this->assertEquals( self::$Success, $response );
        }
  
+       public function testSpecialOption() {
+               $this->mUserMock->expects( $this->never() )
+                       ->method( 'resetOptions' );
+               $this->mUserMock->expects( $this->never() )
+                       ->method( 'saveSettings' );
+               $request = $this->getSampleRequest( array(
+                       'change' => 'special=1'
+               ) );
+               $response = $this->executeQuery( $request );
+               $this->assertEquals( array(
+                       'options' => 'success',
+                       'warnings' => array(
+                               'options' => array(
+                                       '*' => "Validation error for 'special': cannot be set by this module"
+                               )
+                       )
+               ), $response );
+       }
        public function testUnknownOption() {
                $this->mUserMock->expects( $this->never() )
                        ->method( 'resetOptions' );