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