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