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