Merge "Fix title handling in User::getCanonicalName"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 30 Jul 2014 23:27:56 +0000 (23:27 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 30 Jul 2014 23:27:56 +0000 (23:27 +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
   */
  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
                // Try cache
                $key = wfMemcKey( 'user', 'id', $this->mId );
                $data = $wgMemc->get( $key );
 -              if ( !is_array( $data ) || $data['mVersion'] != MW_USER_VERSION ) {
 +              if ( !is_array( $data ) || $data['mVersion'] != self::VERSION ) {
                        // Object is expired, load from DB
                        $data = false;
                }
                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 );
                        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;
         */
        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.
         *
         *
         * Called implicitly from invalidateCache() and saveSettings().
         */
 -      private function clearSharedCache() {
 +      public function clearSharedCache() {
                $this->load();
                if ( $this->mId ) {
                        global $wgMemc;
         *  through the web interface.
         */
        public function setInternalPassword( $str ) {
 -              $this->load();
                $this->setToken();
  
 +              $passwordFactory = self::getPasswordFactory();
                if ( $str === null ) {
 -                      // Save an invalid hash...
 -                      $this->mPassword = '';
 +                      $this->mPassword = $passwordFactory->newFromCiphertext( null );
                } else {
 -                      $this->mPassword = self::crypt( $str );
 +                      $this->mPassword = $passwordFactory->newFromPlaintext( $str );
                }
 -              $this->mNewpassword = '';
 +
 +              $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;
                }
                        $this->mNewpassword = '';
                        $this->mNewpassTime = null;
                } else {
 -                      $this->mNewpassword = self::crypt( $str );
 +                      $this->mNewpassword = self::getPasswordFactory()->newFromPlaintext( $str );
                        if ( $throttle ) {
                                $this->mNewpassTime = wfTimestampNow();
                        }
                if ( $str == $this->mEmail ) {
                        return;
                }
 -              $this->mEmail = $str;
                $this->invalidateEmail();
 +              $this->mEmail = $str;
                wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
        }
  
                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 ),
         */
        public function checkPassword( $password ) {
                global $wgAuth, $wgLegacyEncoding;
 -              $this->load();
 +              $this->loadPasswords();
  
                // Certain authentication plugins do NOT want to save
                // domain passwords in a mysql database, so we should
                        // 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;
                        }
                }
  
                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;
                }
        }
  
                $this->mEmailToken = null;
                $this->mEmailTokenExpires = null;
                $this->setEmailAuthenticationTimestamp( null );
 +              $this->mEmail = '';
                wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
                return true;
        }
                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',
                );