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