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