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