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