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