[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/ -> User.php (source)

   1  <?php
   2  /**
   3   * Implements the User class for the %MediaWiki software.
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   */
  22  
  23  /**
  24   * String Some punctuation to prevent editing from broken text-mangling proxies.
  25   * @ingroup Constants
  26   */
  27  define( 'EDIT_TOKEN_SUFFIX', '+\\' );
  28  
  29  /**
  30   * The User object encapsulates all of the user-specific settings (user_id,
  31   * name, rights, password, email address, options, last login time). Client
  32   * classes use the getXXX() functions to access these fields. These functions
  33   * do all the work of determining whether the user is logged in,
  34   * whether the requested option can be satisfied from cookies or
  35   * whether a database query is needed. Most of the settings needed
  36   * for rendering normal pages are set in the cookie to minimize use
  37   * of the database.
  38   */
  39  class User implements IDBAccessObject {
  40      /**
  41       * @const int Number of characters in user_token field.
  42       */
  43      const TOKEN_LENGTH = 32;
  44  
  45      /**
  46       * Global constant made accessible as class constants so that autoloader
  47       * magic can be used.
  48       */
  49      const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
  50  
  51      /**
  52       * @const int Serialized record version.
  53       */
  54      const VERSION = 10;
  55  
  56      /**
  57       * Maximum items in $mWatchedItems
  58       */
  59      const MAX_WATCHED_ITEMS_CACHE = 100;
  60  
  61      /**
  62       * @var PasswordFactory Lazily loaded factory object for passwords
  63       */
  64      private static $mPasswordFactory = null;
  65  
  66      /**
  67       * Array of Strings List of member variables which are saved to the
  68       * shared cache (memcached). Any operation which changes the
  69       * corresponding database fields must call a cache-clearing function.
  70       * @showinitializer
  71       */
  72      protected static $mCacheVars = array(
  73          // user table
  74          'mId',
  75          'mName',
  76          'mRealName',
  77          'mEmail',
  78          'mTouched',
  79          'mToken',
  80          'mEmailAuthenticated',
  81          'mEmailToken',
  82          'mEmailTokenExpires',
  83          'mRegistration',
  84          'mEditCount',
  85          // user_groups table
  86          'mGroups',
  87          // user_properties table
  88          'mOptionOverrides',
  89      );
  90  
  91      /**
  92       * Array of Strings Core rights.
  93       * Each of these should have a corresponding message of the form
  94       * "right-$right".
  95       * @showinitializer
  96       */
  97      protected static $mCoreRights = array(
  98          'apihighlimits',
  99          'autoconfirmed',
 100          'autopatrol',
 101          'bigdelete',
 102          'block',
 103          'blockemail',
 104          'bot',
 105          'browsearchive',
 106          'createaccount',
 107          'createpage',
 108          'createtalk',
 109          'delete',
 110          'deletedhistory',
 111          'deletedtext',
 112          'deletelogentry',
 113          'deleterevision',
 114          'edit',
 115          'editcontentmodel',
 116          'editinterface',
 117          'editprotected',
 118          'editmyoptions',
 119          'editmyprivateinfo',
 120          'editmyusercss',
 121          'editmyuserjs',
 122          'editmywatchlist',
 123          'editsemiprotected',
 124          'editusercssjs', #deprecated
 125          'editusercss',
 126          'edituserjs',
 127          'hideuser',
 128          'import',
 129          'importupload',
 130          'ipblock-exempt',
 131          'markbotedits',
 132          'mergehistory',
 133          'minoredit',
 134          'move',
 135          'movefile',
 136          'move-categorypages',
 137          'move-rootuserpages',
 138          'move-subpages',
 139          'nominornewtalk',
 140          'noratelimit',
 141          'override-export-depth',
 142          'pagelang',
 143          'passwordreset',
 144          'patrol',
 145          'patrolmarks',
 146          'protect',
 147          'proxyunbannable',
 148          'purge',
 149          'read',
 150          'reupload',
 151          'reupload-own',
 152          'reupload-shared',
 153          'rollback',
 154          'sendemail',
 155          'siteadmin',
 156          'suppressionlog',
 157          'suppressredirect',
 158          'suppressrevision',
 159          'unblockself',
 160          'undelete',
 161          'unwatchedpages',
 162          'upload',
 163          'upload_by_url',
 164          'userrights',
 165          'userrights-interwiki',
 166          'viewmyprivateinfo',
 167          'viewmywatchlist',
 168          'viewsuppressed',
 169          'writeapi',
 170      );
 171  
 172      /**
 173       * String Cached results of getAllRights()
 174       */
 175      protected static $mAllRights = false;
 176  
 177      /** @name Cache variables */
 178      //@{
 179      public $mId;
 180  
 181      public $mName;
 182  
 183      public $mRealName;
 184  
 185      /**
 186       * @todo Make this actually private
 187       * @private
 188       */
 189      public $mPassword;
 190  
 191      /**
 192       * @todo Make this actually private
 193       * @private
 194       */
 195      public $mNewpassword;
 196  
 197      public $mNewpassTime;
 198  
 199      public $mEmail;
 200  
 201      public $mTouched;
 202  
 203      protected $mToken;
 204  
 205      public $mEmailAuthenticated;
 206  
 207      protected $mEmailToken;
 208  
 209      protected $mEmailTokenExpires;
 210  
 211      protected $mRegistration;
 212  
 213      protected $mEditCount;
 214  
 215      public $mGroups;
 216  
 217      protected $mOptionOverrides;
 218  
 219      protected $mPasswordExpires;
 220      //@}
 221  
 222      /**
 223       * Bool Whether the cache variables have been loaded.
 224       */
 225      //@{
 226      public $mOptionsLoaded;
 227  
 228      /**
 229       * Array with already loaded items or true if all items have been loaded.
 230       */
 231      protected $mLoadedItems = array();
 232      //@}
 233  
 234      /**
 235       * String Initialization data source if mLoadedItems!==true. May be one of:
 236       *  - 'defaults'   anonymous user initialised from class defaults
 237       *  - 'name'       initialise from mName
 238       *  - 'id'         initialise from mId
 239       *  - 'session'    log in from cookies or session if possible
 240       *
 241       * Use the User::newFrom*() family of functions to set this.
 242       */
 243      public $mFrom;
 244  
 245      /**
 246       * Lazy-initialized variables, invalidated with clearInstanceCache
 247       */
 248      protected $mNewtalk;
 249  
 250      protected $mDatePreference;
 251  
 252      public $mBlockedby;
 253  
 254      protected $mHash;
 255  
 256      public $mRights;
 257  
 258      protected $mBlockreason;
 259  
 260      protected $mEffectiveGroups;
 261  
 262      protected $mImplicitGroups;
 263  
 264      protected $mFormerGroups;
 265  
 266      protected $mBlockedGlobally;
 267  
 268      protected $mLocked;
 269  
 270      public $mHideName;
 271  
 272      public $mOptions;
 273  
 274      /**
 275       * @var WebRequest
 276       */
 277      private $mRequest;
 278  
 279      /** @var Block */
 280      public $mBlock;
 281  
 282      /** @var bool */
 283      protected $mAllowUsertalk;
 284  
 285      /** @var Block */
 286      private $mBlockedFromCreateAccount = false;
 287  
 288      /** @var array */
 289      private $mWatchedItems = array();
 290  
 291      public static $idCacheByName = array();
 292  
 293      /**
 294       * Lightweight constructor for an anonymous user.
 295       * Use the User::newFrom* factory functions for other kinds of users.
 296       *
 297       * @see newFromName()
 298       * @see newFromId()
 299       * @see newFromConfirmationCode()
 300       * @see newFromSession()
 301       * @see newFromRow()
 302       */
 303  	public function __construct() {
 304          $this->clearInstanceCache( 'defaults' );
 305      }
 306  
 307      /**
 308       * @return string
 309       */
 310  	public function __toString() {
 311          return $this->getName();
 312      }
 313  
 314      /**
 315       * Load the user table data for this object from the source given by mFrom.
 316       */
 317  	public function load() {
 318          if ( $this->mLoadedItems === true ) {
 319              return;
 320          }
 321          wfProfileIn( __METHOD__ );
 322  
 323          // Set it now to avoid infinite recursion in accessors
 324          $this->mLoadedItems = true;
 325  
 326          switch ( $this->mFrom ) {
 327              case 'defaults':
 328                  $this->loadDefaults();
 329                  break;
 330              case 'name':
 331                  $this->mId = self::idFromName( $this->mName );
 332                  if ( !$this->mId ) {
 333                      // Nonexistent user placeholder object
 334                      $this->loadDefaults( $this->mName );
 335                  } else {
 336                      $this->loadFromId();
 337                  }
 338                  break;
 339              case 'id':
 340                  $this->loadFromId();
 341                  break;
 342              case 'session':
 343                  if ( !$this->loadFromSession() ) {
 344                      // Loading from session failed. Load defaults.
 345                      $this->loadDefaults();
 346                  }
 347                  wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
 348                  break;
 349              default:
 350                  wfProfileOut( __METHOD__ );
 351                  throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
 352          }
 353          wfProfileOut( __METHOD__ );
 354      }
 355  
 356      /**
 357       * Load user table data, given mId has already been set.
 358       * @return bool False if the ID does not exist, true otherwise
 359       */
 360  	public function loadFromId() {
 361          global $wgMemc;
 362          if ( $this->mId == 0 ) {
 363              $this->loadDefaults();
 364              return false;
 365          }
 366  
 367          // Try cache
 368          $key = wfMemcKey( 'user', 'id', $this->mId );
 369          $data = $wgMemc->get( $key );
 370          if ( !is_array( $data ) || $data['mVersion'] != self::VERSION ) {
 371              // Object is expired, load from DB
 372              $data = false;
 373          }
 374  
 375          if ( !$data ) {
 376              wfDebug( "User: cache miss for user {$this->mId}\n" );
 377              // Load from DB
 378              if ( !$this->loadFromDatabase() ) {
 379                  // Can't load from ID, user is anonymous
 380                  return false;
 381              }
 382              $this->saveToCache();
 383          } else {
 384              wfDebug( "User: got user {$this->mId} from cache\n" );
 385              // Restore from cache
 386              foreach ( self::$mCacheVars as $name ) {
 387                  $this->$name = $data[$name];
 388              }
 389          }
 390  
 391          $this->mLoadedItems = true;
 392  
 393          return true;
 394      }
 395  
 396      /**
 397       * Save user data to the shared cache
 398       */
 399  	public function saveToCache() {
 400          $this->load();
 401          $this->loadGroups();
 402          $this->loadOptions();
 403          if ( $this->isAnon() ) {
 404              // Anonymous users are uncached
 405              return;
 406          }
 407          $data = array();
 408          foreach ( self::$mCacheVars as $name ) {
 409              $data[$name] = $this->$name;
 410          }
 411          $data['mVersion'] = self::VERSION;
 412          $key = wfMemcKey( 'user', 'id', $this->mId );
 413          global $wgMemc;
 414          $wgMemc->set( $key, $data );
 415      }
 416  
 417      /** @name newFrom*() static factory methods */
 418      //@{
 419  
 420      /**
 421       * Static factory method for creation from username.
 422       *
 423       * This is slightly less efficient than newFromId(), so use newFromId() if
 424       * you have both an ID and a name handy.
 425       *
 426       * @param string $name Username, validated by Title::newFromText()
 427       * @param string|bool $validate Validate username. Takes the same parameters as
 428       *  User::getCanonicalName(), except that true is accepted as an alias
 429       *  for 'valid', for BC.
 430       *
 431       * @return User|bool User object, or false if the username is invalid
 432       *  (e.g. if it contains illegal characters or is an IP address). If the
 433       *  username is not present in the database, the result will be a user object
 434       *  with a name, zero user ID and default settings.
 435       */
 436  	public static function newFromName( $name, $validate = 'valid' ) {
 437          if ( $validate === true ) {
 438              $validate = 'valid';
 439          }
 440          $name = self::getCanonicalName( $name, $validate );
 441          if ( $name === false ) {
 442              return false;
 443          } else {
 444              // Create unloaded user object
 445              $u = new User;
 446              $u->mName = $name;
 447              $u->mFrom = 'name';
 448              $u->setItemLoaded( 'name' );
 449              return $u;
 450          }
 451      }
 452  
 453      /**
 454       * Static factory method for creation from a given user ID.
 455       *
 456       * @param int $id Valid user ID
 457       * @return User The corresponding User object
 458       */
 459  	public static function newFromId( $id ) {
 460          $u = new User;
 461          $u->mId = $id;
 462          $u->mFrom = 'id';
 463          $u->setItemLoaded( 'id' );
 464          return $u;
 465      }
 466  
 467      /**
 468       * Factory method to fetch whichever user has a given email confirmation code.
 469       * This code is generated when an account is created or its e-mail address
 470       * has changed.
 471       *
 472       * If the code is invalid or has expired, returns NULL.
 473       *
 474       * @param string $code Confirmation code
 475       * @return User|null
 476       */
 477  	public static function newFromConfirmationCode( $code ) {
 478          $dbr = wfGetDB( DB_SLAVE );
 479          $id = $dbr->selectField( 'user', 'user_id', array(
 480              'user_email_token' => md5( $code ),
 481              'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
 482              ) );
 483          if ( $id !== false ) {
 484              return User::newFromId( $id );
 485          } else {
 486              return null;
 487          }
 488      }
 489  
 490      /**
 491       * Create a new user object using data from session or cookies. If the
 492       * login credentials are invalid, the result is an anonymous user.
 493       *
 494       * @param WebRequest|null $request Object to use; $wgRequest will be used if omitted.
 495       * @return User
 496       */
 497  	public static function newFromSession( WebRequest $request = null ) {
 498          $user = new User;
 499          $user->mFrom = 'session';
 500          $user->mRequest = $request;
 501          return $user;
 502      }
 503  
 504      /**
 505       * Create a new user object from a user row.
 506       * The row should have the following fields from the user table in it:
 507       * - either user_name or user_id to load further data if needed (or both)
 508       * - user_real_name
 509       * - all other fields (email, password, etc.)
 510       * It is useless to provide the remaining fields if either user_id,
 511       * user_name and user_real_name are not provided because the whole row
 512       * will be loaded once more from the database when accessing them.
 513       *
 514       * @param stdClass $row A row from the user table
 515       * @param array $data Further data to load into the object (see User::loadFromRow for valid keys)
 516       * @return User
 517       */
 518  	public static function newFromRow( $row, $data = null ) {
 519          $user = new User;
 520          $user->loadFromRow( $row, $data );
 521          return $user;
 522      }
 523  
 524      //@}
 525  
 526      /**
 527       * Get the username corresponding to a given user ID
 528       * @param int $id User ID
 529       * @return string|bool The corresponding username
 530       */
 531  	public static function whoIs( $id ) {
 532          return UserCache::singleton()->getProp( $id, 'name' );
 533      }
 534  
 535      /**
 536       * Get the real name of a user given their user ID
 537       *
 538       * @param int $id User ID
 539       * @return string|bool The corresponding user's real name
 540       */
 541  	public static function whoIsReal( $id ) {
 542          return UserCache::singleton()->getProp( $id, 'real_name' );
 543      }
 544  
 545      /**
 546       * Get database id given a user name
 547       * @param string $name Username
 548       * @return int|null The corresponding user's ID, or null if user is nonexistent
 549       */
 550  	public static function idFromName( $name ) {
 551          $nt = Title::makeTitleSafe( NS_USER, $name );
 552          if ( is_null( $nt ) ) {
 553              // Illegal name
 554              return null;
 555          }
 556  
 557          if ( isset( self::$idCacheByName[$name] ) ) {
 558              return self::$idCacheByName[$name];
 559          }
 560  
 561          $dbr = wfGetDB( DB_SLAVE );
 562          $s = $dbr->selectRow(
 563              'user',
 564              array( 'user_id' ),
 565              array( 'user_name' => $nt->getText() ),
 566              __METHOD__
 567          );
 568  
 569          if ( $s === false ) {
 570              $result = null;
 571          } else {
 572              $result = $s->user_id;
 573          }
 574  
 575          self::$idCacheByName[$name] = $result;
 576  
 577          if ( count( self::$idCacheByName ) > 1000 ) {
 578              self::$idCacheByName = array();
 579          }
 580  
 581          return $result;
 582      }
 583  
 584      /**
 585       * Reset the cache used in idFromName(). For use in tests.
 586       */
 587  	public static function resetIdByNameCache() {
 588          self::$idCacheByName = array();
 589      }
 590  
 591      /**
 592       * Does the string match an anonymous IPv4 address?
 593       *
 594       * This function exists for username validation, in order to reject
 595       * usernames which are similar in form to IP addresses. Strings such
 596       * as 300.300.300.300 will return true because it looks like an IP
 597       * address, despite not being strictly valid.
 598       *
 599       * We match "\d{1,3}\.\d{1,3}\.\d{1,3}\.xxx" as an anonymous IP
 600       * address because the usemod software would "cloak" anonymous IP
 601       * addresses like this, if we allowed accounts like this to be created
 602       * new users could get the old edits of these anonymous users.
 603       *
 604       * @param string $name Name to match
 605       * @return bool
 606       */
 607  	public static function isIP( $name ) {
 608          return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name )
 609              || IP::isIPv6( $name );
 610      }
 611  
 612      /**
 613       * Is the input a valid username?
 614       *
 615       * Checks if the input is a valid username, we don't want an empty string,
 616       * an IP address, anything that contains slashes (would mess up subpages),
 617       * is longer than the maximum allowed username size or doesn't begin with
 618       * a capital letter.
 619       *
 620       * @param string $name Name to match
 621       * @return bool
 622       */
 623  	public static function isValidUserName( $name ) {
 624          global $wgContLang, $wgMaxNameChars;
 625  
 626          if ( $name == ''
 627          || User::isIP( $name )
 628          || strpos( $name, '/' ) !== false
 629          || strlen( $name ) > $wgMaxNameChars
 630          || $name != $wgContLang->ucfirst( $name ) ) {
 631              wfDebugLog( 'username', __METHOD__ .
 632                  ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
 633              return false;
 634          }
 635  
 636          // Ensure that the name can't be misresolved as a different title,
 637          // such as with extra namespace keys at the start.
 638          $parsed = Title::newFromText( $name );
 639          if ( is_null( $parsed )
 640              || $parsed->getNamespace()
 641              || strcmp( $name, $parsed->getPrefixedText() ) ) {
 642              wfDebugLog( 'username', __METHOD__ .
 643                  ": '$name' invalid due to ambiguous prefixes" );
 644              return false;
 645          }
 646  
 647          // Check an additional blacklist of troublemaker characters.
 648          // Should these be merged into the title char list?
 649          $unicodeBlacklist = '/[' .
 650              '\x{0080}-\x{009f}' . # iso-8859-1 control chars
 651              '\x{00a0}' .          # non-breaking space
 652              '\x{2000}-\x{200f}' . # various whitespace
 653              '\x{2028}-\x{202f}' . # breaks and control chars
 654              '\x{3000}' .          # ideographic space
 655              '\x{e000}-\x{f8ff}' . # private use
 656              ']/u';
 657          if ( preg_match( $unicodeBlacklist, $name ) ) {
 658              wfDebugLog( 'username', __METHOD__ .
 659                  ": '$name' invalid due to blacklisted characters" );
 660              return false;
 661          }
 662  
 663          return true;
 664      }
 665  
 666      /**
 667       * Usernames which fail to pass this function will be blocked
 668       * from user login and new account registrations, but may be used
 669       * internally by batch processes.
 670       *
 671       * If an account already exists in this form, login will be blocked
 672       * by a failure to pass this function.
 673       *
 674       * @param string $name Name to match
 675       * @return bool
 676       */
 677  	public static function isUsableName( $name ) {
 678          global $wgReservedUsernames;
 679          // Must be a valid username, obviously ;)
 680          if ( !self::isValidUserName( $name ) ) {
 681              return false;
 682          }
 683  
 684          static $reservedUsernames = false;
 685          if ( !$reservedUsernames ) {
 686              $reservedUsernames = $wgReservedUsernames;
 687              wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
 688          }
 689  
 690          // Certain names may be reserved for batch processes.
 691          foreach ( $reservedUsernames as $reserved ) {
 692              if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
 693                  $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text();
 694              }
 695              if ( $reserved == $name ) {
 696                  return false;
 697              }
 698          }
 699          return true;
 700      }
 701  
 702      /**
 703       * Usernames which fail to pass this function will be blocked
 704       * from new account registrations, but may be used internally
 705       * either by batch processes or by user accounts which have
 706       * already been created.
 707       *
 708       * Additional blacklisting may be added here rather than in
 709       * isValidUserName() to avoid disrupting existing accounts.
 710       *
 711       * @param string $name String to match
 712       * @return bool
 713       */
 714  	public static function isCreatableName( $name ) {
 715          global $wgInvalidUsernameCharacters;
 716  
 717          // Ensure that the username isn't longer than 235 bytes, so that
 718          // (at least for the builtin skins) user javascript and css files
 719          // will work. (bug 23080)
 720          if ( strlen( $name ) > 235 ) {
 721              wfDebugLog( 'username', __METHOD__ .
 722                  ": '$name' invalid due to length" );
 723              return false;
 724          }
 725  
 726          // Preg yells if you try to give it an empty string
 727          if ( $wgInvalidUsernameCharacters !== '' ) {
 728              if ( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
 729                  wfDebugLog( 'username', __METHOD__ .
 730                      ": '$name' invalid due to wgInvalidUsernameCharacters" );
 731                  return false;
 732              }
 733          }
 734  
 735          return self::isUsableName( $name );
 736      }
 737  
 738      /**
 739       * Is the input a valid password for this user?
 740       *
 741       * @param string $password Desired password
 742       * @return bool
 743       */
 744  	public function isValidPassword( $password ) {
 745          //simple boolean wrapper for getPasswordValidity
 746          return $this->getPasswordValidity( $password ) === true;
 747      }
 748  
 749  
 750      /**
 751       * Given unvalidated password input, return error message on failure.
 752       *
 753       * @param string $password Desired password
 754       * @return bool|string|array True on success, string or array of error message on failure
 755       */
 756  	public function getPasswordValidity( $password ) {
 757          $result = $this->checkPasswordValidity( $password );
 758          if ( $result->isGood() ) {
 759              return true;
 760          } else {
 761              $messages = array();
 762              foreach ( $result->getErrorsByType( 'error' ) as $error ) {
 763                  $messages[] = $error['message'];
 764              }
 765              foreach ( $result->getErrorsByType( 'warning' ) as $warning ) {
 766                  $messages[] = $warning['message'];
 767              }
 768              if ( count( $messages ) === 1 ) {
 769                  return $messages[0];
 770              }
 771              return $messages;
 772          }
 773      }
 774  
 775      /**
 776       * Check if this is a valid password for this user. Status will be good if
 777       * the password is valid, or have an array of error messages if not.
 778       *
 779       * @param string $password Desired password
 780       * @return Status
 781       * @since 1.23
 782       */
 783  	public function checkPasswordValidity( $password ) {
 784          global $wgMinimalPasswordLength, $wgContLang;
 785  
 786          static $blockedLogins = array(
 787              'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589
 788              'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605
 789          );
 790  
 791          $status = Status::newGood();
 792  
 793          $result = false; //init $result to false for the internal checks
 794  
 795          if ( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) ) {
 796              $status->error( $result );
 797              return $status;
 798          }
 799  
 800          if ( $result === false ) {
 801              if ( strlen( $password ) < $wgMinimalPasswordLength ) {
 802                  $status->error( 'passwordtooshort', $wgMinimalPasswordLength );
 803                  return $status;
 804              } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
 805                  $status->error( 'password-name-match' );
 806                  return $status;
 807              } elseif ( isset( $blockedLogins[$this->getName()] )
 808                  && $password == $blockedLogins[$this->getName()]
 809              ) {
 810                  $status->error( 'password-login-forbidden' );
 811                  return $status;
 812              } else {
 813                  //it seems weird returning a Good status here, but this is because of the
 814                  //initialization of $result to false above. If the hook is never run or it
 815                  //doesn't modify $result, then we will likely get down into this if with
 816                  //a valid password.
 817                  return $status;
 818              }
 819          } elseif ( $result === true ) {
 820              return $status;
 821          } else {
 822              $status->error( $result );
 823              return $status; //the isValidPassword hook set a string $result and returned true
 824          }
 825      }
 826  
 827      /**
 828       * Expire a user's password
 829       * @since 1.23
 830       * @param int $ts Optional timestamp to convert, default 0 for the current time
 831       */
 832  	public function expirePassword( $ts = 0 ) {
 833          $this->loadPasswords();
 834          $timestamp = wfTimestamp( TS_MW, $ts );
 835          $this->mPasswordExpires = $timestamp;
 836          $this->saveSettings();
 837      }
 838  
 839      /**
 840       * Clear the password expiration for a user
 841       * @since 1.23
 842       * @param bool $load Ensure user object is loaded first
 843       */
 844  	public function resetPasswordExpiration( $load = true ) {
 845          global $wgPasswordExpirationDays;
 846          if ( $load ) {
 847              $this->load();
 848          }
 849          $newExpire = null;
 850          if ( $wgPasswordExpirationDays ) {
 851              $newExpire = wfTimestamp(
 852                  TS_MW,
 853                  time() + ( $wgPasswordExpirationDays * 24 * 3600 )
 854              );
 855          }
 856          // Give extensions a chance to force an expiration
 857          wfRunHooks( 'ResetPasswordExpiration', array( $this, &$newExpire ) );
 858          $this->mPasswordExpires = $newExpire;
 859      }
 860  
 861      /**
 862       * Check if the user's password is expired.
 863       * TODO: Put this and password length into a PasswordPolicy object
 864       * @since 1.23
 865       * @return string|bool The expiration type, or false if not expired
 866       *     hard: A password change is required to login
 867       *    soft: Allow login, but encourage password change
 868       *    false: Password is not expired
 869       */
 870  	public function getPasswordExpired() {
 871          global $wgPasswordExpireGrace;
 872          $expired = false;
 873          $now = wfTimestamp();
 874          $expiration = $this->getPasswordExpireDate();
 875          $expUnix = wfTimestamp( TS_UNIX, $expiration );
 876          if ( $expiration !== null && $expUnix < $now ) {
 877              $expired = ( $expUnix + $wgPasswordExpireGrace < $now ) ? 'hard' : 'soft';
 878          }
 879          return $expired;
 880      }
 881  
 882      /**
 883       * Get this user's password expiration date. Since this may be using
 884       * the cached User object, we assume that whatever mechanism is setting
 885       * the expiration date is also expiring the User cache.
 886       * @since 1.23
 887       * @return string|bool The datestamp of the expiration, or null if not set
 888       */
 889  	public function getPasswordExpireDate() {
 890          $this->load();
 891          return $this->mPasswordExpires;
 892      }
 893  
 894      /**
 895       * Given unvalidated user input, return a canonical username, or false if
 896       * the username is invalid.
 897       * @param string $name User input
 898       * @param string|bool $validate Type of validation to use:
 899       *   - false        No validation
 900       *   - 'valid'      Valid for batch processes
 901       *   - 'usable'     Valid for batch processes and login
 902       *   - 'creatable'  Valid for batch processes, login and account creation
 903       *
 904       * @throws MWException
 905       * @return bool|string
 906       */
 907  	public static function getCanonicalName( $name, $validate = 'valid' ) {
 908          // Force usernames to capital
 909          global $wgContLang;
 910          $name = $wgContLang->ucfirst( $name );
 911  
 912          # Reject names containing '#'; these will be cleaned up
 913          # with title normalisation, but then it's too late to
 914          # check elsewhere
 915          if ( strpos( $name, '#' ) !== false ) {
 916              return false;
 917          }
 918  
 919          // Clean up name according to title rules,
 920          // but only when validation is requested (bug 12654)
 921          $t = ( $validate !== false ) ?
 922              Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
 923          // Check for invalid titles
 924          if ( is_null( $t ) ) {
 925              return false;
 926          }
 927  
 928          // Reject various classes of invalid names
 929          global $wgAuth;
 930          $name = $wgAuth->getCanonicalName( $t->getText() );
 931  
 932          switch ( $validate ) {
 933              case false:
 934                  break;
 935              case 'valid':
 936                  if ( !User::isValidUserName( $name ) ) {
 937                      $name = false;
 938                  }
 939                  break;
 940              case 'usable':
 941                  if ( !User::isUsableName( $name ) ) {
 942                      $name = false;
 943                  }
 944                  break;
 945              case 'creatable':
 946                  if ( !User::isCreatableName( $name ) ) {
 947                      $name = false;
 948                  }
 949                  break;
 950              default:
 951                  throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ );
 952          }
 953          return $name;
 954      }
 955  
 956      /**
 957       * Count the number of edits of a user
 958       *
 959       * @param int $uid User ID to check
 960       * @return int The user's edit count
 961       *
 962       * @deprecated since 1.21 in favour of User::getEditCount
 963       */
 964  	public static function edits( $uid ) {
 965          wfDeprecated( __METHOD__, '1.21' );
 966          $user = self::newFromId( $uid );
 967          return $user->getEditCount();
 968      }
 969  
 970      /**
 971       * Return a random password.
 972       *
 973       * @return string New random password
 974       */
 975  	public static function randomPassword() {
 976          global $wgMinimalPasswordLength;
 977          // Decide the final password length based on our min password length,
 978          // stopping at a minimum of 10 chars.
 979          $length = max( 10, $wgMinimalPasswordLength );
 980          // Multiply by 1.25 to get the number of hex characters we need
 981          $length = $length * 1.25;
 982          // Generate random hex chars
 983          $hex = MWCryptRand::generateHex( $length );
 984          // Convert from base 16 to base 32 to get a proper password like string
 985          return wfBaseConvert( $hex, 16, 32 );
 986      }
 987  
 988      /**
 989       * Set cached properties to default.
 990       *
 991       * @note This no longer clears uncached lazy-initialised properties;
 992       *       the constructor does that instead.
 993       *
 994       * @param string|bool $name
 995       */
 996  	public function loadDefaults( $name = false ) {
 997          wfProfileIn( __METHOD__ );
 998  
 999          $passwordFactory = self::getPasswordFactory();
1000  
1001          $this->mId = 0;
1002          $this->mName = $name;
1003          $this->mRealName = '';
1004          $this->mPassword = $passwordFactory->newFromCiphertext( null );
1005          $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
1006          $this->mNewpassTime = null;
1007          $this->mEmail = '';
1008          $this->mOptionOverrides = null;
1009          $this->mOptionsLoaded = false;
1010  
1011          $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
1012          if ( $loggedOut !== null ) {
1013              $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
1014          } else {
1015              $this->mTouched = '1'; # Allow any pages to be cached
1016          }
1017  
1018          $this->mToken = null; // Don't run cryptographic functions till we need a token
1019          $this->mEmailAuthenticated = null;
1020          $this->mEmailToken = '';
1021          $this->mEmailTokenExpires = null;
1022          $this->mPasswordExpires = null;
1023          $this->resetPasswordExpiration( false );
1024          $this->mRegistration = wfTimestamp( TS_MW );
1025          $this->mGroups = array();
1026  
1027          wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
1028  
1029          wfProfileOut( __METHOD__ );
1030      }
1031  
1032      /**
1033       * Return whether an item has been loaded.
1034       *
1035       * @param string $item Item to check. Current possibilities:
1036       *   - id
1037       *   - name
1038       *   - realname
1039       * @param string $all 'all' to check if the whole object has been loaded
1040       *   or any other string to check if only the item is available (e.g.
1041       *   for optimisation)
1042       * @return bool
1043       */
1044  	public function isItemLoaded( $item, $all = 'all' ) {
1045          return ( $this->mLoadedItems === true && $all === 'all' ) ||
1046              ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
1047      }
1048  
1049      /**
1050       * Set that an item has been loaded
1051       *
1052       * @param string $item
1053       */
1054  	protected function setItemLoaded( $item ) {
1055          if ( is_array( $this->mLoadedItems ) ) {
1056              $this->mLoadedItems[$item] = true;
1057          }
1058      }
1059  
1060      /**
1061       * Load user data from the session or login cookie.
1062       * @return bool True if the user is logged in, false otherwise.
1063       */
1064  	private function loadFromSession() {
1065          $result = null;
1066          wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
1067          if ( $result !== null ) {
1068              return $result;
1069          }
1070  
1071          $request = $this->getRequest();
1072  
1073          $cookieId = $request->getCookie( 'UserID' );
1074          $sessId = $request->getSessionData( 'wsUserID' );
1075  
1076          if ( $cookieId !== null ) {
1077              $sId = intval( $cookieId );
1078              if ( $sessId !== null && $cookieId != $sessId ) {
1079                  wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
1080                      cookie user ID ($sId) don't match!" );
1081                  return false;
1082              }
1083              $request->setSessionData( 'wsUserID', $sId );
1084          } elseif ( $sessId !== null && $sessId != 0 ) {
1085              $sId = $sessId;
1086          } else {
1087              return false;
1088          }
1089  
1090          if ( $request->getSessionData( 'wsUserName' ) !== null ) {
1091              $sName = $request->getSessionData( 'wsUserName' );
1092          } elseif ( $request->getCookie( 'UserName' ) !== null ) {
1093              $sName = $request->getCookie( 'UserName' );
1094              $request->setSessionData( 'wsUserName', $sName );
1095          } else {
1096              return false;
1097          }
1098  
1099          $proposedUser = User::newFromId( $sId );
1100          if ( !$proposedUser->isLoggedIn() ) {
1101              // Not a valid ID
1102              return false;
1103          }
1104  
1105          global $wgBlockDisablesLogin;
1106          if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
1107              // User blocked and we've disabled blocked user logins
1108              return false;
1109          }
1110  
1111          if ( $request->getSessionData( 'wsToken' ) ) {
1112              $passwordCorrect =
1113                  ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
1114              $from = 'session';
1115          } elseif ( $request->getCookie( 'Token' ) ) {
1116              # Get the token from DB/cache and clean it up to remove garbage padding.
1117              # This deals with historical problems with bugs and the default column value.
1118              $token = rtrim( $proposedUser->getToken( false ) ); // correct token
1119              // Make comparison in constant time (bug 61346)
1120              $passwordCorrect = strlen( $token )
1121                  && hash_equals( $token, $request->getCookie( 'Token' ) );
1122              $from = 'cookie';
1123          } else {
1124              // No session or persistent login cookie
1125              return false;
1126          }
1127  
1128          if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
1129              $this->loadFromUserObject( $proposedUser );
1130              $request->setSessionData( 'wsToken', $this->mToken );
1131              wfDebug( "User: logged in from $from\n" );
1132              return true;
1133          } else {
1134              // Invalid credentials
1135              wfDebug( "User: can't log in from $from, invalid credentials\n" );
1136              return false;
1137          }
1138      }
1139  
1140      /**
1141       * Load user and user_group data from the database.
1142       * $this->mId must be set, this is how the user is identified.
1143       *
1144       * @param int $flags Supports User::READ_LOCKING
1145       * @return bool True if the user exists, false if the user is anonymous
1146       */
1147  	public function loadFromDatabase( $flags = 0 ) {
1148          // Paranoia
1149          $this->mId = intval( $this->mId );
1150  
1151          // Anonymous user
1152          if ( !$this->mId ) {
1153              $this->loadDefaults();
1154              return false;
1155          }
1156  
1157          $dbr = wfGetDB( DB_MASTER );
1158          $s = $dbr->selectRow(
1159              'user',
1160              self::selectFields(),
1161              array( 'user_id' => $this->mId ),
1162              __METHOD__,
1163              ( $flags & self::READ_LOCKING == self::READ_LOCKING )
1164                  ? array( 'LOCK IN SHARE MODE' )
1165                  : array()
1166          );
1167  
1168          wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
1169  
1170          if ( $s !== false ) {
1171              // Initialise user table data
1172              $this->loadFromRow( $s );
1173              $this->mGroups = null; // deferred
1174              $this->getEditCount(); // revalidation for nulls
1175              return true;
1176          } else {
1177              // Invalid user_id
1178              $this->mId = 0;
1179              $this->loadDefaults();
1180              return false;
1181          }
1182      }
1183  
1184      /**
1185       * Initialize this object from a row from the user table.
1186       *
1187       * @param stdClass $row Row from the user table to load.
1188       * @param array $data Further user data to load into the object
1189       *
1190       *    user_groups        Array with groups out of the user_groups table
1191       *    user_properties        Array with properties out of the user_properties table
1192       */
1193  	public function loadFromRow( $row, $data = null ) {
1194          $all = true;
1195          $passwordFactory = self::getPasswordFactory();
1196  
1197          $this->mGroups = null; // deferred
1198  
1199          if ( isset( $row->user_name ) ) {
1200              $this->mName = $row->user_name;
1201              $this->mFrom = 'name';
1202              $this->setItemLoaded( 'name' );
1203          } else {
1204              $all = false;
1205          }
1206  
1207          if ( isset( $row->user_real_name ) ) {
1208              $this->mRealName = $row->user_real_name;
1209              $this->setItemLoaded( 'realname' );
1210          } else {
1211              $all = false;
1212          }
1213  
1214          if ( isset( $row->user_id ) ) {
1215              $this->mId = intval( $row->user_id );
1216              $this->mFrom = 'id';
1217              $this->setItemLoaded( 'id' );
1218          } else {
1219              $all = false;
1220          }
1221  
1222          if ( isset( $row->user_editcount ) ) {
1223              $this->mEditCount = $row->user_editcount;
1224          } else {
1225              $all = false;
1226          }
1227  
1228          if ( isset( $row->user_password ) ) {
1229              // Check for *really* old password hashes that don't even have a type
1230              // The old hash format was just an md5 hex hash, with no type information
1231              if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
1232                  $row->user_password = ":A:{$this->mId}:{$row->user_password}";
1233              }
1234  
1235              try {
1236                  $this->mPassword = $passwordFactory->newFromCiphertext( $row->user_password );
1237              } catch ( PasswordError $e ) {
1238                  wfDebug( 'Invalid password hash found in database.' );
1239                  $this->mPassword = $passwordFactory->newFromCiphertext( null );
1240              }
1241  
1242              try {
1243                  $this->mNewpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
1244              } catch ( PasswordError $e ) {
1245                  wfDebug( 'Invalid password hash found in database.' );
1246                  $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
1247              }
1248  
1249              $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
1250              $this->mPasswordExpires = wfTimestampOrNull( TS_MW, $row->user_password_expires );
1251          }
1252  
1253          if ( isset( $row->user_email ) ) {
1254              $this->mEmail = $row->user_email;
1255              $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
1256              $this->mToken = $row->user_token;
1257              if ( $this->mToken == '' ) {
1258                  $this->mToken = null;
1259              }
1260              $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
1261              $this->mEmailToken = $row->user_email_token;
1262              $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
1263              $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
1264          } else {
1265              $all = false;
1266          }
1267  
1268          if ( $all ) {
1269              $this->mLoadedItems = true;
1270          }
1271  
1272          if ( is_array( $data ) ) {
1273              if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
1274                  $this->mGroups = $data['user_groups'];
1275              }
1276              if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
1277                  $this->loadOptions( $data['user_properties'] );
1278              }
1279          }
1280      }
1281  
1282      /**
1283       * Load the data for this user object from another user object.
1284       *
1285       * @param User $user
1286       */
1287  	protected function loadFromUserObject( $user ) {
1288          $user->load();
1289          $user->loadGroups();
1290          $user->loadOptions();
1291          foreach ( self::$mCacheVars as $var ) {
1292              $this->$var = $user->$var;
1293          }
1294      }
1295  
1296      /**
1297       * Load the groups from the database if they aren't already loaded.
1298       */
1299  	private function loadGroups() {
1300          if ( is_null( $this->mGroups ) ) {
1301              $dbr = wfGetDB( DB_MASTER );
1302              $res = $dbr->select( 'user_groups',
1303                  array( 'ug_group' ),
1304                  array( 'ug_user' => $this->mId ),
1305                  __METHOD__ );
1306              $this->mGroups = array();
1307              foreach ( $res as $row ) {
1308                  $this->mGroups[] = $row->ug_group;
1309              }
1310          }
1311      }
1312  
1313      /**
1314       * Load the user's password hashes from the database
1315       *
1316       * This is usually called in a scenario where the actual User object was
1317       * loaded from the cache, and then password comparison needs to be performed.
1318       * Password hashes are not stored in memcached.
1319       *
1320       * @since 1.24
1321       */
1322  	private function loadPasswords() {
1323          if ( $this->getId() !== 0 && ( $this->mPassword === null || $this->mNewpassword === null ) ) {
1324              $this->loadFromRow( wfGetDB( DB_MASTER )->selectRow(
1325                      'user',
1326                      array( 'user_password', 'user_newpassword', 'user_newpass_time', 'user_password_expires' ),
1327                      array( 'user_id' => $this->getId() ),
1328                      __METHOD__
1329                  ) );
1330          }
1331      }
1332  
1333      /**
1334       * Add the user to the group if he/she meets given criteria.
1335       *
1336       * Contrary to autopromotion by \ref $wgAutopromote, the group will be
1337       *   possible to remove manually via Special:UserRights. In such case it
1338       *   will not be re-added automatically. The user will also not lose the
1339       *   group if they no longer meet the criteria.
1340       *
1341       * @param string $event Key in $wgAutopromoteOnce (each one has groups/criteria)
1342       *
1343       * @return array Array of groups the user has been promoted to.
1344       *
1345       * @see $wgAutopromoteOnce
1346       */
1347  	public function addAutopromoteOnceGroups( $event ) {
1348          global $wgAutopromoteOnceLogInRC, $wgAuth;
1349  
1350          $toPromote = array();
1351          if ( $this->getId() ) {
1352              $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
1353              if ( count( $toPromote ) ) {
1354                  $oldGroups = $this->getGroups(); // previous groups
1355  
1356                  foreach ( $toPromote as $group ) {
1357                      $this->addGroup( $group );
1358                  }
1359                  // update groups in external authentication database
1360                  $wgAuth->updateExternalDBGroups( $this, $toPromote );
1361  
1362                  $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
1363  
1364                  $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
1365                  $logEntry->setPerformer( $this );
1366                  $logEntry->setTarget( $this->getUserPage() );
1367                  $logEntry->setParameters( array(
1368                      '4::oldgroups' => $oldGroups,
1369                      '5::newgroups' => $newGroups,
1370                  ) );
1371                  $logid = $logEntry->insert();
1372                  if ( $wgAutopromoteOnceLogInRC ) {
1373                      $logEntry->publish( $logid );
1374                  }
1375              }
1376          }
1377          return $toPromote;
1378      }
1379  
1380      /**
1381       * Clear various cached data stored in this object. The cache of the user table
1382       * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
1383       *
1384       * @param bool|string $reloadFrom Reload user and user_groups table data from a
1385       *   given source. May be "name", "id", "defaults", "session", or false for no reload.
1386       */
1387  	public function clearInstanceCache( $reloadFrom = false ) {
1388          $this->mNewtalk = -1;
1389          $this->mDatePreference = null;
1390          $this->mBlockedby = -1; # Unset
1391          $this->mHash = false;
1392          $this->mRights = null;
1393          $this->mEffectiveGroups = null;
1394          $this->mImplicitGroups = null;
1395          $this->mGroups = null;
1396          $this->mOptions = null;
1397          $this->mOptionsLoaded = false;
1398          $this->mEditCount = null;
1399  
1400          if ( $reloadFrom ) {
1401              $this->mLoadedItems = array();
1402              $this->mFrom = $reloadFrom;
1403          }
1404      }
1405  
1406      /**
1407       * Combine the language default options with any site-specific options
1408       * and add the default language variants.
1409       *
1410       * @return array Array of String options
1411       */
1412  	public static function getDefaultOptions() {
1413          global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
1414  
1415          static $defOpt = null;
1416          if ( !defined( 'MW_PHPUNIT_TEST' ) && $defOpt !== null ) {
1417              // Disabling this for the unit tests, as they rely on being able to change $wgContLang
1418              // mid-request and see that change reflected in the return value of this function.
1419              // Which is insane and would never happen during normal MW operation
1420              return $defOpt;
1421          }
1422  
1423          $defOpt = $wgDefaultUserOptions;
1424          // Default language setting
1425          $defOpt['language'] = $wgContLang->getCode();
1426          foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
1427              $defOpt[$langCode == $wgContLang->getCode() ? 'variant' : "variant-$langCode"] = $langCode;
1428          }
1429          foreach ( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
1430              $defOpt['searchNs' . $nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
1431          }
1432          $defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
1433  
1434          wfRunHooks( 'UserGetDefaultOptions', array( &$defOpt ) );
1435  
1436          return $defOpt;
1437      }
1438  
1439      /**
1440       * Get a given default option value.
1441       *
1442       * @param string $opt Name of option to retrieve
1443       * @return string Default option value
1444       */
1445  	public static function getDefaultOption( $opt ) {
1446          $defOpts = self::getDefaultOptions();
1447          if ( isset( $defOpts[$opt] ) ) {
1448              return $defOpts[$opt];
1449          } else {
1450              return null;
1451          }
1452      }
1453  
1454      /**
1455       * Get blocking information
1456       * @param bool $bFromSlave Whether to check the slave database first.
1457       *   To improve performance, non-critical checks are done against slaves.
1458       *   Check when actually saving should be done against master.
1459       */
1460  	private function getBlockedStatus( $bFromSlave = true ) {
1461          global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff;
1462  
1463          if ( -1 != $this->mBlockedby ) {
1464              return;
1465          }
1466  
1467          wfProfileIn( __METHOD__ );
1468          wfDebug( __METHOD__ . ": checking...\n" );
1469  
1470          // Initialize data...
1471          // Otherwise something ends up stomping on $this->mBlockedby when
1472          // things get lazy-loaded later, causing false positive block hits
1473          // due to -1 !== 0. Probably session-related... Nothing should be
1474          // overwriting mBlockedby, surely?
1475          $this->load();
1476  
1477          # We only need to worry about passing the IP address to the Block generator if the
1478          # user is not immune to autoblocks/hardblocks, and they are the current user so we
1479          # know which IP address they're actually coming from
1480          if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->getID() == $wgUser->getID() ) {
1481              $ip = $this->getRequest()->getIP();
1482          } else {
1483              $ip = null;
1484          }
1485  
1486          // User/IP blocking
1487          $block = Block::newFromTarget( $this, $ip, !$bFromSlave );
1488  
1489          // Proxy blocking
1490          if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
1491              && !in_array( $ip, $wgProxyWhitelist )
1492          ) {
1493              // Local list
1494              if ( self::isLocallyBlockedProxy( $ip ) ) {
1495                  $block = new Block;
1496                  $block->setBlocker( wfMessage( 'proxyblocker' )->text() );
1497                  $block->mReason = wfMessage( 'proxyblockreason' )->text();
1498                  $block->setTarget( $ip );
1499              } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
1500                  $block = new Block;
1501                  $block->setBlocker( wfMessage( 'sorbs' )->text() );
1502                  $block->mReason = wfMessage( 'sorbsreason' )->text();
1503                  $block->setTarget( $ip );
1504              }
1505          }
1506  
1507          // (bug 23343) Apply IP blocks to the contents of XFF headers, if enabled
1508          if ( !$block instanceof Block
1509              && $wgApplyIpBlocksToXff
1510              && $ip !== null
1511              && !$this->isAllowed( 'proxyunbannable' )
1512              && !in_array( $ip, $wgProxyWhitelist )
1513          ) {
1514              $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
1515              $xff = array_map( 'trim', explode( ',', $xff ) );
1516              $xff = array_diff( $xff, array( $ip ) );
1517              $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$bFromSlave );
1518              $block = Block::chooseBlock( $xffblocks, $xff );
1519              if ( $block instanceof Block ) {
1520                  # Mangle the reason to alert the user that the block
1521                  # originated from matching the X-Forwarded-For header.
1522                  $block->mReason = wfMessage( 'xffblockreason', $block->mReason )->text();
1523              }
1524          }
1525  
1526          if ( $block instanceof Block ) {
1527              wfDebug( __METHOD__ . ": Found block.\n" );
1528              $this->mBlock = $block;
1529              $this->mBlockedby = $block->getByName();
1530              $this->mBlockreason = $block->mReason;
1531              $this->mHideName = $block->mHideName;
1532              $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
1533          } else {
1534              $this->mBlockedby = '';
1535              $this->mHideName = 0;
1536              $this->mAllowUsertalk = false;
1537          }
1538  
1539          // Extensions
1540          wfRunHooks( 'GetBlockedStatus', array( &$this ) );
1541  
1542          wfProfileOut( __METHOD__ );
1543      }
1544  
1545      /**
1546       * Whether the given IP is in a DNS blacklist.
1547       *
1548       * @param string $ip IP to check
1549       * @param bool $checkWhitelist Whether to check the whitelist first
1550       * @return bool True if blacklisted.
1551       */
1552  	public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
1553          global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist;
1554  
1555          if ( !$wgEnableDnsBlacklist ) {
1556              return false;
1557          }
1558  
1559          if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) {
1560              return false;
1561          }
1562  
1563          return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls );
1564      }
1565  
1566      /**
1567       * Whether the given IP is in a given DNS blacklist.
1568       *
1569       * @param string $ip IP to check
1570       * @param string|array $bases Array of Strings: URL of the DNS blacklist
1571       * @return bool True if blacklisted.
1572       */
1573  	public function inDnsBlacklist( $ip, $bases ) {
1574          wfProfileIn( __METHOD__ );
1575  
1576          $found = false;
1577          // @todo FIXME: IPv6 ???  (http://bugs.php.net/bug.php?id=33170)
1578          if ( IP::isIPv4( $ip ) ) {
1579              // Reverse IP, bug 21255
1580              $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
1581  
1582              foreach ( (array)$bases as $base ) {
1583                  // Make hostname
1584                  // If we have an access key, use that too (ProjectHoneypot, etc.)
1585                  if ( is_array( $base ) ) {
1586                      if ( count( $base ) >= 2 ) {
1587                          // Access key is 1, base URL is 0
1588                          $host = "{$base[1]}.$ipReversed.{$base[0]}";
1589                      } else {
1590                          $host = "$ipReversed.{$base[0]}";
1591                      }
1592                  } else {
1593                      $host = "$ipReversed.$base";
1594                  }
1595  
1596                  // Send query
1597                  $ipList = gethostbynamel( $host );
1598  
1599                  if ( $ipList ) {
1600                      wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!" );
1601                      $found = true;
1602                      break;
1603                  } else {
1604                      wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base." );
1605                  }
1606              }
1607          }
1608  
1609          wfProfileOut( __METHOD__ );
1610          return $found;
1611      }
1612  
1613      /**
1614       * Check if an IP address is in the local proxy list
1615       *
1616       * @param string $ip
1617       *
1618       * @return bool
1619       */
1620  	public static function isLocallyBlockedProxy( $ip ) {
1621          global $wgProxyList;
1622  
1623          if ( !$wgProxyList ) {
1624              return false;
1625          }
1626          wfProfileIn( __METHOD__ );
1627  
1628          if ( !is_array( $wgProxyList ) ) {
1629              // Load from the specified file
1630              $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
1631          }
1632  
1633          if ( !is_array( $wgProxyList ) ) {
1634              $ret = false;
1635          } elseif ( array_search( $ip, $wgProxyList ) !== false ) {
1636              $ret = true;
1637          } elseif ( array_key_exists( $ip, $wgProxyList ) ) {
1638              // Old-style flipped proxy list
1639              $ret = true;
1640          } else {
1641              $ret = false;
1642          }
1643          wfProfileOut( __METHOD__ );
1644          return $ret;
1645      }
1646  
1647      /**
1648       * Is this user subject to rate limiting?
1649       *
1650       * @return bool True if rate limited
1651       */
1652  	public function isPingLimitable() {
1653          global $wgRateLimitsExcludedIPs;
1654          if ( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
1655              // No other good way currently to disable rate limits
1656              // for specific IPs. :P
1657              // But this is a crappy hack and should die.
1658              return false;
1659          }
1660          return !$this->isAllowed( 'noratelimit' );
1661      }
1662  
1663      /**
1664       * Primitive rate limits: enforce maximum actions per time period
1665       * to put a brake on flooding.
1666       *
1667       * The method generates both a generic profiling point and a per action one
1668       * (suffix being "-$action".
1669       *
1670       * @note When using a shared cache like memcached, IP-address
1671       * last-hit counters will be shared across wikis.
1672       *
1673       * @param string $action Action to enforce; 'edit' if unspecified
1674       * @param int $incrBy Positive amount to increment counter by [defaults to 1]
1675       * @return bool True if a rate limiter was tripped
1676       */
1677  	public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
1678          // Call the 'PingLimiter' hook
1679          $result = false;
1680          if ( !wfRunHooks( 'PingLimiter', array( &$this, $action, &$result, $incrBy ) ) ) {
1681              return $result;
1682          }
1683  
1684          global $wgRateLimits;
1685          if ( !isset( $wgRateLimits[$action] ) ) {
1686              return false;
1687          }
1688  
1689          // Some groups shouldn't trigger the ping limiter, ever
1690          if ( !$this->isPingLimitable() ) {
1691              return false;
1692          }
1693  
1694          global $wgMemc;
1695          wfProfileIn( __METHOD__ );
1696          wfProfileIn( __METHOD__ . '-' . $action );
1697  
1698          $limits = $wgRateLimits[$action];
1699          $keys = array();
1700          $id = $this->getId();
1701          $userLimit = false;
1702  
1703          if ( isset( $limits['anon'] ) && $id == 0 ) {
1704              $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1705          }
1706  
1707          if ( isset( $limits['user'] ) && $id != 0 ) {
1708              $userLimit = $limits['user'];
1709          }
1710          if ( $this->isNewbie() ) {
1711              if ( isset( $limits['newbie'] ) && $id != 0 ) {
1712                  $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1713              }
1714              if ( isset( $limits['ip'] ) ) {
1715                  $ip = $this->getRequest()->getIP();
1716                  $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1717              }
1718              if ( isset( $limits['subnet'] ) ) {
1719                  $ip = $this->getRequest()->getIP();
1720                  $matches = array();
1721                  $subnet = false;
1722                  if ( IP::isIPv6( $ip ) ) {
1723                      $parts = IP::parseRange( "$ip/64" );
1724                      $subnet = $parts[0];
1725                  } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1726                      // IPv4
1727                      $subnet = $matches[1];
1728                  }
1729                  if ( $subnet !== false ) {
1730                      $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1731                  }
1732              }
1733          }
1734          // Check for group-specific permissions
1735          // If more than one group applies, use the group with the highest limit
1736          foreach ( $this->getGroups() as $group ) {
1737              if ( isset( $limits[$group] ) ) {
1738                  if ( $userLimit === false || $limits[$group] > $userLimit ) {
1739                      $userLimit = $limits[$group];
1740                  }
1741              }
1742          }
1743          // Set the user limit key
1744          if ( $userLimit !== false ) {
1745              list( $max, $period ) = $userLimit;
1746              wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" );
1747              $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $userLimit;
1748          }
1749  
1750          $triggered = false;
1751          foreach ( $keys as $key => $limit ) {
1752              list( $max, $period ) = $limit;
1753              $summary = "(limit $max in {$period}s)";
1754              $count = $wgMemc->get( $key );
1755              // Already pinged?
1756              if ( $count ) {
1757                  if ( $count >= $max ) {
1758                      wfDebugLog( 'ratelimit', "User '{$this->getName()}' " .
1759                          "(IP {$this->getRequest()->getIP()}) tripped $key at $count $summary" );
1760                      $triggered = true;
1761                  } else {
1762                      wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
1763                  }
1764              } else {
1765                  wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
1766                  if ( $incrBy > 0 ) {
1767                      $wgMemc->add( $key, 0, intval( $period ) ); // first ping
1768                  }
1769              }
1770              if ( $incrBy > 0 ) {
1771                  $wgMemc->incr( $key, $incrBy );
1772              }
1773          }
1774  
1775          wfProfileOut( __METHOD__ . '-' . $action );
1776          wfProfileOut( __METHOD__ );
1777          return $triggered;
1778      }
1779  
1780      /**
1781       * Check if user is blocked
1782       *
1783       * @param bool $bFromSlave Whether to check the slave database instead of
1784       *   the master. Hacked from false due to horrible probs on site.
1785       * @return bool True if blocked, false otherwise
1786       */
1787  	public function isBlocked( $bFromSlave = true ) {
1788          return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' );
1789      }
1790  
1791      /**
1792       * Get the block affecting the user, or null if the user is not blocked
1793       *
1794       * @param bool $bFromSlave Whether to check the slave database instead of the master
1795       * @return Block|null
1796       */
1797  	public function getBlock( $bFromSlave = true ) {
1798          $this->getBlockedStatus( $bFromSlave );
1799          return $this->mBlock instanceof Block ? $this->mBlock : null;
1800      }
1801  
1802      /**
1803       * Check if user is blocked from editing a particular article
1804       *
1805       * @param Title $title Title to check
1806       * @param bool $bFromSlave Whether to check the slave database instead of the master
1807       * @return bool
1808       */
1809  	public function isBlockedFrom( $title, $bFromSlave = false ) {
1810          global $wgBlockAllowsUTEdit;
1811          wfProfileIn( __METHOD__ );
1812  
1813          $blocked = $this->isBlocked( $bFromSlave );
1814          $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
1815          // If a user's name is suppressed, they cannot make edits anywhere
1816          if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName()
1817              && $title->getNamespace() == NS_USER_TALK ) {
1818              $blocked = false;
1819              wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
1820          }
1821  
1822          wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) );
1823  
1824          wfProfileOut( __METHOD__ );
1825          return $blocked;
1826      }
1827  
1828      /**
1829       * If user is blocked, return the name of the user who placed the block
1830       * @return string Name of blocker
1831       */
1832  	public function blockedBy() {
1833          $this->getBlockedStatus();
1834          return $this->mBlockedby;
1835      }
1836  
1837      /**
1838       * If user is blocked, return the specified reason for the block
1839       * @return string Blocking reason
1840       */
1841  	public function blockedFor() {
1842          $this->getBlockedStatus();
1843          return $this->mBlockreason;
1844      }
1845  
1846      /**
1847       * If user is blocked, return the ID for the block
1848       * @return int Block ID
1849       */
1850  	public function getBlockId() {
1851          $this->getBlockedStatus();
1852          return ( $this->mBlock ? $this->mBlock->getId() : false );
1853      }
1854  
1855      /**
1856       * Check if user is blocked on all wikis.
1857       * Do not use for actual edit permission checks!
1858       * This is intended for quick UI checks.
1859       *
1860       * @param string $ip IP address, uses current client if none given
1861       * @return bool True if blocked, false otherwise
1862       */
1863  	public function isBlockedGlobally( $ip = '' ) {
1864          if ( $this->mBlockedGlobally !== null ) {
1865              return $this->mBlockedGlobally;
1866          }
1867          // User is already an IP?
1868          if ( IP::isIPAddress( $this->getName() ) ) {
1869              $ip = $this->getName();
1870          } elseif ( !$ip ) {
1871              $ip = $this->getRequest()->getIP();
1872          }
1873          $blocked = false;
1874          wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
1875          $this->mBlockedGlobally = (bool)$blocked;
1876          return $this->mBlockedGlobally;
1877      }
1878  
1879      /**
1880       * Check if user account is locked
1881       *
1882       * @return bool True if locked, false otherwise
1883       */
1884  	public function isLocked() {
1885          if ( $this->mLocked !== null ) {
1886              return $this->mLocked;
1887          }
1888          global $wgAuth;
1889          $authUser = $wgAuth->getUserInstance( $this );
1890          $this->mLocked = (bool)$authUser->isLocked();
1891          return $this->mLocked;
1892      }
1893  
1894      /**
1895       * Check if user account is hidden
1896       *
1897       * @return bool True if hidden, false otherwise
1898       */
1899  	public function isHidden() {
1900          if ( $this->mHideName !== null ) {
1901              return $this->mHideName;
1902          }
1903          $this->getBlockedStatus();
1904          if ( !$this->mHideName ) {
1905              global $wgAuth;
1906              $authUser = $wgAuth->getUserInstance( $this );
1907              $this->mHideName = (bool)$authUser->isHidden();
1908          }
1909          return $this->mHideName;
1910      }
1911  
1912      /**
1913       * Get the user's ID.
1914       * @return int The user's ID; 0 if the user is anonymous or nonexistent
1915       */
1916  	public function getId() {
1917          if ( $this->mId === null && $this->mName !== null && User::isIP( $this->mName ) ) {
1918              // Special case, we know the user is anonymous
1919              return 0;
1920          } elseif ( !$this->isItemLoaded( 'id' ) ) {
1921              // Don't load if this was initialized from an ID
1922              $this->load();
1923          }
1924          return $this->mId;
1925      }
1926  
1927      /**
1928       * Set the user and reload all fields according to a given ID
1929       * @param int $v User ID to reload
1930       */
1931  	public function setId( $v ) {
1932          $this->mId = $v;
1933          $this->clearInstanceCache( 'id' );
1934      }
1935  
1936      /**
1937       * Get the user name, or the IP of an anonymous user
1938       * @return string User's name or IP address
1939       */
1940  	public function getName() {
1941          if ( $this->isItemLoaded( 'name', 'only' ) ) {
1942              // Special case optimisation
1943              return $this->mName;
1944          } else {
1945              $this->load();
1946              if ( $this->mName === false ) {
1947                  // Clean up IPs
1948                  $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
1949              }
1950              return $this->mName;
1951          }
1952      }
1953  
1954      /**
1955       * Set the user name.
1956       *
1957       * This does not reload fields from the database according to the given
1958       * name. Rather, it is used to create a temporary "nonexistent user" for
1959       * later addition to the database. It can also be used to set the IP
1960       * address for an anonymous user to something other than the current
1961       * remote IP.
1962       *
1963       * @note User::newFromName() has roughly the same function, when the named user
1964       * does not exist.
1965       * @param string $str New user name to set
1966       */
1967  	public function setName( $str ) {
1968          $this->load();
1969          $this->mName = $str;
1970      }
1971  
1972      /**
1973       * Get the user's name escaped by underscores.
1974       * @return string Username escaped by underscores.
1975       */
1976  	public function getTitleKey() {
1977          return str_replace( ' ', '_', $this->getName() );
1978      }
1979  
1980      /**
1981       * Check if the user has new messages.
1982       * @return bool True if the user has new messages
1983       */
1984  	public function getNewtalk() {
1985          $this->load();
1986  
1987          // Load the newtalk status if it is unloaded (mNewtalk=-1)
1988          if ( $this->mNewtalk === -1 ) {
1989              $this->mNewtalk = false; # reset talk page status
1990  
1991              // Check memcached separately for anons, who have no
1992              // entire User object stored in there.
1993              if ( !$this->mId ) {
1994                  global $wgDisableAnonTalk;
1995                  if ( $wgDisableAnonTalk ) {
1996                      // Anon newtalk disabled by configuration.
1997                      $this->mNewtalk = false;
1998                  } else {
1999                      global $wgMemc;
2000                      $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
2001                      $newtalk = $wgMemc->get( $key );
2002                      if ( strval( $newtalk ) !== '' ) {
2003                          $this->mNewtalk = (bool)$newtalk;
2004                      } else {
2005                          // Since we are caching this, make sure it is up to date by getting it
2006                          // from the master
2007                          $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
2008                          $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
2009                      }
2010                  }
2011              } else {
2012                  $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
2013              }
2014          }
2015  
2016          return (bool)$this->mNewtalk;
2017      }
2018  
2019      /**
2020       * Return the data needed to construct links for new talk page message
2021       * alerts. If there are new messages, this will return an associative array
2022       * with the following data:
2023       *     wiki: The database name of the wiki
2024       *     link: Root-relative link to the user's talk page
2025       *     rev: The last talk page revision that the user has seen or null. This
2026       *         is useful for building diff links.
2027       * If there are no new messages, it returns an empty array.
2028       * @note This function was designed to accomodate multiple talk pages, but
2029       * currently only returns a single link and revision.
2030       * @return array
2031       */
2032  	public function getNewMessageLinks() {
2033          $talks = array();
2034          if ( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) {
2035              return $talks;
2036          } elseif ( !$this->getNewtalk() ) {
2037              return array();
2038          }
2039          $utp = $this->getTalkPage();
2040          $dbr = wfGetDB( DB_SLAVE );
2041          // Get the "last viewed rev" timestamp from the oldest message notification
2042          $timestamp = $dbr->selectField( 'user_newtalk',
2043              'MIN(user_last_timestamp)',
2044              $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ),
2045              __METHOD__ );
2046          $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
2047          return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) );
2048      }
2049  
2050      /**
2051       * Get the revision ID for the last talk page revision viewed by the talk
2052       * page owner.
2053       * @return int|null Revision ID or null
2054       */
2055  	public function getNewMessageRevisionId() {
2056          $newMessageRevisionId = null;
2057          $newMessageLinks = $this->getNewMessageLinks();
2058          if ( $newMessageLinks ) {
2059              // Note: getNewMessageLinks() never returns more than a single link
2060              // and it is always for the same wiki, but we double-check here in
2061              // case that changes some time in the future.
2062              if ( count( $newMessageLinks ) === 1
2063                  && $newMessageLinks[0]['wiki'] === wfWikiID()
2064                  && $newMessageLinks[0]['rev']
2065              ) {
2066                  $newMessageRevision = $newMessageLinks[0]['rev'];
2067                  $newMessageRevisionId = $newMessageRevision->getId();
2068              }
2069          }
2070          return $newMessageRevisionId;
2071      }
2072  
2073      /**
2074       * Internal uncached check for new messages
2075       *
2076       * @see getNewtalk()
2077       * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
2078       * @param string|int $id User's IP address for anonymous users, User ID otherwise
2079       * @param bool $fromMaster True to fetch from the master, false for a slave
2080       * @return bool True if the user has new messages
2081       */
2082  	protected function checkNewtalk( $field, $id, $fromMaster = false ) {
2083          if ( $fromMaster ) {
2084              $db = wfGetDB( DB_MASTER );
2085          } else {
2086              $db = wfGetDB( DB_SLAVE );
2087          }
2088          $ok = $db->selectField( 'user_newtalk', $field,
2089              array( $field => $id ), __METHOD__ );
2090          return $ok !== false;
2091      }
2092  
2093      /**
2094       * Add or update the new messages flag
2095       * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
2096       * @param string|int $id User's IP address for anonymous users, User ID otherwise
2097       * @param Revision|null $curRev New, as yet unseen revision of the user talk page. Ignored if null.
2098       * @return bool True if successful, false otherwise
2099       */
2100  	protected function updateNewtalk( $field, $id, $curRev = null ) {
2101          // Get timestamp of the talk page revision prior to the current one
2102          $prevRev = $curRev ? $curRev->getPrevious() : false;
2103          $ts = $prevRev ? $prevRev->getTimestamp() : null;
2104          // Mark the user as having new messages since this revision
2105          $dbw = wfGetDB( DB_MASTER );
2106          $dbw->insert( 'user_newtalk',
2107              array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ),
2108              __METHOD__,
2109              'IGNORE' );
2110          if ( $dbw->affectedRows() ) {
2111              wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
2112              return true;
2113          } else {
2114              wfDebug( __METHOD__ . " already set ($field, $id)\n" );
2115              return false;
2116          }
2117      }
2118  
2119      /**
2120       * Clear the new messages flag for the given user
2121       * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise
2122       * @param string|int $id User's IP address for anonymous users, User ID otherwise
2123       * @return bool True if successful, false otherwise
2124       */
2125  	protected function deleteNewtalk( $field, $id ) {
2126          $dbw = wfGetDB( DB_MASTER );
2127          $dbw->delete( 'user_newtalk',
2128              array( $field => $id ),
2129              __METHOD__ );
2130          if ( $dbw->affectedRows() ) {
2131              wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
2132              return true;
2133          } else {
2134              wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
2135              return false;
2136          }
2137      }
2138  
2139      /**
2140       * Update the 'You have new messages!' status.
2141       * @param bool $val Whether the user has new messages
2142       * @param Revision $curRev New, as yet unseen revision of the user talk
2143       *   page. Ignored if null or !$val.
2144       */
2145  	public function setNewtalk( $val, $curRev = null ) {
2146          if ( wfReadOnly() ) {
2147              return;
2148          }
2149  
2150          $this->load();
2151          $this->mNewtalk = $val;
2152  
2153          if ( $this->isAnon() ) {
2154              $field = 'user_ip';
2155              $id = $this->getName();
2156          } else {
2157              $field = 'user_id';
2158              $id = $this->getId();
2159          }
2160          global $wgMemc;
2161  
2162          if ( $val ) {
2163              $changed = $this->updateNewtalk( $field, $id, $curRev );
2164          } else {
2165              $changed = $this->deleteNewtalk( $field, $id );
2166          }
2167  
2168          if ( $this->isAnon() ) {
2169              // Anons have a separate memcached space, since
2170              // user records aren't kept for them.
2171              $key = wfMemcKey( 'newtalk', 'ip', $id );
2172              $wgMemc->set( $key, $val ? 1 : 0, 1800 );
2173          }
2174          if ( $changed ) {
2175              $this->invalidateCache();
2176          }
2177      }
2178  
2179      /**
2180       * Generate a current or new-future timestamp to be stored in the
2181       * user_touched field when we update things.
2182       * @return string Timestamp in TS_MW format
2183       */
2184  	private static function newTouchedTimestamp() {
2185          global $wgClockSkewFudge;
2186          return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
2187      }
2188  
2189      /**
2190       * Clear user data from memcached.
2191       * Use after applying fun updates to the database; caller's
2192       * responsibility to update user_touched if appropriate.
2193       *
2194       * Called implicitly from invalidateCache() and saveSettings().
2195       */
2196  	public function clearSharedCache() {
2197          $this->load();
2198          if ( $this->mId ) {
2199              global $wgMemc;
2200              $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
2201          }
2202      }
2203  
2204      /**
2205       * Immediately touch the user data cache for this account.
2206       * Updates user_touched field, and removes account data from memcached
2207       * for reload on the next hit.
2208       */
2209  	public function invalidateCache() {
2210          if ( wfReadOnly() ) {
2211              return;
2212          }
2213          $this->load();
2214          if ( $this->mId ) {
2215              $this->mTouched = self::newTouchedTimestamp();
2216  
2217              $dbw = wfGetDB( DB_MASTER );
2218              $userid = $this->mId;
2219              $touched = $this->mTouched;
2220              $method = __METHOD__;
2221              $dbw->onTransactionIdle( function () use ( $dbw, $userid, $touched, $method ) {
2222                  // Prevent contention slams by checking user_touched first
2223                  $encTouched = $dbw->addQuotes( $dbw->timestamp( $touched ) );
2224                  $needsPurge = $dbw->selectField( 'user', '1',
2225                      array( 'user_id' => $userid, 'user_touched < ' . $encTouched ) );
2226                  if ( $needsPurge ) {
2227                      $dbw->update( 'user',
2228                          array( 'user_touched' => $dbw->timestamp( $touched ) ),
2229                          array( 'user_id' => $userid, 'user_touched < ' . $encTouched ),
2230                          $method
2231                      );
2232                  }
2233              } );
2234              $this->clearSharedCache();
2235          }
2236      }
2237  
2238      /**
2239       * Validate the cache for this account.
2240       * @param string $timestamp A timestamp in TS_MW format
2241       * @return bool
2242       */
2243  	public function validateCache( $timestamp ) {
2244          $this->load();
2245          return ( $timestamp >= $this->mTouched );
2246      }
2247  
2248      /**
2249       * Get the user touched timestamp
2250       * @return string Timestamp
2251       */
2252  	public function getTouched() {
2253          $this->load();
2254          return $this->mTouched;
2255      }
2256  
2257      /**
2258       * @return Password
2259       * @since 1.24
2260       */
2261  	public function getPassword() {
2262          $this->loadPasswords();
2263  
2264          return $this->mPassword;
2265      }
2266  
2267      /**
2268       * @return Password
2269       * @since 1.24
2270       */
2271  	public function getTemporaryPassword() {
2272          $this->loadPasswords();
2273  
2274          return $this->mNewpassword;
2275      }
2276  
2277      /**
2278       * Set the password and reset the random token.
2279       * Calls through to authentication plugin if necessary;
2280       * will have no effect if the auth plugin refuses to
2281       * pass the change through or if the legal password
2282       * checks fail.
2283       *
2284       * As a special case, setting the password to null
2285       * wipes it, so the account cannot be logged in until
2286       * a new password is set, for instance via e-mail.
2287       *
2288       * @param string $str New password to set
2289       * @throws PasswordError On failure
2290       *
2291       * @return bool
2292       */
2293  	public function setPassword( $str ) {
2294          global $wgAuth;
2295  
2296          $this->loadPasswords();
2297  
2298          if ( $str !== null ) {
2299              if ( !$wgAuth->allowPasswordChange() ) {
2300                  throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
2301              }
2302  
2303              if ( !$this->isValidPassword( $str ) ) {
2304                  global $wgMinimalPasswordLength;
2305                  $valid = $this->getPasswordValidity( $str );
2306                  if ( is_array( $valid ) ) {
2307                      $message = array_shift( $valid );
2308                      $params = $valid;
2309                  } else {
2310                      $message = $valid;
2311                      $params = array( $wgMinimalPasswordLength );
2312                  }
2313                  throw new PasswordError( wfMessage( $message, $params )->text() );
2314              }
2315          }
2316  
2317          if ( !$wgAuth->setPassword( $this, $str ) ) {
2318              throw new PasswordError( wfMessage( 'externaldberror' )->text() );
2319          }
2320  
2321          $this->setInternalPassword( $str );
2322  
2323          return true;
2324      }
2325  
2326      /**
2327       * Set the password and reset the random token unconditionally.
2328       *
2329       * @param string|null $str New password to set or null to set an invalid
2330       *  password hash meaning that the user will not be able to log in
2331       *  through the web interface.
2332       */
2333  	public function setInternalPassword( $str ) {
2334          $this->setToken();
2335  
2336          $passwordFactory = self::getPasswordFactory();
2337          if ( $str === null ) {
2338              $this->mPassword = $passwordFactory->newFromCiphertext( null );
2339          } else {
2340              $this->mPassword = $passwordFactory->newFromPlaintext( $str );
2341          }
2342  
2343          $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
2344          $this->mNewpassTime = null;
2345      }
2346  
2347      /**
2348       * Get the user's current token.
2349       * @param bool $forceCreation Force the generation of a new token if the
2350       *   user doesn't have one (default=true for backwards compatibility).
2351       * @return string Token
2352       */
2353  	public function getToken( $forceCreation = true ) {
2354          $this->load();
2355          if ( !$this->mToken && $forceCreation ) {
2356              $this->setToken();
2357          }
2358          return $this->mToken;
2359      }
2360  
2361      /**
2362       * Set the random token (used for persistent authentication)
2363       * Called from loadDefaults() among other places.
2364       *
2365       * @param string|bool $token If specified, set the token to this value
2366       */
2367  	public function setToken( $token = false ) {
2368          $this->load();
2369          if ( !$token ) {
2370              $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
2371          } else {
2372              $this->mToken = $token;
2373          }
2374      }
2375  
2376      /**
2377       * Set the password for a password reminder or new account email
2378       *
2379       * @param string $str New password to set or null to set an invalid
2380       *  password hash meaning that the user will not be able to use it
2381       * @param bool $throttle If true, reset the throttle timestamp to the present
2382       */
2383  	public function setNewpassword( $str, $throttle = true ) {
2384          $this->loadPasswords();
2385  
2386          if ( $str === null ) {
2387              $this->mNewpassword = '';
2388              $this->mNewpassTime = null;
2389          } else {
2390              $this->mNewpassword = self::getPasswordFactory()->newFromPlaintext( $str );
2391              if ( $throttle ) {
2392                  $this->mNewpassTime = wfTimestampNow();
2393              }
2394          }
2395      }
2396  
2397      /**
2398       * Has password reminder email been sent within the last
2399       * $wgPasswordReminderResendTime hours?
2400       * @return bool
2401       */
2402  	public function isPasswordReminderThrottled() {
2403          global $wgPasswordReminderResendTime;
2404          $this->load();
2405          if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
2406              return false;
2407          }
2408          $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
2409          return time() < $expiry;
2410      }
2411  
2412      /**
2413       * Get the user's e-mail address
2414       * @return string User's email address
2415       */
2416  	public function getEmail() {
2417          $this->load();
2418          wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
2419          return $this->mEmail;
2420      }
2421  
2422      /**
2423       * Get the timestamp of the user's e-mail authentication
2424       * @return string TS_MW timestamp
2425       */
2426  	public function getEmailAuthenticationTimestamp() {
2427          $this->load();
2428          wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
2429          return $this->mEmailAuthenticated;
2430      }
2431  
2432      /**
2433       * Set the user's e-mail address
2434       * @param string $str New e-mail address
2435       */
2436  	public function setEmail( $str ) {
2437          $this->load();
2438          if ( $str == $this->mEmail ) {
2439              return;
2440          }
2441          $this->invalidateEmail();
2442          $this->mEmail = $str;
2443          wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
2444      }
2445  
2446      /**
2447       * Set the user's e-mail address and a confirmation mail if needed.
2448       *
2449       * @since 1.20
2450       * @param string $str New e-mail address
2451       * @return Status
2452       */
2453  	public function setEmailWithConfirmation( $str ) {
2454          global $wgEnableEmail, $wgEmailAuthentication;
2455  
2456          if ( !$wgEnableEmail ) {
2457              return Status::newFatal( 'emaildisabled' );
2458          }
2459  
2460          $oldaddr = $this->getEmail();
2461          if ( $str === $oldaddr ) {
2462              return Status::newGood( true );
2463          }
2464  
2465          $this->setEmail( $str );
2466  
2467          if ( $str !== '' && $wgEmailAuthentication ) {
2468              // Send a confirmation request to the new address if needed
2469              $type = $oldaddr != '' ? 'changed' : 'set';
2470              $result = $this->sendConfirmationMail( $type );
2471              if ( $result->isGood() ) {
2472                  // Say the the caller that a confirmation mail has been sent
2473                  $result->value = 'eauth';
2474              }
2475          } else {
2476              $result = Status::newGood( true );
2477          }
2478  
2479          return $result;
2480      }
2481  
2482      /**
2483       * Get the user's real name
2484       * @return string User's real name
2485       */
2486  	public function getRealName() {
2487          if ( !$this->isItemLoaded( 'realname' ) ) {
2488              $this->load();
2489          }
2490  
2491          return $this->mRealName;
2492      }
2493  
2494      /**
2495       * Set the user's real name
2496       * @param string $str New real name
2497       */
2498  	public function setRealName( $str ) {
2499          $this->load();
2500          $this->mRealName = $str;
2501      }
2502  
2503      /**
2504       * Get the user's current setting for a given option.
2505       *
2506       * @param string $oname The option to check
2507       * @param string $defaultOverride A default value returned if the option does not exist
2508       * @param bool $ignoreHidden Whether to ignore the effects of $wgHiddenPrefs
2509       * @return string User's current value for the option
2510       * @see getBoolOption()
2511       * @see getIntOption()
2512       */
2513  	public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
2514          global $wgHiddenPrefs;
2515          $this->loadOptions();
2516  
2517          # We want 'disabled' preferences to always behave as the default value for
2518          # users, even if they have set the option explicitly in their settings (ie they
2519          # set it, and then it was disabled removing their ability to change it).  But
2520          # we don't want to erase the preferences in the database in case the preference
2521          # is re-enabled again.  So don't touch $mOptions, just override the returned value
2522          if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) {
2523              return self::getDefaultOption( $oname );
2524          }
2525  
2526          if ( array_key_exists( $oname, $this->mOptions ) ) {
2527              return $this->mOptions[$oname];
2528          } else {
2529              return $defaultOverride;
2530          }
2531      }
2532  
2533      /**
2534       * Get all user's options
2535       *
2536       * @return array
2537       */
2538  	public function getOptions() {
2539          global $wgHiddenPrefs;
2540          $this->loadOptions();
2541          $options = $this->mOptions;
2542  
2543          # We want 'disabled' preferences to always behave as the default value for
2544          # users, even if they have set the option explicitly in their settings (ie they
2545          # set it, and then it was disabled removing their ability to change it).  But
2546          # we don't want to erase the preferences in the database in case the preference
2547          # is re-enabled again.  So don't touch $mOptions, just override the returned value
2548          foreach ( $wgHiddenPrefs as $pref ) {
2549              $default = self::getDefaultOption( $pref );
2550              if ( $default !== null ) {
2551                  $options[$pref] = $default;
2552              }
2553          }
2554  
2555          return $options;
2556      }
2557  
2558      /**
2559       * Get the user's current setting for a given option, as a boolean value.
2560       *
2561       * @param string $oname The option to check
2562       * @return bool User's current value for the option
2563       * @see getOption()
2564       */
2565  	public function getBoolOption( $oname ) {
2566          return (bool)$this->getOption( $oname );
2567      }
2568  
2569      /**
2570       * Get the user's current setting for a given option, as an integer value.
2571       *
2572       * @param string $oname The option to check
2573       * @param int $defaultOverride A default value returned if the option does not exist
2574       * @return int User's current value for the option
2575       * @see getOption()
2576       */
2577  	public function getIntOption( $oname, $defaultOverride = 0 ) {
2578          $val = $this->getOption( $oname );
2579          if ( $val == '' ) {
2580              $val = $defaultOverride;
2581          }
2582          return intval( $val );
2583      }
2584  
2585      /**
2586       * Set the given option for a user.
2587       *
2588       * You need to call saveSettings() to actually write to the database.
2589       *
2590       * @param string $oname The option to set
2591       * @param mixed $val New value to set
2592       */
2593  	public function setOption( $oname, $val ) {
2594          $this->loadOptions();
2595  
2596          // Explicitly NULL values should refer to defaults
2597          if ( is_null( $val ) ) {
2598              $val = self::getDefaultOption( $oname );
2599          }
2600  
2601          $this->mOptions[$oname] = $val;
2602      }
2603  
2604      /**
2605       * Get a token stored in the preferences (like the watchlist one),
2606       * resetting it if it's empty (and saving changes).
2607       *
2608       * @param string $oname The option name to retrieve the token from
2609       * @return string|bool User's current value for the option, or false if this option is disabled.
2610       * @see resetTokenFromOption()
2611       * @see getOption()
2612       */
2613  	public function getTokenFromOption( $oname ) {
2614          global $wgHiddenPrefs;
2615          if ( in_array( $oname, $wgHiddenPrefs ) ) {
2616              return false;
2617          }
2618  
2619          $token = $this->getOption( $oname );
2620          if ( !$token ) {
2621              $token = $this->resetTokenFromOption( $oname );
2622              $this->saveSettings();
2623          }
2624          return $token;
2625      }
2626  
2627      /**
2628       * Reset a token stored in the preferences (like the watchlist one).
2629       * *Does not* save user's preferences (similarly to setOption()).
2630       *
2631       * @param string $oname The option name to reset the token in
2632       * @return string|bool New token value, or false if this option is disabled.
2633       * @see getTokenFromOption()
2634       * @see setOption()
2635       */
2636  	public function resetTokenFromOption( $oname ) {
2637          global $wgHiddenPrefs;
2638          if ( in_array( $oname, $wgHiddenPrefs ) ) {
2639              return false;
2640          }
2641  
2642          $token = MWCryptRand::generateHex( 40 );
2643          $this->setOption( $oname, $token );
2644          return $token;
2645      }
2646  
2647      /**
2648       * Return a list of the types of user options currently returned by
2649       * User::getOptionKinds().
2650       *
2651       * Currently, the option kinds are:
2652       * - 'registered' - preferences which are registered in core MediaWiki or
2653       *                  by extensions using the UserGetDefaultOptions hook.
2654       * - 'registered-multiselect' - as above, using the 'multiselect' type.
2655       * - 'registered-checkmatrix' - as above, using the 'checkmatrix' type.
2656       * - 'userjs' - preferences with names starting with 'userjs-', intended to
2657       *              be used by user scripts.
2658       * - 'special' - "preferences" that are not accessible via User::getOptions
2659       *               or User::setOptions.
2660       * - 'unused' - preferences about which MediaWiki doesn't know anything.
2661       *              These are usually legacy options, removed in newer versions.
2662       *
2663       * The API (and possibly others) use this function to determine the possible
2664       * option types for validation purposes, so make sure to update this when a
2665       * new option kind is added.
2666       *
2667       * @see User::getOptionKinds
2668       * @return array Option kinds
2669       */
2670  	public static function listOptionKinds() {
2671          return array(
2672              'registered',
2673              'registered-multiselect',
2674              'registered-checkmatrix',
2675              'userjs',
2676              'special',
2677              'unused'
2678          );
2679      }
2680  
2681      /**
2682       * Return an associative array mapping preferences keys to the kind of a preference they're
2683       * used for. Different kinds are handled differently when setting or reading preferences.
2684       *
2685       * See User::listOptionKinds for the list of valid option types that can be provided.
2686       *
2687       * @see User::listOptionKinds
2688       * @param IContextSource $context
2689       * @param array $options Assoc. array with options keys to check as keys.
2690       *   Defaults to $this->mOptions.
2691       * @return array The key => kind mapping data
2692       */
2693  	public function getOptionKinds( IContextSource $context, $options = null ) {
2694          $this->loadOptions();
2695          if ( $options === null ) {
2696              $options = $this->mOptions;
2697          }
2698  
2699          $prefs = Preferences::getPreferences( $this, $context );
2700          $mapping = array();
2701  
2702          // Pull out the "special" options, so they don't get converted as
2703          // multiselect or checkmatrix.
2704          $specialOptions = array_fill_keys( Preferences::getSaveBlacklist(), true );
2705          foreach ( $specialOptions as $name => $value ) {
2706              unset( $prefs[$name] );
2707          }
2708  
2709          // Multiselect and checkmatrix options are stored in the database with
2710          // one key per option, each having a boolean value. Extract those keys.
2711          $multiselectOptions = array();
2712          foreach ( $prefs as $name => $info ) {
2713              if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
2714                      ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
2715                  $opts = HTMLFormField::flattenOptions( $info['options'] );
2716                  $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
2717  
2718                  foreach ( $opts as $value ) {
2719                      $multiselectOptions["$prefix$value"] = true;
2720                  }
2721  
2722                  unset( $prefs[$name] );
2723              }
2724          }
2725          $checkmatrixOptions = array();
2726          foreach ( $prefs as $name => $info ) {
2727              if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
2728                      ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
2729                  $columns = HTMLFormField::flattenOptions( $info['columns'] );
2730                  $rows = HTMLFormField::flattenOptions( $info['rows'] );
2731                  $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
2732  
2733                  foreach ( $columns as $column ) {
2734                      foreach ( $rows as $row ) {
2735                          $checkmatrixOptions["$prefix$column-$row"] = true;
2736                      }
2737                  }
2738  
2739                  unset( $prefs[$name] );
2740              }
2741          }
2742  
2743          // $value is ignored
2744          foreach ( $options as $key => $value ) {
2745              if ( isset( $prefs[$key] ) ) {
2746                  $mapping[$key] = 'registered';
2747              } elseif ( isset( $multiselectOptions[$key] ) ) {
2748                  $mapping[$key] = 'registered-multiselect';
2749              } elseif ( isset( $checkmatrixOptions[$key] ) ) {
2750                  $mapping[$key] = 'registered-checkmatrix';
2751              } elseif ( isset( $specialOptions[$key] ) ) {
2752                  $mapping[$key] = 'special';
2753              } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
2754                  $mapping[$key] = 'userjs';
2755              } else {
2756                  $mapping[$key] = 'unused';
2757              }
2758          }
2759  
2760          return $mapping;
2761      }
2762  
2763      /**
2764       * Reset certain (or all) options to the site defaults
2765       *
2766       * The optional parameter determines which kinds of preferences will be reset.
2767       * Supported values are everything that can be reported by getOptionKinds()
2768       * and 'all', which forces a reset of *all* preferences and overrides everything else.
2769       *
2770       * @param array|string $resetKinds Which kinds of preferences to reset. Defaults to
2771       *  array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' )
2772       *  for backwards-compatibility.
2773       * @param IContextSource|null $context Context source used when $resetKinds
2774       *  does not contain 'all', passed to getOptionKinds().
2775       *  Defaults to RequestContext::getMain() when null.
2776       */
2777  	public function resetOptions(
2778          $resetKinds = array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ),
2779          IContextSource $context = null
2780      ) {
2781          $this->load();
2782          $defaultOptions = self::getDefaultOptions();
2783  
2784          if ( !is_array( $resetKinds ) ) {
2785              $resetKinds = array( $resetKinds );
2786          }
2787  
2788          if ( in_array( 'all', $resetKinds ) ) {
2789              $newOptions = $defaultOptions;
2790          } else {
2791              if ( $context === null ) {
2792                  $context = RequestContext::getMain();
2793              }
2794  
2795              $optionKinds = $this->getOptionKinds( $context );
2796              $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
2797              $newOptions = array();
2798  
2799              // Use default values for the options that should be deleted, and
2800              // copy old values for the ones that shouldn't.
2801              foreach ( $this->mOptions as $key => $value ) {
2802                  if ( in_array( $optionKinds[$key], $resetKinds ) ) {
2803                      if ( array_key_exists( $key, $defaultOptions ) ) {
2804                          $newOptions[$key] = $defaultOptions[$key];
2805                      }
2806                  } else {
2807                      $newOptions[$key] = $value;
2808                  }
2809              }
2810          }
2811  
2812          wfRunHooks( 'UserResetAllOptions', array( $this, &$newOptions, $this->mOptions, $resetKinds ) );
2813  
2814          $this->mOptions = $newOptions;
2815          $this->mOptionsLoaded = true;
2816      }
2817  
2818      /**
2819       * Get the user's preferred date format.
2820       * @return string User's preferred date format
2821       */
2822  	public function getDatePreference() {
2823          // Important migration for old data rows
2824          if ( is_null( $this->mDatePreference ) ) {
2825              global $wgLang;
2826              $value = $this->getOption( 'date' );
2827              $map = $wgLang->getDatePreferenceMigrationMap();
2828              if ( isset( $map[$value] ) ) {
2829                  $value = $map[$value];
2830              }
2831              $this->mDatePreference = $value;
2832          }
2833          return $this->mDatePreference;
2834      }
2835  
2836      /**
2837       * Determine based on the wiki configuration and the user's options,
2838       * whether this user must be over HTTPS no matter what.
2839       *
2840       * @return bool
2841       */
2842  	public function requiresHTTPS() {
2843          global $wgSecureLogin;
2844          if ( !$wgSecureLogin ) {
2845              return false;
2846          } else {
2847              $https = $this->getBoolOption( 'prefershttps' );
2848              wfRunHooks( 'UserRequiresHTTPS', array( $this, &$https ) );
2849              if ( $https ) {
2850                  $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
2851              }
2852              return $https;
2853          }
2854      }
2855  
2856      /**
2857       * Get the user preferred stub threshold
2858       *
2859       * @return int
2860       */
2861  	public function getStubThreshold() {
2862          global $wgMaxArticleSize; # Maximum article size, in Kb
2863          $threshold = $this->getIntOption( 'stubthreshold' );
2864          if ( $threshold > $wgMaxArticleSize * 1024 ) {
2865              // If they have set an impossible value, disable the preference
2866              // so we can use the parser cache again.
2867              $threshold = 0;
2868          }
2869          return $threshold;
2870      }
2871  
2872      /**
2873       * Get the permissions this user has.
2874       * @return array Array of String permission names
2875       */
2876  	public function getRights() {
2877          if ( is_null( $this->mRights ) ) {
2878              $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
2879              wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
2880              // Force reindexation of rights when a hook has unset one of them
2881              $this->mRights = array_values( array_unique( $this->mRights ) );
2882          }
2883          return $this->mRights;
2884      }
2885  
2886      /**
2887       * Get the list of explicit group memberships this user has.
2888       * The implicit * and user groups are not included.
2889       * @return array Array of String internal group names
2890       */
2891  	public function getGroups() {
2892          $this->load();
2893          $this->loadGroups();
2894          return $this->mGroups;
2895      }
2896  
2897      /**
2898       * Get the list of implicit group memberships this user has.
2899       * This includes all explicit groups, plus 'user' if logged in,
2900       * '*' for all accounts, and autopromoted groups
2901       * @param bool $recache Whether to avoid the cache
2902       * @return array Array of String internal group names
2903       */
2904  	public function getEffectiveGroups( $recache = false ) {
2905          if ( $recache || is_null( $this->mEffectiveGroups ) ) {
2906              wfProfileIn( __METHOD__ );
2907              $this->mEffectiveGroups = array_unique( array_merge(
2908                  $this->getGroups(), // explicit groups
2909                  $this->getAutomaticGroups( $recache ) // implicit groups
2910              ) );
2911              // Hook for additional groups
2912              wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
2913              // Force reindexation of groups when a hook has unset one of them
2914              $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
2915              wfProfileOut( __METHOD__ );
2916          }
2917          return $this->mEffectiveGroups;
2918      }
2919  
2920      /**
2921       * Get the list of implicit group memberships this user has.
2922       * This includes 'user' if logged in, '*' for all accounts,
2923       * and autopromoted groups
2924       * @param bool $recache Whether to avoid the cache
2925       * @return array Array of String internal group names
2926       */
2927  	public function getAutomaticGroups( $recache = false ) {
2928          if ( $recache || is_null( $this->mImplicitGroups ) ) {
2929              wfProfileIn( __METHOD__ );
2930              $this->mImplicitGroups = array( '*' );
2931              if ( $this->getId() ) {
2932                  $this->mImplicitGroups[] = 'user';
2933  
2934                  $this->mImplicitGroups = array_unique( array_merge(
2935                      $this->mImplicitGroups,
2936                      Autopromote::getAutopromoteGroups( $this )
2937                  ) );
2938              }
2939              if ( $recache ) {
2940                  // Assure data consistency with rights/groups,
2941                  // as getEffectiveGroups() depends on this function
2942                  $this->mEffectiveGroups = null;
2943              }
2944              wfProfileOut( __METHOD__ );
2945          }
2946          return $this->mImplicitGroups;
2947      }
2948  
2949      /**
2950       * Returns the groups the user has belonged to.
2951       *
2952       * The user may still belong to the returned groups. Compare with getGroups().
2953       *
2954       * The function will not return groups the user had belonged to before MW 1.17
2955       *
2956       * @return array Names of the groups the user has belonged to.
2957       */
2958  	public function getFormerGroups() {
2959          if ( is_null( $this->mFormerGroups ) ) {
2960              $dbr = wfGetDB( DB_MASTER );
2961              $res = $dbr->select( 'user_former_groups',
2962                  array( 'ufg_group' ),
2963                  array( 'ufg_user' => $this->mId ),
2964                  __METHOD__ );
2965              $this->mFormerGroups = array();
2966              foreach ( $res as $row ) {
2967                  $this->mFormerGroups[] = $row->ufg_group;
2968              }
2969          }
2970          return $this->mFormerGroups;
2971      }
2972  
2973      /**
2974       * Get the user's edit count.
2975       * @return int|null Null for anonymous users
2976       */
2977  	public function getEditCount() {
2978          if ( !$this->getId() ) {
2979              return null;
2980          }
2981  
2982          if ( $this->mEditCount === null ) {
2983              /* Populate the count, if it has not been populated yet */
2984              wfProfileIn( __METHOD__ );
2985              $dbr = wfGetDB( DB_SLAVE );
2986              // check if the user_editcount field has been initialized
2987              $count = $dbr->selectField(
2988                  'user', 'user_editcount',
2989                  array( 'user_id' => $this->mId ),
2990                  __METHOD__
2991              );
2992  
2993              if ( $count === null ) {
2994                  // it has not been initialized. do so.
2995                  $count = $this->initEditCount();
2996              }
2997              $this->mEditCount = $count;
2998              wfProfileOut( __METHOD__ );
2999          }
3000          return (int)$this->mEditCount;
3001      }
3002  
3003      /**
3004       * Add the user to the given group.
3005       * This takes immediate effect.
3006       * @param string $group Name of the group to add
3007       */
3008  	public function addGroup( $group ) {
3009          if ( wfRunHooks( 'UserAddGroup', array( $this, &$group ) ) ) {
3010              $dbw = wfGetDB( DB_MASTER );
3011              if ( $this->getId() ) {
3012                  $dbw->insert( 'user_groups',
3013                      array(
3014                          'ug_user' => $this->getID(),
3015                          'ug_group' => $group,
3016                      ),
3017                      __METHOD__,
3018                      array( 'IGNORE' ) );
3019              }
3020          }
3021          $this->loadGroups();
3022          $this->mGroups[] = $group;
3023          // In case loadGroups was not called before, we now have the right twice.
3024          // Get rid of the duplicate.
3025          $this->mGroups = array_unique( $this->mGroups );
3026  
3027          // Refresh the groups caches, and clear the rights cache so it will be
3028          // refreshed on the next call to $this->getRights().
3029          $this->getEffectiveGroups( true );
3030          $this->mRights = null;
3031  
3032          $this->invalidateCache();
3033      }
3034  
3035      /**
3036       * Remove the user from the given group.
3037       * This takes immediate effect.
3038       * @param string $group Name of the group to remove
3039       */
3040  	public function removeGroup( $group ) {
3041          $this->load();
3042          if ( wfRunHooks( 'UserRemoveGroup', array( $this, &$group ) ) ) {
3043              $dbw = wfGetDB( DB_MASTER );
3044              $dbw->delete( 'user_groups',
3045                  array(
3046                      'ug_user' => $this->getID(),
3047                      'ug_group' => $group,
3048                  ), __METHOD__ );
3049              // Remember that the user was in this group
3050              $dbw->insert( 'user_former_groups',
3051                  array(
3052                      'ufg_user' => $this->getID(),
3053                      'ufg_group' => $group,
3054                  ),
3055                  __METHOD__,
3056                  array( 'IGNORE' ) );
3057          }
3058          $this->loadGroups();
3059          $this->mGroups = array_diff( $this->mGroups, array( $group ) );
3060  
3061          // Refresh the groups caches, and clear the rights cache so it will be
3062          // refreshed on the next call to $this->getRights().
3063          $this->getEffectiveGroups( true );
3064          $this->mRights = null;
3065  
3066          $this->invalidateCache();
3067      }
3068  
3069      /**
3070       * Get whether the user is logged in
3071       * @return bool
3072       */
3073  	public function isLoggedIn() {
3074          return $this->getID() != 0;
3075      }
3076  
3077      /**
3078       * Get whether the user is anonymous
3079       * @return bool
3080       */
3081  	public function isAnon() {
3082          return !$this->isLoggedIn();
3083      }
3084  
3085      /**
3086       * Check if user is allowed to access a feature / make an action
3087       *
3088       * @param string $permissions,... Permissions to test
3089       * @return bool True if user is allowed to perform *any* of the given actions
3090       */
3091  	public function isAllowedAny( /*...*/ ) {
3092          $permissions = func_get_args();
3093          foreach ( $permissions as $permission ) {
3094              if ( $this->isAllowed( $permission ) ) {
3095                  return true;
3096              }
3097          }
3098          return false;
3099      }
3100  
3101      /**
3102       *
3103       * @param string $permissions,... Permissions to test
3104       * @return bool True if the user is allowed to perform *all* of the given actions
3105       */
3106  	public function isAllowedAll( /*...*/ ) {
3107          $permissions = func_get_args();
3108          foreach ( $permissions as $permission ) {
3109              if ( !$this->isAllowed( $permission ) ) {
3110                  return false;
3111              }
3112          }
3113          return true;
3114      }
3115  
3116      /**
3117       * Internal mechanics of testing a permission
3118       * @param string $action
3119       * @return bool
3120       */
3121  	public function isAllowed( $action = '' ) {
3122          if ( $action === '' ) {
3123              return true; // In the spirit of DWIM
3124          }
3125          // Patrolling may not be enabled
3126          if ( $action === 'patrol' || $action === 'autopatrol' ) {
3127              global $wgUseRCPatrol, $wgUseNPPatrol;
3128              if ( !$wgUseRCPatrol && !$wgUseNPPatrol ) {
3129                  return false;
3130              }
3131          }
3132          // Use strict parameter to avoid matching numeric 0 accidentally inserted
3133          // by misconfiguration: 0 == 'foo'
3134          return in_array( $action, $this->getRights(), true );
3135      }
3136  
3137      /**
3138       * Check whether to enable recent changes patrol features for this user
3139       * @return bool True or false
3140       */
3141  	public function useRCPatrol() {
3142          global $wgUseRCPatrol;
3143          return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
3144      }
3145  
3146      /**
3147       * Check whether to enable new pages patrol features for this user
3148       * @return bool True or false
3149       */
3150  	public function useNPPatrol() {
3151          global $wgUseRCPatrol, $wgUseNPPatrol;
3152          return (
3153              ( $wgUseRCPatrol || $wgUseNPPatrol )
3154                  && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
3155          );
3156      }
3157  
3158      /**
3159       * Get the WebRequest object to use with this object
3160       *
3161       * @return WebRequest
3162       */
3163  	public function getRequest() {
3164          if ( $this->mRequest ) {
3165              return $this->mRequest;
3166          } else {
3167              global $wgRequest;
3168              return $wgRequest;
3169          }
3170      }
3171  
3172      /**
3173       * Get the current skin, loading it if required
3174       * @return Skin The current skin
3175       * @todo FIXME: Need to check the old failback system [AV]
3176       * @deprecated since 1.18 Use ->getSkin() in the most relevant outputting context you have
3177       */
3178  	public function getSkin() {
3179          wfDeprecated( __METHOD__, '1.18' );
3180          return RequestContext::getMain()->getSkin();
3181      }
3182  
3183      /**
3184       * Get a WatchedItem for this user and $title.
3185       *
3186       * @since 1.22 $checkRights parameter added
3187       * @param Title $title
3188       * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3189       *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
3190       * @return WatchedItem
3191       */
3192  	public function getWatchedItem( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
3193          $key = $checkRights . ':' . $title->getNamespace() . ':' . $title->getDBkey();
3194  
3195          if ( isset( $this->mWatchedItems[$key] ) ) {
3196              return $this->mWatchedItems[$key];
3197          }
3198  
3199          if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) {
3200              $this->mWatchedItems = array();
3201          }
3202  
3203          $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title, $checkRights );
3204          return $this->mWatchedItems[$key];
3205      }
3206  
3207      /**
3208       * Check the watched status of an article.
3209       * @since 1.22 $checkRights parameter added
3210       * @param Title $title Title of the article to look at
3211       * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3212       *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
3213       * @return bool
3214       */
3215  	public function isWatched( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
3216          return $this->getWatchedItem( $title, $checkRights )->isWatched();
3217      }
3218  
3219      /**
3220       * Watch an article.
3221       * @since 1.22 $checkRights parameter added
3222       * @param Title $title Title of the article to look at
3223       * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3224       *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
3225       */
3226  	public function addWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
3227          $this->getWatchedItem( $title, $checkRights )->addWatch();
3228          $this->invalidateCache();
3229      }
3230  
3231      /**
3232       * Stop watching an article.
3233       * @since 1.22 $checkRights parameter added
3234       * @param Title $title Title of the article to look at
3235       * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
3236       *     Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
3237       */
3238  	public function removeWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
3239          $this->getWatchedItem( $title, $checkRights )->removeWatch();
3240          $this->invalidateCache();
3241      }
3242  
3243      /**
3244       * Clear the user's notification timestamp for the given title.
3245       * If e-notif e-mails are on, they will receive notification mails on
3246       * the next change of the page if it's watched etc.
3247       * @note If the user doesn't have 'editmywatchlist', this will do nothing.
3248       * @param Title $title Title of the article to look at
3249       * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
3250       */
3251  	public function clearNotification( &$title, $oldid = 0 ) {
3252          global $wgUseEnotif, $wgShowUpdatedMarker;
3253  
3254          // Do nothing if the database is locked to writes
3255          if ( wfReadOnly() ) {
3256              return;
3257          }
3258  
3259          // Do nothing if not allowed to edit the watchlist
3260          if ( !$this->isAllowed( 'editmywatchlist' ) ) {
3261              return;
3262          }
3263  
3264          // If we're working on user's talk page, we should update the talk page message indicator
3265          if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
3266              if ( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this, $oldid ) ) ) {
3267                  return;
3268              }
3269  
3270              $nextid = $oldid ? $title->getNextRevisionID( $oldid ) : null;
3271  
3272              if ( !$oldid || !$nextid ) {
3273                  // If we're looking at the latest revision, we should definitely clear it
3274                  $this->setNewtalk( false );
3275              } else {
3276                  // Otherwise we should update its revision, if it's present
3277                  if ( $this->getNewtalk() ) {
3278                      // Naturally the other one won't clear by itself
3279                      $this->setNewtalk( false );
3280                      $this->setNewtalk( true, Revision::newFromId( $nextid ) );
3281                  }
3282              }
3283          }
3284  
3285          if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
3286              return;
3287          }
3288  
3289          if ( $this->isAnon() ) {
3290              // Nothing else to do...
3291              return;
3292          }
3293  
3294          // Only update the timestamp if the page is being watched.
3295          // The query to find out if it is watched is cached both in memcached and per-invocation,
3296          // and when it does have to be executed, it can be on a slave
3297          // If this is the user's newtalk page, we always update the timestamp
3298          $force = '';
3299          if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
3300              $force = 'force';
3301          }
3302  
3303          $this->getWatchedItem( $title )->resetNotificationTimestamp( $force, $oldid );
3304      }
3305  
3306      /**
3307       * Resets all of the given user's page-change notification timestamps.
3308       * If e-notif e-mails are on, they will receive notification mails on
3309       * the next change of any watched page.
3310       * @note If the user doesn't have 'editmywatchlist', this will do nothing.
3311       */
3312  	public function clearAllNotifications() {
3313          if ( wfReadOnly() ) {
3314              return;
3315          }
3316  
3317          // Do nothing if not allowed to edit the watchlist
3318          if ( !$this->isAllowed( 'editmywatchlist' ) ) {
3319              return;
3320          }
3321  
3322          global $wgUseEnotif, $wgShowUpdatedMarker;
3323          if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
3324              $this->setNewtalk( false );
3325              return;
3326          }
3327          $id = $this->getId();
3328          if ( $id != 0 ) {
3329              $dbw = wfGetDB( DB_MASTER );
3330              $dbw->update( 'watchlist',
3331                  array( /* SET */ 'wl_notificationtimestamp' => null ),
3332                  array( /* WHERE */ 'wl_user' => $id ),
3333                  __METHOD__
3334              );
3335              // We also need to clear here the "you have new message" notification for the own user_talk page;
3336              // it's cleared one page view later in WikiPage::doViewUpdates().
3337          }
3338      }
3339  
3340      /**
3341       * Set a cookie on the user's client. Wrapper for
3342       * WebResponse::setCookie
3343       * @param string $name Name of the cookie to set
3344       * @param string $value Value to set
3345       * @param int $exp Expiration time, as a UNIX time value;
3346       *                   if 0 or not specified, use the default $wgCookieExpiration
3347       * @param bool $secure
3348       *  true: Force setting the secure attribute when setting the cookie
3349       *  false: Force NOT setting the secure attribute when setting the cookie
3350       *  null (default): Use the default ($wgCookieSecure) to set the secure attribute
3351       * @param array $params Array of options sent passed to WebResponse::setcookie()
3352       */
3353  	protected function setCookie( $name, $value, $exp = 0, $secure = null, $params = array() ) {
3354          $params['secure'] = $secure;
3355          $this->getRequest()->response()->setcookie( $name, $value, $exp, $params );
3356      }
3357  
3358      /**
3359       * Clear a cookie on the user's client
3360       * @param string $name Name of the cookie to clear
3361       * @param bool $secure
3362       *  true: Force setting the secure attribute when setting the cookie
3363       *  false: Force NOT setting the secure attribute when setting the cookie
3364       *  null (default): Use the default ($wgCookieSecure) to set the secure attribute
3365       * @param array $params Array of options sent passed to WebResponse::setcookie()
3366       */
3367  	protected function clearCookie( $name, $secure = null, $params = array() ) {
3368          $this->setCookie( $name, '', time() - 86400, $secure, $params );
3369      }
3370  
3371      /**
3372       * Set the default cookies for this session on the user's client.
3373       *
3374       * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null
3375       *        is passed.
3376       * @param bool $secure Whether to force secure/insecure cookies or use default
3377       * @param bool $rememberMe Whether to add a Token cookie for elongated sessions
3378       */
3379  	public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
3380          if ( $request === null ) {
3381              $request = $this->getRequest();
3382          }
3383  
3384          $this->load();
3385          if ( 0 == $this->mId ) {
3386              return;
3387          }
3388          if ( !$this->mToken ) {
3389              // When token is empty or NULL generate a new one and then save it to the database
3390              // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
3391              // Simply by setting every cell in the user_token column to NULL and letting them be
3392              // regenerated as users log back into the wiki.
3393              $this->setToken();
3394              $this->saveSettings();
3395          }
3396          $session = array(
3397              'wsUserID' => $this->mId,
3398              'wsToken' => $this->mToken,
3399              'wsUserName' => $this->getName()
3400          );
3401          $cookies = array(
3402              'UserID' => $this->mId,
3403              'UserName' => $this->getName(),
3404          );
3405          if ( $rememberMe ) {
3406              $cookies['Token'] = $this->mToken;
3407          } else {
3408              $cookies['Token'] = false;
3409          }
3410  
3411          wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
3412  
3413          foreach ( $session as $name => $value ) {
3414              $request->setSessionData( $name, $value );
3415          }
3416          foreach ( $cookies as $name => $value ) {
3417              if ( $value === false ) {
3418                  $this->clearCookie( $name );
3419              } else {
3420                  $this->setCookie( $name, $value, 0, $secure );
3421              }
3422          }
3423  
3424          /**
3425           * If wpStickHTTPS was selected, also set an insecure cookie that
3426           * will cause the site to redirect the user to HTTPS, if they access
3427           * it over HTTP. Bug 29898. Use an un-prefixed cookie, so it's the same
3428           * as the one set by centralauth (bug 53538). Also set it to session, or
3429           * standard time setting, based on if rememberme was set.
3430           */
3431          if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) {
3432              $this->setCookie(
3433                  'forceHTTPS',
3434                  'true',
3435                  $rememberMe ? 0 : null,
3436                  false,
3437                  array( 'prefix' => '' ) // no prefix
3438              );
3439          }
3440      }
3441  
3442      /**
3443       * Log this user out.
3444       */
3445  	public function logout() {
3446          if ( wfRunHooks( 'UserLogout', array( &$this ) ) ) {
3447              $this->doLogout();
3448          }
3449      }
3450  
3451      /**
3452       * Clear the user's cookies and session, and reset the instance cache.
3453       * @see logout()
3454       */
3455  	public function doLogout() {
3456          $this->clearInstanceCache( 'defaults' );
3457  
3458          $this->getRequest()->setSessionData( 'wsUserID', 0 );
3459  
3460          $this->clearCookie( 'UserID' );
3461          $this->clearCookie( 'Token' );
3462          $this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) );
3463  
3464          // Remember when user logged out, to prevent seeing cached pages
3465          $this->setCookie( 'LoggedOut', time(), time() + 86400 );
3466      }
3467  
3468      /**
3469       * Save this user's settings into the database.
3470       * @todo Only rarely do all these fields need to be set!
3471       */
3472  	public function saveSettings() {
3473          global $wgAuth;
3474  
3475          $this->load();
3476          $this->loadPasswords();
3477          if ( wfReadOnly() ) {
3478              return;
3479          }
3480          if ( 0 == $this->mId ) {
3481              return;
3482          }
3483  
3484          $this->mTouched = self::newTouchedTimestamp();
3485          if ( !$wgAuth->allowSetLocalPassword() ) {
3486              $this->mPassword = self::getPasswordFactory()->newFromCiphertext( null );
3487          }
3488  
3489          $dbw = wfGetDB( DB_MASTER );
3490          $dbw->update( 'user',
3491              array( /* SET */
3492                  'user_name' => $this->mName,
3493                  'user_password' => $this->mPassword->toString(),
3494                  'user_newpassword' => $this->mNewpassword->toString(),
3495                  'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
3496                  'user_real_name' => $this->mRealName,
3497                  'user_email' => $this->mEmail,
3498                  'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
3499                  'user_touched' => $dbw->timestamp( $this->mTouched ),
3500                  'user_token' => strval( $this->mToken ),
3501                  'user_email_token' => $this->mEmailToken,
3502                  'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
3503                  'user_password_expires' => $dbw->timestampOrNull( $this->mPasswordExpires ),
3504              ), array( /* WHERE */
3505                  'user_id' => $this->mId
3506              ), __METHOD__
3507          );
3508  
3509          $this->saveOptions();
3510  
3511          wfRunHooks( 'UserSaveSettings', array( $this ) );
3512          $this->clearSharedCache();
3513          $this->getUserPage()->invalidateCache();
3514      }
3515  
3516      /**
3517       * If only this user's username is known, and it exists, return the user ID.
3518       * @return int
3519       */
3520  	public function idForName() {
3521          $s = trim( $this->getName() );
3522          if ( $s === '' ) {
3523              return 0;
3524          }
3525  
3526          $dbr = wfGetDB( DB_SLAVE );
3527          $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
3528          if ( $id === false ) {
3529              $id = 0;
3530          }
3531          return $id;
3532      }
3533  
3534      /**
3535       * Add a user to the database, return the user object
3536       *
3537       * @param string $name Username to add
3538       * @param array $params Array of Strings Non-default parameters to save to
3539       *   the database as user_* fields:
3540       *   - password: The user's password hash. Password logins will be disabled
3541       *     if this is omitted.
3542       *   - newpassword: Hash for a temporary password that has been mailed to
3543       *     the user.
3544       *   - email: The user's email address.
3545       *   - email_authenticated: The email authentication timestamp.
3546       *   - real_name: The user's real name.
3547       *   - options: An associative array of non-default options.
3548       *   - token: Random authentication token. Do not set.
3549       *   - registration: Registration timestamp. Do not set.
3550       *
3551       * @return User|null User object, or null if the username already exists.
3552       */
3553  	public static function createNew( $name, $params = array() ) {
3554          $user = new User;
3555          $user->load();
3556          $user->loadPasswords();
3557          $user->setToken(); // init token
3558          if ( isset( $params['options'] ) ) {
3559              $user->mOptions = $params['options'] + (array)$user->mOptions;
3560              unset( $params['options'] );
3561          }
3562          $dbw = wfGetDB( DB_MASTER );
3563          $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
3564  
3565          $fields = array(
3566              'user_id' => $seqVal,
3567              'user_name' => $name,
3568              'user_password' => $user->mPassword->toString(),
3569              'user_newpassword' => $user->mNewpassword->toString(),
3570              'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ),
3571              'user_email' => $user->mEmail,
3572              'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
3573              'user_real_name' => $user->mRealName,
3574              'user_token' => strval( $user->mToken ),
3575              'user_registration' => $dbw->timestamp( $user->mRegistration ),
3576              'user_editcount' => 0,
3577              'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ),
3578          );
3579          foreach ( $params as $name => $value ) {
3580              $fields["user_$name"] = $value;
3581          }
3582          $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
3583          if ( $dbw->affectedRows() ) {
3584              $newUser = User::newFromId( $dbw->insertId() );
3585          } else {
3586              $newUser = null;
3587          }
3588          return $newUser;
3589      }
3590  
3591      /**
3592       * Add this existing user object to the database. If the user already
3593       * exists, a fatal status object is returned, and the user object is
3594       * initialised with the data from the database.
3595       *
3596       * Previously, this function generated a DB error due to a key conflict
3597       * if the user already existed. Many extension callers use this function
3598       * in code along the lines of:
3599       *
3600       *   $user = User::newFromName( $name );
3601       *   if ( !$user->isLoggedIn() ) {
3602       *       $user->addToDatabase();
3603       *   }
3604       *   // do something with $user...
3605       *
3606       * However, this was vulnerable to a race condition (bug 16020). By
3607       * initialising the user object if the user exists, we aim to support this
3608       * calling sequence as far as possible.
3609       *
3610       * Note that if the user exists, this function will acquire a write lock,
3611       * so it is still advisable to make the call conditional on isLoggedIn(),
3612       * and to commit the transaction after calling.
3613       *
3614       * @throws MWException
3615       * @return Status
3616       */
3617  	public function addToDatabase() {
3618          $this->load();
3619          $this->loadPasswords();
3620          if ( !$this->mToken ) {
3621              $this->setToken(); // init token
3622          }
3623  
3624          $this->mTouched = self::newTouchedTimestamp();
3625  
3626          $dbw = wfGetDB( DB_MASTER );
3627          $inWrite = $dbw->writesOrCallbacksPending();
3628          $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
3629          $dbw->insert( 'user',
3630              array(
3631                  'user_id' => $seqVal,
3632                  'user_name' => $this->mName,
3633                  'user_password' => $this->mPassword->toString(),
3634                  'user_newpassword' => $this->mNewpassword->toString(),
3635                  'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
3636                  'user_email' => $this->mEmail,
3637                  'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
3638                  'user_real_name' => $this->mRealName,
3639                  'user_token' => strval( $this->mToken ),
3640                  'user_registration' => $dbw->timestamp( $this->mRegistration ),
3641                  'user_editcount' => 0,
3642                  'user_touched' => $dbw->timestamp( $this->mTouched ),
3643              ), __METHOD__,
3644              array( 'IGNORE' )
3645          );
3646          if ( !$dbw->affectedRows() ) {
3647              // The queries below cannot happen in the same REPEATABLE-READ snapshot.
3648              // Handle this by COMMIT, if possible, or by LOCK IN SHARE MODE otherwise.
3649              if ( $inWrite ) {
3650                  // Can't commit due to pending writes that may need atomicity.
3651                  // This may cause some lock contention unlike the case below.
3652                  $options = array( 'LOCK IN SHARE MODE' );
3653                  $flags = self::READ_LOCKING;
3654              } else {
3655                  // Often, this case happens early in views before any writes when
3656                  // using CentralAuth. It's should be OK to commit and break the snapshot.
3657                  $dbw->commit( __METHOD__, 'flush' );
3658                  $options = array();
3659                  $flags = 0;
3660              }
3661              $this->mId = $dbw->selectField( 'user', 'user_id',
3662                  array( 'user_name' => $this->mName ), __METHOD__, $options );
3663              $loaded = false;
3664              if ( $this->mId ) {
3665                  if ( $this->loadFromDatabase( $flags ) ) {
3666                      $loaded = true;
3667                  }
3668              }
3669              if ( !$loaded ) {
3670                  throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
3671                      "to insert user '{$this->mName}' row, but it was not present in select!" );
3672              }
3673              return Status::newFatal( 'userexists' );
3674          }
3675          $this->mId = $dbw->insertId();
3676  
3677          // Clear instance cache other than user table data, which is already accurate
3678          $this->clearInstanceCache();
3679  
3680          $this->saveOptions();
3681          return Status::newGood();
3682      }
3683  
3684      /**
3685       * If this user is logged-in and blocked,
3686       * block any IP address they've successfully logged in from.
3687       * @return bool A block was spread
3688       */
3689  	public function spreadAnyEditBlock() {
3690          if ( $this->isLoggedIn() && $this->isBlocked() ) {
3691              return $this->spreadBlock();
3692          }
3693          return false;
3694      }
3695  
3696      /**
3697       * If this (non-anonymous) user is blocked,
3698       * block the IP address they've successfully logged in from.
3699       * @return bool A block was spread
3700       */
3701  	protected function spreadBlock() {
3702          wfDebug( __METHOD__ . "()\n" );
3703          $this->load();
3704          if ( $this->mId == 0 ) {
3705              return false;
3706          }
3707  
3708          $userblock = Block::newFromTarget( $this->getName() );
3709          if ( !$userblock ) {
3710              return false;
3711          }
3712  
3713          return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
3714      }
3715  
3716      /**
3717       * Get whether the user is explicitly blocked from account creation.
3718       * @return bool|Block
3719       */
3720  	public function isBlockedFromCreateAccount() {
3721          $this->getBlockedStatus();
3722          if ( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) {
3723              return $this->mBlock;
3724          }
3725  
3726          # bug 13611: if the IP address the user is trying to create an account from is
3727          # blocked with createaccount disabled, prevent new account creation there even
3728          # when the user is logged in
3729          if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
3730              $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
3731          }
3732          return $this->mBlockedFromCreateAccount instanceof Block
3733              && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
3734              ? $this->mBlockedFromCreateAccount
3735              : false;
3736      }
3737  
3738      /**
3739       * Get whether the user is blocked from using Special:Emailuser.
3740       * @return bool
3741       */
3742  	public function isBlockedFromEmailuser() {
3743          $this->getBlockedStatus();
3744          return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
3745      }
3746  
3747      /**
3748       * Get whether the user is allowed to create an account.
3749       * @return bool
3750       */
3751  	public function isAllowedToCreateAccount() {
3752          return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
3753      }
3754  
3755      /**
3756       * Get this user's personal page title.
3757       *
3758       * @return Title User's personal page title
3759       */
3760  	public function getUserPage() {
3761          return Title::makeTitle( NS_USER, $this->getName() );
3762      }
3763  
3764      /**
3765       * Get this user's talk page title.
3766       *
3767       * @return Title User's talk page title
3768       */
3769  	public function getTalkPage() {
3770          $title = $this->getUserPage();
3771          return $title->getTalkPage();
3772      }
3773  
3774      /**
3775       * Determine whether the user is a newbie. Newbies are either
3776       * anonymous IPs, or the most recently created accounts.
3777       * @return bool
3778       */
3779  	public function isNewbie() {
3780          return !$this->isAllowed( 'autoconfirmed' );
3781      }
3782  
3783      /**
3784       * Check to see if the given clear-text password is one of the accepted passwords
3785       * @param string $password User password
3786       * @return bool True if the given password is correct, otherwise False
3787       */
3788  	public function checkPassword( $password ) {
3789          global $wgAuth, $wgLegacyEncoding;
3790  
3791          $section = new ProfileSection( __METHOD__ );
3792  
3793          $this->loadPasswords();
3794  
3795          // Certain authentication plugins do NOT want to save
3796          // domain passwords in a mysql database, so we should
3797          // check this (in case $wgAuth->strict() is false).
3798          if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
3799              return true;
3800          } elseif ( $wgAuth->strict() ) {
3801              // Auth plugin doesn't allow local authentication
3802              return false;
3803          } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) {
3804              // Auth plugin doesn't allow local authentication for this user name
3805              return false;
3806          }
3807  
3808          $passwordFactory = self::getPasswordFactory();
3809          if ( !$this->mPassword->equals( $password ) ) {
3810              if ( $wgLegacyEncoding ) {
3811                  // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
3812                  // Check for this with iconv
3813                  $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
3814                  if ( $cp1252Password === $password || !$this->mPassword->equals( $cp1252Password ) ) {
3815                      return false;
3816                  }
3817              } else {
3818                  return false;
3819              }
3820          }
3821  
3822          if ( $passwordFactory->needsUpdate( $this->mPassword ) ) {
3823              $this->mPassword = $passwordFactory->newFromPlaintext( $password );
3824              $this->saveSettings();
3825          }
3826  
3827          return true;
3828      }
3829  
3830      /**
3831       * Check if the given clear-text password matches the temporary password
3832       * sent by e-mail for password reset operations.
3833       *
3834       * @param string $plaintext
3835       *
3836       * @return bool True if matches, false otherwise
3837       */
3838  	public function checkTemporaryPassword( $plaintext ) {
3839          global $wgNewPasswordExpiry;
3840  
3841          $this->load();
3842          $this->loadPasswords();
3843          if ( $this->mNewpassword->equals( $plaintext ) ) {
3844              if ( is_null( $this->mNewpassTime ) ) {
3845                  return true;
3846              }
3847              $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
3848              return ( time() < $expiry );
3849          } else {
3850              return false;
3851          }
3852      }
3853  
3854      /**
3855       * Alias for getEditToken.
3856       * @deprecated since 1.19, use getEditToken instead.
3857       *
3858       * @param string|array $salt Array of Strings Optional function-specific data for hashing
3859       * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
3860       * @return string The new edit token
3861       */
3862  	public function editToken( $salt = '', $request = null ) {
3863          wfDeprecated( __METHOD__, '1.19' );
3864          return $this->getEditToken( $salt, $request );
3865      }
3866  
3867      /**
3868       * Initialize (if necessary) and return a session token value
3869       * which can be used in edit forms to show that the user's
3870       * login credentials aren't being hijacked with a foreign form
3871       * submission.
3872       *
3873       * @since 1.19
3874       *
3875       * @param string|array $salt Array of Strings Optional function-specific data for hashing
3876       * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
3877       * @return string The new edit token
3878       */
3879  	public function getEditToken( $salt = '', $request = null ) {
3880          if ( $request == null ) {
3881              $request = $this->getRequest();
3882          }
3883  
3884          if ( $this->isAnon() ) {
3885              return self::EDIT_TOKEN_SUFFIX;
3886          } else {
3887              $token = $request->getSessionData( 'wsEditToken' );
3888              if ( $token === null ) {
3889                  $token = MWCryptRand::generateHex( 32 );
3890                  $request->setSessionData( 'wsEditToken', $token );
3891              }
3892              if ( is_array( $salt ) ) {
3893                  $salt = implode( '|', $salt );
3894              }
3895              return md5( $token . $salt ) . self::EDIT_TOKEN_SUFFIX;
3896          }
3897      }
3898  
3899      /**
3900       * Generate a looking random token for various uses.
3901       *
3902       * @return string The new random token
3903       * @deprecated since 1.20: Use MWCryptRand for secure purposes or
3904       *   wfRandomString for pseudo-randomness.
3905       */
3906  	public static function generateToken() {
3907          return MWCryptRand::generateHex( 32 );
3908      }
3909  
3910      /**
3911       * Check given value against the token value stored in the session.
3912       * A match should confirm that the form was submitted from the
3913       * user's own login session, not a form submission from a third-party
3914       * site.
3915       *
3916       * @param string $val Input value to compare
3917       * @param string $salt Optional function-specific data for hashing
3918       * @param WebRequest|null $request Object to use or null to use $wgRequest
3919       * @return bool Whether the token matches
3920       */
3921  	public function matchEditToken( $val, $salt = '', $request = null ) {
3922          $sessionToken = $this->getEditToken( $salt, $request );
3923          if ( $val != $sessionToken ) {
3924              wfDebug( "User::matchEditToken: broken session data\n" );
3925          }
3926  
3927          return $val == $sessionToken;
3928      }
3929  
3930      /**
3931       * Check given value against the token value stored in the session,
3932       * ignoring the suffix.
3933       *
3934       * @param string $val Input value to compare
3935       * @param string $salt Optional function-specific data for hashing
3936       * @param WebRequest|null $request Object to use or null to use $wgRequest
3937       * @return bool Whether the token matches
3938       */
3939  	public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) {
3940          $sessionToken = $this->getEditToken( $salt, $request );
3941          return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
3942      }
3943  
3944      /**
3945       * Generate a new e-mail confirmation token and send a confirmation/invalidation
3946       * mail to the user's given address.
3947       *
3948       * @param string $type Message to send, either "created", "changed" or "set"
3949       * @return Status
3950       */
3951  	public function sendConfirmationMail( $type = 'created' ) {
3952          global $wgLang;
3953          $expiration = null; // gets passed-by-ref and defined in next line.
3954          $token = $this->confirmationToken( $expiration );
3955          $url = $this->confirmationTokenUrl( $token );
3956          $invalidateURL = $this->invalidationTokenUrl( $token );
3957          $this->saveSettings();
3958  
3959          if ( $type == 'created' || $type === false ) {
3960              $message = 'confirmemail_body';
3961          } elseif ( $type === true ) {
3962              $message = 'confirmemail_body_changed';
3963          } else {
3964              // Messages: confirmemail_body_changed, confirmemail_body_set
3965              $message = 'confirmemail_body_' . $type;
3966          }
3967  
3968          return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
3969              wfMessage( $message,
3970                  $this->getRequest()->getIP(),
3971                  $this->getName(),
3972                  $url,
3973                  $wgLang->timeanddate( $expiration, false ),
3974                  $invalidateURL,
3975                  $wgLang->date( $expiration, false ),
3976                  $wgLang->time( $expiration, false ) )->text() );
3977      }
3978  
3979      /**
3980       * Send an e-mail to this user's account. Does not check for
3981       * confirmed status or validity.
3982       *
3983       * @param string $subject Message subject
3984       * @param string $body Message body
3985       * @param string $from Optional From address; if unspecified, default
3986       *   $wgPasswordSender will be used.
3987       * @param string $replyto Reply-To address
3988       * @return Status
3989       */
3990  	public function sendMail( $subject, $body, $from = null, $replyto = null ) {
3991          if ( is_null( $from ) ) {
3992              global $wgPasswordSender;
3993              $sender = new MailAddress( $wgPasswordSender,
3994                  wfMessage( 'emailsender' )->inContentLanguage()->text() );
3995          } else {
3996              $sender = MailAddress::newFromUser( $from );
3997          }
3998  
3999          $to = MailAddress::newFromUser( $this );
4000          return UserMailer::send( $to, $sender, $subject, $body, $replyto );
4001      }
4002  
4003      /**
4004       * Generate, store, and return a new e-mail confirmation code.
4005       * A hash (unsalted, since it's used as a key) is stored.
4006       *
4007       * @note Call saveSettings() after calling this function to commit
4008       * this change to the database.
4009       *
4010       * @param string &$expiration Accepts the expiration time
4011       * @return string New token
4012       */
4013  	protected function confirmationToken( &$expiration ) {
4014          global $wgUserEmailConfirmationTokenExpiry;
4015          $now = time();
4016          $expires = $now + $wgUserEmailConfirmationTokenExpiry;
4017          $expiration = wfTimestamp( TS_MW, $expires );
4018          $this->load();
4019          $token = MWCryptRand::generateHex( 32 );
4020          $hash = md5( $token );
4021          $this->mEmailToken = $hash;
4022          $this->mEmailTokenExpires = $expiration;
4023          return $token;
4024      }
4025  
4026      /**
4027       * Return a URL the user can use to confirm their email address.
4028       * @param string $token Accepts the email confirmation token
4029       * @return string New token URL
4030       */
4031  	protected function confirmationTokenUrl( $token ) {
4032          return $this->getTokenUrl( 'ConfirmEmail', $token );
4033      }
4034  
4035      /**
4036       * Return a URL the user can use to invalidate their email address.
4037       * @param string $token Accepts the email confirmation token
4038       * @return string New token URL
4039       */
4040  	protected function invalidationTokenUrl( $token ) {
4041          return $this->getTokenUrl( 'InvalidateEmail', $token );
4042      }
4043  
4044      /**
4045       * Internal function to format the e-mail validation/invalidation URLs.
4046       * This uses a quickie hack to use the
4047       * hardcoded English names of the Special: pages, for ASCII safety.
4048       *
4049       * @note Since these URLs get dropped directly into emails, using the
4050       * short English names avoids insanely long URL-encoded links, which
4051       * also sometimes can get corrupted in some browsers/mailers
4052       * (bug 6957 with Gmail and Internet Explorer).
4053       *
4054       * @param string $page Special page
4055       * @param string $token Token
4056       * @return string Formatted URL
4057       */
4058  	protected function getTokenUrl( $page, $token ) {
4059          // Hack to bypass localization of 'Special:'
4060          $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
4061          return $title->getCanonicalURL();
4062      }
4063  
4064      /**
4065       * Mark the e-mail address confirmed.
4066       *
4067       * @note Call saveSettings() after calling this function to commit the change.
4068       *
4069       * @return bool
4070       */
4071  	public function confirmEmail() {
4072          // Check if it's already confirmed, so we don't touch the database
4073          // and fire the ConfirmEmailComplete hook on redundant confirmations.
4074          if ( !$this->isEmailConfirmed() ) {
4075              $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
4076              wfRunHooks( 'ConfirmEmailComplete', array( $this ) );
4077          }
4078          return true;
4079      }
4080  
4081      /**
4082       * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
4083       * address if it was already confirmed.
4084       *
4085       * @note Call saveSettings() after calling this function to commit the change.
4086       * @return bool Returns true
4087       */
4088  	public function invalidateEmail() {
4089          $this->load();
4090          $this->mEmailToken = null;
4091          $this->mEmailTokenExpires = null;
4092          $this->setEmailAuthenticationTimestamp( null );
4093          $this->mEmail = '';
4094          wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
4095          return true;
4096      }
4097  
4098      /**
4099       * Set the e-mail authentication timestamp.
4100       * @param string $timestamp TS_MW timestamp
4101       */
4102  	public function setEmailAuthenticationTimestamp( $timestamp ) {
4103          $this->load();
4104          $this->mEmailAuthenticated = $timestamp;
4105          wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
4106      }
4107  
4108      /**
4109       * Is this user allowed to send e-mails within limits of current
4110       * site configuration?
4111       * @return bool
4112       */
4113  	public function canSendEmail() {
4114          global $wgEnableEmail, $wgEnableUserEmail;
4115          if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
4116              return false;
4117          }
4118          $canSend = $this->isEmailConfirmed();
4119          wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
4120          return $canSend;
4121      }
4122  
4123      /**
4124       * Is this user allowed to receive e-mails within limits of current
4125       * site configuration?
4126       * @return bool
4127       */
4128  	public function canReceiveEmail() {
4129          return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
4130      }
4131  
4132      /**
4133       * Is this user's e-mail address valid-looking and confirmed within
4134       * limits of the current site configuration?
4135       *
4136       * @note If $wgEmailAuthentication is on, this may require the user to have
4137       * confirmed their address by returning a code or using a password
4138       * sent to the address from the wiki.
4139       *
4140       * @return bool
4141       */
4142  	public function isEmailConfirmed() {
4143          global $wgEmailAuthentication;
4144          $this->load();
4145          $confirmed = true;
4146          if ( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
4147              if ( $this->isAnon() ) {
4148                  return false;
4149              }
4150              if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
4151                  return false;
4152              }
4153              if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
4154                  return false;
4155              }
4156              return true;
4157          } else {
4158              return $confirmed;
4159          }
4160      }
4161  
4162      /**
4163       * Check whether there is an outstanding request for e-mail confirmation.
4164       * @return bool
4165       */
4166  	public function isEmailConfirmationPending() {
4167          global $wgEmailAuthentication;
4168          return $wgEmailAuthentication &&
4169              !$this->isEmailConfirmed() &&
4170              $this->mEmailToken &&
4171              $this->mEmailTokenExpires > wfTimestamp();
4172      }
4173  
4174      /**
4175       * Get the timestamp of account creation.
4176       *
4177       * @return string|bool|null Timestamp of account creation, false for
4178       *  non-existent/anonymous user accounts, or null if existing account
4179       *  but information is not in database.
4180       */
4181  	public function getRegistration() {
4182          if ( $this->isAnon() ) {
4183              return false;
4184          }
4185          $this->load();
4186          return $this->mRegistration;
4187      }
4188  
4189      /**
4190       * Get the timestamp of the first edit
4191       *
4192       * @return string|bool Timestamp of first edit, or false for
4193       *  non-existent/anonymous user accounts.
4194       */
4195  	public function getFirstEditTimestamp() {
4196          if ( $this->getId() == 0 ) {
4197              return false; // anons
4198          }
4199          $dbr = wfGetDB( DB_SLAVE );
4200          $time = $dbr->selectField( 'revision', 'rev_timestamp',
4201              array( 'rev_user' => $this->getId() ),
4202              __METHOD__,
4203              array( 'ORDER BY' => 'rev_timestamp ASC' )
4204          );
4205          if ( !$time ) {
4206              return false; // no edits
4207          }
4208          return wfTimestamp( TS_MW, $time );
4209      }
4210  
4211      /**
4212       * Get the permissions associated with a given list of groups
4213       *
4214       * @param array $groups Array of Strings List of internal group names
4215       * @return array Array of Strings List of permission key names for given groups combined
4216       */
4217  	public static function getGroupPermissions( $groups ) {
4218          global $wgGroupPermissions, $wgRevokePermissions;
4219          $rights = array();
4220          // grant every granted permission first
4221          foreach ( $groups as $group ) {
4222              if ( isset( $wgGroupPermissions[$group] ) ) {
4223                  $rights = array_merge( $rights,
4224                      // array_filter removes empty items
4225                      array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
4226              }
4227          }
4228          // now revoke the revoked permissions
4229          foreach ( $groups as $group ) {
4230              if ( isset( $wgRevokePermissions[$group] ) ) {
4231                  $rights = array_diff( $rights,
4232                      array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
4233              }
4234          }
4235          return array_unique( $rights );
4236      }
4237  
4238      /**
4239       * Get all the groups who have a given permission
4240       *
4241       * @param string $role Role to check
4242       * @return array Array of Strings List of internal group names with the given permission
4243       */
4244  	public static function getGroupsWithPermission( $role ) {
4245          global $wgGroupPermissions;
4246          $allowedGroups = array();
4247          foreach ( array_keys( $wgGroupPermissions ) as $group ) {
4248              if ( self::groupHasPermission( $group, $role ) ) {
4249                  $allowedGroups[] = $group;
4250              }
4251          }
4252          return $allowedGroups;
4253      }
4254  
4255      /**
4256       * Check, if the given group has the given permission
4257       *
4258       * If you're wanting to check whether all users have a permission, use
4259       * User::isEveryoneAllowed() instead. That properly checks if it's revoked
4260       * from anyone.
4261       *
4262       * @since 1.21
4263       * @param string $group Group to check
4264       * @param string $role Role to check
4265       * @return bool
4266       */
4267  	public static function groupHasPermission( $group, $role ) {
4268          global $wgGroupPermissions, $wgRevokePermissions;
4269          return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
4270              && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
4271      }
4272  
4273      /**
4274       * Check if all users have the given permission
4275       *
4276       * @since 1.22
4277       * @param string $right Right to check
4278       * @return bool
4279       */
4280  	public static function isEveryoneAllowed( $right ) {
4281          global $wgGroupPermissions, $wgRevokePermissions;
4282          static $cache = array();
4283  
4284          // Use the cached results, except in unit tests which rely on
4285          // being able change the permission mid-request
4286          if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) {
4287              return $cache[$right];
4288          }
4289  
4290          if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) {
4291              $cache[$right] = false;
4292              return false;
4293          }
4294  
4295          // If it's revoked anywhere, then everyone doesn't have it
4296          foreach ( $wgRevokePermissions as $rights ) {
4297              if ( isset( $rights[$right] ) && $rights[$right] ) {
4298                  $cache[$right] = false;
4299                  return false;
4300              }
4301          }
4302  
4303          // Allow extensions (e.g. OAuth) to say false
4304          if ( !wfRunHooks( 'UserIsEveryoneAllowed', array( $right ) ) ) {
4305              $cache[$right] = false;
4306              return false;
4307          }
4308  
4309          $cache[$right] = true;
4310          return true;
4311      }
4312  
4313      /**
4314       * Get the localized descriptive name for a group, if it exists
4315       *
4316       * @param string $group Internal group name
4317       * @return string Localized descriptive group name
4318       */
4319  	public static function getGroupName( $group ) {
4320          $msg = wfMessage( "group-$group" );
4321          return $msg->isBlank() ? $group : $msg->text();
4322      }
4323  
4324      /**
4325       * Get the localized descriptive name for a member of a group, if it exists
4326       *
4327       * @param string $group Internal group name
4328       * @param string $username Username for gender (since 1.19)
4329       * @return string Localized name for group member
4330       */
4331  	public static function getGroupMember( $group, $username = '#' ) {
4332          $msg = wfMessage( "group-$group-member", $username );
4333          return $msg->isBlank() ? $group : $msg->text();
4334      }
4335  
4336      /**
4337       * Return the set of defined explicit groups.
4338       * The implicit groups (by default *, 'user' and 'autoconfirmed')
4339       * are not included, as they are defined automatically, not in the database.
4340       * @return array Array of internal group names
4341       */
4342  	public static function getAllGroups() {
4343          global $wgGroupPermissions, $wgRevokePermissions;
4344          return array_diff(
4345              array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
4346              self::getImplicitGroups()
4347          );
4348      }
4349  
4350      /**
4351       * Get a list of all available permissions.
4352       * @return array Array of permission names
4353       */
4354  	public static function getAllRights() {
4355          if ( self::$mAllRights === false ) {
4356              global $wgAvailableRights;
4357              if ( count( $wgAvailableRights ) ) {
4358                  self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
4359              } else {
4360                  self::$mAllRights = self::$mCoreRights;
4361              }
4362              wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
4363          }
4364          return self::$mAllRights;
4365      }
4366  
4367      /**
4368       * Get a list of implicit groups
4369       * @return array Array of Strings Array of internal group names
4370       */
4371  	public static function getImplicitGroups() {
4372          global $wgImplicitGroups;
4373  
4374          $groups = $wgImplicitGroups;
4375          # Deprecated, use $wgImplictGroups instead
4376          wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );
4377  
4378          return $groups;
4379      }
4380  
4381      /**
4382       * Get the title of a page describing a particular group
4383       *
4384       * @param string $group Internal group name
4385       * @return Title|bool Title of the page if it exists, false otherwise
4386       */
4387  	public static function getGroupPage( $group ) {
4388          $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage();
4389          if ( $msg->exists() ) {
4390              $title = Title::newFromText( $msg->text() );
4391              if ( is_object( $title ) ) {
4392                  return $title;
4393              }
4394          }
4395          return false;
4396      }
4397  
4398      /**
4399       * Create a link to the group in HTML, if available;
4400       * else return the group name.
4401       *
4402       * @param string $group Internal name of the group
4403       * @param string $text The text of the link
4404       * @return string HTML link to the group
4405       */
4406  	public static function makeGroupLinkHTML( $group, $text = '' ) {
4407          if ( $text == '' ) {
4408              $text = self::getGroupName( $group );
4409          }
4410          $title = self::getGroupPage( $group );
4411          if ( $title ) {
4412              return Linker::link( $title, htmlspecialchars( $text ) );
4413          } else {
4414              return $text;
4415          }
4416      }
4417  
4418      /**
4419       * Create a link to the group in Wikitext, if available;
4420       * else return the group name.
4421       *
4422       * @param string $group Internal name of the group
4423       * @param string $text The text of the link
4424       * @return string Wikilink to the group
4425       */
4426  	public static function makeGroupLinkWiki( $group, $text = '' ) {
4427          if ( $text == '' ) {
4428              $text = self::getGroupName( $group );
4429          }
4430          $title = self::getGroupPage( $group );
4431          if ( $title ) {
4432              $page = $title->getPrefixedText();
4433              return "[[$page|$text]]";
4434          } else {
4435              return $text;
4436          }
4437      }
4438  
4439      /**
4440       * Returns an array of the groups that a particular group can add/remove.
4441       *
4442       * @param string $group The group to check for whether it can add/remove
4443       * @return array Array( 'add' => array( addablegroups ),
4444       *     'remove' => array( removablegroups ),
4445       *     'add-self' => array( addablegroups to self),
4446       *     'remove-self' => array( removable groups from self) )
4447       */
4448  	public static function changeableByGroup( $group ) {
4449          global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
4450  
4451          $groups = array(
4452              'add' => array(),
4453              'remove' => array(),
4454              'add-self' => array(),
4455              'remove-self' => array()
4456          );
4457  
4458          if ( empty( $wgAddGroups[$group] ) ) {
4459              // Don't add anything to $groups
4460          } elseif ( $wgAddGroups[$group] === true ) {
4461              // You get everything
4462              $groups['add'] = self::getAllGroups();
4463          } elseif ( is_array( $wgAddGroups[$group] ) ) {
4464              $groups['add'] = $wgAddGroups[$group];
4465          }
4466  
4467          // Same thing for remove
4468          if ( empty( $wgRemoveGroups[$group] ) ) {
4469          } elseif ( $wgRemoveGroups[$group] === true ) {
4470              $groups['remove'] = self::getAllGroups();
4471          } elseif ( is_array( $wgRemoveGroups[$group] ) ) {
4472              $groups['remove'] = $wgRemoveGroups[$group];
4473          }
4474  
4475          // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
4476          if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) {
4477              foreach ( $wgGroupsAddToSelf as $key => $value ) {
4478                  if ( is_int( $key ) ) {
4479                      $wgGroupsAddToSelf['user'][] = $value;
4480                  }
4481              }
4482          }
4483  
4484          if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) {
4485              foreach ( $wgGroupsRemoveFromSelf as $key => $value ) {
4486                  if ( is_int( $key ) ) {
4487                      $wgGroupsRemoveFromSelf['user'][] = $value;
4488                  }
4489              }
4490          }
4491  
4492          // Now figure out what groups the user can add to him/herself
4493          if ( empty( $wgGroupsAddToSelf[$group] ) ) {
4494          } elseif ( $wgGroupsAddToSelf[$group] === true ) {
4495              // No idea WHY this would be used, but it's there
4496              $groups['add-self'] = User::getAllGroups();
4497          } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
4498              $groups['add-self'] = $wgGroupsAddToSelf[$group];
4499          }
4500  
4501          if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
4502          } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
4503              $groups['remove-self'] = User::getAllGroups();
4504          } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
4505              $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
4506          }
4507  
4508          return $groups;
4509      }
4510  
4511      /**
4512       * Returns an array of groups that this user can add and remove
4513       * @return array Array( 'add' => array( addablegroups ),
4514       *  'remove' => array( removablegroups ),
4515       *  'add-self' => array( addablegroups to self),
4516       *  'remove-self' => array( removable groups from self) )
4517       */
4518  	public function changeableGroups() {
4519          if ( $this->isAllowed( 'userrights' ) ) {
4520              // This group gives the right to modify everything (reverse-
4521              // compatibility with old "userrights lets you change
4522              // everything")
4523              // Using array_merge to make the groups reindexed
4524              $all = array_merge( User::getAllGroups() );
4525              return array(
4526                  'add' => $all,
4527                  'remove' => $all,
4528                  'add-self' => array(),
4529                  'remove-self' => array()
4530              );
4531          }
4532  
4533          // Okay, it's not so simple, we will have to go through the arrays
4534          $groups = array(
4535              'add' => array(),
4536              'remove' => array(),
4537              'add-self' => array(),
4538              'remove-self' => array()
4539          );
4540          $addergroups = $this->getEffectiveGroups();
4541  
4542          foreach ( $addergroups as $addergroup ) {
4543              $groups = array_merge_recursive(
4544                  $groups, $this->changeableByGroup( $addergroup )
4545              );
4546              $groups['add'] = array_unique( $groups['add'] );
4547              $groups['remove'] = array_unique( $groups['remove'] );
4548              $groups['add-self'] = array_unique( $groups['add-self'] );
4549              $groups['remove-self'] = array_unique( $groups['remove-self'] );
4550          }
4551          return $groups;
4552      }
4553  
4554      /**
4555       * Increment the user's edit-count field.
4556       * Will have no effect for anonymous users.
4557       */
4558  	public function incEditCount() {
4559          if ( !$this->isAnon() ) {
4560              $dbw = wfGetDB( DB_MASTER );
4561              $dbw->update(
4562                  'user',
4563                  array( 'user_editcount=user_editcount+1' ),
4564                  array( 'user_id' => $this->getId() ),
4565                  __METHOD__
4566              );
4567  
4568              // Lazy initialization check...
4569              if ( $dbw->affectedRows() == 0 ) {
4570                  // Now here's a goddamn hack...
4571                  $dbr = wfGetDB( DB_SLAVE );
4572                  if ( $dbr !== $dbw ) {
4573                      // If we actually have a slave server, the count is
4574                      // at least one behind because the current transaction
4575                      // has not been committed and replicated.
4576                      $this->initEditCount( 1 );
4577                  } else {
4578                      // But if DB_SLAVE is selecting the master, then the
4579                      // count we just read includes the revision that was
4580                      // just added in the working transaction.
4581                      $this->initEditCount();
4582                  }
4583              }
4584          }
4585          // edit count in user cache too
4586          $this->invalidateCache();
4587      }
4588  
4589      /**
4590       * Initialize user_editcount from data out of the revision table
4591       *
4592       * @param int $add Edits to add to the count from the revision table
4593       * @return int Number of edits
4594       */
4595  	protected function initEditCount( $add = 0 ) {
4596          // Pull from a slave to be less cruel to servers
4597          // Accuracy isn't the point anyway here
4598          $dbr = wfGetDB( DB_SLAVE );
4599          $count = (int)$dbr->selectField(
4600              'revision',
4601              'COUNT(rev_user)',
4602              array( 'rev_user' => $this->getId() ),
4603              __METHOD__
4604          );
4605          $count = $count + $add;
4606  
4607          $dbw = wfGetDB( DB_MASTER );
4608          $dbw->update(
4609              'user',
4610              array( 'user_editcount' => $count ),
4611              array( 'user_id' => $this->getId() ),
4612              __METHOD__
4613          );
4614  
4615          return $count;
4616      }
4617  
4618      /**
4619       * Get the description of a given right
4620       *
4621       * @param string $right Right to query
4622       * @return string Localized description of the right
4623       */
4624  	public static function getRightDescription( $right ) {
4625          $key = "right-$right";
4626          $msg = wfMessage( $key );
4627          return $msg->isBlank() ? $right : $msg->text();
4628      }
4629  
4630      /**
4631       * Make a new-style password hash
4632       *
4633       * @param string $password Plain-text password
4634       * @param bool|string $salt Optional salt, may be random or the user ID.
4635       *  If unspecified or false, will generate one automatically
4636       * @return string Password hash
4637       * @deprecated since 1.24, use Password class
4638       */
4639  	public static function crypt( $password, $salt = false ) {
4640          wfDeprecated( __METHOD__, '1.24' );
4641          $hash = self::getPasswordFactory()->newFromPlaintext( $password );
4642          return $hash->toString();
4643      }
4644  
4645      /**
4646       * Compare a password hash with a plain-text password. Requires the user
4647       * ID if there's a chance that the hash is an old-style hash.
4648       *
4649       * @param string $hash Password hash
4650       * @param string $password Plain-text password to compare
4651       * @param string|bool $userId User ID for old-style password salt
4652       *
4653       * @return bool
4654       * @deprecated since 1.24, use Password class
4655       */
4656  	public static function comparePasswords( $hash, $password, $userId = false ) {
4657          wfDeprecated( __METHOD__, '1.24' );
4658  
4659          // Check for *really* old password hashes that don't even have a type
4660          // The old hash format was just an md5 hex hash, with no type information
4661          if ( preg_match( '/^[0-9a-f]{32}$/', $hash ) ) {
4662              global $wgPasswordSalt;
4663              if ( $wgPasswordSalt ) {
4664                  $password = ":B:{$userId}:{$hash}";
4665              } else {
4666                  $password = ":A:{$hash}";
4667              }
4668          }
4669  
4670          $hash = self::getPasswordFactory()->newFromCiphertext( $hash );
4671          return $hash->equals( $password );
4672      }
4673  
4674      /**
4675       * Add a newuser log entry for this user.
4676       * Before 1.19 the return value was always true.
4677       *
4678       * @param string|bool $action Account creation type.
4679       *   - String, one of the following values:
4680       *     - 'create' for an anonymous user creating an account for himself.
4681       *       This will force the action's performer to be the created user itself,
4682       *       no matter the value of $wgUser
4683       *     - 'create2' for a logged in user creating an account for someone else
4684       *     - 'byemail' when the created user will receive its password by e-mail
4685       *     - 'autocreate' when the user is automatically created (such as by CentralAuth).
4686       *   - Boolean means whether the account was created by e-mail (deprecated):
4687       *     - true will be converted to 'byemail'
4688       *     - false will be converted to 'create' if this object is the same as
4689       *       $wgUser and to 'create2' otherwise
4690       *
4691       * @param string $reason User supplied reason
4692       *
4693       * @return int|bool True if not $wgNewUserLog; otherwise ID of log item or 0 on failure
4694       */
4695  	public function addNewUserLogEntry( $action = false, $reason = '' ) {
4696          global $wgUser, $wgNewUserLog;
4697          if ( empty( $wgNewUserLog ) ) {
4698              return true; // disabled
4699          }
4700  
4701          if ( $action === true ) {
4702              $action = 'byemail';
4703          } elseif ( $action === false ) {
4704              if ( $this->getName() == $wgUser->getName() ) {
4705                  $action = 'create';
4706              } else {
4707                  $action = 'create2';
4708              }
4709          }
4710  
4711          if ( $action === 'create' || $action === 'autocreate' ) {
4712              $performer = $this;
4713          } else {
4714              $performer = $wgUser;
4715          }
4716  
4717          $logEntry = new ManualLogEntry( 'newusers', $action );
4718          $logEntry->setPerformer( $performer );
4719          $logEntry->setTarget( $this->getUserPage() );
4720          $logEntry->setComment( $reason );
4721          $logEntry->setParameters( array(
4722              '4::userid' => $this->getId(),
4723          ) );
4724          $logid = $logEntry->insert();
4725  
4726          if ( $action !== 'autocreate' ) {
4727              $logEntry->publish( $logid );
4728          }
4729  
4730          return (int)$logid;
4731      }
4732  
4733      /**
4734       * Add an autocreate newuser log entry for this user
4735       * Used by things like CentralAuth and perhaps other authplugins.
4736       * Consider calling addNewUserLogEntry() directly instead.
4737       *
4738       * @return bool
4739       */
4740  	public function addNewUserLogEntryAutoCreate() {
4741          $this->addNewUserLogEntry( 'autocreate' );
4742  
4743          return true;
4744      }
4745  
4746      /**
4747       * Load the user options either from cache, the database or an array
4748       *
4749       * @param array $data Rows for the current user out of the user_properties table
4750       */
4751  	protected function loadOptions( $data = null ) {
4752          global $wgContLang;
4753  
4754          $this->load();
4755  
4756          if ( $this->mOptionsLoaded ) {
4757              return;
4758          }
4759  
4760          $this->mOptions = self::getDefaultOptions();
4761  
4762          if ( !$this->getId() ) {
4763              // For unlogged-in users, load language/variant options from request.
4764              // There's no need to do it for logged-in users: they can set preferences,
4765              // and handling of page content is done by $pageLang->getPreferredVariant() and such,
4766              // so don't override user's choice (especially when the user chooses site default).
4767              $variant = $wgContLang->getDefaultVariant();
4768              $this->mOptions['variant'] = $variant;
4769              $this->mOptions['language'] = $variant;
4770              $this->mOptionsLoaded = true;
4771              return;
4772          }
4773  
4774          // Maybe load from the object
4775          if ( !is_null( $this->mOptionOverrides ) ) {
4776              wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
4777              foreach ( $this->mOptionOverrides as $key => $value ) {
4778                  $this->mOptions[$key] = $value;
4779              }
4780          } else {
4781              if ( !is_array( $data ) ) {
4782                  wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
4783                  // Load from database
4784                  $dbr = wfGetDB( DB_SLAVE );
4785  
4786                  $res = $dbr->select(
4787                      'user_properties',
4788                      array( 'up_property', 'up_value' ),
4789                      array( 'up_user' => $this->getId() ),
4790                      __METHOD__
4791                  );
4792  
4793                  $this->mOptionOverrides = array();
4794                  $data = array();
4795                  foreach ( $res as $row ) {
4796                      $data[$row->up_property] = $row->up_value;
4797                  }
4798              }
4799              foreach ( $data as $property => $value ) {
4800                  $this->mOptionOverrides[$property] = $value;
4801                  $this->mOptions[$property] = $value;
4802              }
4803          }
4804  
4805          $this->mOptionsLoaded = true;
4806  
4807          wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
4808      }
4809  
4810      /**
4811       * Saves the non-default options for this user, as previously set e.g. via
4812       * setOption(), in the database's "user_properties" (preferences) table.
4813       * Usually used via saveSettings().
4814       */
4815  	protected function saveOptions() {
4816          $this->loadOptions();
4817  
4818          // Not using getOptions(), to keep hidden preferences in database
4819          $saveOptions = $this->mOptions;
4820  
4821          // Allow hooks to abort, for instance to save to a global profile.
4822          // Reset options to default state before saving.
4823          if ( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) {
4824              return;
4825          }
4826  
4827          $userId = $this->getId();
4828  
4829          $insert_rows = array(); // all the new preference rows
4830          foreach ( $saveOptions as $key => $value ) {
4831              // Don't bother storing default values
4832              $defaultOption = self::getDefaultOption( $key );
4833              if ( ( $defaultOption === null && $value !== false && $value !== null )
4834                  || $value != $defaultOption
4835              ) {
4836                  $insert_rows[] = array(
4837                      'up_user' => $userId,
4838                      'up_property' => $key,
4839                      'up_value' => $value,
4840                  );
4841              }
4842          }
4843  
4844          $dbw = wfGetDB( DB_MASTER );
4845  
4846          $res = $dbw->select( 'user_properties',
4847              array( 'up_property', 'up_value' ), array( 'up_user' => $userId ), __METHOD__ );
4848  
4849          // Find prior rows that need to be removed or updated. These rows will
4850          // all be deleted (the later so that INSERT IGNORE applies the new values).
4851          $keysDelete = array();
4852          foreach ( $res as $row ) {
4853              if ( !isset( $saveOptions[$row->up_property] )
4854                  || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0
4855              ) {
4856                  $keysDelete[] = $row->up_property;
4857              }
4858          }
4859  
4860          if ( count( $keysDelete ) ) {
4861              // Do the DELETE by PRIMARY KEY for prior rows.
4862              // In the past a very large portion of calls to this function are for setting
4863              // 'rememberpassword' for new accounts (a preference that has since been removed).
4864              // Doing a blanket per-user DELETE for new accounts with no rows in the table
4865              // caused gap locks on [max user ID,+infinity) which caused high contention since
4866              // updates would pile up on each other as they are for higher (newer) user IDs.
4867              // It might not be necessary these days, but it shouldn't hurt either.
4868              $dbw->delete( 'user_properties',
4869                  array( 'up_user' => $userId, 'up_property' => $keysDelete ), __METHOD__ );
4870          }
4871          // Insert the new preference rows
4872          $dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) );
4873      }
4874  
4875      /**
4876       * Lazily instantiate and return a factory object for making passwords
4877       *
4878       * @return PasswordFactory
4879       */
4880  	public static function getPasswordFactory() {
4881          if ( self::$mPasswordFactory === null ) {
4882              self::$mPasswordFactory = new PasswordFactory();
4883              self::$mPasswordFactory->init( RequestContext::getMain()->getConfig() );
4884          }
4885  
4886          return self::$mPasswordFactory;
4887      }
4888  
4889      /**
4890       * Provide an array of HTML5 attributes to put on an input element
4891       * intended for the user to enter a new password.  This may include
4892       * required, title, and/or pattern, depending on $wgMinimalPasswordLength.
4893       *
4894       * Do *not* use this when asking the user to enter his current password!
4895       * Regardless of configuration, users may have invalid passwords for whatever
4896       * reason (e.g., they were set before requirements were tightened up).
4897       * Only use it when asking for a new password, like on account creation or
4898       * ResetPass.
4899       *
4900       * Obviously, you still need to do server-side checking.
4901       *
4902       * NOTE: A combination of bugs in various browsers means that this function
4903       * actually just returns array() unconditionally at the moment.  May as
4904       * well keep it around for when the browser bugs get fixed, though.
4905       *
4906       * @todo FIXME: This does not belong here; put it in Html or Linker or somewhere
4907       *
4908       * @return array Array of HTML attributes suitable for feeding to
4909       *   Html::element(), directly or indirectly.  (Don't feed to Xml::*()!
4910       *   That will get confused by the boolean attribute syntax used.)
4911       */
4912  	public static function passwordChangeInputAttribs() {
4913          global $wgMinimalPasswordLength;
4914  
4915          if ( $wgMinimalPasswordLength == 0 ) {
4916              return array();
4917          }
4918  
4919          # Note that the pattern requirement will always be satisfied if the
4920          # input is empty, so we need required in all cases.
4921          #
4922          # @todo FIXME: Bug 23769: This needs to not claim the password is required
4923          # if e-mail confirmation is being used.  Since HTML5 input validation
4924          # is b0rked anyway in some browsers, just return nothing.  When it's
4925          # re-enabled, fix this code to not output required for e-mail
4926          # registration.
4927          #$ret = array( 'required' );
4928          $ret = array();
4929  
4930          # We can't actually do this right now, because Opera 9.6 will print out
4931          # the entered password visibly in its error message!  When other
4932          # browsers add support for this attribute, or Opera fixes its support,
4933          # we can add support with a version check to avoid doing this on Opera
4934          # versions where it will be a problem.  Reported to Opera as
4935          # DSK-262266, but they don't have a public bug tracker for us to follow.
4936          /*
4937          if ( $wgMinimalPasswordLength > 1 ) {
4938              $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
4939              $ret['title'] = wfMessage( 'passwordtooshort' )
4940                  ->numParams( $wgMinimalPasswordLength )->text();
4941          }
4942          */
4943  
4944          return $ret;
4945      }
4946  
4947      /**
4948       * Return the list of user fields that should be selected to create
4949       * a new user object.
4950       * @return array
4951       */
4952  	public static function selectFields() {
4953          return array(
4954              'user_id',
4955              'user_name',
4956              'user_real_name',
4957              'user_email',
4958              'user_touched',
4959              'user_token',
4960              'user_email_authenticated',
4961              'user_email_token',
4962              'user_email_token_expires',
4963              'user_registration',
4964              'user_editcount',
4965          );
4966      }
4967  
4968      /**
4969       * Factory function for fatal permission-denied errors
4970       *
4971       * @since 1.22
4972       * @param string $permission User right required
4973       * @return Status
4974       */
4975  	static function newFatalPermissionDeniedStatus( $permission ) {
4976          global $wgLang;
4977  
4978          $groups = array_map(
4979              array( 'User', 'makeGroupLinkWiki' ),
4980              User::getGroupsWithPermission( $permission )
4981          );
4982  
4983          if ( $groups ) {
4984              return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
4985          } else {
4986              return Status::newFatal( 'badaccess-group0' );
4987          }
4988      }
4989  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1