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