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