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