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