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