PageRenderTime 76ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/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
  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();
  1685. $this->mRealName = $str;
  1686. }
  1687. /**
  1688. * Get the user's current setting for a given option.
  1689. *
  1690. * @param $oname \string The option to check
  1691. * @param $defaultOverride \string A default value returned if the option does not exist
  1692. * @return \string User's current value for the option
  1693. * @see getBoolOption()
  1694. * @see getIntOption()
  1695. */
  1696. function getOption( $oname, $defaultOverride = '' ) {
  1697. $this->load();
  1698. if ( is_null( $this->mOptions ) ) {
  1699. if($defaultOverride != '') {
  1700. return $defaultOverride;
  1701. }
  1702. $this->mOptions = User::getDefaultOptions();
  1703. }
  1704. if ( array_key_exists( $oname, $this->mOptions ) ) {
  1705. return trim( $this->mOptions[$oname] );
  1706. } else {
  1707. return $defaultOverride;
  1708. }
  1709. }
  1710. /**
  1711. * Get the user's current setting for a given option, as a boolean value.
  1712. *
  1713. * @param $oname \string The option to check
  1714. * @return \bool User's current value for the option
  1715. * @see getOption()
  1716. */
  1717. function getBoolOption( $oname ) {
  1718. return (bool)$this->getOption( $oname );
  1719. }
  1720. /**
  1721. * Get the user's current setting for a given option, as a boolean value.
  1722. *
  1723. * @param $oname \string The option to check
  1724. * @param $defaultOverride \int A default value returned if the option does not exist
  1725. * @return \int User's current value for the option
  1726. * @see getOption()
  1727. */
  1728. function getIntOption( $oname, $defaultOverride=0 ) {
  1729. $val = $this->getOption( $oname );
  1730. if( $val == '' ) {
  1731. $val = $defaultOverride;
  1732. }
  1733. return intval( $val );
  1734. }
  1735. /**
  1736. * Set the given option for a user.
  1737. *
  1738. * @param $oname \string The option to set
  1739. * @param $val \mixed New value to set
  1740. */
  1741. function setOption( $oname, $val ) {
  1742. $this->load();
  1743. if ( is_null( $this->mOptions ) ) {
  1744. $this->mOptions = User::getDefaultOptions();
  1745. }
  1746. if ( $oname == 'skin' ) {
  1747. # Clear cached skin, so the new one displays immediately in Special:Preferences
  1748. unset( $this->mSkin );
  1749. }
  1750. // Filter out any newlines that may have passed through input validation.
  1751. // Newlines are used to separate items in the options blob.
  1752. if( $val ) {
  1753. $val = str_replace( "\r\n", "\n", $val );
  1754. $val = str_replace( "\r", "\n", $val );
  1755. $val = str_replace( "\n", " ", $val );
  1756. }
  1757. // Explicitly NULL values should refer to defaults
  1758. global $wgDefaultUserOptions;
  1759. if( is_null($val) && isset($wgDefaultUserOptions[$oname]) ) {
  1760. $val = $wgDefaultUserOptions[$oname];
  1761. }
  1762. $this->mOptions[$oname] = $val;
  1763. }
  1764. /**
  1765. * Reset all options to the site defaults
  1766. */
  1767. function restoreOptions() {
  1768. $this->mOptions = User::getDefaultOptions();
  1769. }
  1770. /**
  1771. * Get the user's preferred date format.
  1772. * @return \string User's preferred date format
  1773. */
  1774. function getDatePreference() {
  1775. // Important migration for old data rows
  1776. if ( is_null( $this->mDatePreference ) ) {
  1777. global $wgLang;
  1778. $value = $this->getOption( 'date' );
  1779. $map = $wgLang->getDatePreferenceMigrationMap();
  1780. if ( isset( $map[$value] ) ) {
  1781. $value = $map[$value];
  1782. }
  1783. $this->mDatePreference = $value;
  1784. }
  1785. return $this->mDatePreference;
  1786. }
  1787. /**
  1788. * Get the permissions this user has.
  1789. * @return \type{\arrayof{\string}} Array of permission names
  1790. */
  1791. function getRights() {
  1792. if ( is_null( $this->mRights ) ) {
  1793. $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
  1794. wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
  1795. // Force reindexation of rights when a hook has unset one of them
  1796. $this->mRights = array_values( $this->mRights );
  1797. }
  1798. return $this->mRights;
  1799. }
  1800. /**
  1801. * Get the list of explicit group memberships this user has.
  1802. * The implicit * and user groups are not included.
  1803. * @return \type{\arrayof{\string}} Array of internal group names
  1804. */
  1805. function getGroups() {
  1806. $this->load();
  1807. return $this->mGroups;
  1808. }
  1809. /**
  1810. * Get the list of implicit group memberships this user has.
  1811. * This includes all explicit groups, plus 'user' if logged in,
  1812. * '*' for all accounts and autopromoted groups
  1813. * @param $recache \bool Whether to avoid the cache
  1814. * @return \type{\arrayof{\string}} Array of internal group names
  1815. */
  1816. function getEffectiveGroups( $recache = false ) {
  1817. if ( $recache || is_null( $this->mEffectiveGroups ) ) {
  1818. $this->mEffectiveGroups = $this->getGroups();
  1819. $this->mEffectiveGroups[] = '*';
  1820. if( $this->getId() ) {
  1821. $this->mEffectiveGroups[] = 'user';
  1822. $this->mEffectiveGroups = array_unique( array_merge(
  1823. $this->mEffectiveGroups,
  1824. Autopromote::getAutopromoteGroups( $this )
  1825. ) );
  1826. # Hook for additional groups
  1827. wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
  1828. }
  1829. }
  1830. return $this->mEffectiveGroups;
  1831. }
  1832. /**
  1833. * Get the user's edit count.
  1834. * @return \int User'e edit count
  1835. */
  1836. function getEditCount() {
  1837. if ($this->getId()) {
  1838. if ( !isset( $this->mEditCount ) ) {
  1839. /* Populate the count, if it has not been populated yet */
  1840. $this->mEditCount = User::edits($this->mId);
  1841. }
  1842. return $this->mEditCount;
  1843. } else {
  1844. /* nil */
  1845. return null;
  1846. }
  1847. }
  1848. /**
  1849. * Add the user to the given group.
  1850. * This takes immediate effect.
  1851. * @param $group \string Name of the group to add
  1852. */
  1853. function addGroup( $group ) {
  1854. $dbw = wfGetDB( DB_MASTER );
  1855. if( $this->getId() ) {
  1856. $dbw->insert( 'user_groups',
  1857. array(
  1858. 'ug_user' => $this->getID(),
  1859. 'ug_group' => $group,
  1860. ),
  1861. 'User::addGroup',
  1862. array( 'IGNORE' ) );
  1863. }
  1864. $this->loadGroups();
  1865. $this->mGroups[] = $group;
  1866. $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
  1867. $this->invalidateCache();
  1868. }
  1869. /**
  1870. * Remove the user from the given group.
  1871. * This takes immediate effect.
  1872. * @param $group \string Name of the group to remove
  1873. */
  1874. function removeGroup( $group ) {
  1875. $this->load();
  1876. $dbw = wfGetDB( DB_MASTER );
  1877. $dbw->delete( 'user_groups',
  1878. array(
  1879. 'ug_user' => $this->getID(),
  1880. 'ug_group' => $group,
  1881. ),
  1882. 'User::removeGroup' );
  1883. $this->loadGroups();
  1884. $this->mGroups = array_diff( $this->mGroups, array( $group ) );
  1885. $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
  1886. $this->invalidateCache();
  1887. }
  1888. /**
  1889. * Get whether the user is logged in
  1890. * @return \bool True or false
  1891. */
  1892. function isLoggedIn() {
  1893. return $this->getID() != 0;
  1894. }
  1895. /**
  1896. * Get whether the user is anonymous
  1897. * @return \bool True or false
  1898. */
  1899. function isAnon() {
  1900. return !$this->isLoggedIn();
  1901. }
  1902. /**
  1903. * Get whether the user is a bot
  1904. * @return \bool True or false
  1905. * @deprecated
  1906. */
  1907. function isBot() {
  1908. wfDeprecated( __METHOD__ );
  1909. return $this->isAllowed( 'bot' );
  1910. }
  1911. /**
  1912. * Check if user is allowed to access a feature / make an action
  1913. * @param $action \string action to be checked
  1914. * @return \bool True if action is allowed, else false
  1915. */
  1916. function isAllowed( $action = '' ) {
  1917. if ( $action === '' )
  1918. return true; // In the spirit of DWIM
  1919. # Patrolling may not be enabled
  1920. if( $action === 'patrol' || $action === 'autopatrol' ) {
  1921. global $wgUseRCPatrol, $wgUseNPPatrol;
  1922. if( !$wgUseRCPatrol && !$wgUseNPPatrol )
  1923. return false;
  1924. }
  1925. # Use strict parameter to avoid matching numeric 0 accidentally inserted
  1926. # by misconfiguration: 0 == 'foo'
  1927. return in_array( $action, $this->getRights(), true );
  1928. }
  1929. /**
  1930. * Check whether to enable recent changes patrol features for this user
  1931. * @return \bool True or false
  1932. */
  1933. public function useRCPatrol() {
  1934. global $wgUseRCPatrol;
  1935. return( $wgUseRCPatrol && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
  1936. }
  1937. /**
  1938. * Check whether to enable new pages patrol features for this user
  1939. * @return \bool True or false
  1940. */
  1941. public function useNPPatrol() {
  1942. global $wgUseRCPatrol, $wgUseNPPatrol;
  1943. return( ($wgUseRCPatrol || $wgUseNPPatrol) && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
  1944. }
  1945. /**
  1946. * Get the current skin, loading it if required
  1947. * @return \type{Skin} Current skin
  1948. * @todo FIXME : need to check the old failback system [AV]
  1949. */
  1950. function &getSkin() {
  1951. global $wgRequest, $wgAllowUserSkin, $wgDefaultSkin;
  1952. if ( ! isset( $this->mSkin ) ) {
  1953. wfProfileIn( __METHOD__ );
  1954. if( $wgAllowUserSkin ) {
  1955. # get the user skin
  1956. $userSkin = $this->getOption( 'skin' );
  1957. $userSkin = $wgRequest->getVal('useskin', $userSkin);
  1958. } else {
  1959. # if we're not allowing users to override, then use the default
  1960. $userSkin = $wgDefaultSkin;
  1961. }
  1962. $this->mSkin =& Skin::newFromKey( $userSkin );
  1963. wfProfileOut( __METHOD__ );
  1964. }
  1965. return $this->mSkin;
  1966. }
  1967. /**
  1968. * Check the watched status of an article.
  1969. * @param $title \type{Title} Title of the article to look at
  1970. * @return \bool True if article is watched
  1971. */
  1972. function isWatched( $title ) {
  1973. $wl = WatchedItem::fromUserTitle( $this, $title );
  1974. return $wl->isWatched();
  1975. }
  1976. /**
  1977. * Watch an article.
  1978. * @param $title \type{Title} Title of the article to look at
  1979. */
  1980. function addWatch( $title ) {
  1981. $wl = WatchedItem::fromUserTitle( $this, $title );
  1982. $wl->addWatch();
  1983. $this->invalidateCache();
  1984. }
  1985. /**
  1986. * Stop watching an article.
  1987. * @param $title \type{Title} Title of the article to look at
  1988. */
  1989. function removeWatch( $title ) {
  1990. $wl = WatchedItem::fromUserTitle( $this, $title );
  1991. $wl->removeWatch();
  1992. $this->invalidateCache();
  1993. }
  1994. /**
  1995. * Clear the user's notification timestamp for the given title.
  1996. * If e-notif e-mails are on, they will receive notification mails on
  1997. * the next change of the page if it's watched etc.
  1998. * @param $title \type{Title} Title of the article to look at
  1999. */
  2000. function clearNotification( &$title ) {
  2001. global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
  2002. # Do nothing if the database is locked to writes
  2003. if( wfReadOnly() ) {
  2004. return;
  2005. }
  2006. if ($title->getNamespace() == NS_USER_TALK &&
  2007. $title->getText() == $this->getName() ) {
  2008. if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
  2009. return;
  2010. $this->setNewtalk( false );
  2011. }
  2012. if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
  2013. return;
  2014. }
  2015. if( $this->isAnon() ) {
  2016. // Nothing else to do...
  2017. return;
  2018. }
  2019. // Only update the timestamp if the page is being watched.
  2020. // The query to find out if it is watched is cached both in memcached and per-invocation,
  2021. // and when it does have to be executed, it can be on a slave
  2022. // If this is the user's newtalk page, we always update the timestamp
  2023. if ($title->getNamespace() == NS_USER_TALK &&
  2024. $title->getText() == $wgUser->getName())
  2025. {
  2026. $watched = true;
  2027. } elseif ( $this->getId() == $wgUser->getId() ) {
  2028. $watched = $title->userIsWatching();
  2029. } else {
  2030. $watched = true;
  2031. }
  2032. // If the page is watched by the user (or may be watched), update the timestamp on any
  2033. // any matching rows
  2034. if ( $watched ) {
  2035. $dbw = wfGetDB( DB_MASTER );
  2036. $dbw->update( 'watchlist',
  2037. array( /* SET */
  2038. 'wl_notificationtimestamp' => NULL
  2039. ), array( /* WHERE */
  2040. 'wl_title' => $title->getDBkey(),
  2041. 'wl_namespace' => $title->getNamespace(),
  2042. 'wl_user' => $this->getID()
  2043. ), __METHOD__
  2044. );
  2045. }
  2046. }
  2047. /**
  2048. * Resets all of the given user's page-change notification timestamps.
  2049. * If e-notif e-mails are on, they will receive notification mails on
  2050. * the next change of any watched page.
  2051. *
  2052. * @param $currentUser \int User ID
  2053. */
  2054. function clearAllNotifications( $currentUser ) {
  2055. global $wgUseEnotif, $wgShowUpdatedMarker;
  2056. if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
  2057. $this->setNewtalk( false );
  2058. return;
  2059. }
  2060. if( $currentUser != 0 ) {
  2061. $dbw = wfGetDB( DB_MASTER );
  2062. $dbw->update( 'watchlist',
  2063. array( /* SET */
  2064. 'wl_notificationtimestamp' => NULL
  2065. ), array( /* WHERE */
  2066. 'wl_user' => $currentUser
  2067. ), __METHOD__
  2068. );
  2069. # We also need to clear here the "you have new message" notification for the own user_talk page
  2070. # This is cleared one page view later in Article::viewUpdates();
  2071. }
  2072. }
  2073. /**
  2074. * Encode this user's options as a string
  2075. * @return \string Encoded options
  2076. * @private
  2077. */
  2078. function encodeOptions() {
  2079. $this->load();
  2080. if ( is_null( $this->mOptions ) ) {
  2081. $this->mOptions = User::getDefaultOptions();
  2082. }
  2083. $a = array();
  2084. foreach ( $this->mOptions as $oname => $oval ) {
  2085. array_push( $a, $oname.'='.$oval );
  2086. }
  2087. $s = implode( "\n", $a );
  2088. return $s;
  2089. }
  2090. /**
  2091. * Set this user's options from an encoded string
  2092. * @param $str \string Encoded options to import
  2093. * @private
  2094. */
  2095. function decodeOptions( $str ) {
  2096. $this->mOptions = array();
  2097. $a = explode( "\n", $str );
  2098. foreach ( $a as $s ) {
  2099. $m = array();
  2100. if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
  2101. $this->mOptions[$m[1]] = $m[2];
  2102. }
  2103. }
  2104. }
  2105. /**
  2106. * Set a cookie on the user's client. Wrapper for
  2107. * WebResponse::setCookie
  2108. * @param $name \string Name of the cookie to set
  2109. * @param $value \string Value to set
  2110. * @param $exp \int Expiration time, as a UNIX time value;
  2111. * if 0 or not specified, use the default $wgCookieExpiration
  2112. */
  2113. protected function setCookie( $name, $value, $exp=0 ) {
  2114. global $wgRequest;
  2115. $wgRequest->response()->setcookie( $name, $value, $exp );
  2116. }
  2117. /**
  2118. * Clear a cookie on the user's client
  2119. * @param $name \string Name of the cookie to clear
  2120. */
  2121. protected function clearCookie( $name ) {
  2122. $this->setCookie( $name, '', time() - 86400 );
  2123. }
  2124. /**
  2125. * Set the default cookies for this session on the user's client.
  2126. */
  2127. function setCookies() {
  2128. $this->load();
  2129. if ( 0 == $this->mId ) return;
  2130. $session = array(
  2131. 'wsUserID' => $this->mId,
  2132. 'wsToken' => $this->mToken,
  2133. 'wsUserName' => $this->getName()
  2134. );
  2135. $cookies = array(
  2136. 'UserID' => $this->mId,
  2137. 'UserName' => $this->getName(),
  2138. );
  2139. if ( 1 == $this->getOption( 'rememberpassword' ) ) {
  2140. $cookies['Token'] = $this->mToken;
  2141. } else {
  2142. $cookies['Token'] = false;
  2143. }
  2144. wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
  2145. #check for null, since the hook could cause a null value
  2146. if ( !is_null( $session ) && isset( $_SESSION ) ){
  2147. $_SESSION = $session + $_SESSION;
  2148. }
  2149. foreach ( $cookies as $name => $value ) {
  2150. if ( $value === false ) {
  2151. $this->clearCookie( $name );
  2152. } else {
  2153. $this->setCookie( $name, $value );
  2154. }
  2155. }
  2156. }
  2157. /**
  2158. * Log this user out.
  2159. */
  2160. function logout() {
  2161. global $wgUser;
  2162. if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
  2163. $this->doLogout();
  2164. }
  2165. }
  2166. /**
  2167. * Clear the user's cookies and session, and reset the instance cache.
  2168. * @private
  2169. * @see logout()
  2170. */
  2171. function doLogout() {
  2172. $this->clearInstanceCache( 'defaults' );
  2173. $_SESSION['wsUserID'] = 0;
  2174. $this->clearCookie( 'UserID' );
  2175. $this->clearCookie( 'Token' );
  2176. # Remember when user logged out, to prevent seeing cached pages
  2177. $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
  2178. }
  2179. /**
  2180. * Save this user's settings into the database.
  2181. * @todo Only rarely do all these fields need to be set!
  2182. */
  2183. function saveSettings() {
  2184. $this->load();
  2185. if ( wfReadOnly() ) { return; }
  2186. if ( 0 == $this->mId ) { return; }
  2187. $this->mTouched = self::newTouchedTimestamp();
  2188. $dbw = wfGetDB( DB_MASTER );
  2189. $dbw->update( 'user',
  2190. array( /* SET */
  2191. 'user_name' => $this->mName,
  2192. 'user_password' => $this->mPassword,
  2193. 'user_newpassword' => $this->mNewpassword,
  2194. 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
  2195. 'user_real_name' => $this->mRealName,
  2196. 'user_email' => $this->mEmail,
  2197. 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
  2198. 'user_options' => $this->encodeOptions(),
  2199. 'user_touched' => $dbw->timestamp($this->mTouched),
  2200. 'user_token' => $this->mToken,
  2201. 'user_email_token' => $this->mEmailToken,
  2202. 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
  2203. ), array( /* WHERE */
  2204. 'user_id' => $this->mId
  2205. ), __METHOD__
  2206. );
  2207. wfRunHooks( 'UserSaveSettings', array( $this ) );
  2208. $this->clearSharedCache();
  2209. $this->getUserPage()->invalidateCache();
  2210. }
  2211. /**
  2212. * If only this user's username is known, and it exists, return the user ID.
  2213. */
  2214. function idForName() {
  2215. $s = trim( $this->getName() );
  2216. if ( $s === '' ) return 0;
  2217. $dbr = wfGetDB( DB_SLAVE );
  2218. $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
  2219. if ( $id === false ) {
  2220. $id = 0;
  2221. }
  2222. return $id;
  2223. }
  2224. /**
  2225. * Add a user to the database, return the user object
  2226. *
  2227. * @param $name \string Username to add
  2228. * @param $params \type{\arrayof{\string}} Non-default parameters to save to the database:
  2229. * - password The user's password. Password logins will be disabled if this is omitted.
  2230. * - newpassword A temporary password mailed to the user
  2231. * - email The user's email address
  2232. * - email_authenticated The email authentication timestamp
  2233. * - real_name The user's real name
  2234. * - options An associative array of non-default options
  2235. * - token Random authentication token. Do not set.
  2236. * - registration Registration timestamp. Do not set.
  2237. *
  2238. * @return \type{User} A new User object, or null if the username already exists
  2239. */
  2240. static function createNew( $name, $params = array() ) {
  2241. $user = new User;
  2242. $user->load();
  2243. if ( isset( $params['options'] ) ) {
  2244. $user->mOptions = $params['options'] + $user->mOptions;
  2245. unset( $params['options'] );
  2246. }
  2247. $dbw = wfGetDB( DB_MASTER );
  2248. $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
  2249. $fields = array(
  2250. 'user_id' => $seqVal,
  2251. 'user_name' => $name,
  2252. 'user_password' => $user->mPassword,
  2253. 'user_newpassword' => $user->mNewpassword,
  2254. 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
  2255. 'user_email' => $user->mEmail,
  2256. 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
  2257. 'user_real_name' => $user->mRealName,
  2258. 'user_options' => $user->encodeOptions(),
  2259. 'user_token' => $user->mToken,
  2260. 'user_registration' => $dbw->timestamp( $user->mRegistration ),
  2261. 'user_editcount' => 0,
  2262. );
  2263. foreach ( $params as $name => $value ) {
  2264. $fields["user_$name"] = $value;
  2265. }
  2266. $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
  2267. if ( $dbw->affectedRows() ) {
  2268. $newUser = User::newFromId( $dbw->insertId() );
  2269. } else {
  2270. $newUser = null;
  2271. }
  2272. return $newUser;
  2273. }
  2274. /**
  2275. * Add this existing user object to the database
  2276. */
  2277. function addToDatabase() {
  2278. $this->load();
  2279. $dbw = wfGetDB( DB_MASTER );
  2280. $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
  2281. $dbw->insert( 'user',
  2282. array(
  2283. 'user_id' => $seqVal,
  2284. 'user_name' => $this->mName,
  2285. 'user_password' => $this->mPassword,
  2286. 'user_newpassword' => $this->mNewpassword,
  2287. 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
  2288. 'user_email' => $this->mEmail,
  2289. 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
  2290. 'user_real_name' => $this->mRealName,
  2291. 'user_options' => $this->encodeOptions(),
  2292. 'user_token' => $this->mToken,
  2293. 'user_registration' => $dbw->timestamp( $this->mRegistration ),
  2294. 'user_editcount' => 0,
  2295. ), __METHOD__
  2296. );
  2297. $this->mId = $dbw->insertId();
  2298. // Clear instance cache other than user table data, which is already accurate
  2299. $this->clearInstanceCache();
  2300. }
  2301. /**
  2302. * If this (non-anonymous) user is blocked, block any IP address
  2303. * they've successfully logged in from.
  2304. */
  2305. function spreadBlock() {
  2306. wfDebug( __METHOD__."()\n" );
  2307. $this->load();
  2308. if ( $this->mId == 0 ) {
  2309. return;
  2310. }
  2311. $userblock = Block::newFromDB( '', $this->mId );
  2312. if ( !$userblock ) {
  2313. return;
  2314. }
  2315. $userblock->doAutoblock( wfGetIp() );
  2316. }
  2317. /**
  2318. * Generate a string which will be different for any combination of
  2319. * user options which would produce different parser output.
  2320. * This will be used as part of the hash key for the parser cache,
  2321. * so users will the same options can share the same cached data
  2322. * safely.
  2323. *
  2324. * Extensions which require it should install 'PageRenderingHash' hook,
  2325. * which will give them a chance to modify this key based on their own
  2326. * settings.
  2327. *
  2328. * @return \string Page rendering hash
  2329. */
  2330. function getPageRenderingHash() {
  2331. global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
  2332. if( $this->mHash ){
  2333. return $this->mHash;
  2334. }
  2335. // stubthreshold is only included below for completeness,
  2336. // it will always be 0 when this function is called by parsercache.
  2337. $confstr = $this->getOption( 'math' );
  2338. $confstr .= '!' . $this->getOption( 'stubthreshold' );
  2339. if ( $wgUseDynamicDates ) {
  2340. $confstr .= '!' . $this->getDatePreference();
  2341. }
  2342. $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
  2343. $confstr .= '!' . $wgLang->getCode();
  2344. $confstr .= '!' . $this->getOption( 'thumbsize' );
  2345. // add in language specific options, if any
  2346. $extra = $wgContLang->getExtraHashOptions();
  2347. $confstr .= $extra;
  2348. $confstr .= $wgRenderHashAppend;
  2349. // Give a chance for extensions to modify the hash, if they have
  2350. // extra options or other effects on the parser cache.
  2351. wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
  2352. // Make it a valid memcached key fragment
  2353. $confstr = str_replace( ' ', '_', $confstr );
  2354. $this->mHash = $confstr;
  2355. return $confstr;
  2356. }
  2357. /**
  2358. * Get whether the user is explicitly blocked from account creation.
  2359. * @return \bool True if blocked
  2360. */
  2361. function isBlockedFromCreateAccount() {
  2362. $this->getBlockedStatus();
  2363. return $this->mBlock && $this->mBlock->mCreateAccount;
  2364. }
  2365. /**
  2366. * Get whether the user is blocked from using Special:Emailuser.
  2367. * @return \bool True if blocked
  2368. */
  2369. function isBlockedFromEmailuser() {
  2370. $this->getBlockedStatus();
  2371. return $this->mBlock && $this->mBlock->mBlockEmail;
  2372. }
  2373. /**
  2374. * Get whether the user is allowed to create an account.
  2375. * @return \bool True if allowed
  2376. */
  2377. function isAllowedToCreateAccount() {
  2378. return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
  2379. }
  2380. /**
  2381. * @deprecated
  2382. */
  2383. function setLoaded( $loaded ) {
  2384. wfDeprecated( __METHOD__ );
  2385. }
  2386. /**
  2387. * Get this user's personal page title.
  2388. *
  2389. * @return \type{Title} User's personal page title
  2390. */
  2391. function getUserPage() {
  2392. return Title::makeTitle( NS_USER, $this->getName() );
  2393. }
  2394. /**
  2395. * Get this user's talk page title.
  2396. *
  2397. * @return \type{Title} User's talk page title
  2398. */
  2399. function getTalkPage() {
  2400. $title = $this->getUserPage();
  2401. return $title->getTalkPage();
  2402. }
  2403. /**
  2404. * Get the maximum valid user ID.
  2405. * @return \int User ID
  2406. * @static
  2407. */
  2408. function getMaxID() {
  2409. static $res; // cache
  2410. if ( isset( $res ) )
  2411. return $res;
  2412. else {
  2413. $dbr = wfGetDB( DB_SLAVE );
  2414. return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
  2415. }
  2416. }
  2417. /**
  2418. * Determine whether the user is a newbie. Newbies are either
  2419. * anonymous IPs, or the most recently created accounts.
  2420. * @return \bool True if the user is a newbie
  2421. */
  2422. function isNewbie() {
  2423. return !$this->isAllowed( 'autoconfirmed' );
  2424. }
  2425. /**
  2426. * Is the user active? We check to see if they've made at least
  2427. * X number of edits in the last Y days.
  2428. *
  2429. * @return \bool True if the user is active, false if not.
  2430. */
  2431. public function isActiveEditor() {
  2432. global $wgActiveUserEditCount, $wgActiveUserDays;
  2433. $dbr = wfGetDB( DB_SLAVE );
  2434. // Stolen without shame from RC
  2435. $cutoff_unixtime = time() - ( $wgActiveUserDays * 86400 );
  2436. $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
  2437. $oldTime = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
  2438. $res = $dbr->select( 'revision', '1',
  2439. array( 'rev_user_text' => $this->getName(), "rev_timestamp > $oldTime"),
  2440. __METHOD__,
  2441. array('LIMIT' => $wgActiveUserEditCount ) );
  2442. $count = $dbr->numRows($res);
  2443. $dbr->freeResult($res);
  2444. return $count == $wgActiveUserEditCount;
  2445. }
  2446. /**
  2447. * Check to see if the given clear-text password is one of the accepted passwords
  2448. * @param $password \string user password.
  2449. * @return \bool True if the given password is correct, otherwise False.
  2450. */
  2451. function checkPassword( $password ) {
  2452. global $wgAuth;
  2453. $this->load();
  2454. // Even though we stop people from creating passwords that
  2455. // are shorter than this, doesn't mean people wont be able
  2456. // to. Certain authentication plugins do NOT want to save
  2457. // domain passwords in a mysql database, so we should
  2458. // check this (incase $wgAuth->strict() is false).
  2459. if( !$this->isValidPassword( $password ) ) {
  2460. return false;
  2461. }
  2462. if( $wgAuth->authenticate( $this->getName(), $password ) ) {
  2463. return true;
  2464. } elseif( $wgAuth->strict() ) {
  2465. /* Auth plugin doesn't allow local authentication */
  2466. return false;
  2467. } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
  2468. /* Auth plugin doesn't allow local authentication for this user name */
  2469. return false;
  2470. }
  2471. if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
  2472. return true;
  2473. } elseif ( function_exists( 'iconv' ) ) {
  2474. # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
  2475. # Check for this with iconv
  2476. $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
  2477. if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
  2478. return true;
  2479. }
  2480. }
  2481. return false;
  2482. }
  2483. /**
  2484. * Check if the given clear-text password matches the temporary password
  2485. * sent by e-mail for password reset operations.
  2486. * @return \bool True if matches, false otherwise
  2487. */
  2488. function checkTemporaryPassword( $plaintext ) {
  2489. global $wgNewPasswordExpiry;
  2490. if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
  2491. $this->load();
  2492. $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
  2493. return ( time() < $expiry );
  2494. } else {
  2495. return false;
  2496. }
  2497. }
  2498. /**
  2499. * Initialize (if necessary) and return a session token value
  2500. * which can be used in edit forms to show that the user's
  2501. * login credentials aren't being hijacked with a foreign form
  2502. * submission.
  2503. *
  2504. * @param $salt \types{\string,\arrayof{\string}} Optional function-specific data for hashing
  2505. * @return \string The new edit token
  2506. */
  2507. function editToken( $salt = '' ) {
  2508. if ( $this->isAnon() ) {
  2509. return EDIT_TOKEN_SUFFIX;
  2510. } else {
  2511. if( !isset( $_SESSION['wsEditToken'] ) ) {
  2512. $token = self::generateToken();
  2513. $_SESSION['wsEditToken'] = $token;
  2514. } else {
  2515. $token = $_SESSION['wsEditToken'];
  2516. }
  2517. if( is_array( $salt ) ) {
  2518. $salt = implode( '|', $salt );
  2519. }
  2520. return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
  2521. }
  2522. }
  2523. /**
  2524. * Generate a looking random token for various uses.
  2525. *
  2526. * @param $salt \string Optional salt value
  2527. * @return \string The new random token
  2528. */
  2529. public static function generateToken( $salt = '' ) {
  2530. $token = dechex( mt_rand() ) . dechex( mt_rand() );
  2531. return md5( $token . $salt );
  2532. }
  2533. /**
  2534. * Check given value against the token value stored in the session.
  2535. * A match should confirm that the form was submitted from the
  2536. * user's own login session, not a form submission from a third-party
  2537. * site.
  2538. *
  2539. * @param $val \string Input value to compare
  2540. * @param $salt \string Optional function-specific data for hashing
  2541. * @return \bool Whether the token matches
  2542. */
  2543. function matchEditToken( $val, $salt = '' ) {
  2544. $sessionToken = $this->editToken( $salt );
  2545. if ( $val != $sessionToken ) {
  2546. wfDebug( "User::matchEditToken: broken session data\n" );
  2547. }
  2548. return $val == $sessionToken;
  2549. }
  2550. /**
  2551. * Check given value against the token value stored in the session,
  2552. * ignoring the suffix.
  2553. *
  2554. * @param $val \string Input value to compare
  2555. * @param $salt \string Optional function-specific data for hashing
  2556. * @return \bool Whether the token matches
  2557. */
  2558. function matchEditTokenNoSuffix( $val, $salt = '' ) {
  2559. $sessionToken = $this->editToken( $salt );
  2560. return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
  2561. }
  2562. /**
  2563. * Generate a new e-mail confirmation token and send a confirmation/invalidation
  2564. * mail to the user's given address.
  2565. *
  2566. * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure.
  2567. */
  2568. function sendConfirmationMail() {
  2569. global $wgLang;
  2570. $expiration = null; // gets passed-by-ref and defined in next line.
  2571. $token = $this->confirmationToken( $expiration );
  2572. $url = $this->confirmationTokenUrl( $token );
  2573. $invalidateURL = $this->invalidationTokenUrl( $token );
  2574. $this->saveSettings();
  2575. return $this->sendMail( wfMsg( 'confirmemail_subject' ),
  2576. wfMsg( 'confirmemail_body',
  2577. wfGetIP(),
  2578. $this->getName(),
  2579. $url,
  2580. $wgLang->timeanddate( $expiration, false ),
  2581. $invalidateURL ) );
  2582. }
  2583. /**
  2584. * Send an e-mail to this user's account. Does not check for
  2585. * confirmed status or validity.
  2586. *
  2587. * @param $subject \string Message subject
  2588. * @param $body \string Message body
  2589. * @param $from \string Optional From address; if unspecified, default $wgPasswordSender will be used
  2590. * @param $replyto \string Reply-To address
  2591. * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure
  2592. */
  2593. function sendMail( $subject, $body, $from = null, $replyto = null ) {
  2594. if( is_null( $from ) ) {
  2595. global $wgPasswordSender;
  2596. $from = $wgPasswordSender;
  2597. }
  2598. $to = new MailAddress( $this );
  2599. $sender = new MailAddress( $from );
  2600. return UserMailer::send( $to, $sender, $subject, $body, $replyto );
  2601. }
  2602. /**
  2603. * Generate, store, and return a new e-mail confirmation code.
  2604. * A hash (unsalted, since it's used as a key) is stored.
  2605. *
  2606. * @note Call saveSettings() after calling this function to commit
  2607. * this change to the database.
  2608. *
  2609. * @param[out] &$expiration \mixed Accepts the expiration time
  2610. * @return \string New token
  2611. * @private
  2612. */
  2613. function confirmationToken( &$expiration ) {
  2614. $now = time();
  2615. $expires = $now + 7 * 24 * 60 * 60;
  2616. $expiration = wfTimestamp( TS_MW, $expires );
  2617. $token = self::generateToken( $this->mId . $this->mEmail . $expires );
  2618. $hash = md5( $token );
  2619. $this->load();
  2620. $this->mEmailToken = $hash;
  2621. $this->mEmailTokenExpires = $expiration;
  2622. return $token;
  2623. }
  2624. /**
  2625. * Return a URL the user can use to confirm their email address.
  2626. * @param $token \string Accepts the email confirmation token
  2627. * @return \string New token URL
  2628. * @private
  2629. */
  2630. function confirmationTokenUrl( $token ) {
  2631. return $this->getTokenUrl( 'ConfirmEmail', $token );
  2632. }
  2633. /**
  2634. * Return a URL the user can use to invalidate their email address.
  2635. * @param $token \string Accepts the email confirmation token
  2636. * @return \string New token URL
  2637. * @private
  2638. */
  2639. function invalidationTokenUrl( $token ) {
  2640. return $this->getTokenUrl( 'Invalidateemail', $token );
  2641. }
  2642. /**
  2643. * Internal function to format the e-mail validation/invalidation URLs.
  2644. * This uses $wgArticlePath directly as a quickie hack to use the
  2645. * hardcoded English names of the Special: pages, for ASCII safety.
  2646. *
  2647. * @note Since these URLs get dropped directly into emails, using the
  2648. * short English names avoids insanely long URL-encoded links, which
  2649. * also sometimes can get corrupted in some browsers/mailers
  2650. * (bug 6957 with Gmail and Internet Explorer).
  2651. *
  2652. * @param $page \string Special page
  2653. * @param $token \string Token
  2654. * @return \string Formatted URL
  2655. */
  2656. protected function getTokenUrl( $page, $token ) {
  2657. global $wgArticlePath;
  2658. return wfExpandUrl(
  2659. str_replace(
  2660. '$1',
  2661. "Special:$page/$token",
  2662. $wgArticlePath ) );
  2663. }
  2664. /**
  2665. * Mark the e-mail address confirmed.
  2666. *
  2667. * @note Call saveSettings() after calling this function to commit the change.
  2668. */
  2669. function confirmEmail() {
  2670. $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
  2671. return true;
  2672. }
  2673. /**
  2674. * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
  2675. * address if it was already confirmed.
  2676. *
  2677. * @note Call saveSettings() after calling this function to commit the change.
  2678. */
  2679. function invalidateEmail() {
  2680. $this->load();
  2681. $this->mEmailToken = null;
  2682. $this->mEmailTokenExpires = null;
  2683. $this->setEmailAuthenticationTimestamp( null );
  2684. return true;
  2685. }
  2686. /**
  2687. * Set the e-mail authentication timestamp.
  2688. * @param $timestamp \string TS_MW timestamp
  2689. */
  2690. function setEmailAuthenticationTimestamp( $timestamp ) {
  2691. $this->load();
  2692. $this->mEmailAuthenticated = $timestamp;
  2693. wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
  2694. }
  2695. /**
  2696. * Is this user allowed to send e-mails within limits of current
  2697. * site configuration?
  2698. * @return \bool True if allowed
  2699. */
  2700. function canSendEmail() {
  2701. global $wgEnableEmail, $wgEnableUserEmail;
  2702. if( !$wgEnableEmail || !$wgEnableUserEmail ) {
  2703. return false;
  2704. }
  2705. $canSend = $this->isEmailConfirmed();
  2706. wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
  2707. return $canSend;
  2708. }
  2709. /**
  2710. * Is this user allowed to receive e-mails within limits of current
  2711. * site configuration?
  2712. * @return \bool True if allowed
  2713. */
  2714. function canReceiveEmail() {
  2715. return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
  2716. }
  2717. /**
  2718. * Is this user's e-mail address valid-looking and confirmed within
  2719. * limits of the current site configuration?
  2720. *
  2721. * @note If $wgEmailAuthentication is on, this may require the user to have
  2722. * confirmed their address by returning a code or using a password
  2723. * sent to the address from the wiki.
  2724. *
  2725. * @return \bool True if confirmed
  2726. */
  2727. function isEmailConfirmed() {
  2728. global $wgEmailAuthentication;
  2729. $this->load();
  2730. $confirmed = true;
  2731. if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
  2732. if( $this->isAnon() )
  2733. return false;
  2734. if( !self::isValidEmailAddr( $this->mEmail ) )
  2735. return false;
  2736. if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
  2737. return false;
  2738. return true;
  2739. } else {
  2740. return $confirmed;
  2741. }
  2742. }
  2743. /**
  2744. * Check whether there is an outstanding request for e-mail confirmation.
  2745. * @return \bool True if pending
  2746. */
  2747. function isEmailConfirmationPending() {
  2748. global $wgEmailAuthentication;
  2749. return $wgEmailAuthentication &&
  2750. !$this->isEmailConfirmed() &&
  2751. $this->mEmailToken &&
  2752. $this->mEmailTokenExpires > wfTimestamp();
  2753. }
  2754. /**
  2755. * Get the timestamp of account creation.
  2756. *
  2757. * @return \types{\string,\bool} string Timestamp of account creation, or false for
  2758. * non-existent/anonymous user accounts.
  2759. */
  2760. public function getRegistration() {
  2761. return $this->getId() > 0
  2762. ? $this->mRegistration
  2763. : false;
  2764. }
  2765. /**
  2766. * Get the timestamp of the first edit
  2767. *
  2768. * @return \types{\string,\bool} string Timestamp of first edit, or false for
  2769. * non-existent/anonymous user accounts.
  2770. */
  2771. public function getFirstEditTimestamp() {
  2772. if( $this->getId() == 0 ) return false; // anons
  2773. $dbr = wfGetDB( DB_SLAVE );
  2774. $time = $dbr->selectField( 'revision', 'rev_timestamp',
  2775. array( 'rev_user' => $this->getId() ),
  2776. __METHOD__,
  2777. array( 'ORDER BY' => 'rev_timestamp ASC' )
  2778. );
  2779. if( !$time ) return false; // no edits
  2780. return wfTimestamp( TS_MW, $time );
  2781. }
  2782. /**
  2783. * Get the permissions associated with a given list of groups
  2784. *
  2785. * @param $groups \type{\arrayof{\string}} List of internal group names
  2786. * @return \type{\arrayof{\string}} List of permission key names for given groups combined
  2787. */
  2788. static function getGroupPermissions( $groups ) {
  2789. global $wgGroupPermissions;
  2790. $rights = array();
  2791. foreach( $groups as $group ) {
  2792. if( isset( $wgGroupPermissions[$group] ) ) {
  2793. $rights = array_merge( $rights,
  2794. // array_filter removes empty items
  2795. array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
  2796. }
  2797. }
  2798. return array_unique($rights);
  2799. }
  2800. /**
  2801. * Get all the groups who have a given permission
  2802. *
  2803. * @param $role \string Role to check
  2804. * @return \type{\arrayof{\string}} List of internal group names with the given permission
  2805. */
  2806. static function getGroupsWithPermission( $role ) {
  2807. global $wgGroupPermissions;
  2808. $allowedGroups = array();
  2809. foreach ( $wgGroupPermissions as $group => $rights ) {
  2810. if ( isset( $rights[$role] ) && $rights[$role] ) {
  2811. $allowedGroups[] = $group;
  2812. }
  2813. }
  2814. return $allowedGroups;
  2815. }
  2816. /**
  2817. * Get the localized descriptive name for a group, if it exists
  2818. *
  2819. * @param $group \string Internal group name
  2820. * @return \string Localized descriptive group name
  2821. */
  2822. static function getGroupName( $group ) {
  2823. global $wgMessageCache;
  2824. $wgMessageCache->loadAllMessages();
  2825. $key = "group-$group";
  2826. $name = wfMsg( $key );
  2827. return $name == '' || wfEmptyMsg( $key, $name )
  2828. ? $group
  2829. : $name;
  2830. }
  2831. /**
  2832. * Get the localized descriptive name for a member of a group, if it exists
  2833. *
  2834. * @param $group \string Internal group name
  2835. * @return \string Localized name for group member
  2836. */
  2837. static function getGroupMember( $group ) {
  2838. global $wgMessageCache;
  2839. $wgMessageCache->loadAllMessages();
  2840. $key = "group-$group-member";
  2841. $name = wfMsg( $key );
  2842. return $name == '' || wfEmptyMsg( $key, $name )
  2843. ? $group
  2844. : $name;
  2845. }
  2846. /**
  2847. * Return the set of defined explicit groups.
  2848. * The implicit groups (by default *, 'user' and 'autoconfirmed')
  2849. * are not included, as they are defined automatically, not in the database.
  2850. * @return \type{\arrayof{\string}} Array of internal group names
  2851. */
  2852. static function getAllGroups() {
  2853. global $wgGroupPermissions;
  2854. return array_diff(
  2855. array_keys( $wgGroupPermissions ),
  2856. self::getImplicitGroups()
  2857. );
  2858. }
  2859. /**
  2860. * Get a list of all available permissions.
  2861. * @return \type{\arrayof{\string}} Array of permission names
  2862. */
  2863. static function getAllRights() {
  2864. if ( self::$mAllRights === false ) {
  2865. global $wgAvailableRights;
  2866. if ( count( $wgAvailableRights ) ) {
  2867. self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
  2868. } else {
  2869. self::$mAllRights = self::$mCoreRights;
  2870. }
  2871. wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
  2872. }
  2873. return self::$mAllRights;
  2874. }
  2875. /**
  2876. * Get a list of implicit groups
  2877. * @return \type{\arrayof{\string}} Array of internal group names
  2878. */
  2879. public static function getImplicitGroups() {
  2880. global $wgImplicitGroups;
  2881. $groups = $wgImplicitGroups;
  2882. wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
  2883. return $groups;
  2884. }
  2885. /**
  2886. * Get the title of a page describing a particular group
  2887. *
  2888. * @param $group \string Internal group name
  2889. * @return \types{\type{Title},\bool} Title of the page if it exists, false otherwise
  2890. */
  2891. static function getGroupPage( $group ) {
  2892. global $wgMessageCache;
  2893. $wgMessageCache->loadAllMessages();
  2894. $page = wfMsgForContent( 'grouppage-' . $group );
  2895. if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
  2896. $title = Title::newFromText( $page );
  2897. if( is_object( $title ) )
  2898. return $title;
  2899. }
  2900. return false;
  2901. }
  2902. /**
  2903. * Create a link to the group in HTML, if available;
  2904. * else return the group name.
  2905. *
  2906. * @param $group \string Internal name of the group
  2907. * @param $text \string The text of the link
  2908. * @return \string HTML link to the group
  2909. */
  2910. static function makeGroupLinkHTML( $group, $text = '' ) {
  2911. if( $text == '' ) {
  2912. $text = self::getGroupName( $group );
  2913. }
  2914. $title = self::getGroupPage( $group );
  2915. if( $title ) {
  2916. global $wgUser;
  2917. $sk = $wgUser->getSkin();
  2918. return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
  2919. } else {
  2920. return $text;
  2921. }
  2922. }
  2923. /**
  2924. * Create a link to the group in Wikitext, if available;
  2925. * else return the group name.
  2926. *
  2927. * @param $group \string Internal name of the group
  2928. * @param $text \string The text of the link
  2929. * @return \string Wikilink to the group
  2930. */
  2931. static function makeGroupLinkWiki( $group, $text = '' ) {
  2932. if( $text == '' ) {
  2933. $text = self::getGroupName( $group );
  2934. }
  2935. $title = self::getGroupPage( $group );
  2936. if( $title ) {
  2937. $page = $title->getPrefixedText();
  2938. return "[[$page|$text]]";
  2939. } else {
  2940. return $text;
  2941. }
  2942. }
  2943. /**
  2944. * Increment the user's edit-count field.
  2945. * Will have no effect for anonymous users.
  2946. */
  2947. function incEditCount() {
  2948. if( !$this->isAnon() ) {
  2949. $dbw = wfGetDB( DB_MASTER );
  2950. $dbw->update( 'user',
  2951. array( 'user_editcount=user_editcount+1' ),
  2952. array( 'user_id' => $this->getId() ),
  2953. __METHOD__ );
  2954. // Lazy initialization check...
  2955. if( $dbw->affectedRows() == 0 ) {
  2956. // Pull from a slave to be less cruel to servers
  2957. // Accuracy isn't the point anyway here
  2958. $dbr = wfGetDB( DB_SLAVE );
  2959. $count = $dbr->selectField( 'revision',
  2960. 'COUNT(rev_user)',
  2961. array( 'rev_user' => $this->getId() ),
  2962. __METHOD__ );
  2963. // Now here's a goddamn hack...
  2964. if( $dbr !== $dbw ) {
  2965. // If we actually have a slave server, the count is
  2966. // at least one behind because the current transaction
  2967. // has not been committed and replicated.
  2968. $count++;
  2969. } else {
  2970. // But if DB_SLAVE is selecting the master, then the
  2971. // count we just read includes the revision that was
  2972. // just added in the working transaction.
  2973. }
  2974. $dbw->update( 'user',
  2975. array( 'user_editcount' => $count ),
  2976. array( 'user_id' => $this->getId() ),
  2977. __METHOD__ );
  2978. }
  2979. }
  2980. // edit count in user cache too
  2981. $this->invalidateCache();
  2982. }
  2983. /**
  2984. * Get the description of a given right
  2985. *
  2986. * @param $right \string Right to query
  2987. * @return \string Localized description of the right
  2988. */
  2989. static function getRightDescription( $right ) {
  2990. global $wgMessageCache;
  2991. $wgMessageCache->loadAllMessages();
  2992. $key = "right-$right";
  2993. $name = wfMsg( $key );
  2994. return $name == '' || wfEmptyMsg( $key, $name )
  2995. ? $right
  2996. : $name;
  2997. }
  2998. /**
  2999. * Make an old-style password hash
  3000. *
  3001. * @param $password \string Plain-text password
  3002. * @param $userId \string User ID
  3003. * @return \string Password hash
  3004. */
  3005. static function oldCrypt( $password, $userId ) {
  3006. global $wgPasswordSalt;
  3007. if ( $wgPasswordSalt ) {
  3008. return md5( $userId . '-' . md5( $password ) );
  3009. } else {
  3010. return md5( $password );
  3011. }
  3012. }
  3013. /**
  3014. * Make a new-style password hash
  3015. *
  3016. * @param $password \string Plain-text password
  3017. * @param $salt \string Optional salt, may be random or the user ID.
  3018. * If unspecified or false, will generate one automatically
  3019. * @return \string Password hash
  3020. */
  3021. static function crypt( $password, $salt = false ) {
  3022. global $wgPasswordSalt;
  3023. $hash = '';
  3024. if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
  3025. return $hash;
  3026. }
  3027. if( $wgPasswordSalt ) {
  3028. if ( $salt === false ) {
  3029. $salt = substr( wfGenerateToken(), 0, 8 );
  3030. }
  3031. return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
  3032. } else {
  3033. return ':A:' . md5( $password );
  3034. }
  3035. }
  3036. /**
  3037. * Compare a password hash with a plain-text password. Requires the user
  3038. * ID if there's a chance that the hash is an old-style hash.
  3039. *
  3040. * @param $hash \string Password hash
  3041. * @param $password \string Plain-text password to compare
  3042. * @param $userId \string User ID for old-style password salt
  3043. * @return \bool
  3044. */
  3045. static function comparePasswords( $hash, $password, $userId = false ) {
  3046. $m = false;
  3047. $type = substr( $hash, 0, 3 );
  3048. $result = false;
  3049. if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
  3050. return $result;
  3051. }
  3052. if ( $type == ':A:' ) {
  3053. # Unsalted
  3054. return md5( $password ) === substr( $hash, 3 );
  3055. } elseif ( $type == ':B:' ) {
  3056. # Salted
  3057. list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
  3058. return md5( $salt.'-'.md5( $password ) ) == $realHash;
  3059. } else {
  3060. # Old-style
  3061. return self::oldCrypt( $password, $userId ) === $hash;
  3062. }
  3063. }
  3064. /**
  3065. * Add a newuser log entry for this user
  3066. * @param $byEmail Boolean: account made by email?
  3067. */
  3068. public function addNewUserLogEntry( $byEmail = false ) {
  3069. global $wgUser, $wgContLang, $wgNewUserLog;
  3070. if( empty($wgNewUserLog) ) {
  3071. return true; // disabled
  3072. }
  3073. $talk = $wgContLang->getFormattedNsText( NS_TALK );
  3074. if( $this->getName() == $wgUser->getName() ) {
  3075. $action = 'create';
  3076. $message = '';
  3077. } else {
  3078. $action = 'create2';
  3079. $message = $byEmail ? wfMsgForContent( 'newuserlog-byemail' ) : '';
  3080. }
  3081. $log = new LogPage( 'newusers' );
  3082. $log->addEntry( $action, $this->getUserPage(), $message, array( $this->getId() ) );
  3083. return true;
  3084. }
  3085. /**
  3086. * Add an autocreate newuser log entry for this user
  3087. * Used by things like CentralAuth and perhaps other authplugins.
  3088. */
  3089. public function addNewUserLogEntryAutoCreate() {
  3090. global $wgNewUserLog;
  3091. if( empty($wgNewUserLog) ) {
  3092. return true; // disabled
  3093. }
  3094. $log = new LogPage( 'newusers', false );
  3095. $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
  3096. return true;
  3097. }
  3098. }