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