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