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