Merge "Fix for Bug 63980 - Comparison of limits in pingLimiter is incorrect"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sat, 4 Oct 2014 20:35:30 +0000 (20:35 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sat, 4 Oct 2014 20:35:30 +0000 (20:35 +0000)
1  2 
includes/User.php

diff --combined includes/User.php
   * @file
   */
  
 -/**
 - * Int Number of characters in user_token field.
 - * @ingroup Constants
 - */
 -define( 'USER_TOKEN_LENGTH', 32 );
 -
 -/**
 - * Int Serialized record version.
 - * @ingroup Constants
 - */
 -define( 'MW_USER_VERSION', 9 );
 -
  /**
   * String Some punctuation to prevent editing from broken text-mangling proxies.
   * @ingroup Constants
   */
  define( 'EDIT_TOKEN_SUFFIX', '+\\' );
  
 -/**
 - * Thrown by User::setPassword() on error.
 - * @ingroup Exception
 - */
 -class PasswordError extends MWException {
 -      // NOP
 -}
 -
  /**
   * The User object encapsulates all of the user-specific settings (user_id,
   * name, rights, password, email address, options, last login time). Client
   * for rendering normal pages are set in the cookie to minimize use
   * of the database.
   */
 -class User {
 +class User implements IDBAccessObject {
        /**
 -       * Global constants made accessible as class constants so that autoloader
 +       * @const int Number of characters in user_token field.
 +       */
 +      const TOKEN_LENGTH = 32;
 +
 +      /**
 +       * Global constant made accessible as class constants so that autoloader
         * magic can be used.
         */
 -      const USER_TOKEN_LENGTH = USER_TOKEN_LENGTH;
 -      const MW_USER_VERSION = MW_USER_VERSION;
        const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
  
 +      /**
 +       * @const int Serialized record version.
 +       */
 +      const VERSION = 10;
 +
        /**
         * Maximum items in $mWatchedItems
         */
        const MAX_WATCHED_ITEMS_CACHE = 100;
  
 +      /**
 +       * @var PasswordFactory Lazily loaded factory object for passwords
 +       */
 +      private static $mPasswordFactory = null;
 +
        /**
         * Array of Strings List of member variables which are saved to the
         * shared cache (memcached). Any operation which changes the
                'mId',
                'mName',
                'mRealName',
 -              'mPassword',
 -              'mNewpassword',
 -              'mNewpassTime',
                'mEmail',
                'mTouched',
                'mToken',
                'mEmailAuthenticated',
                'mEmailToken',
                'mEmailTokenExpires',
 -              'mPasswordExpires',
                'mRegistration',
                'mEditCount',
                // user_groups table
                'nominornewtalk',
                'noratelimit',
                'override-export-depth',
 +              'pagelang',
                'passwordreset',
                'patrol',
                'patrolmarks',
                'userrights-interwiki',
                'viewmyprivateinfo',
                'viewmywatchlist',
 +              'viewsuppressed',
                'writeapi',
        );
  
  
        public $mRealName;
  
 +      /**
 +       * @todo Make this actually private
 +       * @private
 +       */
        public $mPassword;
  
 +      /**
 +       * @todo Make this actually private
 +       * @private
 +       */
        public $mNewpassword;
  
        public $mNewpassTime;
  
        /**
         * Load user table data, given mId has already been set.
 -       * @return bool false if the ID does not exist, true otherwise
 +       * @return bool False if the ID does not exist, true otherwise
         */
        public function loadFromId() {
 -              global $wgMemc;
                if ( $this->mId == 0 ) {
                        $this->loadDefaults();
                        return false;
                }
  
                // Try cache
 -              $key = wfMemcKey( 'user', 'id', $this->mId );
 -              $data = $wgMemc->get( $key );
 -              if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
 -                      // Object is expired, load from DB
 -                      $data = false;
 -              }
 -
 -              if ( !$data ) {
 +              $cache = $this->loadFromCache();
 +              if ( !$cache ) {
                        wfDebug( "User: cache miss for user {$this->mId}\n" );
                        // Load from DB
                        if ( !$this->loadFromDatabase() ) {
                                return false;
                        }
                        $this->saveToCache();
 -              } else {
 -                      wfDebug( "User: got user {$this->mId} from cache\n" );
 -                      // Restore from cache
 -                      foreach ( self::$mCacheVars as $name ) {
 -                              $this->$name = $data[$name];
 -                      }
                }
  
                $this->mLoadedItems = true;
                return true;
        }
  
 +      /**
 +       * Load user data from shared cache, given mId has already been set.
 +       *
 +       * @return bool false if the ID does not exist or data is invalid, true otherwise
 +       * @since 1.25
 +       */
 +      public function loadFromCache() {
 +              global $wgMemc;
 +
 +              if ( $this->mId == 0 ) {
 +                      $this->loadDefaults();
 +                      return false;
 +              }
 +
 +              $key = wfMemcKey( 'user', 'id', $this->mId );
 +              $data = $wgMemc->get( $key );
 +              if ( !is_array( $data ) || $data['mVersion'] < self::VERSION ) {
 +                      // Object is expired
 +                      return false;
 +              }
 +
 +              wfDebug( "User: got user {$this->mId} from cache\n" );
 +
 +              // Restore from cache
 +              foreach ( self::$mCacheVars as $name ) {
 +                      $this->$name = $data[$name];
 +              }
 +
 +              return true;
 +      }
 +
        /**
         * Save user data to the shared cache
         */
                foreach ( self::$mCacheVars as $name ) {
                        $data[$name] = $this->$name;
                }
 -              $data['mVersion'] = MW_USER_VERSION;
 +              $data['mVersion'] = self::VERSION;
                $key = wfMemcKey( 'user', 'id', $this->mId );
                global $wgMemc;
                $wgMemc->set( $key, $data );
         * Given unvalidated password input, return error message on failure.
         *
         * @param string $password Desired password
 -       * @return bool|string|array true on success, string or array of error message on failure
 +       * @return bool|string|array True on success, string or array of error message on failure
         */
        public function getPasswordValidity( $password ) {
                $result = $this->checkPasswordValidity( $password );
         * @param int $ts Optional timestamp to convert, default 0 for the current time
         */
        public function expirePassword( $ts = 0 ) {
 -              $this->load();
 +              $this->loadPasswords();
                $timestamp = wfTimestamp( TS_MW, $ts );
                $this->mPasswordExpires = $timestamp;
                $this->saveSettings();
                return $this->mPasswordExpires;
        }
  
 -      /**
 -       * Does a string look like an e-mail address?
 -       *
 -       * This validates an email address using an HTML5 specification found at:
 -       * http://www.whatwg.org/html/states-of-the-type-attribute.html#valid-e-mail-address
 -       * Which as of 2011-01-24 says:
 -       *
 -       *     A valid e-mail address is a string that matches the ABNF production
 -       *   1*( atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined
 -       *   in RFC 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section
 -       *   3.5.
 -       *
 -       * This function is an implementation of the specification as requested in
 -       * bug 22449.
 -       *
 -       * Client-side forms will use the same standard validation rules via JS or
 -       * HTML 5 validation; additional restrictions can be enforced server-side
 -       * by extensions via the 'isValidEmailAddr' hook.
 -       *
 -       * Note that this validation doesn't 100% match RFC 2822, but is believed
 -       * to be liberal enough for wide use. Some invalid addresses will still
 -       * pass validation here.
 -       *
 -       * @param string $addr E-mail address
 -       * @return bool
 -       * @deprecated since 1.18 call Sanitizer::isValidEmail() directly
 -       */
 -      public static function isValidEmailAddr( $addr ) {
 -              wfDeprecated( __METHOD__, '1.18' );
 -              return Sanitizer::validateEmail( $addr );
 -      }
 -
        /**
         * Given unvalidated user input, return a canonical username, or false if
         * the username is invalid.
                        return false;
                }
  
 -              // Clean up name according to title rules
 -              $t = ( $validate === 'valid' ) ?
 +              // Clean up name according to title rules,
 +              // but only when validation is requested (bug 12654)
 +              $t = ( $validate !== false ) ?
                        Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
                // Check for invalid titles
                if ( is_null( $t ) ) {
        public function loadDefaults( $name = false ) {
                wfProfileIn( __METHOD__ );
  
 +              $passwordFactory = self::getPasswordFactory();
 +
                $this->mId = 0;
                $this->mName = $name;
                $this->mRealName = '';
 -              $this->mPassword = $this->mNewpassword = '';
 +              $this->mPassword = $passwordFactory->newFromCiphertext( null );
 +              $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
                $this->mNewpassTime = null;
                $this->mEmail = '';
                $this->mOptionOverrides = null;
         * Load user and user_group data from the database.
         * $this->mId must be set, this is how the user is identified.
         *
 +       * @param int $flags Supports User::READ_LOCKING
         * @return bool True if the user exists, false if the user is anonymous
         */
 -      public function loadFromDatabase() {
 +      public function loadFromDatabase( $flags = 0 ) {
                // Paranoia
                $this->mId = intval( $this->mId );
  
                        'user',
                        self::selectFields(),
                        array( 'user_id' => $this->mId ),
 -                      __METHOD__
 +                      __METHOD__,
 +                      ( $flags & self::READ_LOCKING == self::READ_LOCKING )
 +                              ? array( 'LOCK IN SHARE MODE' )
 +                              : array()
                );
  
                wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
         */
        public function loadFromRow( $row, $data = null ) {
                $all = true;
 +              $passwordFactory = self::getPasswordFactory();
  
                $this->mGroups = null; // deferred
  
                }
  
                if ( isset( $row->user_password ) ) {
 -                      $this->mPassword = $row->user_password;
 -                      $this->mNewpassword = $row->user_newpassword;
 +                      // Check for *really* old password hashes that don't even have a type
 +                      // The old hash format was just an md5 hex hash, with no type information
 +                      if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
 +                              $row->user_password = ":A:{$this->mId}:{$row->user_password}";
 +                      }
 +
 +                      try {
 +                              $this->mPassword = $passwordFactory->newFromCiphertext( $row->user_password );
 +                      } catch ( PasswordError $e ) {
 +                              wfDebug( 'Invalid password hash found in database.' );
 +                              $this->mPassword = $passwordFactory->newFromCiphertext( null );
 +                      }
 +
 +                      try {
 +                              $this->mNewpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
 +                      } catch ( PasswordError $e ) {
 +                              wfDebug( 'Invalid password hash found in database.' );
 +                              $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
 +                      }
 +
                        $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
 +                      $this->mPasswordExpires = wfTimestampOrNull( TS_MW, $row->user_password_expires );
 +              }
 +
 +              if ( isset( $row->user_email ) ) {
                        $this->mEmail = $row->user_email;
                        $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
                        $this->mToken = $row->user_token;
                        $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
                        $this->mEmailToken = $row->user_email_token;
                        $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
 -                      $this->mPasswordExpires = wfTimestampOrNull( TS_MW, $row->user_password_expires );
                        $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
                } else {
                        $all = false;
                }
        }
  
 +      /**
 +       * Load the user's password hashes from the database
 +       *
 +       * This is usually called in a scenario where the actual User object was
 +       * loaded from the cache, and then password comparison needs to be performed.
 +       * Password hashes are not stored in memcached.
 +       *
 +       * @since 1.24
 +       */
 +      private function loadPasswords() {
 +              if ( $this->getId() !== 0 && ( $this->mPassword === null || $this->mNewpassword === null ) ) {
 +                      $this->loadFromRow( wfGetDB( DB_MASTER )->selectRow(
 +                                      'user',
 +                                      array( 'user_password', 'user_newpassword', 'user_newpass_time', 'user_password_expires' ),
 +                                      array( 'user_id' => $this->getId() ),
 +                                      __METHOD__
 +                              ) );
 +              }
 +      }
 +
        /**
         * Add the user to the group if he/she meets given criteria.
         *
                foreach ( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
                        $defOpt['searchNs' . $nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
                }
 -              $defOpt['skin'] = $wgDefaultSkin;
 +              $defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
  
                wfRunHooks( 'UserGetDefaultOptions', array( &$defOpt ) );
  
         * @return bool True if blacklisted.
         */
        public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
 -              global $wgEnableSorbs, $wgEnableDnsBlacklist,
 -                      $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist;
 +              global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist;
  
 -              if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs ) {
 +              if ( !$wgEnableDnsBlacklist ) {
                        return false;
                }
  
                        return false;
                }
  
 -              $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl );
 -              return $this->inDnsBlacklist( $ip, $urls );
 +              return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls );
        }
  
        /**
                // If more than one group applies, use the group with the highest limit
                foreach ( $this->getGroups() as $group ) {
                        if ( isset( $limits[$group] ) ) {
-                               if ( $userLimit === false || $limits[$group] > $userLimit ) {
+                               if ( $userLimit === false
+                                       || $limits[$group][0] / $limits[$group][1] > $userLimit[0] / $userLimit[1]
+                               ) {
                                        $userLimit = $limits[$group];
                                }
                        }
                        return $this->mLocked;
                }
                global $wgAuth;
 -              StubObject::unstub( $wgAuth );
                $authUser = $wgAuth->getUserInstance( $this );
                $this->mLocked = (bool)$authUser->isLocked();
                return $this->mLocked;
                $this->getBlockedStatus();
                if ( !$this->mHideName ) {
                        global $wgAuth;
 -                      StubObject::unstub( $wgAuth );
                        $authUser = $wgAuth->getUserInstance( $this );
                        $this->mHideName = (bool)$authUser->isHidden();
                }
         * @see getNewtalk()
         * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
         * @param string|int $id User's IP address for anonymous users, User ID otherwise
 -       * @param bool $fromMaster true to fetch from the master, false for a slave
 +       * @param bool $fromMaster True to fetch from the master, false for a slave
         * @return bool True if the user has new messages
         */
        protected function checkNewtalk( $field, $id, $fromMaster = false ) {
         *
         * Called implicitly from invalidateCache() and saveSettings().
         */
 -      private function clearSharedCache() {
 +      public function clearSharedCache() {
                $this->load();
                if ( $this->mId ) {
                        global $wgMemc;
                        $userid = $this->mId;
                        $touched = $this->mTouched;
                        $method = __METHOD__;
 -                      $dbw->onTransactionIdle( function() use ( $dbw, $userid, $touched, $method ) {
 +                      $dbw->onTransactionIdle( function () use ( $dbw, $userid, $touched, $method ) {
                                // Prevent contention slams by checking user_touched first
                                $encTouched = $dbw->addQuotes( $dbw->timestamp( $touched ) );
                                $needsPurge = $dbw->selectField( 'user', '1',
                return $this->mTouched;
        }
  
 +      /**
 +       * @return Password
 +       * @since 1.24
 +       */
 +      public function getPassword() {
 +              $this->loadPasswords();
 +
 +              return $this->mPassword;
 +      }
 +
 +      /**
 +       * @return Password
 +       * @since 1.24
 +       */
 +      public function getTemporaryPassword() {
 +              $this->loadPasswords();
 +
 +              return $this->mNewpassword;
 +      }
 +
        /**
         * Set the password and reset the random token.
         * Calls through to authentication plugin if necessary;
         * a new password is set, for instance via e-mail.
         *
         * @param string $str New password to set
 -       * @throws PasswordError on failure
 +       * @throws PasswordError On failure
         *
         * @return bool
         */
        public function setPassword( $str ) {
                global $wgAuth;
  
 +              $this->loadPasswords();
 +
                if ( $str !== null ) {
                        if ( !$wgAuth->allowPasswordChange() ) {
                                throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
         *  through the web interface.
         */
        public function setInternalPassword( $str ) {
 -              $this->load();
                $this->setToken();
  
 -              if ( $str === null ) {
 -                      // Save an invalid hash...
 -                      $this->mPassword = '';
 -              } else {
 -                      $this->mPassword = self::crypt( $str );
 -              }
 -              $this->mNewpassword = '';
 +              $passwordFactory = self::getPasswordFactory();
 +              $this->mPassword = $passwordFactory->newFromPlaintext( $str );
 +
 +              $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
                $this->mNewpassTime = null;
        }
  
        public function setToken( $token = false ) {
                $this->load();
                if ( !$token ) {
 -                      $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH );
 +                      $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
                } else {
                        $this->mToken = $token;
                }
         * @param bool $throttle If true, reset the throttle timestamp to the present
         */
        public function setNewpassword( $str, $throttle = true ) {
 -              $this->load();
 +              $this->loadPasswords();
  
 +              $this->mNewpassword = self::getPasswordFactory()->newFromPlaintext( $str );
                if ( $str === null ) {
 -                      $this->mNewpassword = '';
                        $this->mNewpassTime = null;
 -              } else {
 -                      $this->mNewpassword = self::crypt( $str );
 -                      if ( $throttle ) {
 -                              $this->mNewpassTime = wfTimestampNow();
 -                      }
 +              } elseif ( $throttle ) {
 +                      $this->mNewpassTime = wfTimestampNow();
                }
        }
  
                if ( $str == $this->mEmail ) {
                        return;
                }
 -              $this->mEmail = $str;
                $this->invalidateEmail();
 +              $this->mEmail = $str;
                wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
        }
  
         * @param IContextSource $context
         * @param array $options Assoc. array with options keys to check as keys.
         *   Defaults to $this->mOptions.
 -       * @return array the key => kind mapping data
 +       * @return array The key => kind mapping data
         */
        public function getOptionKinds( IContextSource $context, $options = null ) {
                $this->loadOptions();
  
                                foreach ( $columns as $column ) {
                                        foreach ( $rows as $row ) {
 -                                              $checkmatrixOptions["$prefix-$column-$row"] = true;
 +                                              $checkmatrixOptions["$prefix$column-$row"] = true;
                                        }
                                }
  
                        }
                }
  
 +              wfRunHooks( 'UserResetAllOptions', array( $this, &$newOptions, $this->mOptions, $resetKinds ) );
 +
                $this->mOptions = $newOptions;
                $this->mOptionsLoaded = true;
        }
  
        /**
         * Get the user's edit count.
 -       * @return int|null null for anonymous users
 +       * @return int|null Null for anonymous users
         */
        public function getEditCount() {
                if ( !$this->getId() ) {
                        return null;
                }
  
 -              if ( !isset( $this->mEditCount ) ) {
 +              if ( $this->mEditCount === null ) {
                        /* Populate the count, if it has not been populated yet */
                        wfProfileIn( __METHOD__ );
                        $dbr = wfGetDB( DB_SLAVE );
        /**
         * Check if user is allowed to access a feature / make an action
         *
 -       * @internal param \String $varargs permissions to test
 +       * @param string $permissions,... Permissions to test
         * @return bool True if user is allowed to perform *any* of the given actions
 -       *
 -       * @return bool
         */
        public function isAllowedAny( /*...*/ ) {
                $permissions = func_get_args();
  
        /**
         *
 -       * @internal param $varargs string
 +       * @param string $permissions,... Permissions to test
         * @return bool True if the user is allowed to perform *all* of the given actions
         */
        public function isAllowedAll( /*...*/ ) {
                global $wgAuth;
  
                $this->load();
 +              $this->loadPasswords();
                if ( wfReadOnly() ) {
                        return;
                }
  
                $this->mTouched = self::newTouchedTimestamp();
                if ( !$wgAuth->allowSetLocalPassword() ) {
 -                      $this->mPassword = '';
 +                      $this->mPassword = self::getPasswordFactory()->newFromCiphertext( null );
                }
  
                $dbw = wfGetDB( DB_MASTER );
                $dbw->update( 'user',
                        array( /* SET */
                                'user_name' => $this->mName,
 -                              'user_password' => $this->mPassword,
 -                              'user_newpassword' => $this->mNewpassword,
 +                              'user_password' => $this->mPassword->toString(),
 +                              'user_newpassword' => $this->mNewpassword->toString(),
                                'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
                                'user_real_name' => $this->mRealName,
                                'user_email' => $this->mEmail,
        public static function createNew( $name, $params = array() ) {
                $user = new User;
                $user->load();
 +              $user->loadPasswords();
                $user->setToken(); // init token
                if ( isset( $params['options'] ) ) {
                        $user->mOptions = $params['options'] + (array)$user->mOptions;
                $fields = array(
                        'user_id' => $seqVal,
                        'user_name' => $name,
 -                      'user_password' => $user->mPassword,
 -                      'user_newpassword' => $user->mNewpassword,
 +                      'user_password' => $user->mPassword->toString(),
 +                      'user_newpassword' => $user->mNewpassword->toString(),
                        'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ),
                        'user_email' => $user->mEmail,
                        'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
         */
        public function addToDatabase() {
                $this->load();
 +              $this->loadPasswords();
                if ( !$this->mToken ) {
                        $this->setToken(); // init token
                }
                        array(
                                'user_id' => $seqVal,
                                'user_name' => $this->mName,
 -                              'user_password' => $this->mPassword,
 -                              'user_newpassword' => $this->mNewpassword,
 +                              'user_password' => $this->mPassword->toString(),
 +                              'user_newpassword' => $this->mNewpassword->toString(),
                                'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
                                'user_email' => $this->mEmail,
                                'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
                        array( 'IGNORE' )
                );
                if ( !$dbw->affectedRows() ) {
 -                      if ( !$inWrite ) {
 -                              // XXX: Get out of REPEATABLE-READ so the SELECT below works.
 -                              // Often this case happens early in views before any writes.
 -                              // This shows up at least with CentralAuth.
 +                      // The queries below cannot happen in the same REPEATABLE-READ snapshot.
 +                      // Handle this by COMMIT, if possible, or by LOCK IN SHARE MODE otherwise.
 +                      if ( $inWrite ) {
 +                              // Can't commit due to pending writes that may need atomicity.
 +                              // This may cause some lock contention unlike the case below.
 +                              $options = array( 'LOCK IN SHARE MODE' );
 +                              $flags = self::READ_LOCKING;
 +                      } else {
 +                              // Often, this case happens early in views before any writes when
 +                              // using CentralAuth. It's should be OK to commit and break the snapshot.
                                $dbw->commit( __METHOD__, 'flush' );
 +                              $options = array();
 +                              $flags = 0;
                        }
                        $this->mId = $dbw->selectField( 'user', 'user_id',
 -                              array( 'user_name' => $this->mName ), __METHOD__ );
 +                              array( 'user_name' => $this->mName ), __METHOD__, $options );
                        $loaded = false;
                        if ( $this->mId ) {
 -                              if ( $this->loadFromDatabase() ) {
 +                              if ( $this->loadFromDatabase( $flags ) ) {
                                        $loaded = true;
                                }
                        }
  
        /**
         * Check to see if the given clear-text password is one of the accepted passwords
 -       * @param string $password user password.
 -       * @return bool True if the given password is correct, otherwise False.
 +       * @param string $password User password
 +       * @return bool True if the given password is correct, otherwise False
         */
        public function checkPassword( $password ) {
                global $wgAuth, $wgLegacyEncoding;
 -              $this->load();
 +
 +              $section = new ProfileSection( __METHOD__ );
 +
 +              $this->loadPasswords();
  
                // Certain authentication plugins do NOT want to save
                // domain passwords in a mysql database, so we should
                // check this (in case $wgAuth->strict() is false).
 -
                if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
                        return true;
                } elseif ( $wgAuth->strict() ) {
                        // Auth plugin doesn't allow local authentication for this user name
                        return false;
                }
 -              if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
 -                      return true;
 -              } elseif ( $wgLegacyEncoding ) {
 -                      // 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 )
 -                      ) {
 -                              return true;
 +
 +              $passwordFactory = self::getPasswordFactory();
 +              if ( !$this->mPassword->equals( $password ) ) {
 +                      if ( $wgLegacyEncoding ) {
 +                              // 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 || !$this->mPassword->equals( $cp1252Password ) ) {
 +                                      return false;
 +                              }
 +                      } else {
 +                              return false;
                        }
                }
 -              return false;
 +
 +              if ( $passwordFactory->needsUpdate( $this->mPassword ) ) {
 +                      $this->mPassword = $passwordFactory->newFromPlaintext( $password );
 +                      $this->saveSettings();
 +              }
 +
 +              return true;
        }
  
        /**
                global $wgNewPasswordExpiry;
  
                $this->load();
 -              if ( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
 +              $this->loadPasswords();
 +              if ( $this->mNewpassword->equals( $plaintext ) ) {
                        if ( is_null( $this->mNewpassTime ) ) {
                                return true;
                        }
         * Alias for getEditToken.
         * @deprecated since 1.19, use getEditToken instead.
         *
 -       * @param string|array $salt of Strings Optional function-specific data for hashing
 +       * @param string|array $salt Array of Strings Optional function-specific data for hashing
         * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
         * @return string The new edit token
         */
         *
         * @since 1.19
         *
 -       * @param string|array $salt of Strings Optional function-specific data for hashing
 +       * @param string|array $salt Array of Strings Optional function-specific data for hashing
         * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
         * @return string The new edit token
         */
                }
  
                if ( $this->isAnon() ) {
 -                      return EDIT_TOKEN_SUFFIX;
 +                      return self::EDIT_TOKEN_SUFFIX;
                } else {
                        $token = $request->getSessionData( 'wsEditToken' );
                        if ( $token === null ) {
                        if ( is_array( $salt ) ) {
                                $salt = implode( '|', $salt );
                        }
 -                      return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
 +                      return md5( $token . $salt ) . self::EDIT_TOKEN_SUFFIX;
                }
        }
  
         *
         * @param string $val Input value to compare
         * @param string $salt Optional function-specific data for hashing
 -       * @param WebRequest|null $request object to use or null to use $wgRequest
 +       * @param WebRequest|null $request Object to use or null to use $wgRequest
         * @return bool Whether the token matches
         */
        public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) {
                        $sender = new MailAddress( $wgPasswordSender,
                                wfMessage( 'emailsender' )->inContentLanguage()->text() );
                } else {
 -                      $sender = new MailAddress( $from );
 +                      $sender = MailAddress::newFromUser( $from );
                }
  
 -              $to = new MailAddress( $this );
 +              $to = MailAddress::newFromUser( $this );
                return UserMailer::send( $to, $sender, $subject, $body, $replyto );
        }
  
                $this->mEmailToken = null;
                $this->mEmailTokenExpires = null;
                $this->setEmailAuthenticationTimestamp( null );
 +              $this->mEmail = '';
                wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
                return true;
        }
         * Returns an array of the groups that a particular group can add/remove.
         *
         * @param string $group The group to check for whether it can add/remove
 -       * @return array array( 'add' => array( addablegroups ),
 +       * @return array Array( 'add' => array( addablegroups ),
         *     'remove' => array( removablegroups ),
         *     'add-self' => array( addablegroups to self),
         *     'remove-self' => array( removable groups from self) )
  
        /**
         * Returns an array of groups that this user can add and remove
 -       * @return array array( 'add' => array( addablegroups ),
 +       * @return array Array( 'add' => array( addablegroups ),
         *  'remove' => array( removablegroups ),
         *  'add-self' => array( addablegroups to self),
         *  'remove-self' => array( removable groups from self) )
                return $msg->isBlank() ? $right : $msg->text();
        }
  
 -      /**
 -       * Make an old-style password hash
 -       *
 -       * @param string $password Plain-text password
 -       * @param string $userId User ID
 -       * @return string Password hash
 -       */
 -      public static function oldCrypt( $password, $userId ) {
 -              global $wgPasswordSalt;
 -              if ( $wgPasswordSalt ) {
 -                      return md5( $userId . '-' . md5( $password ) );
 -              } else {
 -                      return md5( $password );
 -              }
 -      }
 -
        /**
         * Make a new-style password hash
         *
         * @param bool|string $salt Optional salt, may be random or the user ID.
         *  If unspecified or false, will generate one automatically
         * @return string Password hash
 +       * @deprecated since 1.24, use Password class
         */
        public static function crypt( $password, $salt = false ) {
 -              global $wgPasswordSalt;
 -
 -              $hash = '';
 -              if ( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
 -                      return $hash;
 -              }
 -
 -              if ( $wgPasswordSalt ) {
 -                      if ( $salt === false ) {
 -                              $salt = MWCryptRand::generateHex( 8 );
 -                      }
 -                      return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
 -              } else {
 -                      return ':A:' . md5( $password );
 -              }
 +              wfDeprecated( __METHOD__, '1.24' );
 +              $hash = self::getPasswordFactory()->newFromPlaintext( $password );
 +              return $hash->toString();
        }
  
        /**
         * @param string|bool $userId User ID for old-style password salt
         *
         * @return bool
 +       * @deprecated since 1.24, use Password class
         */
        public static function comparePasswords( $hash, $password, $userId = false ) {
 -              $type = substr( $hash, 0, 3 );
 -
 -              $result = false;
 -              if ( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
 -                      return $result;
 +              wfDeprecated( __METHOD__, '1.24' );
 +
 +              // Check for *really* old password hashes that don't even have a type
 +              // The old hash format was just an md5 hex hash, with no type information
 +              if ( preg_match( '/^[0-9a-f]{32}$/', $hash ) ) {
 +                      global $wgPasswordSalt;
 +                      if ( $wgPasswordSalt ) {
 +                              $password = ":B:{$userId}:{$hash}";
 +                      } else {
 +                              $password = ":A:{$hash}";
 +                      }
                }
  
 -              if ( $type == ':A:' ) {
 -                      // Unsalted
 -                      return md5( $password ) === substr( $hash, 3 );
 -              } elseif ( $type == ':B:' ) {
 -                      // Salted
 -                      list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
 -                      return md5( $salt . '-' . md5( $password ) ) === $realHash;
 -              } else {
 -                      // Old-style
 -                      return self::oldCrypt( $password, $userId ) === $hash;
 -              }
 +              $hash = self::getPasswordFactory()->newFromCiphertext( $hash );
 +              return $hash->equals( $password );
        }
  
        /**
                $dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) );
        }
  
 +      /**
 +       * Lazily instantiate and return a factory object for making passwords
 +       *
 +       * @return PasswordFactory
 +       */
 +      public static function getPasswordFactory() {
 +              if ( self::$mPasswordFactory === null ) {
 +                      self::$mPasswordFactory = new PasswordFactory();
 +                      self::$mPasswordFactory->init( RequestContext::getMain()->getConfig() );
 +              }
 +
 +              return self::$mPasswordFactory;
 +      }
 +
        /**
         * Provide an array of HTML5 attributes to put on an input element
         * intended for the user to enter a new password.  This may include
                        'user_id',
                        'user_name',
                        'user_real_name',
 -                      'user_password',
 -                      'user_newpassword',
 -                      'user_newpass_time',
                        'user_email',
                        'user_touched',
                        'user_token',
                        'user_email_authenticated',
                        'user_email_token',
                        'user_email_token_expires',
 -                      'user_password_expires',
                        'user_registration',
                        'user_editcount',
                );