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