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