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