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