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