c43507177699de2a05aca038ee5e896dcf0d7407
[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', 7 );
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 $allowUsertalk = ($wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false);
1277 # If a user's name is suppressed, they cannot make edits anywhere
1278 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
1279 $title->getNamespace() == NS_USER_TALK ) {
1280 $blocked = false;
1281 wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1282 }
1283 wfProfileOut( __METHOD__ );
1284 return $blocked;
1285 }
1286
1287 /**
1288 * If user is blocked, return the name of the user who placed the block
1289 * @return \type{\string} name of blocker
1290 */
1291 function blockedBy() {
1292 $this->getBlockedStatus();
1293 return $this->mBlockedby;
1294 }
1295
1296 /**
1297 * If user is blocked, return the specified reason for the block
1298 * @return \type{\string} Blocking reason
1299 */
1300 function blockedFor() {
1301 $this->getBlockedStatus();
1302 return $this->mBlockreason;
1303 }
1304
1305 /**
1306 * Get the user's ID.
1307 * @return \type{\int} The user's ID; 0 if the user is anonymous or nonexistent
1308 */
1309 function getId() {
1310 if( $this->mId === null and $this->mName !== null
1311 and User::isIP( $this->mName ) ) {
1312 // Special case, we know the user is anonymous
1313 return 0;
1314 } elseif( $this->mId === null ) {
1315 // Don't load if this was initialized from an ID
1316 $this->load();
1317 }
1318 return $this->mId;
1319 }
1320
1321 /**
1322 * Set the user and reload all fields according to a given ID
1323 * @param $v \type{\int} %User ID to reload
1324 */
1325 function setId( $v ) {
1326 $this->mId = $v;
1327 $this->clearInstanceCache( 'id' );
1328 }
1329
1330 /**
1331 * Get the user name, or the IP of an anonymous user
1332 * @return \type{\string} User's name or IP address
1333 */
1334 function getName() {
1335 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1336 # Special case optimisation
1337 return $this->mName;
1338 } else {
1339 $this->load();
1340 if ( $this->mName === false ) {
1341 # Clean up IPs
1342 $this->mName = IP::sanitizeIP( wfGetIP() );
1343 }
1344 return $this->mName;
1345 }
1346 }
1347
1348 /**
1349 * Set the user name.
1350 *
1351 * This does not reload fields from the database according to the given
1352 * name. Rather, it is used to create a temporary "nonexistent user" for
1353 * later addition to the database. It can also be used to set the IP
1354 * address for an anonymous user to something other than the current
1355 * remote IP.
1356 *
1357 * @note User::newFromName() has rougly the same function, when the named user
1358 * does not exist.
1359 * @param $str \type{\string} New user name to set
1360 */
1361 function setName( $str ) {
1362 $this->load();
1363 $this->mName = $str;
1364 }
1365
1366 /**
1367 * Get the user's name escaped by underscores.
1368 * @return \type{\string} Username escaped by underscores
1369 */
1370 function getTitleKey() {
1371 return str_replace( ' ', '_', $this->getName() );
1372 }
1373
1374 /**
1375 * Check if the user has new messages.
1376 * @return \type{\bool} True if the user has new messages
1377 */
1378 function getNewtalk() {
1379 $this->load();
1380
1381 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1382 if( $this->mNewtalk === -1 ) {
1383 $this->mNewtalk = false; # reset talk page status
1384
1385 # Check memcached separately for anons, who have no
1386 # entire User object stored in there.
1387 if( !$this->mId ) {
1388 global $wgMemc;
1389 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1390 $newtalk = $wgMemc->get( $key );
1391 if( strval( $newtalk ) !== '' ) {
1392 $this->mNewtalk = (bool)$newtalk;
1393 } else {
1394 // Since we are caching this, make sure it is up to date by getting it
1395 // from the master
1396 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1397 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1398 }
1399 } else {
1400 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1401 }
1402 }
1403
1404 return (bool)$this->mNewtalk;
1405 }
1406
1407 /**
1408 * Return the talk page(s) this user has new messages on.
1409 * @return \type{\arrayof{\string}} Array of page URLs
1410 */
1411 function getNewMessageLinks() {
1412 $talks = array();
1413 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1414 return $talks;
1415
1416 if (!$this->getNewtalk())
1417 return array();
1418 $up = $this->getUserPage();
1419 $utp = $up->getTalkPage();
1420 return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1421 }
1422
1423
1424 /**
1425 * Internal uncached check for new messages
1426 *
1427 * @see getNewtalk()
1428 * @param $field \type{\string} 'user_ip' for anonymous users, 'user_id' otherwise
1429 * @param $id \twotypes{\string,\int} User's IP address for anonymous users, %User ID otherwise
1430 * @param $fromMaster \type{\bool} true to fetch from the master, false for a slave
1431 * @return \type{\bool} True if the user has new messages
1432 * @private
1433 */
1434 function checkNewtalk( $field, $id, $fromMaster = false ) {
1435 if ( $fromMaster ) {
1436 $db = wfGetDB( DB_MASTER );
1437 } else {
1438 $db = wfGetDB( DB_SLAVE );
1439 }
1440 $ok = $db->selectField( 'user_newtalk', $field,
1441 array( $field => $id ), __METHOD__ );
1442 return $ok !== false;
1443 }
1444
1445 /**
1446 * Add or update the new messages flag
1447 * @param $field \type{\string} 'user_ip' for anonymous users, 'user_id' otherwise
1448 * @param $id \twotypes{string,\int} User's IP address for anonymous users, %User ID otherwise
1449 * @return \type{\bool} True if successful, false otherwise
1450 * @private
1451 */
1452 function updateNewtalk( $field, $id ) {
1453 $dbw = wfGetDB( DB_MASTER );
1454 $dbw->insert( 'user_newtalk',
1455 array( $field => $id ),
1456 __METHOD__,
1457 'IGNORE' );
1458 if ( $dbw->affectedRows() ) {
1459 wfDebug( __METHOD__.": set on ($field, $id)\n" );
1460 return true;
1461 } else {
1462 wfDebug( __METHOD__." already set ($field, $id)\n" );
1463 return false;
1464 }
1465 }
1466
1467 /**
1468 * Clear the new messages flag for the given user
1469 * @param $field \type{\string} 'user_ip' for anonymous users, 'user_id' otherwise
1470 * @param $id \twotypes{\string,\int} User's IP address for anonymous users, %User ID otherwise
1471 * @return \type{\bool} True if successful, false otherwise
1472 * @private
1473 */
1474 function deleteNewtalk( $field, $id ) {
1475 $dbw = wfGetDB( DB_MASTER );
1476 $dbw->delete( 'user_newtalk',
1477 array( $field => $id ),
1478 __METHOD__ );
1479 if ( $dbw->affectedRows() ) {
1480 wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1481 return true;
1482 } else {
1483 wfDebug( __METHOD__.": already gone ($field, $id)\n" );
1484 return false;
1485 }
1486 }
1487
1488 /**
1489 * Update the 'You have new messages!' status.
1490 * @param $val \type{\bool} Whether the user has new messages
1491 */
1492 function setNewtalk( $val ) {
1493 if( wfReadOnly() ) {
1494 return;
1495 }
1496
1497 $this->load();
1498 $this->mNewtalk = $val;
1499
1500 if( $this->isAnon() ) {
1501 $field = 'user_ip';
1502 $id = $this->getName();
1503 } else {
1504 $field = 'user_id';
1505 $id = $this->getId();
1506 }
1507 global $wgMemc;
1508
1509 if( $val ) {
1510 $changed = $this->updateNewtalk( $field, $id );
1511 } else {
1512 $changed = $this->deleteNewtalk( $field, $id );
1513 }
1514
1515 if( $this->isAnon() ) {
1516 // Anons have a separate memcached space, since
1517 // user records aren't kept for them.
1518 $key = wfMemcKey( 'newtalk', 'ip', $id );
1519 $wgMemc->set( $key, $val ? 1 : 0, 1800 );
1520 }
1521 if ( $changed ) {
1522 $this->invalidateCache();
1523 }
1524 }
1525
1526 /**
1527 * Generate a current or new-future timestamp to be stored in the
1528 * user_touched field when we update things.
1529 * @return \type{\string} Timestamp in TS_MW format
1530 */
1531 private static function newTouchedTimestamp() {
1532 global $wgClockSkewFudge;
1533 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1534 }
1535
1536 /**
1537 * Clear user data from memcached.
1538 * Use after applying fun updates to the database; caller's
1539 * responsibility to update user_touched if appropriate.
1540 *
1541 * Called implicitly from invalidateCache() and saveSettings().
1542 */
1543 private function clearSharedCache() {
1544 if( $this->mId ) {
1545 global $wgMemc;
1546 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1547 }
1548 }
1549
1550 /**
1551 * Immediately touch the user data cache for this account.
1552 * Updates user_touched field, and removes account data from memcached
1553 * for reload on the next hit.
1554 */
1555 function invalidateCache() {
1556 $this->load();
1557 if( $this->mId ) {
1558 $this->mTouched = self::newTouchedTimestamp();
1559
1560 $dbw = wfGetDB( DB_MASTER );
1561 $dbw->update( 'user',
1562 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1563 array( 'user_id' => $this->mId ),
1564 __METHOD__ );
1565
1566 $this->clearSharedCache();
1567 }
1568 }
1569
1570 /**
1571 * Validate the cache for this account.
1572 * @param $timestamp \type{\string} A timestamp in TS_MW format
1573 */
1574 function validateCache( $timestamp ) {
1575 $this->load();
1576 return ($timestamp >= $this->mTouched);
1577 }
1578
1579 /**
1580 * Get the user touched timestamp
1581 */
1582 function getTouched() {
1583 $this->load();
1584 return $this->mTouched;
1585 }
1586
1587 /**
1588 * Set the password and reset the random token.
1589 * Calls through to authentication plugin if necessary;
1590 * will have no effect if the auth plugin refuses to
1591 * pass the change through or if the legal password
1592 * checks fail.
1593 *
1594 * As a special case, setting the password to null
1595 * wipes it, so the account cannot be logged in until
1596 * a new password is set, for instance via e-mail.
1597 *
1598 * @param $str \type{\string} New password to set
1599 * @throws PasswordError on failure
1600 */
1601 function setPassword( $str ) {
1602 global $wgAuth;
1603
1604 if( $str !== null ) {
1605 if( !$wgAuth->allowPasswordChange() ) {
1606 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1607 }
1608
1609 if( !$this->isValidPassword( $str ) ) {
1610 global $wgMinimalPasswordLength;
1611 throw new PasswordError( wfMsgExt( 'passwordtooshort', array( 'parsemag' ),
1612 $wgMinimalPasswordLength ) );
1613 }
1614 }
1615
1616 if( !$wgAuth->setPassword( $this, $str ) ) {
1617 throw new PasswordError( wfMsg( 'externaldberror' ) );
1618 }
1619
1620 $this->setInternalPassword( $str );
1621
1622 return true;
1623 }
1624
1625 /**
1626 * Set the password and reset the random token unconditionally.
1627 *
1628 * @param $str \type{\string} New password to set
1629 */
1630 function setInternalPassword( $str ) {
1631 $this->load();
1632 $this->setToken();
1633
1634 if( $str === null ) {
1635 // Save an invalid hash...
1636 $this->mPassword = '';
1637 } else {
1638 $this->mPassword = self::crypt( $str );
1639 }
1640 $this->mNewpassword = '';
1641 $this->mNewpassTime = null;
1642 }
1643
1644 /**
1645 * Get the user's current token.
1646 * @return \type{\string} Token
1647 */
1648 function getToken() {
1649 $this->load();
1650 return $this->mToken;
1651 }
1652
1653 /**
1654 * Set the random token (used for persistent authentication)
1655 * Called from loadDefaults() among other places.
1656 *
1657 * @param $token \type{\string} If specified, set the token to this value
1658 * @private
1659 */
1660 function setToken( $token = false ) {
1661 global $wgSecretKey, $wgProxyKey;
1662 $this->load();
1663 if ( !$token ) {
1664 if ( $wgSecretKey ) {
1665 $key = $wgSecretKey;
1666 } elseif ( $wgProxyKey ) {
1667 $key = $wgProxyKey;
1668 } else {
1669 $key = microtime();
1670 }
1671 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1672 } else {
1673 $this->mToken = $token;
1674 }
1675 }
1676
1677 /**
1678 * Set the cookie password
1679 *
1680 * @param $str \type{\string} New cookie password
1681 * @private
1682 */
1683 function setCookiePassword( $str ) {
1684 $this->load();
1685 $this->mCookiePassword = md5( $str );
1686 }
1687
1688 /**
1689 * Set the password for a password reminder or new account email
1690 *
1691 * @param $str \type{\string} New password to set
1692 * @param $throttle \type{\bool} If true, reset the throttle timestamp to the present
1693 */
1694 function setNewpassword( $str, $throttle = true ) {
1695 $this->load();
1696 $this->mNewpassword = self::crypt( $str );
1697 if ( $throttle ) {
1698 $this->mNewpassTime = wfTimestampNow();
1699 }
1700 }
1701
1702 /**
1703 * Has password reminder email been sent within the last
1704 * $wgPasswordReminderResendTime hours?
1705 * @return \type{\bool} True or false
1706 */
1707 function isPasswordReminderThrottled() {
1708 global $wgPasswordReminderResendTime;
1709 $this->load();
1710 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1711 return false;
1712 }
1713 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1714 return time() < $expiry;
1715 }
1716
1717 /**
1718 * Get the user's e-mail address
1719 * @return \type{\string} User's e-mail address
1720 */
1721 function getEmail() {
1722 $this->load();
1723 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
1724 return $this->mEmail;
1725 }
1726
1727 /**
1728 * Get the timestamp of the user's e-mail authentication
1729 * @return \type{\string} TS_MW timestamp
1730 */
1731 function getEmailAuthenticationTimestamp() {
1732 $this->load();
1733 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
1734 return $this->mEmailAuthenticated;
1735 }
1736
1737 /**
1738 * Set the user's e-mail address
1739 * @param $str \type{\string} New e-mail address
1740 */
1741 function setEmail( $str ) {
1742 $this->load();
1743 $this->mEmail = $str;
1744 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
1745 }
1746
1747 /**
1748 * Get the user's real name
1749 * @return \type{\string} User's real name
1750 */
1751 function getRealName() {
1752 $this->load();
1753 return $this->mRealName;
1754 }
1755
1756 /**
1757 * Set the user's real name
1758 * @param $str \type{\string} New real name
1759 */
1760 function setRealName( $str ) {
1761 $this->load();
1762 $this->mRealName = $str;
1763 }
1764
1765 /**
1766 * Get the user's current setting for a given option.
1767 *
1768 * @param $oname \type{\string} The option to check
1769 * @param $defaultOverride \type{\string} A default value returned if the option does not exist
1770 * @return \type{\string} User's current value for the option
1771 * @see getBoolOption()
1772 * @see getIntOption()
1773 */
1774 function getOption( $oname, $defaultOverride = '' ) {
1775 $this->load();
1776
1777 if ( is_null( $this->mOptions ) ) {
1778 if($defaultOverride != '') {
1779 return $defaultOverride;
1780 }
1781 $this->mOptions = User::getDefaultOptions();
1782 }
1783
1784 if ( array_key_exists( $oname, $this->mOptions ) ) {
1785 return trim( $this->mOptions[$oname] );
1786 } else {
1787 return $defaultOverride;
1788 }
1789 }
1790
1791 /**
1792 * Get the user's current setting for a given option, as a boolean value.
1793 *
1794 * @param $oname \type{\string} The option to check
1795 * @return \type{\bool} User's current value for the option
1796 * @see getOption()
1797 */
1798 function getBoolOption( $oname ) {
1799 return (bool)$this->getOption( $oname );
1800 }
1801
1802
1803 /**
1804 * Get the user's current setting for a given option, as a boolean value.
1805 *
1806 * @param $oname \type{\string} The option to check
1807 * @param $defaultOverride \type{\int} A default value returned if the option does not exist
1808 * @return \type{\int} User's current value for the option
1809 * @see getOption()
1810 */
1811 function getIntOption( $oname, $defaultOverride=0 ) {
1812 $val = $this->getOption( $oname );
1813 if( $val == '' ) {
1814 $val = $defaultOverride;
1815 }
1816 return intval( $val );
1817 }
1818
1819 /**
1820 * Set the given option for a user.
1821 *
1822 * @param $oname \type{\string} The option to set
1823 * @param $val \type{\mixed} New value to set
1824 */
1825 function setOption( $oname, $val ) {
1826 $this->load();
1827 if ( is_null( $this->mOptions ) ) {
1828 $this->mOptions = User::getDefaultOptions();
1829 }
1830 if ( $oname == 'skin' ) {
1831 # Clear cached skin, so the new one displays immediately in Special:Preferences
1832 unset( $this->mSkin );
1833 }
1834 // Filter out any newlines that may have passed through input validation.
1835 // Newlines are used to separate items in the options blob.
1836 if( $val ) {
1837 $val = str_replace( "\r\n", "\n", $val );
1838 $val = str_replace( "\r", "\n", $val );
1839 $val = str_replace( "\n", " ", $val );
1840 }
1841 // Explicitly NULL values should refer to defaults
1842 global $wgDefaultUserOptions;
1843 if( is_null($val) && isset($wgDefaultUserOptions[$oname]) ) {
1844 $val = $wgDefaultUserOptions[$oname];
1845 }
1846 $this->mOptions[$oname] = $val;
1847 }
1848
1849 /**
1850 * Get the user's preferred date format.
1851 * @return \type{\string} User's preferred date format
1852 */
1853 function getDatePreference() {
1854 // Important migration for old data rows
1855 if ( is_null( $this->mDatePreference ) ) {
1856 global $wgLang;
1857 $value = $this->getOption( 'date' );
1858 $map = $wgLang->getDatePreferenceMigrationMap();
1859 if ( isset( $map[$value] ) ) {
1860 $value = $map[$value];
1861 }
1862 $this->mDatePreference = $value;
1863 }
1864 return $this->mDatePreference;
1865 }
1866
1867 /**
1868 * Get the permissions this user has.
1869 * @return \type{\arrayof{\string}} Array of permission names
1870 */
1871 function getRights() {
1872 if ( is_null( $this->mRights ) ) {
1873 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
1874 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
1875 // Force reindexation of rights when a hook has unset one of them
1876 $this->mRights = array_values( $this->mRights );
1877 }
1878 return $this->mRights;
1879 }
1880
1881 /**
1882 * Get the list of explicit group memberships this user has.
1883 * The implicit * and user groups are not included.
1884 * @return \type{\arrayof{\string}} Array of internal group names
1885 */
1886 function getGroups() {
1887 $this->load();
1888 return $this->mGroups;
1889 }
1890
1891 /**
1892 * Get the list of implicit group memberships this user has.
1893 * This includes all explicit groups, plus 'user' if logged in,
1894 * '*' for all accounts and autopromoted groups
1895 *
1896 * @param $recache \type{\bool} Whether to avoid the cache
1897 * @return \type{\arrayof{\string}} Array of internal group names
1898 */
1899 function getEffectiveGroups( $recache = false ) {
1900 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
1901 $this->mEffectiveGroups = $this->getGroups();
1902 $this->mEffectiveGroups[] = '*';
1903 if( $this->getId() ) {
1904 $this->mEffectiveGroups[] = 'user';
1905
1906 $this->mEffectiveGroups = array_unique( array_merge(
1907 $this->mEffectiveGroups,
1908 Autopromote::getAutopromoteGroups( $this )
1909 ) );
1910
1911 # Hook for additional groups
1912 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
1913 }
1914 }
1915 return $this->mEffectiveGroups;
1916 }
1917
1918 /**
1919 * Get the user's edit count.
1920 * @return \type{\int} User's edit count
1921 */
1922 function getEditCount() {
1923 if ($this->mId) {
1924 if ( !isset( $this->mEditCount ) ) {
1925 /* Populate the count, if it has not been populated yet */
1926 $this->mEditCount = User::edits($this->mId);
1927 }
1928 return $this->mEditCount;
1929 } else {
1930 /* nil */
1931 return null;
1932 }
1933 }
1934
1935 /**
1936 * Add the user to the given group.
1937 * This takes immediate effect.
1938 * @param $group \type{\string} Name of the group to add
1939 */
1940 function addGroup( $group ) {
1941 $dbw = wfGetDB( DB_MASTER );
1942 if( $this->getId() ) {
1943 $dbw->insert( 'user_groups',
1944 array(
1945 'ug_user' => $this->getID(),
1946 'ug_group' => $group,
1947 ),
1948 'User::addGroup',
1949 array( 'IGNORE' ) );
1950 }
1951
1952 $this->loadGroups();
1953 $this->mGroups[] = $group;
1954 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1955
1956 $this->invalidateCache();
1957 }
1958
1959 /**
1960 * Remove the user from the given group.
1961 * This takes immediate effect.
1962 * @param $group \type{\string} Name of the group to remove
1963 */
1964 function removeGroup( $group ) {
1965 $this->load();
1966 $dbw = wfGetDB( DB_MASTER );
1967 $dbw->delete( 'user_groups',
1968 array(
1969 'ug_user' => $this->getID(),
1970 'ug_group' => $group,
1971 ),
1972 'User::removeGroup' );
1973
1974 $this->loadGroups();
1975 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1976 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1977
1978 $this->invalidateCache();
1979 }
1980
1981
1982 /**
1983 * Get whether the user is logged in
1984 * @return \type{\bool} True or false
1985 */
1986 function isLoggedIn() {
1987 return $this->getID() != 0;
1988 }
1989
1990 /**
1991 * Get whether the user is anonymous
1992 * @return \type{\bool} True or false
1993 */
1994 function isAnon() {
1995 return !$this->isLoggedIn();
1996 }
1997
1998 /**
1999 * Get whether the user is a bot
2000 * @return \type{\bool} True or false
2001 * @deprecated
2002 */
2003 function isBot() {
2004 wfDeprecated( __METHOD__ );
2005 return $this->isAllowed( 'bot' );
2006 }
2007
2008 /**
2009 * Check if user is allowed to access a feature / make an action
2010 * @param $action \type{\string} action to be checked
2011 * @return \type{\bool} True if action is allowed, else false
2012 */
2013 function isAllowed($action='') {
2014 if ( $action === '' )
2015 // In the spirit of DWIM
2016 return true;
2017
2018 # Use strict parameter to avoid matching numeric 0 accidentally inserted
2019 # by misconfiguration: 0 == 'foo'
2020 return in_array( $action, $this->getRights(), true );
2021 }
2022
2023 /**
2024 * Check whether to enable recent changes patrol features for this user
2025 * @return \type{\bool} True or false
2026 */
2027 public function useRCPatrol() {
2028 global $wgUseRCPatrol;
2029 return( $wgUseRCPatrol && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2030 }
2031
2032 /**
2033 * Check whether to enable new pages patrol features for this user
2034 * @return \type{\bool} True or false
2035 */
2036 public function useNPPatrol() {
2037 global $wgUseRCPatrol, $wgUseNPPatrol;
2038 return( ($wgUseRCPatrol || $wgUseNPPatrol) && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2039 }
2040
2041 /**
2042 * Get the current skin, loading it if required
2043 * @return \type{Skin} Current skin
2044 * @todo FIXME : need to check the old failback system [AV]
2045 */
2046 function &getSkin() {
2047 global $wgRequest;
2048 if ( ! isset( $this->mSkin ) ) {
2049 wfProfileIn( __METHOD__ );
2050
2051 # get the user skin
2052 $userSkin = $this->getOption( 'skin' );
2053 $userSkin = $wgRequest->getVal('useskin', $userSkin);
2054
2055 $this->mSkin =& Skin::newFromKey( $userSkin );
2056 wfProfileOut( __METHOD__ );
2057 }
2058 return $this->mSkin;
2059 }
2060
2061 /**
2062 * Check the watched status of an article.
2063 * @param $title \type{Title} Title of the article to look at
2064 * @return \type{\bool} True if article is watched
2065 */
2066 function isWatched( $title ) {
2067 $wl = WatchedItem::fromUserTitle( $this, $title );
2068 return $wl->isWatched();
2069 }
2070
2071 /**
2072 * Watch an article.
2073 * @param $title \type{Title} Title of the article to look at
2074 */
2075 function addWatch( $title ) {
2076 $wl = WatchedItem::fromUserTitle( $this, $title );
2077 $wl->addWatch();
2078 $this->invalidateCache();
2079 }
2080
2081 /**
2082 * Stop watching an article.
2083 * @param $title \type{Title} Title of the article to look at
2084 */
2085 function removeWatch( $title ) {
2086 $wl = WatchedItem::fromUserTitle( $this, $title );
2087 $wl->removeWatch();
2088 $this->invalidateCache();
2089 }
2090
2091 /**
2092 * Clear the user's notification timestamp for the given title.
2093 * If e-notif e-mails are on, they will receive notification mails on
2094 * the next change of the page if it's watched etc.
2095 * @param $title \type{Title} Title of the article to look at
2096 */
2097 function clearNotification( &$title ) {
2098 global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
2099
2100 # Do nothing if the database is locked to writes
2101 if( wfReadOnly() ) {
2102 return;
2103 }
2104
2105 if ($title->getNamespace() == NS_USER_TALK &&
2106 $title->getText() == $this->getName() ) {
2107 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
2108 return;
2109 $this->setNewtalk( false );
2110 }
2111
2112 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2113 return;
2114 }
2115
2116 if( $this->isAnon() ) {
2117 // Nothing else to do...
2118 return;
2119 }
2120
2121 // Only update the timestamp if the page is being watched.
2122 // The query to find out if it is watched is cached both in memcached and per-invocation,
2123 // and when it does have to be executed, it can be on a slave
2124 // If this is the user's newtalk page, we always update the timestamp
2125 if ($title->getNamespace() == NS_USER_TALK &&
2126 $title->getText() == $wgUser->getName())
2127 {
2128 $watched = true;
2129 } elseif ( $this->getId() == $wgUser->getId() ) {
2130 $watched = $title->userIsWatching();
2131 } else {
2132 $watched = true;
2133 }
2134
2135 // If the page is watched by the user (or may be watched), update the timestamp on any
2136 // any matching rows
2137 if ( $watched ) {
2138 $dbw = wfGetDB( DB_MASTER );
2139 $dbw->update( 'watchlist',
2140 array( /* SET */
2141 'wl_notificationtimestamp' => NULL
2142 ), array( /* WHERE */
2143 'wl_title' => $title->getDBkey(),
2144 'wl_namespace' => $title->getNamespace(),
2145 'wl_user' => $this->getID()
2146 ), __METHOD__
2147 );
2148 }
2149 }
2150
2151 /**
2152 * Resets all of the given user's page-change notification timestamps.
2153 * If e-notif e-mails are on, they will receive notification mails on
2154 * the next change of any watched page.
2155 *
2156 * @param $currentUser \type{\int} %User ID
2157 */
2158 function clearAllNotifications( $currentUser ) {
2159 global $wgUseEnotif, $wgShowUpdatedMarker;
2160 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2161 $this->setNewtalk( false );
2162 return;
2163 }
2164 if( $currentUser != 0 ) {
2165 $dbw = wfGetDB( DB_MASTER );
2166 $dbw->update( 'watchlist',
2167 array( /* SET */
2168 'wl_notificationtimestamp' => NULL
2169 ), array( /* WHERE */
2170 'wl_user' => $currentUser
2171 ), __METHOD__
2172 );
2173 # We also need to clear here the "you have new message" notification for the own user_talk page
2174 # This is cleared one page view later in Article::viewUpdates();
2175 }
2176 }
2177
2178 /**
2179 * Encode this user's options as a string
2180 * @return \type{\string} Encoded options
2181 * @private
2182 */
2183 function encodeOptions() {
2184 $this->load();
2185 if ( is_null( $this->mOptions ) ) {
2186 $this->mOptions = User::getDefaultOptions();
2187 }
2188 $a = array();
2189 foreach ( $this->mOptions as $oname => $oval ) {
2190 array_push( $a, $oname.'='.$oval );
2191 }
2192 $s = implode( "\n", $a );
2193 return $s;
2194 }
2195
2196 /**
2197 * Set this user's options from an encoded string
2198 * @param $str \type{\string} Encoded options to import
2199 * @private
2200 */
2201 function decodeOptions( $str ) {
2202 $this->mOptions = array();
2203 $a = explode( "\n", $str );
2204 foreach ( $a as $s ) {
2205 $m = array();
2206 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
2207 $this->mOptions[$m[1]] = $m[2];
2208 }
2209 }
2210 }
2211
2212 /**
2213 * Set a cookie on the user's client. Wrapper for
2214 * WebResponse::setCookie
2215 */
2216 protected function setCookie( $name, $value, $exp=0 ) {
2217 global $wgRequest;
2218 $wgRequest->response()->setcookie( $name, $value, $exp );
2219 }
2220
2221 /**
2222 * Clear a cookie on the user's client
2223 * @param $name \type{\string} Name of the cookie to clear
2224 */
2225 protected function clearCookie( $name ) {
2226 $this->setCookie( $name, '', time() - 86400 );
2227 }
2228
2229 /**
2230 * Set the default cookies for this session on the user's client.
2231 */
2232 function setCookies() {
2233 $this->load();
2234 if ( 0 == $this->mId ) return;
2235 $session = array(
2236 'wsUserID' => $this->mId,
2237 'wsToken' => $this->mToken,
2238 'wsUserName' => $this->getName()
2239 );
2240 $cookies = array(
2241 'UserID' => $this->mId,
2242 'UserName' => $this->getName(),
2243 );
2244 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
2245 $cookies['Token'] = $this->mToken;
2246 } else {
2247 $cookies['Token'] = false;
2248 }
2249
2250 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
2251 $_SESSION = $session + $_SESSION;
2252 foreach ( $cookies as $name => $value ) {
2253 if ( $value === false ) {
2254 $this->clearCookie( $name );
2255 } else {
2256 $this->setCookie( $name, $value );
2257 }
2258 }
2259 }
2260
2261 /**
2262 * Log this user out.
2263 */
2264 function logout() {
2265 global $wgUser;
2266 if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
2267 $this->doLogout();
2268 }
2269 }
2270
2271 /**
2272 * Clear the user's cookies and session, and reset the instance cache.
2273 * @private
2274 * @see logout()
2275 */
2276 function doLogout() {
2277 $this->clearInstanceCache( 'defaults' );
2278
2279 $_SESSION['wsUserID'] = 0;
2280
2281 $this->clearCookie( 'UserID' );
2282 $this->clearCookie( 'Token' );
2283
2284 # Remember when user logged out, to prevent seeing cached pages
2285 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
2286 }
2287
2288 /**
2289 * Save this user's settings into the database.
2290 * @todo Only rarely do all these fields need to be set!
2291 */
2292 function saveSettings() {
2293 $this->load();
2294 if ( wfReadOnly() ) { return; }
2295 if ( 0 == $this->mId ) { return; }
2296
2297 $this->mTouched = self::newTouchedTimestamp();
2298
2299 $dbw = wfGetDB( DB_MASTER );
2300 $dbw->update( 'user',
2301 array( /* SET */
2302 'user_name' => $this->mName,
2303 'user_password' => $this->mPassword,
2304 'user_newpassword' => $this->mNewpassword,
2305 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2306 'user_real_name' => $this->mRealName,
2307 'user_email' => $this->mEmail,
2308 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2309 'user_options' => $this->encodeOptions(),
2310 'user_touched' => $dbw->timestamp($this->mTouched),
2311 'user_token' => $this->mToken,
2312 'user_email_token' => $this->mEmailToken,
2313 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
2314 ), array( /* WHERE */
2315 'user_id' => $this->mId
2316 ), __METHOD__
2317 );
2318 wfRunHooks( 'UserSaveSettings', array( $this ) );
2319 $this->clearSharedCache();
2320 }
2321
2322 /**
2323 * If only this user's username is known, and it exists, return the user ID.
2324 */
2325 function idForName() {
2326 $s = trim( $this->getName() );
2327 if ( $s === '' ) return 0;
2328
2329 $dbr = wfGetDB( DB_SLAVE );
2330 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
2331 if ( $id === false ) {
2332 $id = 0;
2333 }
2334 return $id;
2335 }
2336
2337 /**
2338 * Add a user to the database, return the user object
2339 *
2340 * @param $name \type{\string} Username to add
2341 * @param $params \type{\arrayof{\string}} Non-default parameters to save to the database:
2342 * - password The user's password. Password logins will be disabled if this is omitted.
2343 * - newpassword A temporary password mailed to the user
2344 * - email The user's email address
2345 * - email_authenticated The email authentication timestamp
2346 * - real_name The user's real name
2347 * - options An associative array of non-default options
2348 * - token Random authentication token. Do not set.
2349 * - registration Registration timestamp. Do not set.
2350 *
2351 * @return \type{User} A new User object, or null if the username already exists
2352 */
2353 static function createNew( $name, $params = array() ) {
2354 $user = new User;
2355 $user->load();
2356 if ( isset( $params['options'] ) ) {
2357 $user->mOptions = $params['options'] + $user->mOptions;
2358 unset( $params['options'] );
2359 }
2360 $dbw = wfGetDB( DB_MASTER );
2361 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2362 $fields = array(
2363 'user_id' => $seqVal,
2364 'user_name' => $name,
2365 'user_password' => $user->mPassword,
2366 'user_newpassword' => $user->mNewpassword,
2367 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2368 'user_email' => $user->mEmail,
2369 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2370 'user_real_name' => $user->mRealName,
2371 'user_options' => $user->encodeOptions(),
2372 'user_token' => $user->mToken,
2373 'user_registration' => $dbw->timestamp( $user->mRegistration ),
2374 'user_editcount' => 0,
2375 );
2376 foreach ( $params as $name => $value ) {
2377 $fields["user_$name"] = $value;
2378 }
2379 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2380 if ( $dbw->affectedRows() ) {
2381 $newUser = User::newFromId( $dbw->insertId() );
2382 } else {
2383 $newUser = null;
2384 }
2385 return $newUser;
2386 }
2387
2388 /**
2389 * Add this existing user object to the database
2390 */
2391 function addToDatabase() {
2392 $this->load();
2393 $dbw = wfGetDB( DB_MASTER );
2394 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2395 $dbw->insert( 'user',
2396 array(
2397 'user_id' => $seqVal,
2398 'user_name' => $this->mName,
2399 'user_password' => $this->mPassword,
2400 'user_newpassword' => $this->mNewpassword,
2401 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2402 'user_email' => $this->mEmail,
2403 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2404 'user_real_name' => $this->mRealName,
2405 'user_options' => $this->encodeOptions(),
2406 'user_token' => $this->mToken,
2407 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2408 'user_editcount' => 0,
2409 ), __METHOD__
2410 );
2411 $this->mId = $dbw->insertId();
2412
2413 // Clear instance cache other than user table data, which is already accurate
2414 $this->clearInstanceCache();
2415 }
2416
2417 /**
2418 * If this (non-anonymous) user is blocked, block any IP address
2419 * they've successfully logged in from.
2420 */
2421 function spreadBlock() {
2422 wfDebug( __METHOD__."()\n" );
2423 $this->load();
2424 if ( $this->mId == 0 ) {
2425 return;
2426 }
2427
2428 $userblock = Block::newFromDB( '', $this->mId );
2429 if ( !$userblock ) {
2430 return;
2431 }
2432
2433 $userblock->doAutoblock( wfGetIp() );
2434
2435 }
2436
2437 /**
2438 * Generate a string which will be different for any combination of
2439 * user options which would produce different parser output.
2440 * This will be used as part of the hash key for the parser cache,
2441 * so users will the same options can share the same cached data
2442 * safely.
2443 *
2444 * Extensions which require it should install 'PageRenderingHash' hook,
2445 * which will give them a chance to modify this key based on their own
2446 * settings.
2447 *
2448 * @return \type{\string} Page rendering hash
2449 */
2450 function getPageRenderingHash() {
2451 global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
2452 if( $this->mHash ){
2453 return $this->mHash;
2454 }
2455
2456 // stubthreshold is only included below for completeness,
2457 // it will always be 0 when this function is called by parsercache.
2458
2459 $confstr = $this->getOption( 'math' );
2460 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2461 if ( $wgUseDynamicDates ) {
2462 $confstr .= '!' . $this->getDatePreference();
2463 }
2464 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2465 $confstr .= '!' . $wgLang->getCode();
2466 $confstr .= '!' . $this->getOption( 'thumbsize' );
2467 // add in language specific options, if any
2468 $extra = $wgContLang->getExtraHashOptions();
2469 $confstr .= $extra;
2470
2471 $confstr .= $wgRenderHashAppend;
2472
2473 // Give a chance for extensions to modify the hash, if they have
2474 // extra options or other effects on the parser cache.
2475 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2476
2477 // Make it a valid memcached key fragment
2478 $confstr = str_replace( ' ', '_', $confstr );
2479 $this->mHash = $confstr;
2480 return $confstr;
2481 }
2482
2483 /**
2484 * Get whether the user is explicitly blocked from account creation.
2485 * @return \type{\bool} True if blocked
2486 */
2487 function isBlockedFromCreateAccount() {
2488 $this->getBlockedStatus();
2489 return $this->mBlock && $this->mBlock->mCreateAccount;
2490 }
2491
2492 /**
2493 * Get whether the user is blocked from using Special:Emailuser.
2494 * @return \type{\bool} True if blocked
2495 */
2496 function isBlockedFromEmailuser() {
2497 $this->getBlockedStatus();
2498 return $this->mBlock && $this->mBlock->mBlockEmail;
2499 }
2500
2501 /**
2502 * Get whether the user is allowed to create an account.
2503 * @return \type{\bool} True if allowed
2504 */
2505 function isAllowedToCreateAccount() {
2506 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2507 }
2508
2509 /**
2510 * @deprecated
2511 */
2512 function setLoaded( $loaded ) {
2513 wfDeprecated( __METHOD__ );
2514 }
2515
2516 /**
2517 * Get this user's personal page title.
2518 *
2519 * @return \type{Title} User's personal page title
2520 */
2521 function getUserPage() {
2522 return Title::makeTitle( NS_USER, $this->getName() );
2523 }
2524
2525 /**
2526 * Get this user's talk page title.
2527 *
2528 * @return \type{Title} User's talk page title
2529 */
2530 function getTalkPage() {
2531 $title = $this->getUserPage();
2532 return $title->getTalkPage();
2533 }
2534
2535 /**
2536 * Get the maximum valid user ID.
2537 * @return \type{\int} %User ID
2538 * @static
2539 */
2540 function getMaxID() {
2541 static $res; // cache
2542
2543 if ( isset( $res ) )
2544 return $res;
2545 else {
2546 $dbr = wfGetDB( DB_SLAVE );
2547 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2548 }
2549 }
2550
2551 /**
2552 * Determine whether the user is a newbie. Newbies are either
2553 * anonymous IPs, or the most recently created accounts.
2554 * @return \type{\bool} True if the user is a newbie
2555 */
2556 function isNewbie() {
2557 return !$this->isAllowed( 'autoconfirmed' );
2558 }
2559
2560 /**
2561 * Is the user active? We check to see if they've made at least
2562 * X number of edits in the last Y days.
2563 *
2564 * @return \type{\bool} True if the user is active, false if not.
2565 */
2566 public function isActiveEditor() {
2567 global $wgActiveUserEditCount, $wgActiveUserDays;
2568 $dbr = wfGetDB( DB_SLAVE );
2569
2570 // Stolen without shame from RC
2571 $cutoff_unixtime = time() - ( $wgActiveUserDays * 86400 );
2572 $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
2573 $oldTime = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
2574
2575 $res = $dbr->select( 'revision', '1',
2576 array( 'rev_user_text' => $this->getName(), "rev_timestamp > $oldTime"),
2577 __METHOD__,
2578 array('LIMIT' => $wgActiveUserEditCount ) );
2579
2580 $count = $dbr->numRows($res);
2581 $dbr->freeResult($res);
2582
2583 return $count == $wgActiveUserEditCount;
2584 }
2585
2586 /**
2587 * Check to see if the given clear-text password is one of the accepted passwords
2588 * @param $password \type{\string} user password.
2589 * @return \type{\bool} True if the given password is correct, otherwise False.
2590 */
2591 function checkPassword( $password ) {
2592 global $wgAuth;
2593 $this->load();
2594
2595 // Even though we stop people from creating passwords that
2596 // are shorter than this, doesn't mean people wont be able
2597 // to. Certain authentication plugins do NOT want to save
2598 // domain passwords in a mysql database, so we should
2599 // check this (incase $wgAuth->strict() is false).
2600 if( !$this->isValidPassword( $password ) ) {
2601 return false;
2602 }
2603
2604 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2605 return true;
2606 } elseif( $wgAuth->strict() ) {
2607 /* Auth plugin doesn't allow local authentication */
2608 return false;
2609 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2610 /* Auth plugin doesn't allow local authentication for this user name */
2611 return false;
2612 }
2613 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
2614 return true;
2615 } elseif ( function_exists( 'iconv' ) ) {
2616 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2617 # Check for this with iconv
2618 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
2619 if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
2620 return true;
2621 }
2622 }
2623 return false;
2624 }
2625
2626 /**
2627 * Check if the given clear-text password matches the temporary password
2628 * sent by e-mail for password reset operations.
2629 * @return \type{\bool} True if matches, false otherwise
2630 */
2631 function checkTemporaryPassword( $plaintext ) {
2632 return self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() );
2633 }
2634
2635 /**
2636 * Initialize (if necessary) and return a session token value
2637 * which can be used in edit forms to show that the user's
2638 * login credentials aren't being hijacked with a foreign form
2639 * submission.
2640 *
2641 * @param $salt \twotypes{\string,\arrayof{\string}} Optional function-specific data for hashing
2642 * @return \type{\string} The new edit token
2643 */
2644 function editToken( $salt = '' ) {
2645 if ( $this->isAnon() ) {
2646 return EDIT_TOKEN_SUFFIX;
2647 } else {
2648 if( !isset( $_SESSION['wsEditToken'] ) ) {
2649 $token = $this->generateToken();
2650 $_SESSION['wsEditToken'] = $token;
2651 } else {
2652 $token = $_SESSION['wsEditToken'];
2653 }
2654 if( is_array( $salt ) ) {
2655 $salt = implode( '|', $salt );
2656 }
2657 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2658 }
2659 }
2660
2661 /**
2662 * Generate a looking random token for various uses.
2663 *
2664 * @param $salt \type{\string} Optional salt value
2665 * @return \type{\string} The new random token
2666 */
2667 function generateToken( $salt = '' ) {
2668 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2669 return md5( $token . $salt );
2670 }
2671
2672 /**
2673 * Check given value against the token value stored in the session.
2674 * A match should confirm that the form was submitted from the
2675 * user's own login session, not a form submission from a third-party
2676 * site.
2677 *
2678 * @param $val \type{\string} Input value to compare
2679 * @param $salt \type{\string} Optional function-specific data for hashing
2680 * @return \type{\bool} Whether the token matches
2681 */
2682 function matchEditToken( $val, $salt = '' ) {
2683 $sessionToken = $this->editToken( $salt );
2684 if ( $val != $sessionToken ) {
2685 wfDebug( "User::matchEditToken: broken session data\n" );
2686 }
2687 return $val == $sessionToken;
2688 }
2689
2690 /**
2691 * Check given value against the token value stored in the session,
2692 * ignoring the suffix.
2693 *
2694 * @param $val \type{\string} Input value to compare
2695 * @param $salt \type{\string} Optional function-specific data for hashing
2696 * @return \type{\bool} Whether the token matches
2697 */
2698 function matchEditTokenNoSuffix( $val, $salt = '' ) {
2699 $sessionToken = $this->editToken( $salt );
2700 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2701 }
2702
2703 /**
2704 * Generate a new e-mail confirmation token and send a confirmation/invalidation
2705 * mail to the user's given address.
2706 *
2707 * @return \twotypes{\bool,WikiError} True on success, a WikiError object on failure.
2708 */
2709 function sendConfirmationMail() {
2710 global $wgLang;
2711 $expiration = null; // gets passed-by-ref and defined in next line.
2712 $token = $this->confirmationToken( $expiration );
2713 $url = $this->confirmationTokenUrl( $token );
2714 $invalidateURL = $this->invalidationTokenUrl( $token );
2715 $this->saveSettings();
2716
2717 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2718 wfMsg( 'confirmemail_body',
2719 wfGetIP(),
2720 $this->getName(),
2721 $url,
2722 $wgLang->timeanddate( $expiration, false ),
2723 $invalidateURL ) );
2724 }
2725
2726 /**
2727 * Send an e-mail to this user's account. Does not check for
2728 * confirmed status or validity.
2729 *
2730 * @param $subject \type{\string} Message subject
2731 * @param $body \type{\string} Message body
2732 * @param $from \type{\string} Optional From address; if unspecified, default $wgPasswordSender will be used
2733 * @param $replyto \type{\string} Reply-to address
2734 * @return \twotypes{\bool,WikiError} True on success, a WikiError object on failure
2735 */
2736 function sendMail( $subject, $body, $from = null, $replyto = null ) {
2737 if( is_null( $from ) ) {
2738 global $wgPasswordSender;
2739 $from = $wgPasswordSender;
2740 }
2741
2742 $to = new MailAddress( $this );
2743 $sender = new MailAddress( $from );
2744 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2745 }
2746
2747 /**
2748 * Generate, store, and return a new e-mail confirmation code.
2749 * A hash (unsalted, since it's used as a key) is stored.
2750 *
2751 * @note Call saveSettings() after calling this function to commit
2752 * this change to the database.
2753 *
2754 * @param[out] &$expiration \type{\mixed} Accepts the expiration time
2755 * @return \type{\string} New token
2756 * @private
2757 */
2758 function confirmationToken( &$expiration ) {
2759 $now = time();
2760 $expires = $now + 7 * 24 * 60 * 60;
2761 $expiration = wfTimestamp( TS_MW, $expires );
2762 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2763 $hash = md5( $token );
2764 $this->load();
2765 $this->mEmailToken = $hash;
2766 $this->mEmailTokenExpires = $expiration;
2767 return $token;
2768 }
2769
2770 /**
2771 * Return a URL the user can use to confirm their email address.
2772 * @param $token \type{\string} Accepts the email confirmation token
2773 * @return \type{\string} New token URL
2774 * @private
2775 */
2776 function confirmationTokenUrl( $token ) {
2777 return $this->getTokenUrl( 'ConfirmEmail', $token );
2778 }
2779 /**
2780 * Return a URL the user can use to invalidate their email address.
2781 *
2782 * @param $token \type{\string} Accepts the email confirmation token
2783 * @return \type{\string} New token URL
2784 * @private
2785 */
2786 function invalidationTokenUrl( $token ) {
2787 return $this->getTokenUrl( 'Invalidateemail', $token );
2788 }
2789
2790 /**
2791 * Internal function to format the e-mail validation/invalidation URLs.
2792 * This uses $wgArticlePath directly as a quickie hack to use the
2793 * hardcoded English names of the Special: pages, for ASCII safety.
2794 *
2795 * @note Since these URLs get dropped directly into emails, using the
2796 * short English names avoids insanely long URL-encoded links, which
2797 * also sometimes can get corrupted in some browsers/mailers
2798 * (bug 6957 with Gmail and Internet Explorer).
2799 *
2800 * @param $page \type{\string} Special page
2801 * @param $token \type{\string} Token
2802 * @return \type{\string} Formatted URL
2803 */
2804 protected function getTokenUrl( $page, $token ) {
2805 global $wgArticlePath;
2806 return wfExpandUrl(
2807 str_replace(
2808 '$1',
2809 "Special:$page/$token",
2810 $wgArticlePath ) );
2811 }
2812
2813 /**
2814 * Mark the e-mail address confirmed.
2815 *
2816 * @note Call saveSettings() after calling this function to commit the change.
2817 */
2818 function confirmEmail() {
2819 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
2820 return true;
2821 }
2822
2823 /**
2824 * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
2825 * address if it was already confirmed.
2826 *
2827 * @note Call saveSettings() after calling this function to commit the change.
2828 */
2829 function invalidateEmail() {
2830 $this->load();
2831 $this->mEmailToken = null;
2832 $this->mEmailTokenExpires = null;
2833 $this->setEmailAuthenticationTimestamp( null );
2834 return true;
2835 }
2836
2837 /**
2838 * Set the e-mail authentication timestamp.
2839 * @param $timestamp \type{\string} TS_MW timestamp
2840 */
2841 function setEmailAuthenticationTimestamp( $timestamp ) {
2842 $this->load();
2843 $this->mEmailAuthenticated = $timestamp;
2844 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
2845 }
2846
2847 /**
2848 * Is this user allowed to send e-mails within limits of current
2849 * site configuration?
2850 * @return \type{\bool} True if allowed
2851 */
2852 function canSendEmail() {
2853 $canSend = $this->isEmailConfirmed();
2854 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
2855 return $canSend;
2856 }
2857
2858 /**
2859 * Is this user allowed to receive e-mails within limits of current
2860 * site configuration?
2861 * @return \type{\bool} True if allowed
2862 */
2863 function canReceiveEmail() {
2864 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
2865 }
2866
2867 /**
2868 * Is this user's e-mail address valid-looking and confirmed within
2869 * limits of the current site configuration?
2870 *
2871 * @note If $wgEmailAuthentication is on, this may require the user to have
2872 * confirmed their address by returning a code or using a password
2873 * sent to the address from the wiki.
2874 *
2875 * @return \type{\bool} True if confirmed
2876 */
2877 function isEmailConfirmed() {
2878 global $wgEmailAuthentication;
2879 $this->load();
2880 $confirmed = true;
2881 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2882 if( $this->isAnon() )
2883 return false;
2884 if( !self::isValidEmailAddr( $this->mEmail ) )
2885 return false;
2886 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
2887 return false;
2888 return true;
2889 } else {
2890 return $confirmed;
2891 }
2892 }
2893
2894 /**
2895 * Check whether there is an outstanding request for e-mail confirmation.
2896 * @return \type{\bool} True if pending
2897 */
2898 function isEmailConfirmationPending() {
2899 global $wgEmailAuthentication;
2900 return $wgEmailAuthentication &&
2901 !$this->isEmailConfirmed() &&
2902 $this->mEmailToken &&
2903 $this->mEmailTokenExpires > wfTimestamp();
2904 }
2905
2906 /**
2907 * Get the timestamp of account creation.
2908 *
2909 * @return \twotypes{\string,\bool} string Timestamp of account creation, or false for
2910 * non-existent/anonymous user accounts.
2911 */
2912 public function getRegistration() {
2913 return $this->mId > 0
2914 ? $this->mRegistration
2915 : false;
2916 }
2917
2918 /**
2919 * Get the permissions associated with a given list of groups
2920 *
2921 * @param $groups \type{\arrayof{\string}} List of internal group names
2922 * @return \type{\arrayof{\string}} List of permission key names for given groups combined
2923 */
2924 static function getGroupPermissions( $groups ) {
2925 global $wgGroupPermissions;
2926 $rights = array();
2927 foreach( $groups as $group ) {
2928 if( isset( $wgGroupPermissions[$group] ) ) {
2929 $rights = array_merge( $rights,
2930 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2931 }
2932 }
2933 return $rights;
2934 }
2935
2936 /**
2937 * Get all the groups who have a given permission
2938 *
2939 * @param $role \type{\string} Role to check
2940 * @return \type{\arrayof{\string}} List of internal group names with the given permission
2941 */
2942 static function getGroupsWithPermission( $role ) {
2943 global $wgGroupPermissions;
2944 $allowedGroups = array();
2945 foreach ( $wgGroupPermissions as $group => $rights ) {
2946 if ( isset( $rights[$role] ) && $rights[$role] ) {
2947 $allowedGroups[] = $group;
2948 }
2949 }
2950 return $allowedGroups;
2951 }
2952
2953 /**
2954 * Get the localized descriptive name for a group, if it exists
2955 *
2956 * @param $group \type{\string} Internal group name
2957 * @return \type{\string} Localized descriptive group name
2958 */
2959 static function getGroupName( $group ) {
2960 global $wgMessageCache;
2961 $wgMessageCache->loadAllMessages();
2962 $key = "group-$group";
2963 $name = wfMsg( $key );
2964 return $name == '' || wfEmptyMsg( $key, $name )
2965 ? $group
2966 : $name;
2967 }
2968
2969 /**
2970 * Get the localized descriptive name for a member of a group, if it exists
2971 *
2972 * @param $group \type{\string} Internal group name
2973 * @return \type{\string} Localized name for group member
2974 */
2975 static function getGroupMember( $group ) {
2976 global $wgMessageCache;
2977 $wgMessageCache->loadAllMessages();
2978 $key = "group-$group-member";
2979 $name = wfMsg( $key );
2980 return $name == '' || wfEmptyMsg( $key, $name )
2981 ? $group
2982 : $name;
2983 }
2984
2985 /**
2986 * Return the set of defined explicit groups.
2987 * The implicit groups (by default *, 'user' and 'autoconfirmed')
2988 * are not included, as they are defined automatically, not in the database.
2989 * @return \type{\arrayof{\string}} Array of internal group names
2990 */
2991 static function getAllGroups() {
2992 global $wgGroupPermissions;
2993 return array_diff(
2994 array_keys( $wgGroupPermissions ),
2995 self::getImplicitGroups()
2996 );
2997 }
2998
2999 /**
3000 * Get a list of all available permissions.
3001 * @return \type{\arrayof{\string}} Array of permission names
3002 */
3003 static function getAllRights() {
3004 if ( self::$mAllRights === false ) {
3005 global $wgAvailableRights;
3006 if ( count( $wgAvailableRights ) ) {
3007 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
3008 } else {
3009 self::$mAllRights = self::$mCoreRights;
3010 }
3011 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
3012 }
3013 return self::$mAllRights;
3014 }
3015
3016 /**
3017 * Get a list of implicit groups
3018 * @return \type{\arrayof{\string}} Array of internal group names
3019 */
3020 public static function getImplicitGroups() {
3021 global $wgImplicitGroups;
3022 $groups = $wgImplicitGroups;
3023 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
3024 return $groups;
3025 }
3026
3027 /**
3028 * Get the title of a page describing a particular group
3029 *
3030 * @param $group \type{\string} Internal group name
3031 * @return \twotypes{Title,\bool} Title of the page if it exists, false otherwise
3032 */
3033 static function getGroupPage( $group ) {
3034 global $wgMessageCache;
3035 $wgMessageCache->loadAllMessages();
3036 $page = wfMsgForContent( 'grouppage-' . $group );
3037 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
3038 $title = Title::newFromText( $page );
3039 if( is_object( $title ) )
3040 return $title;
3041 }
3042 return false;
3043 }
3044
3045 /**
3046 * Create a link to the group in HTML, if available;
3047 * else return the group name.
3048 *
3049 * @param $group \type{\string} Internal name of the group
3050 * @param $text \type{\string} The text of the link
3051 * @return \type{\string} HTML link to the group
3052 */
3053 static function makeGroupLinkHTML( $group, $text = '' ) {
3054 if( $text == '' ) {
3055 $text = self::getGroupName( $group );
3056 }
3057 $title = self::getGroupPage( $group );
3058 if( $title ) {
3059 global $wgUser;
3060 $sk = $wgUser->getSkin();
3061 return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
3062 } else {
3063 return $text;
3064 }
3065 }
3066
3067 /**
3068 * Create a link to the group in Wikitext, if available;
3069 * else return the group name.
3070 *
3071 * @param $group \type{\string} Internal name of the group
3072 * @param $text \type{\string} The text of the link
3073 * @return \type{\string} Wikilink to the group
3074 */
3075 static function makeGroupLinkWiki( $group, $text = '' ) {
3076 if( $text == '' ) {
3077 $text = self::getGroupName( $group );
3078 }
3079 $title = self::getGroupPage( $group );
3080 if( $title ) {
3081 $page = $title->getPrefixedText();
3082 return "[[$page|$text]]";
3083 } else {
3084 return $text;
3085 }
3086 }
3087
3088 /**
3089 * Increment the user's edit-count field.
3090 * Will have no effect for anonymous users.
3091 */
3092 function incEditCount() {
3093 if( !$this->isAnon() ) {
3094 $dbw = wfGetDB( DB_MASTER );
3095 $dbw->update( 'user',
3096 array( 'user_editcount=user_editcount+1' ),
3097 array( 'user_id' => $this->getId() ),
3098 __METHOD__ );
3099
3100 // Lazy initialization check...
3101 if( $dbw->affectedRows() == 0 ) {
3102 // Pull from a slave to be less cruel to servers
3103 // Accuracy isn't the point anyway here
3104 $dbr = wfGetDB( DB_SLAVE );
3105 $count = $dbr->selectField( 'revision',
3106 'COUNT(rev_user)',
3107 array( 'rev_user' => $this->getId() ),
3108 __METHOD__ );
3109
3110 // Now here's a goddamn hack...
3111 if( $dbr !== $dbw ) {
3112 // If we actually have a slave server, the count is
3113 // at least one behind because the current transaction
3114 // has not been committed and replicated.
3115 $count++;
3116 } else {
3117 // But if DB_SLAVE is selecting the master, then the
3118 // count we just read includes the revision that was
3119 // just added in the working transaction.
3120 }
3121
3122 $dbw->update( 'user',
3123 array( 'user_editcount' => $count ),
3124 array( 'user_id' => $this->getId() ),
3125 __METHOD__ );
3126 }
3127 }
3128 // edit count in user cache too
3129 $this->invalidateCache();
3130 }
3131
3132 /**
3133 * Get the description of a given right
3134 *
3135 * @param $right \type{\string} Right to query
3136 * @return \type{\string} Localized description of the right
3137 */
3138 static function getRightDescription( $right ) {
3139 global $wgMessageCache;
3140 $wgMessageCache->loadAllMessages();
3141 $key = "right-$right";
3142 $name = wfMsg( $key );
3143 return $name == '' || wfEmptyMsg( $key, $name )
3144 ? $right
3145 : $name;
3146 }
3147
3148 /**
3149 * Make an old-style password hash
3150 *
3151 * @param $password \type{\string} Plain-text password
3152 * @param $userId \type{\string} %User ID
3153 * @return \type{\string} Password hash
3154 */
3155 static function oldCrypt( $password, $userId ) {
3156 global $wgPasswordSalt;
3157 if ( $wgPasswordSalt ) {
3158 return md5( $userId . '-' . md5( $password ) );
3159 } else {
3160 return md5( $password );
3161 }
3162 }
3163
3164 /**
3165 * Make a new-style password hash
3166 *
3167 * @param $password \type{\string} Plain-text password
3168 * @param $salt \type{\string} Optional salt, may be random or the user ID.
3169 * If unspecified or false, will generate one automatically
3170 * @return \type{\string} Password hash
3171 */
3172 static function crypt( $password, $salt = false ) {
3173 global $wgPasswordSalt;
3174
3175 if($wgPasswordSalt) {
3176 if ( $salt === false ) {
3177 $salt = substr( wfGenerateToken(), 0, 8 );
3178 }
3179 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
3180 } else {
3181 return ':A:' . md5( $password);
3182 }
3183 }
3184
3185 /**
3186 * Compare a password hash with a plain-text password. Requires the user
3187 * ID if there's a chance that the hash is an old-style hash.
3188 *
3189 * @param $hash \type{\string} Password hash
3190 * @param $password \type{\string} Plain-text password to compare
3191 * @param $userId \type{\string} %User ID for old-style password salt
3192 * @return \type{\bool} True if matches, false otherwise
3193 */
3194 static function comparePasswords( $hash, $password, $userId = false ) {
3195 $m = false;
3196 $type = substr( $hash, 0, 3 );
3197 if ( $type == ':A:' ) {
3198 # Unsalted
3199 return md5( $password ) === substr( $hash, 3 );
3200 } elseif ( $type == ':B:' ) {
3201 # Salted
3202 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
3203 return md5( $salt.'-'.md5( $password ) ) == $realHash;
3204 } else {
3205 # Old-style
3206 return self::oldCrypt( $password, $userId ) === $hash;
3207 }
3208 }
3209
3210 /**
3211 * Add a newuser log entry for this user
3212 * @param bool $byEmail, account made by email?
3213 */
3214 public function addNewUserLogEntry( $byEmail = false ) {
3215 global $wgUser, $wgContLang, $wgNewUserLog;
3216 if( empty($wgNewUserLog) ) {
3217 return true; // disabled
3218 }
3219 $talk = $wgContLang->getFormattedNsText( NS_TALK );
3220 if( $this->getName() == $wgUser->getName() ) {
3221 $action = 'create';
3222 $message = '';
3223 } else {
3224 $action = 'create2';
3225 $message = $byEmail ? wfMsgForContent( 'newuserlog-byemail' ) : '';
3226 }
3227 $log = new LogPage( 'newusers' );
3228 $log->addEntry( $action, $this->getUserPage(), $message, array( $this->getId() ) );
3229 return true;
3230 }
3231
3232 /**
3233 * Add an autocreate newuser log entry for this user
3234 * Used by things like CentralAuth and perhaps other authplugins.
3235 */
3236 public function addNewUserLogEntryAutoCreate() {
3237 global $wgNewUserLog;
3238 if( empty($wgNewUserLog) ) {
3239 return true; // disabled
3240 }
3241 $log = new LogPage( 'newusers', false );
3242 $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
3243 return true;
3244 }
3245
3246 // Restrictions-related block
3247
3248 public function loadRestrictions() {
3249 if( is_null( $this->mRestrictions ) )
3250 $this->mRestrictions = UserRestriction::fetchForUser( $this->isLoggedIn() ?
3251 intval( $this->getId() ) : $this->getName() );
3252 }
3253
3254 public function getRestrictions() {
3255 $this->loadRestrictions();
3256
3257 // Check for expired restrictions. Recache if found expired ones
3258 static $checked = false;
3259 if( !$checked ) {
3260 $expired = false;
3261 $old = $this->mRestrictions;
3262 $this->mRestrictions = array();
3263 foreach( $old as $restriction ) {
3264 if( $restriction->deleteIfExpired() )
3265 $expired = true;
3266 else
3267 $this->mRestrictions[] = $restriction;
3268 }
3269 if( $expired )
3270 $this->saveToCache();
3271 $checked = true;
3272 }
3273
3274 return $this->mRestrictions;
3275 }
3276
3277 public function getRestrictionForPage( Title $page ) {
3278 foreach( $this->getRestrictions() as $r ) {
3279 if( $r->isPage() && $page->equals( $r->getPage() ) )
3280 return $r;
3281 }
3282 return null;
3283 }
3284
3285 public function getRestrictionForNamespace( $nsid ) {
3286 foreach( $this->getRestrictions() as $r ) {
3287 if( $r->isNamespace() && $r->getNamespace() == $nsid )
3288 return $r;
3289 }
3290 return null;
3291 }
3292 }