PageRenderTime 59ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/includes/User.php

https://github.com/tav/confluence
PHP | 3404 lines | 2066 code | 280 blank | 1058 comment | 307 complexity | 8653b2a9a6500407554f9532e5a27a41 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Implements the User class for the %MediaWiki software.
  4. * @file
  5. */
  6. /**
  7. * \int Number of characters in user_token field.
  8. * @ingroup Constants
  9. */
  10. define( 'USER_TOKEN_LENGTH', 32 );
  11. /**
  12. * \int Serialized record version.
  13. * @ingroup Constants
  14. */
  15. define( 'MW_USER_VERSION', 6 );
  16. /**
  17. * \string Some punctuation to prevent editing from broken text-mangling proxies.
  18. * @ingroup Constants
  19. */
  20. define( 'EDIT_TOKEN_SUFFIX', '+\\' );
  21. /**
  22. * Thrown by User::setPassword() on error.
  23. * @ingroup Exception
  24. */
  25. class PasswordError extends MWException {
  26. // NOP
  27. }
  28. /**
  29. * The User object encapsulates all of the user-specific settings (user_id,
  30. * name, rights, password, email address, options, last login time). Client
  31. * classes use the getXXX() functions to access these fields. These functions
  32. * do all the work of determining whether the user is logged in,
  33. * whether the requested option can be satisfied from cookies or
  34. * whether a database query is needed. Most of the settings needed
  35. * for rendering normal pages are set in the cookie to minimize use
  36. * of the database.
  37. */
  38. class User {
  39. /**
  40. * \type{\arrayof{\string}} A list of default user toggles, i.e., boolean user
  41. * preferences that are displayed by Special:Preferences as checkboxes.
  42. * This list can be extended via the UserToggles hook or by
  43. * $wgContLang::getExtraUserToggles().
  44. * @showinitializer
  45. */
  46. public static $mToggles = array(
  47. 'highlightbroken',
  48. 'justify',
  49. 'hideminor',
  50. 'extendwatchlist',
  51. 'usenewrc',
  52. 'numberheadings',
  53. 'showtoolbar',
  54. 'editondblclick',
  55. 'editsection',
  56. 'editsectiononrightclick',
  57. 'showtoc',
  58. 'rememberpassword',
  59. 'editwidth',
  60. 'watchcreations',
  61. 'watchdefault',
  62. 'watchmoves',
  63. 'watchdeletion',
  64. 'minordefault',
  65. 'previewontop',
  66. 'previewonfirst',
  67. 'nocache',
  68. 'enotifwatchlistpages',
  69. 'enotifusertalkpages',
  70. 'enotifminoredits',
  71. 'enotifrevealaddr',
  72. 'shownumberswatching',
  73. 'fancysig',
  74. 'externaleditor',
  75. 'externaldiff',
  76. 'showjumplinks',
  77. 'uselivepreview',
  78. 'forceeditsummary',
  79. 'watchlisthideminor',
  80. 'watchlisthidebots',
  81. 'watchlisthideown',
  82. 'watchlisthideanons',
  83. 'watchlisthideliu',
  84. 'ccmeonemails',
  85. 'diffonly',
  86. 'showhiddencats',
  87. 'noconvertlink',
  88. 'norollbackdiff',
  89. );
  90. /**
  91. * \type{\arrayof{\string}} List of member variables which are saved to the
  92. * shared cache (memcached). Any operation which changes the
  93. * corresponding database fields must call a cache-clearing function.
  94. * @showinitializer
  95. */
  96. static $mCacheVars = array(
  97. // user table
  98. 'mId',
  99. 'mName',
  100. 'mRealName',
  101. 'mPassword',
  102. 'mNewpassword',
  103. 'mNewpassTime',
  104. 'mEmail',
  105. 'mOptions',
  106. 'mTouched',
  107. 'mToken',
  108. 'mEmailAuthenticated',
  109. 'mEmailToken',
  110. 'mEmailTokenExpires',
  111. 'mRegistration',
  112. 'mEditCount',
  113. // user_group table
  114. 'mGroups',
  115. );
  116. /**
  117. * \type{\arrayof{\string}} Core rights.
  118. * Each of these should have a corresponding message of the form
  119. * "right-$right".
  120. * @showinitializer
  121. */
  122. static $mCoreRights = array(
  123. 'apihighlimits',
  124. 'autoconfirmed',
  125. 'autopatrol',
  126. 'bigdelete',
  127. 'block',
  128. 'blockemail',
  129. 'bot',
  130. 'browsearchive',
  131. 'createaccount',
  132. 'createpage',
  133. 'createtalk',
  134. 'delete',
  135. 'deletedhistory',
  136. 'deleterevision',
  137. 'edit',
  138. 'editinterface',
  139. 'editusercssjs',
  140. 'hideuser',
  141. 'import',
  142. 'importupload',
  143. 'ipblock-exempt',
  144. 'markbotedits',
  145. 'minoredit',
  146. 'move',
  147. 'movefile',
  148. 'move-rootuserpages',
  149. 'move-subpages',
  150. 'nominornewtalk',
  151. 'noratelimit',
  152. 'override-export-depth',
  153. 'patrol',
  154. 'protect',
  155. 'proxyunbannable',
  156. 'purge',
  157. 'read',
  158. 'reupload',
  159. 'reupload-shared',
  160. 'rollback',
  161. 'siteadmin',
  162. 'suppressionlog',
  163. 'suppressredirect',
  164. 'suppressrevision',
  165. 'trackback',
  166. 'undelete',
  167. 'unwatchedpages',
  168. 'upload',
  169. 'upload_by_url',
  170. 'userrights',
  171. 'userrights-interwiki',
  172. 'writeapi',
  173. );
  174. /**
  175. * \string Cached results of getAllRights()
  176. */
  177. static $mAllRights = false;
  178. /** @name Cache variables */
  179. //@{
  180. var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
  181. $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
  182. $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
  183. //@}
  184. /**
  185. * \bool Whether the cache variables have been loaded.
  186. */
  187. var $mDataLoaded, $mAuthLoaded;
  188. /**
  189. * \string Initialization data source if mDataLoaded==false. May be one of:
  190. * - 'defaults' anonymous user initialised from class defaults
  191. * - 'name' initialise from mName
  192. * - 'id' initialise from mId
  193. * - 'session' log in from cookies or session if possible
  194. *
  195. * Use the User::newFrom*() family of functions to set this.
  196. */
  197. var $mFrom;
  198. /** @name Lazy-initialized variables, invalidated with clearInstanceCache */
  199. //@{
  200. var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
  201. $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally,
  202. $mLocked, $mHideName;
  203. //@}
  204. /**
  205. * Lightweight constructor for an anonymous user.
  206. * Use the User::newFrom* factory functions for other kinds of users.
  207. *
  208. * @see newFromName()
  209. * @see newFromId()
  210. * @see newFromConfirmationCode()
  211. * @see newFromSession()
  212. * @see newFromRow()
  213. */
  214. function User() {
  215. $this->clearInstanceCache( 'defaults' );
  216. }
  217. /**
  218. * Load the user table data for this object from the source given by mFrom.
  219. */
  220. function load() {
  221. if ( $this->mDataLoaded ) {
  222. return;
  223. }
  224. wfProfileIn( __METHOD__ );
  225. # Set it now to avoid infinite recursion in accessors
  226. $this->mDataLoaded = true;
  227. switch ( $this->mFrom ) {
  228. case 'defaults':
  229. $this->loadDefaults();
  230. break;
  231. case 'name':
  232. $this->mId = self::idFromName( $this->mName );
  233. if ( !$this->mId ) {
  234. # Nonexistent user placeholder object
  235. $this->loadDefaults( $this->mName );
  236. } else {
  237. $this->loadFromId();
  238. }
  239. break;
  240. case 'id':
  241. $this->loadFromId();
  242. break;
  243. case 'session':
  244. $this->loadFromSession();
  245. wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
  246. break;
  247. default:
  248. throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
  249. }
  250. wfProfileOut( __METHOD__ );
  251. }
  252. /**
  253. * Load user table data, given mId has already been set.
  254. * @return \bool false if the ID does not exist, true otherwise
  255. * @private
  256. */
  257. function loadFromId() {
  258. global $wgMemc;
  259. if ( $this->mId == 0 ) {
  260. $this->loadDefaults();
  261. return false;
  262. }
  263. # Try cache
  264. $key = wfMemcKey( 'user', 'id', $this->mId );
  265. $data = $wgMemc->get( $key );
  266. if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
  267. # Object is expired, load from DB
  268. $data = false;
  269. }
  270. if ( !$data ) {
  271. wfDebug( "Cache miss for user {$this->mId}\n" );
  272. # Load from DB
  273. if ( !$this->loadFromDatabase() ) {
  274. # Can't load from ID, user is anonymous
  275. return false;
  276. }
  277. $this->saveToCache();
  278. } else {
  279. wfDebug( "Got user {$this->mId} from cache\n" );
  280. # Restore from cache
  281. foreach ( self::$mCacheVars as $name ) {
  282. $this->$name = $data[$name];
  283. }
  284. }
  285. return true;
  286. }
  287. /**
  288. * Save user data to the shared cache
  289. */
  290. function saveToCache() {
  291. $this->load();
  292. $this->loadGroups();
  293. if ( $this->isAnon() ) {
  294. // Anonymous users are uncached
  295. return;
  296. }
  297. $data = array();
  298. foreach ( self::$mCacheVars as $name ) {
  299. $data[$name] = $this->$name;
  300. }
  301. $data['mVersion'] = MW_USER_VERSION;
  302. $key = wfMemcKey( 'user', 'id', $this->mId );
  303. global $wgMemc;
  304. $wgMemc->set( $key, $data );
  305. }
  306. /** @name newFrom*() static factory methods */
  307. //@{
  308. /**
  309. * Static factory method for creation from username.
  310. *
  311. * This is slightly less efficient than newFromId(), so use newFromId() if
  312. * you have both an ID and a name handy.
  313. *
  314. * @param $name \string Username, validated by Title::newFromText()
  315. * @param $validate \mixed Validate username. Takes the same parameters as
  316. * User::getCanonicalName(), except that true is accepted as an alias
  317. * for 'valid', for BC.
  318. *
  319. * @return \type{User} The User object, or null if the username is invalid. If the
  320. * username is not present in the database, the result will be a user object
  321. * with a name, zero user ID and default settings.
  322. */
  323. static function newFromName( $name, $validate = 'valid' ) {
  324. if ( $validate === true ) {
  325. $validate = 'valid';
  326. }
  327. $name = self::getCanonicalName( $name, $validate );
  328. if ( $name === false ) {
  329. return null;
  330. } else {
  331. # Create unloaded user object
  332. $u = new User;
  333. $u->mName = $name;
  334. $u->mFrom = 'name';
  335. return $u;
  336. }
  337. }
  338. /**
  339. * Static factory method for creation from a given user ID.
  340. *
  341. * @param $id \int Valid user ID
  342. * @return \type{User} The corresponding User object
  343. */
  344. static function newFromId( $id ) {
  345. $u = new User;
  346. $u->mId = $id;
  347. $u->mFrom = 'id';
  348. return $u;
  349. }
  350. /**
  351. * Factory method to fetch whichever user has a given email confirmation code.
  352. * This code is generated when an account is created or its e-mail address
  353. * has changed.
  354. *
  355. * If the code is invalid or has expired, returns NULL.
  356. *
  357. * @param $code \string Confirmation code
  358. * @return \type{User}
  359. */
  360. static function newFromConfirmationCode( $code ) {
  361. $dbr = wfGetDB( DB_SLAVE );
  362. $id = $dbr->selectField( 'user', 'user_id', array(
  363. 'user_email_token' => md5( $code ),
  364. 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
  365. ) );
  366. if( $id !== false ) {
  367. return User::newFromId( $id );
  368. } else {
  369. return null;
  370. }
  371. }
  372. /**
  373. * Create a new user object using data from session or cookies. If the
  374. * login credentials are invalid, the result is an anonymous user.
  375. *
  376. * @return \type{User}
  377. */
  378. static function newFromSession() {
  379. $user = new User;
  380. $user->mFrom = 'session';
  381. return $user;
  382. }
  383. /**
  384. * Create a new user object from a user row.
  385. * The row should have all fields from the user table in it.
  386. * @param $row array A row from the user table
  387. * @return \type{User}
  388. */
  389. static function newFromRow( $row ) {
  390. $user = new User;
  391. $user->loadFromRow( $row );
  392. return $user;
  393. }
  394. //@}
  395. /**
  396. * Get the username corresponding to a given user ID
  397. * @param $id \int User ID
  398. * @return \string The corresponding username
  399. */
  400. static function whoIs( $id ) {
  401. $dbr = wfGetDB( DB_SLAVE );
  402. return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
  403. }
  404. /**
  405. * Get the real name of a user given their user ID
  406. *
  407. * @param $id \int User ID
  408. * @return \string The corresponding user's real name
  409. */
  410. static function whoIsReal( $id ) {
  411. $dbr = wfGetDB( DB_SLAVE );
  412. return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
  413. }
  414. /**
  415. * Get database id given a user name
  416. * @param $name \string Username
  417. * @return \types{\int,\null} The corresponding user's ID, or null if user is nonexistent
  418. */
  419. static function idFromName( $name ) {
  420. $nt = Title::makeTitleSafe( NS_USER, $name );
  421. if( is_null( $nt ) ) {
  422. # Illegal name
  423. return null;
  424. }
  425. $dbr = wfGetDB( DB_SLAVE );
  426. $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
  427. if ( $s === false ) {
  428. return 0;
  429. } else {
  430. return $s->user_id;
  431. }
  432. }
  433. /**
  434. * Does the string match an anonymous IPv4 address?
  435. *
  436. * This function exists for username validation, in order to reject
  437. * usernames which are similar in form to IP addresses. Strings such
  438. * as 300.300.300.300 will return true because it looks like an IP
  439. * address, despite not being strictly valid.
  440. *
  441. * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
  442. * address because the usemod software would "cloak" anonymous IP
  443. * addresses like this, if we allowed accounts like this to be created
  444. * new users could get the old edits of these anonymous users.
  445. *
  446. * @param $name \string String to match
  447. * @return \bool True or false
  448. */
  449. static function isIP( $name ) {
  450. return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
  451. }
  452. /**
  453. * Is the input a valid username?
  454. *
  455. * Checks if the input is a valid username, we don't want an empty string,
  456. * an IP address, anything that containins slashes (would mess up subpages),
  457. * is longer than the maximum allowed username size or doesn't begin with
  458. * a capital letter.
  459. *
  460. * @param $name \string String to match
  461. * @return \bool True or false
  462. */
  463. static function isValidUserName( $name ) {
  464. global $wgContLang, $wgMaxNameChars;
  465. if ( $name == ''
  466. || User::isIP( $name )
  467. || strpos( $name, '/' ) !== false
  468. || strlen( $name ) > $wgMaxNameChars
  469. || $name != $wgContLang->ucfirst( $name ) ) {
  470. wfDebugLog( 'username', __METHOD__ .
  471. ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
  472. return false;
  473. }
  474. // Ensure that the name can't be misresolved as a different title,
  475. // such as with extra namespace keys at the start.
  476. $parsed = Title::newFromText( $name );
  477. if( is_null( $parsed )
  478. || $parsed->getNamespace()
  479. || strcmp( $name, $parsed->getPrefixedText() ) ) {
  480. wfDebugLog( 'username', __METHOD__ .
  481. ": '$name' invalid due to ambiguous prefixes" );
  482. return false;
  483. }
  484. // Check an additional blacklist of troublemaker characters.
  485. // Should these be merged into the title char list?
  486. $unicodeBlacklist = '/[' .
  487. '\x{0080}-\x{009f}' . # iso-8859-1 control chars
  488. '\x{00a0}' . # non-breaking space
  489. '\x{2000}-\x{200f}' . # various whitespace
  490. '\x{2028}-\x{202f}' . # breaks and control chars
  491. '\x{3000}' . # ideographic space
  492. '\x{e000}-\x{f8ff}' . # private use
  493. ']/u';
  494. if( preg_match( $unicodeBlacklist, $name ) ) {
  495. wfDebugLog( 'username', __METHOD__ .
  496. ": '$name' invalid due to blacklisted characters" );
  497. return false;
  498. }
  499. return true;
  500. }
  501. /**
  502. * Usernames which fail to pass this function will be blocked
  503. * from user login and new account registrations, but may be used
  504. * internally by batch processes.
  505. *
  506. * If an account already exists in this form, login will be blocked
  507. * by a failure to pass this function.
  508. *
  509. * @param $name \string String to match
  510. * @return \bool True or false
  511. */
  512. static function isUsableName( $name ) {
  513. global $wgReservedUsernames;
  514. // Must be a valid username, obviously ;)
  515. if ( !self::isValidUserName( $name ) ) {
  516. return false;
  517. }
  518. static $reservedUsernames = false;
  519. if ( !$reservedUsernames ) {
  520. $reservedUsernames = $wgReservedUsernames;
  521. wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
  522. }
  523. // Certain names may be reserved for batch processes.
  524. foreach ( $reservedUsernames as $reserved ) {
  525. if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
  526. $reserved = wfMsgForContent( substr( $reserved, 4 ) );
  527. }
  528. if ( $reserved == $name ) {
  529. return false;
  530. }
  531. }
  532. return true;
  533. }
  534. /**
  535. * Usernames which fail to pass this function will be blocked
  536. * from new account registrations, but may be used internally
  537. * either by batch processes or by user accounts which have
  538. * already been created.
  539. *
  540. * Additional character blacklisting may be added here
  541. * rather than in isValidUserName() to avoid disrupting
  542. * existing accounts.
  543. *
  544. * @param $name \string String to match
  545. * @return \bool True or false
  546. */
  547. static function isCreatableName( $name ) {
  548. global $wgInvalidUsernameCharacters;
  549. return
  550. self::isUsableName( $name ) &&
  551. // Registration-time character blacklisting...
  552. !preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name );
  553. }
  554. /**
  555. * Is the input a valid password for this user?
  556. *
  557. * @param $password \string Desired password
  558. * @return \bool True or false
  559. */
  560. function isValidPassword( $password ) {
  561. global $wgMinimalPasswordLength, $wgContLang;
  562. $result = null;
  563. if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
  564. return $result;
  565. if( $result === false )
  566. return false;
  567. // Password needs to be long enough, and can't be the same as the username
  568. return strlen( $password ) >= $wgMinimalPasswordLength
  569. && $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName );
  570. }
  571. /**
  572. * Does a string look like an e-mail address?
  573. *
  574. * There used to be a regular expression here, it got removed because it
  575. * rejected valid addresses. Actually just check if there is '@' somewhere
  576. * in the given address.
  577. *
  578. * @todo Check for RFC 2822 compilance (bug 959)
  579. *
  580. * @param $addr \string E-mail address
  581. * @return \bool True or false
  582. */
  583. public static function isValidEmailAddr( $addr ) {
  584. $result = null;
  585. if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
  586. return $result;
  587. }
  588. return strpos( $addr, '@' ) !== false;
  589. }
  590. /**
  591. * Given unvalidated user input, return a canonical username, or false if
  592. * the username is invalid.
  593. * @param $name \string User input
  594. * @param $validate \types{\string,\bool} Type of validation to use:
  595. * - false No validation
  596. * - 'valid' Valid for batch processes
  597. * - 'usable' Valid for batch processes and login
  598. * - 'creatable' Valid for batch processes, login and account creation
  599. */
  600. static function getCanonicalName( $name, $validate = 'valid' ) {
  601. # Force usernames to capital
  602. global $wgContLang;
  603. $name = $wgContLang->ucfirst( $name );
  604. # Reject names containing '#'; these will be cleaned up
  605. # with title normalisation, but then it's too late to
  606. # check elsewhere
  607. if( strpos( $name, '#' ) !== false )
  608. return false;
  609. # Clean up name according to title rules
  610. $t = ($validate === 'valid') ?
  611. Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
  612. # Check for invalid titles
  613. if( is_null( $t ) ) {
  614. return false;
  615. }
  616. # Reject various classes of invalid names
  617. $name = $t->getText();
  618. global $wgAuth;
  619. $name = $wgAuth->getCanonicalName( $t->getText() );
  620. switch ( $validate ) {
  621. case false:
  622. break;
  623. case 'valid':
  624. if ( !User::isValidUserName( $name ) ) {
  625. $name = false;
  626. }
  627. break;
  628. case 'usable':
  629. if ( !User::isUsableName( $name ) ) {
  630. $name = false;
  631. }
  632. break;
  633. case 'creatable':
  634. if ( !User::isCreatableName( $name ) ) {
  635. $name = false;
  636. }
  637. break;
  638. default:
  639. throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
  640. }
  641. return $name;
  642. }
  643. /**
  644. * Count the number of edits of a user
  645. * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas
  646. *
  647. * @param $uid \int User ID to check
  648. * @return \int The user's edit count
  649. */
  650. static function edits( $uid ) {
  651. wfProfileIn( __METHOD__ );
  652. $dbr = wfGetDB( DB_SLAVE );
  653. // check if the user_editcount field has been initialized
  654. $field = $dbr->selectField(
  655. 'user', 'user_editcount',
  656. array( 'user_id' => $uid ),
  657. __METHOD__
  658. );
  659. if( $field === null ) { // it has not been initialized. do so.
  660. $dbw = wfGetDB( DB_MASTER );
  661. $count = $dbr->selectField(
  662. 'revision', 'count(*)',
  663. array( 'rev_user' => $uid ),
  664. __METHOD__
  665. );
  666. $dbw->update(
  667. 'user',
  668. array( 'user_editcount' => $count ),
  669. array( 'user_id' => $uid ),
  670. __METHOD__
  671. );
  672. } else {
  673. $count = $field;
  674. }
  675. wfProfileOut( __METHOD__ );
  676. return $count;
  677. }
  678. /**
  679. * Return a random password. Sourced from mt_rand, so it's not particularly secure.
  680. * @todo hash random numbers to improve security, like generateToken()
  681. *
  682. * @return \string New random password
  683. */
  684. static function randomPassword() {
  685. global $wgMinimalPasswordLength;
  686. $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
  687. $l = strlen( $pwchars ) - 1;
  688. $pwlength = max( 7, $wgMinimalPasswordLength );
  689. $digit = mt_rand(0, $pwlength - 1);
  690. $np = '';
  691. for ( $i = 0; $i < $pwlength; $i++ ) {
  692. $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
  693. }
  694. return $np;
  695. }
  696. /**
  697. * Set cached properties to default.
  698. *
  699. * @note This no longer clears uncached lazy-initialised properties;
  700. * the constructor does that instead.
  701. * @private
  702. */
  703. function loadDefaults( $name = false ) {
  704. wfProfileIn( __METHOD__ );
  705. global $wgCookiePrefix;
  706. $this->mId = 0;
  707. $this->mName = $name;
  708. $this->mRealName = '';
  709. $this->mPassword = $this->mNewpassword = '';
  710. $this->mNewpassTime = null;
  711. $this->mEmail = '';
  712. $this->mOptions = null; # Defer init
  713. if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
  714. $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
  715. } else {
  716. $this->mTouched = '0'; # Allow any pages to be cached
  717. }
  718. $this->setToken(); # Random
  719. $this->mEmailAuthenticated = null;
  720. $this->mEmailToken = '';
  721. $this->mEmailTokenExpires = null;
  722. $this->mRegistration = wfTimestamp( TS_MW );
  723. $this->mGroups = array();
  724. wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
  725. wfProfileOut( __METHOD__ );
  726. }
  727. /**
  728. * @deprecated Use wfSetupSession().
  729. */
  730. function SetupSession() {
  731. wfDeprecated( __METHOD__ );
  732. wfSetupSession();
  733. }
  734. /**
  735. * Load user data from the session or login cookie. If there are no valid
  736. * credentials, initialises the user as an anonymous user.
  737. * @return \bool True if the user is logged in, false otherwise.
  738. */
  739. private function loadFromSession() {
  740. global $wgMemc, $wgCookiePrefix;
  741. $result = null;
  742. wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
  743. if ( $result !== null ) {
  744. return $result;
  745. }
  746. if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
  747. $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
  748. if( isset( $_SESSION['wsUserID'] ) && $sId != $_SESSION['wsUserID'] ) {
  749. $this->loadDefaults(); // Possible collision!
  750. wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and
  751. cookie user ID ($sId) don't match!" );
  752. return false;
  753. }
  754. $_SESSION['wsUserID'] = $sId;
  755. } else if ( isset( $_SESSION['wsUserID'] ) ) {
  756. if ( $_SESSION['wsUserID'] != 0 ) {
  757. $sId = $_SESSION['wsUserID'];
  758. } else {
  759. $this->loadDefaults();
  760. return false;
  761. }
  762. } else {
  763. $this->loadDefaults();
  764. return false;
  765. }
  766. if ( isset( $_SESSION['wsUserName'] ) ) {
  767. $sName = $_SESSION['wsUserName'];
  768. } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
  769. $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
  770. $_SESSION['wsUserName'] = $sName;
  771. } else {
  772. $this->loadDefaults();
  773. return false;
  774. }
  775. $passwordCorrect = FALSE;
  776. $this->mId = $sId;
  777. if ( !$this->loadFromId() ) {
  778. # Not a valid ID, loadFromId has switched the object to anon for us
  779. return false;
  780. }
  781. if ( isset( $_SESSION['wsToken'] ) ) {
  782. $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
  783. $from = 'session';
  784. } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
  785. $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
  786. $from = 'cookie';
  787. } else {
  788. # No session or persistent login cookie
  789. $this->loadDefaults();
  790. return false;
  791. }
  792. if ( ( $sName == $this->mName ) && $passwordCorrect ) {
  793. $_SESSION['wsToken'] = $this->mToken;
  794. wfDebug( "Logged in from $from\n" );
  795. return true;
  796. } else {
  797. # Invalid credentials
  798. wfDebug( "Can't log in from $from, invalid credentials\n" );
  799. $this->loadDefaults();
  800. return false;
  801. }
  802. }
  803. /**
  804. * Load user and user_group data from the database.
  805. * $this::mId must be set, this is how the user is identified.
  806. *
  807. * @return \bool True if the user exists, false if the user is anonymous
  808. * @private
  809. */
  810. function loadFromDatabase() {
  811. # Paranoia
  812. $this->mId = intval( $this->mId );
  813. /** Anonymous user */
  814. if( !$this->mId ) {
  815. $this->loadDefaults();
  816. return false;
  817. }
  818. $dbr = wfGetDB( DB_MASTER );
  819. $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
  820. wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
  821. if ( $s !== false ) {
  822. # Initialise user table data
  823. $this->loadFromRow( $s );
  824. $this->mGroups = null; // deferred
  825. $this->getEditCount(); // revalidation for nulls
  826. return true;
  827. } else {
  828. # Invalid user_id
  829. $this->mId = 0;
  830. $this->loadDefaults();
  831. return false;
  832. }
  833. }
  834. /**
  835. * Initialize this object from a row from the user table.
  836. *
  837. * @param $row \type{\arrayof{\mixed}} Row from the user table to load.
  838. */
  839. function loadFromRow( $row ) {
  840. $this->mDataLoaded = true;
  841. if ( isset( $row->user_id ) ) {
  842. $this->mId = intval( $row->user_id );
  843. }
  844. $this->mName = $row->user_name;
  845. $this->mRealName = $row->user_real_name;
  846. $this->mPassword = $row->user_password;
  847. $this->mNewpassword = $row->user_newpassword;
  848. $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
  849. $this->mEmail = $row->user_email;
  850. $this->decodeOptions( $row->user_options );
  851. $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
  852. $this->mToken = $row->user_token;
  853. $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
  854. $this->mEmailToken = $row->user_email_token;
  855. $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
  856. $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
  857. $this->mEditCount = $row->user_editcount;
  858. }
  859. /**
  860. * Load the groups from the database if they aren't already loaded.
  861. * @private
  862. */
  863. function loadGroups() {
  864. if ( is_null( $this->mGroups ) ) {
  865. $dbr = wfGetDB( DB_MASTER );
  866. $res = $dbr->select( 'user_groups',
  867. array( 'ug_group' ),
  868. array( 'ug_user' => $this->mId ),
  869. __METHOD__ );
  870. $this->mGroups = array();
  871. while( $row = $dbr->fetchObject( $res ) ) {
  872. $this->mGroups[] = $row->ug_group;
  873. }
  874. }
  875. }
  876. /**
  877. * Clear various cached data stored in this object.
  878. * @param $reloadFrom \string Reload user and user_groups table data from a
  879. * given source. May be "name", "id", "defaults", "session", or false for
  880. * no reload.
  881. */
  882. function clearInstanceCache( $reloadFrom = false ) {
  883. $this->mNewtalk = -1;
  884. $this->mDatePreference = null;
  885. $this->mBlockedby = -1; # Unset
  886. $this->mHash = false;
  887. $this->mSkin = null;
  888. $this->mRights = null;
  889. $this->mEffectiveGroups = null;
  890. if ( $reloadFrom ) {
  891. $this->mDataLoaded = false;
  892. $this->mFrom = $reloadFrom;
  893. }
  894. }
  895. /**
  896. * Combine the language default options with any site-specific options
  897. * and add the default language variants.
  898. *
  899. * @return \type{\arrayof{\string}} Array of options
  900. */
  901. static function getDefaultOptions() {
  902. global $wgNamespacesToBeSearchedDefault;
  903. /**
  904. * Site defaults will override the global/language defaults
  905. */
  906. global $wgDefaultUserOptions, $wgContLang;
  907. $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
  908. /**
  909. * default language setting
  910. */
  911. $variant = $wgContLang->getPreferredVariant( false );
  912. $defOpt['variant'] = $variant;
  913. $defOpt['language'] = $variant;
  914. foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
  915. $defOpt['searchNs'.$nsnum] = $val;
  916. }
  917. return $defOpt;
  918. }
  919. /**
  920. * Get a given default option value.
  921. *
  922. * @param $opt \string Name of option to retrieve
  923. * @return \string Default option value
  924. */
  925. public static function getDefaultOption( $opt ) {
  926. $defOpts = self::getDefaultOptions();
  927. if( isset( $defOpts[$opt] ) ) {
  928. return $defOpts[$opt];
  929. } else {
  930. return '';
  931. }
  932. }
  933. /**
  934. * Get a list of user toggle names
  935. * @return \type{\arrayof{\string}} Array of user toggle names
  936. */
  937. static function getToggles() {
  938. global $wgContLang, $wgUseRCPatrol;
  939. $extraToggles = array();
  940. wfRunHooks( 'UserToggles', array( &$extraToggles ) );
  941. if( $wgUseRCPatrol ) {
  942. $extraToggles[] = 'hidepatrolled';
  943. $extraToggles[] = 'newpageshidepatrolled';
  944. $extraToggles[] = 'watchlisthidepatrolled';
  945. }
  946. return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
  947. }
  948. /**
  949. * Get blocking information
  950. * @private
  951. * @param $bFromSlave \bool Whether to check the slave database first. To
  952. * improve performance, non-critical checks are done
  953. * against slaves. Check when actually saving should be
  954. * done against master.
  955. */
  956. function getBlockedStatus( $bFromSlave = true ) {
  957. global $wgEnableSorbs, $wgProxyWhitelist;
  958. if ( -1 != $this->mBlockedby ) {
  959. wfDebug( "User::getBlockedStatus: already loaded.\n" );
  960. return;
  961. }
  962. wfProfileIn( __METHOD__ );
  963. wfDebug( __METHOD__.": checking...\n" );
  964. // Initialize data...
  965. // Otherwise something ends up stomping on $this->mBlockedby when
  966. // things get lazy-loaded later, causing false positive block hits
  967. // due to -1 !== 0. Probably session-related... Nothing should be
  968. // overwriting mBlockedby, surely?
  969. $this->load();
  970. $this->mBlockedby = 0;
  971. $this->mHideName = 0;
  972. $this->mAllowUsertalk = 0;
  973. $ip = wfGetIP();
  974. if ($this->isAllowed( 'ipblock-exempt' ) ) {
  975. # Exempt from all types of IP-block
  976. $ip = '';
  977. }
  978. # User/IP blocking
  979. $this->mBlock = new Block();
  980. $this->mBlock->fromMaster( !$bFromSlave );
  981. if ( $this->mBlock->load( $ip , $this->mId ) ) {
  982. wfDebug( __METHOD__.": Found block.\n" );
  983. $this->mBlockedby = $this->mBlock->mBy;
  984. $this->mBlockreason = $this->mBlock->mReason;
  985. $this->mHideName = $this->mBlock->mHideName;
  986. $this->mAllowUsertalk = $this->mBlock->mAllowUsertalk;
  987. if ( $this->isLoggedIn() ) {
  988. $this->spreadBlock();
  989. }
  990. } else {
  991. // Bug 13611: don't remove mBlock here, to allow account creation blocks to
  992. // apply to users. Note that the existence of $this->mBlock is not used to
  993. // check for edit blocks, $this->mBlockedby is instead.
  994. }
  995. # Proxy blocking
  996. if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
  997. # Local list
  998. if ( wfIsLocallyBlockedProxy( $ip ) ) {
  999. $this->mBlockedby = wfMsg( 'proxyblocker' );
  1000. $this->mBlockreason = wfMsg( 'proxyblockreason' );
  1001. }
  1002. # DNSBL
  1003. if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
  1004. if ( $this->inSorbsBlacklist( $ip ) ) {
  1005. $this->mBlockedby = wfMsg( 'sorbs' );
  1006. $this->mBlockreason = wfMsg( 'sorbsreason' );
  1007. }
  1008. }
  1009. }
  1010. # Extensions
  1011. wfRunHooks( 'GetBlockedStatus', array( &$this ) );
  1012. wfProfileOut( __METHOD__ );
  1013. }
  1014. /**
  1015. * Whether the given IP is in the SORBS blacklist.
  1016. *
  1017. * @param $ip \string IP to check
  1018. * @return \bool True if blacklisted.
  1019. */
  1020. function inSorbsBlacklist( $ip ) {
  1021. global $wgEnableSorbs, $wgSorbsUrl;
  1022. return $wgEnableSorbs &&
  1023. $this->inDnsBlacklist( $ip, $wgSorbsUrl );
  1024. }
  1025. /**
  1026. * Whether the given IP is in a given DNS blacklist.
  1027. *
  1028. * @param $ip \string IP to check
  1029. * @param $base \string URL of the DNS blacklist
  1030. * @return \bool True if blacklisted.
  1031. */
  1032. function inDnsBlacklist( $ip, $base ) {
  1033. wfProfileIn( __METHOD__ );
  1034. $found = false;
  1035. $host = '';
  1036. // FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170)
  1037. if( IP::isIPv4($ip) ) {
  1038. # Make hostname
  1039. $host = "$ip.$base";
  1040. # Send query
  1041. $ipList = gethostbynamel( $host );
  1042. if( $ipList ) {
  1043. wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
  1044. $found = true;
  1045. } else {
  1046. wfDebug( "Requested $host, not found in $base.\n" );
  1047. }
  1048. }
  1049. wfProfileOut( __METHOD__ );
  1050. return $found;
  1051. }
  1052. /**
  1053. * Is this user subject to rate limiting?
  1054. *
  1055. * @return \bool True if rate limited
  1056. */
  1057. public function isPingLimitable() {
  1058. global $wgRateLimitsExcludedGroups;
  1059. global $wgRateLimitsExcludedIPs;
  1060. if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) {
  1061. // Deprecated, but kept for backwards-compatibility config
  1062. return false;
  1063. }
  1064. if( in_array( wfGetIP(), $wgRateLimitsExcludedIPs ) ) {
  1065. // No other good way currently to disable rate limits
  1066. // for specific IPs. :P
  1067. // But this is a crappy hack and should die.
  1068. return false;
  1069. }
  1070. return !$this->isAllowed('noratelimit');
  1071. }
  1072. /**
  1073. * Primitive rate limits: enforce maximum actions per time period
  1074. * to put a brake on flooding.
  1075. *
  1076. * @note When using a shared cache like memcached, IP-address
  1077. * last-hit counters will be shared across wikis.
  1078. *
  1079. * @param $action \string Action to enforce; 'edit' if unspecified
  1080. * @return \bool True if a rate limiter was tripped
  1081. */
  1082. function pingLimiter( $action='edit' ) {
  1083. # Call the 'PingLimiter' hook
  1084. $result = false;
  1085. if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
  1086. return $result;
  1087. }
  1088. global $wgRateLimits;
  1089. if( !isset( $wgRateLimits[$action] ) ) {
  1090. return false;
  1091. }
  1092. # Some groups shouldn't trigger the ping limiter, ever
  1093. if( !$this->isPingLimitable() )
  1094. return false;
  1095. global $wgMemc, $wgRateLimitLog;
  1096. wfProfileIn( __METHOD__ );
  1097. $limits = $wgRateLimits[$action];
  1098. $keys = array();
  1099. $id = $this->getId();
  1100. $ip = wfGetIP();
  1101. $userLimit = false;
  1102. if( isset( $limits['anon'] ) && $id == 0 ) {
  1103. $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
  1104. }
  1105. if( isset( $limits['user'] ) && $id != 0 ) {
  1106. $userLimit = $limits['user'];
  1107. }
  1108. if( $this->isNewbie() ) {
  1109. if( isset( $limits['newbie'] ) && $id != 0 ) {
  1110. $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
  1111. }
  1112. if( isset( $limits['ip'] ) ) {
  1113. $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
  1114. }
  1115. $matches = array();
  1116. if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
  1117. $subnet = $matches[1];
  1118. $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
  1119. }
  1120. }
  1121. // Check for group-specific permissions
  1122. // If more than one group applies, use the group with the highest limit
  1123. foreach ( $this->getGroups() as $group ) {
  1124. if ( isset( $limits[$group] ) ) {
  1125. if ( $userLimit === false || $limits[$group] > $userLimit ) {
  1126. $userLimit = $limits[$group];
  1127. }
  1128. }
  1129. }
  1130. // Set the user limit key
  1131. if ( $userLimit !== false ) {
  1132. wfDebug( __METHOD__.": effective user limit: $userLimit\n" );
  1133. $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
  1134. }
  1135. $triggered = false;
  1136. foreach( $keys as $key => $limit ) {
  1137. list( $max, $period ) = $limit;
  1138. $summary = "(limit $max in {$period}s)";
  1139. $count = $wgMemc->get( $key );
  1140. if( $count ) {
  1141. if( $count > $max ) {
  1142. wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
  1143. if( $wgRateLimitLog ) {
  1144. @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
  1145. }
  1146. $triggered = true;
  1147. } else {
  1148. wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
  1149. }
  1150. } else {
  1151. wfDebug( __METHOD__.": adding record for $key $summary\n" );
  1152. $wgMemc->add( $key, 1, intval( $period ) );
  1153. }
  1154. $wgMemc->incr( $key );
  1155. }
  1156. wfProfileOut( __METHOD__ );
  1157. return $triggered;
  1158. }
  1159. /**
  1160. * Check if user is blocked
  1161. *
  1162. * @param $bFromSlave \bool Whether to check the slave database instead of the master
  1163. * @return \bool True if blocked, false otherwise
  1164. */
  1165. function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
  1166. wfDebug( "User::isBlocked: enter\n" );
  1167. $this->getBlockedStatus( $bFromSlave );
  1168. return $this->mBlockedby !== 0;
  1169. }
  1170. /**
  1171. * Check if user is blocked from editing a particular article
  1172. *
  1173. * @param $title \string Title to check
  1174. * @param $bFromSlave \bool Whether to check the slave database instead of the master
  1175. * @return \bool True if blocked, false otherwise
  1176. */
  1177. function isBlockedFrom( $title, $bFromSlave = false ) {
  1178. global $wgBlockAllowsUTEdit;
  1179. wfProfileIn( __METHOD__ );
  1180. wfDebug( __METHOD__.": enter\n" );
  1181. wfDebug( __METHOD__.": asking isBlocked()\n" );
  1182. $blocked = $this->isBlocked( $bFromSlave );
  1183. $allowUsertalk = ($wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false);
  1184. # If a user's name is suppressed, they cannot make edits anywhere
  1185. if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
  1186. $title->getNamespace() == NS_USER_TALK ) {
  1187. $blocked = false;
  1188. wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
  1189. }
  1190. wfProfileOut( __METHOD__ );
  1191. return $blocked;
  1192. }
  1193. /**
  1194. * If user is blocked, return the name of the user who placed the block
  1195. * @return \string name of blocker
  1196. */
  1197. function blockedBy() {
  1198. $this->getBlockedStatus();
  1199. return $this->mBlockedby;
  1200. }
  1201. /**
  1202. * If user is blocked, return the specified reason for the block
  1203. * @return \string Blocking reason
  1204. */
  1205. function blockedFor() {
  1206. $this->getBlockedStatus();
  1207. return $this->mBlockreason;
  1208. }
  1209. /**
  1210. * If user is blocked, return the ID for the block
  1211. * @return \int Block ID
  1212. */
  1213. function getBlockId() {
  1214. $this->getBlockedStatus();
  1215. return ($this->mBlock ? $this->mBlock->mId : false);
  1216. }
  1217. /**
  1218. * Check if user is blocked on all wikis.
  1219. * Do not use for actual edit permission checks!
  1220. * This is intented for quick UI checks.
  1221. *
  1222. * @param $ip \type{\string} IP address, uses current client if none given
  1223. * @return \type{\bool} True if blocked, false otherwise
  1224. */
  1225. function isBlockedGlobally( $ip = '' ) {
  1226. if( $this->mBlockedGlobally !== null ) {
  1227. return $this->mBlockedGlobally;
  1228. }
  1229. // User is already an IP?
  1230. if( IP::isIPAddress( $this->getName() ) ) {
  1231. $ip = $this->getName();
  1232. } else if( !$ip ) {
  1233. $ip = wfGetIP();
  1234. }
  1235. $blocked = false;
  1236. wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
  1237. $this->mBlockedGlobally = (bool)$blocked;
  1238. return $this->mBlockedGlobally;
  1239. }
  1240. /**
  1241. * Check if user account is locked
  1242. *
  1243. * @return \type{\bool} True if locked, false otherwise
  1244. */
  1245. function isLocked() {
  1246. if( $this->mLocked !== null ) {
  1247. return $this->mLocked;
  1248. }
  1249. global $wgAuth;
  1250. $authUser = $wgAuth->getUserInstance( $this );
  1251. $this->mLocked = (bool)$authUser->isLocked();
  1252. return $this->mLocked;
  1253. }
  1254. /**
  1255. * Check if user account is hidden
  1256. *
  1257. * @return \type{\bool} True if hidden, false otherwise
  1258. */
  1259. function isHidden() {
  1260. if( $this->mHideName !== null ) {
  1261. return $this->mHideName;
  1262. }
  1263. $this->getBlockedStatus();
  1264. if( !$this->mHideName ) {
  1265. global $wgAuth;
  1266. $authUser = $wgAuth->getUserInstance( $this );
  1267. $this->mHideName = (bool)$authUser->isHidden();
  1268. }
  1269. return $this->mHideName;
  1270. }
  1271. /**
  1272. * Get the user's ID.
  1273. * @return \int The user's ID; 0 if the user is anonymous or nonexistent
  1274. */
  1275. function getId() {
  1276. if( $this->mId === null and $this->mName !== null
  1277. and User::isIP( $this->mName ) ) {
  1278. // Special case, we know the user is anonymous
  1279. return 0;
  1280. } elseif( $this->mId === null ) {
  1281. // Don't load if this was initialized from an ID
  1282. $this->load();
  1283. }
  1284. return $this->mId;
  1285. }
  1286. /**
  1287. * Set the user and reload all fields according to a given ID
  1288. * @param $v \int User ID to reload
  1289. */
  1290. function setId( $v ) {
  1291. $this->mId = $v;
  1292. $this->clearInstanceCache( 'id' );
  1293. }
  1294. /**
  1295. * Get the user name, or the IP of an anonymous user
  1296. * @return \string User's name or IP address
  1297. */
  1298. function getName() {
  1299. if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
  1300. # Special case optimisation
  1301. return $this->mName;
  1302. } else {
  1303. $this->load();
  1304. if ( $this->mName === false ) {
  1305. # Clean up IPs
  1306. $this->mName = IP::sanitizeIP( wfGetIP() );
  1307. }
  1308. return $this->mName;
  1309. }
  1310. }
  1311. /**
  1312. * Set the user name.
  1313. *
  1314. * This does not reload fields from the database according to the given
  1315. * name. Rather, it is used to create a temporary "nonexistent user" for
  1316. * later addition to the database. It can also be used to set the IP
  1317. * address for an anonymous user to something other than the current
  1318. * remote IP.
  1319. *
  1320. * @note User::newFromName() has rougly the same function, when the named user
  1321. * does not exist.
  1322. * @param $str \string New user name to set
  1323. */
  1324. function setName( $str ) {
  1325. $this->load();
  1326. $this->mName = $str;
  1327. }
  1328. /**
  1329. * Get the user's name escaped by underscores.
  1330. * @return \string Username escaped by underscores.
  1331. */
  1332. function getTitleKey() {
  1333. return str_replace( ' ', '_', $this->getName() );
  1334. }
  1335. /**
  1336. * Check if the user has new messages.
  1337. * @return \bool True if the user has new messages
  1338. */
  1339. function getNewtalk() {
  1340. $this->load();
  1341. # Load the newtalk status if it is unloaded (mNewtalk=-1)
  1342. if( $this->mNewtalk === -1 ) {
  1343. $this->mNewtalk = false; # reset talk page status
  1344. # Check memcached separately for anons, who have no
  1345. # entire User object stored in there.
  1346. if( !$this->mId ) {
  1347. global $wgMemc;
  1348. $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
  1349. $newtalk = $wgMemc->get( $key );
  1350. if( strval( $newtalk ) !== '' ) {
  1351. $this->mNewtalk = (bool)$newtalk;
  1352. } else {
  1353. // Since we are caching this, make sure it is up to date by getting it
  1354. // from the master
  1355. $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
  1356. $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
  1357. }
  1358. } else {
  1359. $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
  1360. }
  1361. }
  1362. return (bool)$this->mNewtalk;
  1363. }
  1364. /**
  1365. * Return the talk page(s) this user has new messages on.
  1366. * @return \type{\arrayof{\string}} Array of page URLs
  1367. */
  1368. function getNewMessageLinks() {
  1369. $talks = array();
  1370. if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
  1371. return $talks;
  1372. if (!$this->getNewtalk())
  1373. return array();
  1374. $up = $this->getUserPage();
  1375. $utp = $up->getTalkPage();
  1376. return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
  1377. }
  1378. /**
  1379. * Internal uncached check for new messages
  1380. *
  1381. * @see getNewtalk()
  1382. * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
  1383. * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
  1384. * @param $fromMaster \bool true to fetch from the master, false for a slave
  1385. * @return \bool True if the user has new messages
  1386. * @private
  1387. */
  1388. function checkNewtalk( $field, $id, $fromMaster = false ) {
  1389. if ( $fromMaster ) {
  1390. $db = wfGetDB( DB_MASTER );
  1391. } else {
  1392. $db = wfGetDB( DB_SLAVE );
  1393. }
  1394. $ok = $db->selectField( 'user_newtalk', $field,
  1395. array( $field => $id ), __METHOD__ );
  1396. return $ok !== false;
  1397. }
  1398. /**
  1399. * Add or update the new messages flag
  1400. * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
  1401. * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
  1402. * @return \bool True if successful, false otherwise
  1403. * @private
  1404. */
  1405. function updateNewtalk( $field, $id ) {
  1406. $dbw = wfGetDB( DB_MASTER );
  1407. $dbw->insert( 'user_newtalk',
  1408. array( $field => $id ),
  1409. __METHOD__,
  1410. 'IGNORE' );
  1411. if ( $dbw->affectedRows() ) {
  1412. wfDebug( __METHOD__.": set on ($field, $id)\n" );
  1413. return true;
  1414. } else {
  1415. wfDebug( __METHOD__." already set ($field, $id)\n" );
  1416. return false;
  1417. }
  1418. }
  1419. /**
  1420. * Clear the new messages flag for the given user
  1421. * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
  1422. * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
  1423. * @return \bool True if successful, false otherwise
  1424. * @private
  1425. */
  1426. function deleteNewtalk( $field, $id ) {
  1427. $dbw = wfGetDB( DB_MASTER );
  1428. $dbw->delete( 'user_newtalk',
  1429. array( $field => $id ),
  1430. __METHOD__ );
  1431. if ( $dbw->affectedRows() ) {
  1432. wfDebug( __METHOD__.": killed on ($field, $id)\n" );
  1433. return true;
  1434. } else {
  1435. wfDebug( __METHOD__.": already gone ($field, $id)\n" );
  1436. return false;
  1437. }
  1438. }
  1439. /**
  1440. * Update the 'You have new messages!' status.
  1441. * @param $val \bool Whether the user has new messages
  1442. */
  1443. function setNewtalk( $val ) {
  1444. if( wfReadOnly() ) {
  1445. return;
  1446. }
  1447. $this->load();
  1448. $this->mNewtalk = $val;
  1449. if( $this->isAnon() ) {
  1450. $field = 'user_ip';
  1451. $id = $this->getName();
  1452. } else {
  1453. $field = 'user_id';
  1454. $id = $this->getId();
  1455. }
  1456. global $wgMemc;
  1457. if( $val ) {
  1458. $changed = $this->updateNewtalk( $field, $id );
  1459. } else {
  1460. $changed = $this->deleteNewtalk( $field, $id );
  1461. }
  1462. if( $this->isAnon() ) {
  1463. // Anons have a separate memcached space, since
  1464. // user records aren't kept for them.
  1465. $key = wfMemcKey( 'newtalk', 'ip', $id );
  1466. $wgMemc->set( $key, $val ? 1 : 0, 1800 );
  1467. }
  1468. if ( $changed ) {
  1469. $this->invalidateCache();
  1470. }
  1471. }
  1472. /**
  1473. * Generate a current or new-future timestamp to be stored in the
  1474. * user_touched field when we update things.
  1475. * @return \string Timestamp in TS_MW format
  1476. */
  1477. private static function newTouchedTimestamp() {
  1478. global $wgClockSkewFudge;
  1479. return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
  1480. }
  1481. /**
  1482. * Clear user data from memcached.
  1483. * Use after applying fun updates to the database; caller's
  1484. * responsibility to update user_touched if appropriate.
  1485. *
  1486. * Called implicitly from invalidateCache() and saveSettings().
  1487. */
  1488. private function clearSharedCache() {
  1489. $this->load();
  1490. if( $this->mId ) {
  1491. global $wgMemc;
  1492. $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
  1493. }
  1494. }
  1495. /**
  1496. * Immediately touch the user data cache for this account.
  1497. * Updates user_touched field, and removes account data from memcached
  1498. * for reload on the next hit.
  1499. */
  1500. function invalidateCache() {
  1501. $this->load();
  1502. if( $this->mId ) {
  1503. $this->mTouched = self::newTouchedTimestamp();
  1504. $dbw = wfGetDB( DB_MASTER );
  1505. $dbw->update( 'user',
  1506. array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
  1507. array( 'user_id' => $this->mId ),
  1508. __METHOD__ );
  1509. $this->clearSharedCache();
  1510. }
  1511. }
  1512. /**
  1513. * Validate the cache for this account.
  1514. * @param $timestamp \string A timestamp in TS_MW format
  1515. */
  1516. function validateCache( $timestamp ) {
  1517. $this->load();
  1518. return ($timestamp >= $this->mTouched);
  1519. }
  1520. /**
  1521. * Get the user touched timestamp
  1522. */
  1523. function getTouched() {
  1524. $this->load();
  1525. return $this->mTouched;
  1526. }
  1527. /**
  1528. * Set the password and reset the random token.
  1529. * Calls through to authentication plugin if necessary;
  1530. * will have no effect if the auth plugin refuses to
  1531. * pass the change through or if the legal password
  1532. * checks fail.
  1533. *
  1534. * As a special case, setting the password to null
  1535. * wipes it, so the account cannot be logged in until
  1536. * a new password is set, for instance via e-mail.
  1537. *
  1538. * @param $str \string New password to set
  1539. * @throws PasswordError on failure
  1540. */
  1541. function setPassword( $str ) {
  1542. global $wgAuth;
  1543. if( $str !== null ) {
  1544. if( !$wgAuth->allowPasswordChange() ) {
  1545. throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
  1546. }
  1547. if( !$this->isValidPassword( $str ) ) {
  1548. global $wgMinimalPasswordLength;
  1549. throw new PasswordError( wfMsgExt( 'passwordtooshort', array( 'parsemag' ),
  1550. $wgMinimalPasswordLength ) );
  1551. }
  1552. }
  1553. if( !$wgAuth->setPassword( $this, $str ) ) {
  1554. throw new PasswordError( wfMsg( 'externaldberror' ) );
  1555. }
  1556. $this->setInternalPassword( $str );
  1557. return true;
  1558. }
  1559. /**
  1560. * Set the password and reset the random token unconditionally.
  1561. *
  1562. * @param $str \string New password to set
  1563. */
  1564. function setInternalPassword( $str ) {
  1565. $this->load();
  1566. $this->setToken();
  1567. if( $str === null ) {
  1568. // Save an invalid hash...
  1569. $this->mPassword = '';
  1570. } else {
  1571. $this->mPassword = self::crypt( $str );
  1572. }
  1573. $this->mNewpassword = '';
  1574. $this->mNewpassTime = null;
  1575. }
  1576. /**
  1577. * Get the user's current token.
  1578. * @return \string Token
  1579. */
  1580. function getToken() {
  1581. $this->load();
  1582. return $this->mToken;
  1583. }
  1584. /**
  1585. * Set the random token (used for persistent authentication)
  1586. * Called from loadDefaults() among other places.
  1587. *
  1588. * @param $token \string If specified, set the token to this value
  1589. * @private
  1590. */
  1591. function setToken( $token = false ) {
  1592. global $wgSecretKey, $wgProxyKey;
  1593. $this->load();
  1594. if ( !$token ) {
  1595. if ( $wgSecretKey ) {
  1596. $key = $wgSecretKey;
  1597. } elseif ( $wgProxyKey ) {
  1598. $key = $wgProxyKey;
  1599. } else {
  1600. $key = microtime();
  1601. }
  1602. $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
  1603. } else {
  1604. $this->mToken = $token;
  1605. }
  1606. }
  1607. /**
  1608. * Set the cookie password
  1609. *
  1610. * @param $str \string New cookie password
  1611. * @private
  1612. */
  1613. function setCookiePassword( $str ) {
  1614. $this->load();
  1615. $this->mCookiePassword = md5( $str );
  1616. }
  1617. /**
  1618. * Set the password for a password reminder or new account email
  1619. *
  1620. * @param $str \string New password to set
  1621. * @param $throttle \bool If true, reset the throttle timestamp to the present
  1622. */
  1623. function setNewpassword( $str, $throttle = true ) {
  1624. $this->load();
  1625. $this->mNewpassword = self::crypt( $str );
  1626. if ( $throttle ) {
  1627. $this->mNewpassTime = wfTimestampNow();
  1628. }
  1629. }
  1630. /**
  1631. * Has password reminder email been sent within the last
  1632. * $wgPasswordReminderResendTime hours?
  1633. * @return \bool True or false
  1634. */
  1635. function isPasswordReminderThrottled() {
  1636. global $wgPasswordReminderResendTime;
  1637. $this->load();
  1638. if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
  1639. return false;
  1640. }
  1641. $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
  1642. return time() < $expiry;
  1643. }
  1644. /**
  1645. * Get the user's e-mail address
  1646. * @return \string User's email address
  1647. */
  1648. function getEmail() {
  1649. $this->load();
  1650. wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
  1651. return $this->mEmail;
  1652. }
  1653. /**
  1654. * Get the timestamp of the user's e-mail authentication
  1655. * @return \string TS_MW timestamp
  1656. */
  1657. function getEmailAuthenticationTimestamp() {
  1658. $this->load();
  1659. wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
  1660. return $this->mEmailAuthenticated;
  1661. }
  1662. /**
  1663. * Set the user's e-mail address
  1664. * @param $str \string New e-mail address
  1665. */
  1666. function setEmail( $str ) {
  1667. $this->load();
  1668. $this->mEmail = $str;
  1669. wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
  1670. }
  1671. /**
  1672. * Get the user's real name
  1673. * @return \string User's real name
  1674. */
  1675. function getRealName() {
  1676. $this->load();
  1677. return $this->mRealName;
  1678. }
  1679. /**
  1680. * Set the user's real name
  1681. * @param $str \string New real name
  1682. */
  1683. function setRealName( $str ) {
  1684. $this->load();

Large files files are truncated, but you can click here to view the full file