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