* (bug 10132, 10134) Restore back-compatibility Image::imageUrl() function
[lhc/web/wiklou.git] / includes / User.php
1 <?php
2 /**
3 * See user.txt
4 *
5 */
6
7 # Number of characters in user_token field
8 define( 'USER_TOKEN_LENGTH', 32 );
9
10 # Serialized record version
11 define( 'MW_USER_VERSION', 5 );
12
13 # Some punctuation to prevent editing from broken text-mangling proxies.
14 # FIXME: this is embedded unescaped into HTML attributes in various
15 # places, so we can't safely include ' or " even though we really should.
16 define( 'EDIT_TOKEN_SUFFIX', '\\' );
17
18 /**
19 * Thrown by User::setPassword() on error
20 * @addtogroup Exception
21 */
22 class PasswordError extends MWException {
23 // NOP
24 }
25
26 /**
27 * The User object encapsulates all of the user-specific settings (user_id,
28 * name, rights, password, email address, options, last login time). Client
29 * classes use the getXXX() functions to access these fields. These functions
30 * do all the work of determining whether the user is logged in,
31 * whether the requested option can be satisfied from cookies or
32 * whether a database query is needed. Most of the settings needed
33 * for rendering normal pages are set in the cookie to minimize use
34 * of the database.
35 */
36 class User {
37
38 /**
39 * A list of default user toggles, i.e. boolean user preferences that are
40 * displayed by Special:Preferences as checkboxes. This list can be
41 * extended via the UserToggles hook or $wgContLang->getExtraUserToggles().
42 */
43 static public $mToggles = array(
44 'highlightbroken',
45 'justify',
46 'hideminor',
47 'extendwatchlist',
48 'usenewrc',
49 'numberheadings',
50 'showtoolbar',
51 'editondblclick',
52 'editsection',
53 'editsectiononrightclick',
54 'showtoc',
55 'rememberpassword',
56 'editwidth',
57 'watchcreations',
58 'watchdefault',
59 'watchmoves',
60 'watchdeletion',
61 'minordefault',
62 'previewontop',
63 'previewonfirst',
64 'nocache',
65 'enotifwatchlistpages',
66 'enotifusertalkpages',
67 'enotifminoredits',
68 'enotifrevealaddr',
69 'shownumberswatching',
70 'fancysig',
71 'externaleditor',
72 'externaldiff',
73 'showjumplinks',
74 'uselivepreview',
75 'forceeditsummary',
76 'watchlisthideown',
77 'watchlisthidebots',
78 'watchlisthideminor',
79 'ccmeonemails',
80 'diffonly',
81 );
82
83 /**
84 * List of member variables which are saved to the shared cache (memcached).
85 * Any operation which changes the corresponding database fields must
86 * call a cache-clearing function.
87 */
88 static $mCacheVars = array(
89 # user table
90 'mId',
91 'mName',
92 'mRealName',
93 'mPassword',
94 'mNewpassword',
95 'mNewpassTime',
96 'mEmail',
97 'mOptions',
98 'mTouched',
99 'mToken',
100 'mEmailAuthenticated',
101 'mEmailToken',
102 'mEmailTokenExpires',
103 'mRegistration',
104 'mEditCount',
105 # user_group table
106 'mGroups',
107 );
108
109 /**
110 * The cache variable declarations
111 */
112 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
113 $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
114 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
115
116 /**
117 * Whether the cache variables have been loaded
118 */
119 var $mDataLoaded;
120
121 /**
122 * Initialisation data source if mDataLoaded==false. May be one of:
123 * defaults anonymous user initialised from class defaults
124 * name initialise from mName
125 * id initialise from mId
126 * session log in from cookies or session if possible
127 *
128 * Use the User::newFrom*() family of functions to set this.
129 */
130 var $mFrom;
131
132 /**
133 * Lazy-initialised variables, invalidated with clearInstanceCache
134 */
135 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
136 $mBlockreason, $mBlock, $mEffectiveGroups;
137
138 /**
139 * Lightweight constructor for anonymous user
140 * Use the User::newFrom* factory functions for other kinds of users
141 */
142 function User() {
143 $this->clearInstanceCache( 'defaults' );
144 }
145
146 /**
147 * Load the user table data for this object from the source given by mFrom
148 */
149 function load() {
150 if ( $this->mDataLoaded ) {
151 return;
152 }
153 wfProfileIn( __METHOD__ );
154
155 # Set it now to avoid infinite recursion in accessors
156 $this->mDataLoaded = true;
157
158 switch ( $this->mFrom ) {
159 case 'defaults':
160 $this->loadDefaults();
161 break;
162 case 'name':
163 $this->mId = self::idFromName( $this->mName );
164 if ( !$this->mId ) {
165 # Nonexistent user placeholder object
166 $this->loadDefaults( $this->mName );
167 } else {
168 $this->loadFromId();
169 }
170 break;
171 case 'id':
172 $this->loadFromId();
173 break;
174 case 'session':
175 $this->loadFromSession();
176 break;
177 default:
178 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
179 }
180 wfProfileOut( __METHOD__ );
181 }
182
183 /**
184 * Load user table data given mId
185 * @return false if the ID does not exist, true otherwise
186 * @private
187 */
188 function loadFromId() {
189 global $wgMemc;
190 if ( $this->mId == 0 ) {
191 $this->loadDefaults();
192 return false;
193 }
194
195 # Try cache
196 $key = wfMemcKey( 'user', 'id', $this->mId );
197 $data = $wgMemc->get( $key );
198 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
199 # Object is expired, load from DB
200 $data = false;
201 }
202
203 if ( !$data ) {
204 wfDebug( "Cache miss for user {$this->mId}\n" );
205 # Load from DB
206 if ( !$this->loadFromDatabase() ) {
207 # Can't load from ID, user is anonymous
208 return false;
209 }
210
211 # Save to cache
212 $data = array();
213 foreach ( self::$mCacheVars as $name ) {
214 $data[$name] = $this->$name;
215 }
216 $data['mVersion'] = MW_USER_VERSION;
217 $wgMemc->set( $key, $data );
218 } else {
219 wfDebug( "Got user {$this->mId} from cache\n" );
220 # Restore from cache
221 foreach ( self::$mCacheVars as $name ) {
222 $this->$name = $data[$name];
223 }
224 }
225 return true;
226 }
227
228 /**
229 * Static factory method for creation from username.
230 *
231 * This is slightly less efficient than newFromId(), so use newFromId() if
232 * you have both an ID and a name handy.
233 *
234 * @param string $name Username, validated by Title:newFromText()
235 * @param mixed $validate Validate username. Takes the same parameters as
236 * User::getCanonicalName(), except that true is accepted as an alias
237 * for 'valid', for BC.
238 *
239 * @return User object, or null if the username is invalid. If the username
240 * is not present in the database, the result will be a user object with
241 * a name, zero user ID and default settings.
242 * @static
243 */
244 static function newFromName( $name, $validate = 'valid' ) {
245 if ( $validate === true ) {
246 $validate = 'valid';
247 }
248 $name = self::getCanonicalName( $name, $validate );
249 if ( $name === false ) {
250 return null;
251 } else {
252 # Create unloaded user object
253 $u = new User;
254 $u->mName = $name;
255 $u->mFrom = 'name';
256 return $u;
257 }
258 }
259
260 static function newFromId( $id ) {
261 $u = new User;
262 $u->mId = $id;
263 $u->mFrom = 'id';
264 return $u;
265 }
266
267 /**
268 * Factory method to fetch whichever user has a given email confirmation code.
269 * This code is generated when an account is created or its e-mail address
270 * has changed.
271 *
272 * If the code is invalid or has expired, returns NULL.
273 *
274 * @param string $code
275 * @return User
276 * @static
277 */
278 static function newFromConfirmationCode( $code ) {
279 $dbr = wfGetDB( DB_SLAVE );
280 $id = $dbr->selectField( 'user', 'user_id', array(
281 'user_email_token' => md5( $code ),
282 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
283 ) );
284 if( $id !== false ) {
285 return User::newFromId( $id );
286 } else {
287 return null;
288 }
289 }
290
291 /**
292 * Create a new user object using data from session or cookies. If the
293 * login credentials are invalid, the result is an anonymous user.
294 *
295 * @return User
296 * @static
297 */
298 static function newFromSession() {
299 $user = new User;
300 $user->mFrom = 'session';
301 return $user;
302 }
303
304 /**
305 * Get username given an id.
306 * @param integer $id Database user id
307 * @return string Nickname of a user
308 * @static
309 */
310 static function whoIs( $id ) {
311 $dbr = wfGetDB( DB_SLAVE );
312 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
313 }
314
315 /**
316 * Get real username given an id.
317 * @param integer $id Database user id
318 * @return string Realname of a user
319 * @static
320 */
321 static function whoIsReal( $id ) {
322 $dbr = wfGetDB( DB_SLAVE );
323 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' );
324 }
325
326 /**
327 * Get database id given a user name
328 * @param string $name Nickname of a user
329 * @return integer|null Database user id (null: if non existent
330 * @static
331 */
332 static function idFromName( $name ) {
333 $nt = Title::newFromText( $name );
334 if( is_null( $nt ) ) {
335 # Illegal name
336 return null;
337 }
338 $dbr = wfGetDB( DB_SLAVE );
339 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
340
341 if ( $s === false ) {
342 return 0;
343 } else {
344 return $s->user_id;
345 }
346 }
347
348 /**
349 * Does the string match an anonymous IPv4 address?
350 *
351 * This function exists for username validation, in order to reject
352 * usernames which are similar in form to IP addresses. Strings such
353 * as 300.300.300.300 will return true because it looks like an IP
354 * address, despite not being strictly valid.
355 *
356 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
357 * address because the usemod software would "cloak" anonymous IP
358 * addresses like this, if we allowed accounts like this to be created
359 * new users could get the old edits of these anonymous users.
360 *
361 * @static
362 * @param string $name Nickname of a user
363 * @return bool
364 */
365 static function isIP( $name ) {
366 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || User::isIPv6($name);
367 /*return preg_match("/^
368 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
369 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
370 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
371 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
372 $/x", $name);*/
373 }
374
375 /**
376 * Check if $name is an IPv6 IP.
377 */
378 static function isIPv6($name) {
379 /*
380 * if it has any non-valid characters, it can't be a valid IPv6
381 * address.
382 */
383 if (preg_match("/[^:a-fA-F0-9]/", $name))
384 return false;
385
386 $parts = explode(":", $name);
387 if (count($parts) < 3)
388 return false;
389 foreach ($parts as $part) {
390 if (!preg_match("/^[0-9a-fA-F]{0,4}$/", $part))
391 return false;
392 }
393 return true;
394 }
395
396 /**
397 * Is the input a valid username?
398 *
399 * Checks if the input is a valid username, we don't want an empty string,
400 * an IP address, anything that containins slashes (would mess up subpages),
401 * is longer than the maximum allowed username size or doesn't begin with
402 * a capital letter.
403 *
404 * @param string $name
405 * @return bool
406 * @static
407 */
408 static function isValidUserName( $name ) {
409 global $wgContLang, $wgMaxNameChars;
410
411 if ( $name == ''
412 || User::isIP( $name )
413 || strpos( $name, '/' ) !== false
414 || strlen( $name ) > $wgMaxNameChars
415 || $name != $wgContLang->ucfirst( $name ) )
416 return false;
417
418 // Ensure that the name can't be misresolved as a different title,
419 // such as with extra namespace keys at the start.
420 $parsed = Title::newFromText( $name );
421 if( is_null( $parsed )
422 || $parsed->getNamespace()
423 || strcmp( $name, $parsed->getPrefixedText() ) )
424 return false;
425
426 // Check an additional blacklist of troublemaker characters.
427 // Should these be merged into the title char list?
428 $unicodeBlacklist = '/[' .
429 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
430 '\x{00a0}' . # non-breaking space
431 '\x{2000}-\x{200f}' . # various whitespace
432 '\x{2028}-\x{202f}' . # breaks and control chars
433 '\x{3000}' . # ideographic space
434 '\x{e000}-\x{f8ff}' . # private use
435 ']/u';
436 if( preg_match( $unicodeBlacklist, $name ) ) {
437 return false;
438 }
439
440 return true;
441 }
442
443 /**
444 * Usernames which fail to pass this function will be blocked
445 * from user login and new account registrations, but may be used
446 * internally by batch processes.
447 *
448 * If an account already exists in this form, login will be blocked
449 * by a failure to pass this function.
450 *
451 * @param string $name
452 * @return bool
453 */
454 static function isUsableName( $name ) {
455 global $wgReservedUsernames;
456 return
457 // Must be a usable username, obviously ;)
458 self::isValidUserName( $name ) &&
459
460 // Certain names may be reserved for batch processes.
461 !in_array( $name, $wgReservedUsernames );
462 }
463
464 /**
465 * Usernames which fail to pass this function will be blocked
466 * from new account registrations, but may be used internally
467 * either by batch processes or by user accounts which have
468 * already been created.
469 *
470 * Additional character blacklisting may be added here
471 * rather than in isValidUserName() to avoid disrupting
472 * existing accounts.
473 *
474 * @param string $name
475 * @return bool
476 */
477 static function isCreatableName( $name ) {
478 return
479 self::isUsableName( $name ) &&
480
481 // Registration-time character blacklisting...
482 strpos( $name, '@' ) === false;
483 }
484
485 /**
486 * Is the input a valid password?
487 *
488 * @param string $password
489 * @return bool
490 */
491 function isValidPassword( $password ) {
492 global $wgMinimalPasswordLength, $wgContLang;
493
494 $result = null;
495 if( !wfRunHooks( 'isValidPassword', array( $password, &$result ) ) ) return $result;
496 if ($result === false) return false;
497 return (strlen( $password ) >= $wgMinimalPasswordLength) &&
498 ($wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName ));
499 }
500
501 /**
502 * Does the string match roughly an email address ?
503 *
504 * There used to be a regular expression here, it got removed because it
505 * rejected valid addresses. Actually just check if there is '@' somewhere
506 * in the given address.
507 *
508 * @todo Check for RFC 2822 compilance (bug 959)
509 *
510 * @param string $addr email address
511 * @static
512 * @return bool
513 */
514 static function isValidEmailAddr ( $addr ) {
515 return ( trim( $addr ) != '' ) &&
516 (false !== strpos( $addr, '@' ) );
517 }
518
519 /**
520 * Given unvalidated user input, return a canonical username, or false if
521 * the username is invalid.
522 * @param string $name
523 * @param mixed $validate Type of validation to use:
524 * false No validation
525 * 'valid' Valid for batch processes
526 * 'usable' Valid for batch processes and login
527 * 'creatable' Valid for batch processes, login and account creation
528 */
529 static function getCanonicalName( $name, $validate = 'valid' ) {
530 # Force usernames to capital
531 global $wgContLang;
532 $name = $wgContLang->ucfirst( $name );
533
534 # Reject names containing '#'; these will be cleaned up
535 # with title normalisation, but then it's too late to
536 # check elsewhere
537 if( strpos( $name, '#' ) !== false )
538 return false;
539
540 # Clean up name according to title rules
541 $t = Title::newFromText( $name );
542 if( is_null( $t ) ) {
543 return false;
544 }
545
546 # Reject various classes of invalid names
547 $name = $t->getText();
548 global $wgAuth;
549 $name = $wgAuth->getCanonicalName( $t->getText() );
550
551 switch ( $validate ) {
552 case false:
553 break;
554 case 'valid':
555 if ( !User::isValidUserName( $name ) ) {
556 $name = false;
557 }
558 break;
559 case 'usable':
560 if ( !User::isUsableName( $name ) ) {
561 $name = false;
562 }
563 break;
564 case 'creatable':
565 if ( !User::isCreatableName( $name ) ) {
566 $name = false;
567 }
568 break;
569 default:
570 throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
571 }
572 return $name;
573 }
574
575 /**
576 * Count the number of edits of a user
577 *
578 * It should not be static and some day should be merged as proper member function / deprecated -- domas
579 *
580 * @param int $uid The user ID to check
581 * @return int
582 * @static
583 */
584 static function edits( $uid ) {
585 wfProfileIn( __METHOD__ );
586 $dbr = wfGetDB( DB_SLAVE );
587 // check if the user_editcount field has been initialized
588 $field = $dbr->selectField(
589 'user', 'user_editcount',
590 array( 'user_id' => $uid ),
591 __METHOD__
592 );
593
594 if( $field === null ) { // it has not been initialized. do so.
595 $dbw = wfGetDb( DB_MASTER );
596 $count = $dbr->selectField(
597 'revision', 'count(*)',
598 array( 'rev_user' => $uid ),
599 __METHOD__
600 );
601 $dbw->update(
602 'user',
603 array( 'user_editcount' => $count ),
604 array( 'user_id' => $uid ),
605 __METHOD__
606 );
607 } else {
608 $count = $field;
609 }
610 wfProfileOut( __METHOD__ );
611 return $count;
612 }
613
614 /**
615 * Return a random password. Sourced from mt_rand, so it's not particularly secure.
616 * @todo hash random numbers to improve security, like generateToken()
617 *
618 * @return string
619 * @static
620 */
621 static function randomPassword() {
622 global $wgMinimalPasswordLength;
623 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
624 $l = strlen( $pwchars ) - 1;
625
626 $pwlength = max( 7, $wgMinimalPasswordLength );
627 $digit = mt_rand(0, $pwlength - 1);
628 $np = '';
629 for ( $i = 0; $i < $pwlength; $i++ ) {
630 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
631 }
632 return $np;
633 }
634
635 /**
636 * Set cached properties to default. Note: this no longer clears
637 * uncached lazy-initialised properties. The constructor does that instead.
638 *
639 * @private
640 */
641 function loadDefaults( $name = false ) {
642 wfProfileIn( __METHOD__ );
643
644 global $wgCookiePrefix;
645
646 $this->mId = 0;
647 $this->mName = $name;
648 $this->mRealName = '';
649 $this->mPassword = $this->mNewpassword = '';
650 $this->mNewpassTime = null;
651 $this->mEmail = '';
652 $this->mOptions = null; # Defer init
653
654 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
655 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
656 } else {
657 $this->mTouched = '0'; # Allow any pages to be cached
658 }
659
660 $this->setToken(); # Random
661 $this->mEmailAuthenticated = null;
662 $this->mEmailToken = '';
663 $this->mEmailTokenExpires = null;
664 $this->mRegistration = wfTimestamp( TS_MW );
665 $this->mGroups = array();
666
667 wfProfileOut( __METHOD__ );
668 }
669
670 /**
671 * Initialise php session
672 * @deprecated use wfSetupSession()
673 */
674 function SetupSession() {
675 wfSetupSession();
676 }
677
678 /**
679 * Load user data from the session or login cookie. If there are no valid
680 * credentials, initialises the user as an anon.
681 * @return true if the user is logged in, false otherwise
682 */
683 private function loadFromSession() {
684 global $wgMemc, $wgCookiePrefix;
685
686 if ( isset( $_SESSION['wsUserID'] ) ) {
687 if ( 0 != $_SESSION['wsUserID'] ) {
688 $sId = $_SESSION['wsUserID'];
689 } else {
690 $this->loadDefaults();
691 return false;
692 }
693 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
694 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
695 $_SESSION['wsUserID'] = $sId;
696 } else {
697 $this->loadDefaults();
698 return false;
699 }
700 if ( isset( $_SESSION['wsUserName'] ) ) {
701 $sName = $_SESSION['wsUserName'];
702 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
703 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
704 $_SESSION['wsUserName'] = $sName;
705 } else {
706 $this->loadDefaults();
707 return false;
708 }
709
710 $passwordCorrect = FALSE;
711 $this->mId = $sId;
712 if ( !$this->loadFromId() ) {
713 # Not a valid ID, loadFromId has switched the object to anon for us
714 return false;
715 }
716
717 if ( isset( $_SESSION['wsToken'] ) ) {
718 $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
719 $from = 'session';
720 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
721 $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
722 $from = 'cookie';
723 } else {
724 # No session or persistent login cookie
725 $this->loadDefaults();
726 return false;
727 }
728
729 if ( ( $sName == $this->mName ) && $passwordCorrect ) {
730 wfDebug( "Logged in from $from\n" );
731 return true;
732 } else {
733 # Invalid credentials
734 wfDebug( "Can't log in from $from, invalid credentials\n" );
735 $this->loadDefaults();
736 return false;
737 }
738 }
739
740 /**
741 * Load user and user_group data from the database
742 * $this->mId must be set, this is how the user is identified.
743 *
744 * @return true if the user exists, false if the user is anonymous
745 * @private
746 */
747 function loadFromDatabase() {
748 # Paranoia
749 $this->mId = intval( $this->mId );
750
751 /** Anonymous user */
752 if( !$this->mId ) {
753 $this->loadDefaults();
754 return false;
755 }
756
757 $dbr = wfGetDB( DB_MASTER );
758 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
759
760 if ( $s !== false ) {
761 # Initialise user table data
762 $this->mName = $s->user_name;
763 $this->mRealName = $s->user_real_name;
764 $this->mPassword = $s->user_password;
765 $this->mNewpassword = $s->user_newpassword;
766 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $s->user_newpass_time );
767 $this->mEmail = $s->user_email;
768 $this->decodeOptions( $s->user_options );
769 $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
770 $this->mToken = $s->user_token;
771 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
772 $this->mEmailToken = $s->user_email_token;
773 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $s->user_email_token_expires );
774 $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
775 $this->mEditCount = $s->user_editcount;
776 $this->getEditCount(); // revalidation for nulls
777
778 # Load group data
779 $res = $dbr->select( 'user_groups',
780 array( 'ug_group' ),
781 array( 'ug_user' => $this->mId ),
782 __METHOD__ );
783 $this->mGroups = array();
784 while( $row = $dbr->fetchObject( $res ) ) {
785 $this->mGroups[] = $row->ug_group;
786 }
787 return true;
788 } else {
789 # Invalid user_id
790 $this->mId = 0;
791 $this->loadDefaults();
792 return false;
793 }
794 }
795
796 /**
797 * Clear various cached data stored in this object.
798 * @param string $reloadFrom Reload user and user_groups table data from a
799 * given source. May be "name", "id", "defaults", "session" or false for
800 * no reload.
801 */
802 function clearInstanceCache( $reloadFrom = false ) {
803 $this->mNewtalk = -1;
804 $this->mDatePreference = null;
805 $this->mBlockedby = -1; # Unset
806 $this->mHash = false;
807 $this->mSkin = null;
808 $this->mRights = null;
809 $this->mEffectiveGroups = null;
810
811 if ( $reloadFrom ) {
812 $this->mDataLoaded = false;
813 $this->mFrom = $reloadFrom;
814 }
815 }
816
817 /**
818 * Combine the language default options with any site-specific options
819 * and add the default language variants.
820 * Not really private cause it's called by Language class
821 * @return array
822 * @static
823 * @private
824 */
825 static function getDefaultOptions() {
826 global $wgNamespacesToBeSearchedDefault;
827 /**
828 * Site defaults will override the global/language defaults
829 */
830 global $wgDefaultUserOptions, $wgContLang;
831 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
832
833 /**
834 * default language setting
835 */
836 $variant = $wgContLang->getPreferredVariant( false );
837 $defOpt['variant'] = $variant;
838 $defOpt['language'] = $variant;
839
840 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
841 $defOpt['searchNs'.$nsnum] = $val;
842 }
843 return $defOpt;
844 }
845
846 /**
847 * Get a given default option value.
848 *
849 * @param string $opt
850 * @return string
851 * @static
852 * @public
853 */
854 function getDefaultOption( $opt ) {
855 $defOpts = User::getDefaultOptions();
856 if( isset( $defOpts[$opt] ) ) {
857 return $defOpts[$opt];
858 } else {
859 return '';
860 }
861 }
862
863 /**
864 * Get a list of user toggle names
865 * @return array
866 */
867 static function getToggles() {
868 global $wgContLang;
869 $extraToggles = array();
870 wfRunHooks( 'UserToggles', array( &$extraToggles ) );
871 return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
872 }
873
874
875 /**
876 * Get blocking information
877 * @private
878 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
879 * non-critical checks are done against slaves. Check when actually saving should be done against
880 * master.
881 */
882 function getBlockedStatus( $bFromSlave = true ) {
883 global $wgEnableSorbs, $wgProxyWhitelist;
884
885 if ( -1 != $this->mBlockedby ) {
886 wfDebug( "User::getBlockedStatus: already loaded.\n" );
887 return;
888 }
889
890 wfProfileIn( __METHOD__ );
891 wfDebug( __METHOD__.": checking...\n" );
892
893 $this->mBlockedby = 0;
894 $this->mHideName = 0;
895 $ip = wfGetIP();
896
897 if ($this->isAllowed( 'ipblock-exempt' ) ) {
898 # Exempt from all types of IP-block
899 $ip = '';
900 }
901
902 # User/IP blocking
903 $this->mBlock = new Block();
904 $this->mBlock->fromMaster( !$bFromSlave );
905 if ( $this->mBlock->load( $ip , $this->mId ) ) {
906 wfDebug( __METHOD__.": Found block.\n" );
907 $this->mBlockedby = $this->mBlock->mBy;
908 $this->mBlockreason = $this->mBlock->mReason;
909 $this->mHideName = $this->mBlock->mHideName;
910 if ( $this->isLoggedIn() ) {
911 $this->spreadBlock();
912 }
913 } else {
914 $this->mBlock = null;
915 wfDebug( __METHOD__.": No block.\n" );
916 }
917
918 # Proxy blocking
919 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
920
921 # Local list
922 if ( wfIsLocallyBlockedProxy( $ip ) ) {
923 $this->mBlockedby = wfMsg( 'proxyblocker' );
924 $this->mBlockreason = wfMsg( 'proxyblockreason' );
925 }
926
927 # DNSBL
928 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
929 if ( $this->inSorbsBlacklist( $ip ) ) {
930 $this->mBlockedby = wfMsg( 'sorbs' );
931 $this->mBlockreason = wfMsg( 'sorbsreason' );
932 }
933 }
934 }
935
936 # Extensions
937 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
938
939 wfProfileOut( __METHOD__ );
940 }
941
942 function inSorbsBlacklist( $ip ) {
943 global $wgEnableSorbs, $wgSorbsUrl;
944
945 return $wgEnableSorbs &&
946 $this->inDnsBlacklist( $ip, $wgSorbsUrl );
947 }
948
949 function inDnsBlacklist( $ip, $base ) {
950 wfProfileIn( __METHOD__ );
951
952 $found = false;
953 $host = '';
954
955 $m = array();
956 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
957 # Make hostname
958 for ( $i=4; $i>=1; $i-- ) {
959 $host .= $m[$i] . '.';
960 }
961 $host .= $base;
962
963 # Send query
964 $ipList = gethostbynamel( $host );
965
966 if ( $ipList ) {
967 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
968 $found = true;
969 } else {
970 wfDebug( "Requested $host, not found in $base.\n" );
971 }
972 }
973
974 wfProfileOut( __METHOD__ );
975 return $found;
976 }
977
978 /**
979 * Is this user subject to rate limiting?
980 *
981 * @return bool
982 */
983 public function isPingLimitable() {
984 global $wgRateLimitsExcludedGroups;
985 return array_intersect($this->getEffectiveGroups(), $wgRateLimitsExcludedGroups) == array();
986 }
987
988 /**
989 * Primitive rate limits: enforce maximum actions per time period
990 * to put a brake on flooding.
991 *
992 * Note: when using a shared cache like memcached, IP-address
993 * last-hit counters will be shared across wikis.
994 *
995 * @return bool true if a rate limiter was tripped
996 * @public
997 */
998 function pingLimiter( $action='edit' ) {
999
1000 # Call the 'PingLimiter' hook
1001 $result = false;
1002 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
1003 return $result;
1004 }
1005
1006 global $wgRateLimits, $wgRateLimitsExcludedGroups;
1007 if( !isset( $wgRateLimits[$action] ) ) {
1008 return false;
1009 }
1010
1011 # Some groups shouldn't trigger the ping limiter, ever
1012 if( !$this->isPingLimitable() )
1013 return false;
1014
1015 global $wgMemc, $wgRateLimitLog;
1016 wfProfileIn( __METHOD__ );
1017
1018 $limits = $wgRateLimits[$action];
1019 $keys = array();
1020 $id = $this->getId();
1021 $ip = wfGetIP();
1022
1023 if( isset( $limits['anon'] ) && $id == 0 ) {
1024 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1025 }
1026
1027 if( isset( $limits['user'] ) && $id != 0 ) {
1028 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['user'];
1029 }
1030 if( $this->isNewbie() ) {
1031 if( isset( $limits['newbie'] ) && $id != 0 ) {
1032 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1033 }
1034 if( isset( $limits['ip'] ) ) {
1035 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1036 }
1037 $matches = array();
1038 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1039 $subnet = $matches[1];
1040 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1041 }
1042 }
1043
1044 $triggered = false;
1045 foreach( $keys as $key => $limit ) {
1046 list( $max, $period ) = $limit;
1047 $summary = "(limit $max in {$period}s)";
1048 $count = $wgMemc->get( $key );
1049 if( $count ) {
1050 if( $count > $max ) {
1051 wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
1052 if( $wgRateLimitLog ) {
1053 @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1054 }
1055 $triggered = true;
1056 } else {
1057 wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
1058 }
1059 } else {
1060 wfDebug( __METHOD__.": adding record for $key $summary\n" );
1061 $wgMemc->add( $key, 1, intval( $period ) );
1062 }
1063 $wgMemc->incr( $key );
1064 }
1065
1066 wfProfileOut( __METHOD__ );
1067 return $triggered;
1068 }
1069
1070 /**
1071 * Check if user is blocked
1072 * @return bool True if blocked, false otherwise
1073 */
1074 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1075 wfDebug( "User::isBlocked: enter\n" );
1076 $this->getBlockedStatus( $bFromSlave );
1077 return $this->mBlockedby !== 0;
1078 }
1079
1080 /**
1081 * Check if user is blocked from editing a particular article
1082 */
1083 function isBlockedFrom( $title, $bFromSlave = false ) {
1084 global $wgBlockAllowsUTEdit;
1085 wfProfileIn( __METHOD__ );
1086 wfDebug( __METHOD__.": enter\n" );
1087
1088 wfDebug( __METHOD__.": asking isBlocked()\n" );
1089 $blocked = $this->isBlocked( $bFromSlave );
1090 # If a user's name is suppressed, they cannot make edits anywhere
1091 if ( !$this->mHideName && $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
1092 $title->getNamespace() == NS_USER_TALK ) {
1093 $blocked = false;
1094 wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1095 }
1096 wfProfileOut( __METHOD__ );
1097 return $blocked;
1098 }
1099
1100 /**
1101 * Get name of blocker
1102 * @return string name of blocker
1103 */
1104 function blockedBy() {
1105 $this->getBlockedStatus();
1106 return $this->mBlockedby;
1107 }
1108
1109 /**
1110 * Get blocking reason
1111 * @return string Blocking reason
1112 */
1113 function blockedFor() {
1114 $this->getBlockedStatus();
1115 return $this->mBlockreason;
1116 }
1117
1118 /**
1119 * Get the user ID. Returns 0 if the user is anonymous or nonexistent.
1120 */
1121 function getID() {
1122 $this->load();
1123 return $this->mId;
1124 }
1125
1126 /**
1127 * Set the user and reload all fields according to that ID
1128 * @deprecated use User::newFromId()
1129 */
1130 function setID( $v ) {
1131 $this->mId = $v;
1132 $this->clearInstanceCache( 'id' );
1133 }
1134
1135 /**
1136 * Get the user name, or the IP for anons
1137 */
1138 function getName() {
1139 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1140 # Special case optimisation
1141 return $this->mName;
1142 } else {
1143 $this->load();
1144 if ( $this->mName === false ) {
1145 # Clean up IPs
1146 $this->mName = IP::sanitizeIP( wfGetIP() );
1147 }
1148 return $this->mName;
1149 }
1150 }
1151
1152 /**
1153 * Set the user name.
1154 *
1155 * This does not reload fields from the database according to the given
1156 * name. Rather, it is used to create a temporary "nonexistent user" for
1157 * later addition to the database. It can also be used to set the IP
1158 * address for an anonymous user to something other than the current
1159 * remote IP.
1160 *
1161 * User::newFromName() has rougly the same function, when the named user
1162 * does not exist.
1163 */
1164 function setName( $str ) {
1165 $this->load();
1166 $this->mName = $str;
1167 }
1168
1169 /**
1170 * Return the title dbkey form of the name, for eg user pages.
1171 * @return string
1172 * @public
1173 */
1174 function getTitleKey() {
1175 return str_replace( ' ', '_', $this->getName() );
1176 }
1177
1178 function getNewtalk() {
1179 $this->load();
1180
1181 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1182 if( $this->mNewtalk === -1 ) {
1183 $this->mNewtalk = false; # reset talk page status
1184
1185 # Check memcached separately for anons, who have no
1186 # entire User object stored in there.
1187 if( !$this->mId ) {
1188 global $wgMemc;
1189 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1190 $newtalk = $wgMemc->get( $key );
1191 if( $newtalk != "" ) {
1192 $this->mNewtalk = (bool)$newtalk;
1193 } else {
1194 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
1195 $wgMemc->set( $key, (int)$this->mNewtalk, time() + 1800 );
1196 }
1197 } else {
1198 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1199 }
1200 }
1201
1202 return (bool)$this->mNewtalk;
1203 }
1204
1205 /**
1206 * Return the talk page(s) this user has new messages on.
1207 */
1208 function getNewMessageLinks() {
1209 $talks = array();
1210 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1211 return $talks;
1212
1213 if (!$this->getNewtalk())
1214 return array();
1215 $up = $this->getUserPage();
1216 $utp = $up->getTalkPage();
1217 return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1218 }
1219
1220
1221 /**
1222 * Perform a user_newtalk check on current slaves; if the memcached data
1223 * is funky we don't want newtalk state to get stuck on save, as that's
1224 * damn annoying.
1225 *
1226 * @param string $field
1227 * @param mixed $id
1228 * @return bool
1229 * @private
1230 */
1231 function checkNewtalk( $field, $id ) {
1232 $dbr = wfGetDB( DB_SLAVE );
1233 $ok = $dbr->selectField( 'user_newtalk', $field,
1234 array( $field => $id ), __METHOD__ );
1235 return $ok !== false;
1236 }
1237
1238 /**
1239 * Add or update the
1240 * @param string $field
1241 * @param mixed $id
1242 * @private
1243 */
1244 function updateNewtalk( $field, $id ) {
1245 if( $this->checkNewtalk( $field, $id ) ) {
1246 wfDebug( __METHOD__." already set ($field, $id), ignoring\n" );
1247 return false;
1248 }
1249 $dbw = wfGetDB( DB_MASTER );
1250 $dbw->insert( 'user_newtalk',
1251 array( $field => $id ),
1252 __METHOD__,
1253 'IGNORE' );
1254 wfDebug( __METHOD__.": set on ($field, $id)\n" );
1255 return true;
1256 }
1257
1258 /**
1259 * Clear the new messages flag for the given user
1260 * @param string $field
1261 * @param mixed $id
1262 * @private
1263 */
1264 function deleteNewtalk( $field, $id ) {
1265 if( !$this->checkNewtalk( $field, $id ) ) {
1266 wfDebug( __METHOD__.": already gone ($field, $id), ignoring\n" );
1267 return false;
1268 }
1269 $dbw = wfGetDB( DB_MASTER );
1270 $dbw->delete( 'user_newtalk',
1271 array( $field => $id ),
1272 __METHOD__ );
1273 wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1274 return true;
1275 }
1276
1277 /**
1278 * Update the 'You have new messages!' status.
1279 * @param bool $val
1280 */
1281 function setNewtalk( $val ) {
1282 if( wfReadOnly() ) {
1283 return;
1284 }
1285
1286 $this->load();
1287 $this->mNewtalk = $val;
1288
1289 if( $this->isAnon() ) {
1290 $field = 'user_ip';
1291 $id = $this->getName();
1292 } else {
1293 $field = 'user_id';
1294 $id = $this->getId();
1295 }
1296
1297 if( $val ) {
1298 $changed = $this->updateNewtalk( $field, $id );
1299 } else {
1300 $changed = $this->deleteNewtalk( $field, $id );
1301 }
1302
1303 if( $changed ) {
1304 if( $this->isAnon() ) {
1305 // Anons have a separate memcached space, since
1306 // user records aren't kept for them.
1307 global $wgMemc;
1308 $key = wfMemcKey( 'newtalk', 'ip', $val );
1309 $wgMemc->set( $key, $val ? 1 : 0 );
1310 } else {
1311 if( $val ) {
1312 // Make sure the user page is watched, so a notification
1313 // will be sent out if enabled.
1314 $this->addWatch( $this->getTalkPage() );
1315 }
1316 }
1317 $this->invalidateCache();
1318 }
1319 }
1320
1321 /**
1322 * Generate a current or new-future timestamp to be stored in the
1323 * user_touched field when we update things.
1324 */
1325 private static function newTouchedTimestamp() {
1326 global $wgClockSkewFudge;
1327 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1328 }
1329
1330 /**
1331 * Clear user data from memcached.
1332 * Use after applying fun updates to the database; caller's
1333 * responsibility to update user_touched if appropriate.
1334 *
1335 * Called implicitly from invalidateCache() and saveSettings().
1336 */
1337 private function clearSharedCache() {
1338 if( $this->mId ) {
1339 global $wgMemc;
1340 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1341 }
1342 }
1343
1344 /**
1345 * Immediately touch the user data cache for this account.
1346 * Updates user_touched field, and removes account data from memcached
1347 * for reload on the next hit.
1348 */
1349 function invalidateCache() {
1350 $this->load();
1351 if( $this->mId ) {
1352 $this->mTouched = self::newTouchedTimestamp();
1353
1354 $dbw = wfGetDB( DB_MASTER );
1355 $dbw->update( 'user',
1356 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1357 array( 'user_id' => $this->mId ),
1358 __METHOD__ );
1359
1360 $this->clearSharedCache();
1361 }
1362 }
1363
1364 function validateCache( $timestamp ) {
1365 $this->load();
1366 return ($timestamp >= $this->mTouched);
1367 }
1368
1369 /**
1370 * Encrypt a password.
1371 * It can eventuall salt a password @see User::addSalt()
1372 * @param string $p clear Password.
1373 * @return string Encrypted password.
1374 */
1375 function encryptPassword( $p ) {
1376 $this->load();
1377 return wfEncryptPassword( $this->mId, $p );
1378 }
1379
1380 /**
1381 * Set the password and reset the random token
1382 * Calls through to authentication plugin if necessary;
1383 * will have no effect if the auth plugin refuses to
1384 * pass the change through or if the legal password
1385 * checks fail.
1386 *
1387 * As a special case, setting the password to null
1388 * wipes it, so the account cannot be logged in until
1389 * a new password is set, for instance via e-mail.
1390 *
1391 * @param string $str
1392 * @throws PasswordError on failure
1393 */
1394 function setPassword( $str ) {
1395 global $wgAuth;
1396
1397 if( $str !== null ) {
1398 if( !$wgAuth->allowPasswordChange() ) {
1399 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1400 }
1401
1402 if( !$this->isValidPassword( $str ) ) {
1403 global $wgMinimalPasswordLength;
1404 throw new PasswordError( wfMsg( 'passwordtooshort',
1405 $wgMinimalPasswordLength ) );
1406 }
1407 }
1408
1409 if( !$wgAuth->setPassword( $this, $str ) ) {
1410 throw new PasswordError( wfMsg( 'externaldberror' ) );
1411 }
1412
1413 $this->setInternalPassword( $str );
1414
1415 return true;
1416 }
1417
1418 /**
1419 * Set the password and reset the random token no matter
1420 * what.
1421 *
1422 * @param string $str
1423 */
1424 function setInternalPassword( $str ) {
1425 $this->load();
1426 $this->setToken();
1427
1428 if( $str === null ) {
1429 // Save an invalid hash...
1430 $this->mPassword = '';
1431 } else {
1432 $this->mPassword = $this->encryptPassword( $str );
1433 }
1434 $this->mNewpassword = '';
1435 $this->mNewpassTime = null;
1436 }
1437 /**
1438 * Set the random token (used for persistent authentication)
1439 * Called from loadDefaults() among other places.
1440 * @private
1441 */
1442 function setToken( $token = false ) {
1443 global $wgSecretKey, $wgProxyKey;
1444 $this->load();
1445 if ( !$token ) {
1446 if ( $wgSecretKey ) {
1447 $key = $wgSecretKey;
1448 } elseif ( $wgProxyKey ) {
1449 $key = $wgProxyKey;
1450 } else {
1451 $key = microtime();
1452 }
1453 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1454 } else {
1455 $this->mToken = $token;
1456 }
1457 }
1458
1459 function setCookiePassword( $str ) {
1460 $this->load();
1461 $this->mCookiePassword = md5( $str );
1462 }
1463
1464 /**
1465 * Set the password for a password reminder or new account email
1466 * Sets the user_newpass_time field if $throttle is true
1467 */
1468 function setNewpassword( $str, $throttle = true ) {
1469 $this->load();
1470 $this->mNewpassword = $this->encryptPassword( $str );
1471 if ( $throttle ) {
1472 $this->mNewpassTime = wfTimestampNow();
1473 }
1474 }
1475
1476 /**
1477 * Returns true if a password reminder email has already been sent within
1478 * the last $wgPasswordReminderResendTime hours
1479 */
1480 function isPasswordReminderThrottled() {
1481 global $wgPasswordReminderResendTime;
1482 $this->load();
1483 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1484 return false;
1485 }
1486 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1487 return time() < $expiry;
1488 }
1489
1490 function getEmail() {
1491 $this->load();
1492 return $this->mEmail;
1493 }
1494
1495 function getEmailAuthenticationTimestamp() {
1496 $this->load();
1497 return $this->mEmailAuthenticated;
1498 }
1499
1500 function setEmail( $str ) {
1501 $this->load();
1502 $this->mEmail = $str;
1503 }
1504
1505 function getRealName() {
1506 $this->load();
1507 return $this->mRealName;
1508 }
1509
1510 function setRealName( $str ) {
1511 $this->load();
1512 $this->mRealName = $str;
1513 }
1514
1515 /**
1516 * @param string $oname The option to check
1517 * @param string $defaultOverride A default value returned if the option does not exist
1518 * @return string
1519 */
1520 function getOption( $oname, $defaultOverride = '' ) {
1521 $this->load();
1522
1523 if ( is_null( $this->mOptions ) ) {
1524 if($defaultOverride != '') {
1525 return $defaultOverride;
1526 }
1527 $this->mOptions = User::getDefaultOptions();
1528 }
1529
1530 if ( array_key_exists( $oname, $this->mOptions ) ) {
1531 return trim( $this->mOptions[$oname] );
1532 } else {
1533 return $defaultOverride;
1534 }
1535 }
1536
1537 /**
1538 * Get the user's date preference, including some important migration for
1539 * old user rows.
1540 */
1541 function getDatePreference() {
1542 if ( is_null( $this->mDatePreference ) ) {
1543 global $wgLang;
1544 $value = $this->getOption( 'date' );
1545 $map = $wgLang->getDatePreferenceMigrationMap();
1546 if ( isset( $map[$value] ) ) {
1547 $value = $map[$value];
1548 }
1549 $this->mDatePreference = $value;
1550 }
1551 return $this->mDatePreference;
1552 }
1553
1554 /**
1555 * @param string $oname The option to check
1556 * @return bool False if the option is not selected, true if it is
1557 */
1558 function getBoolOption( $oname ) {
1559 return (bool)$this->getOption( $oname );
1560 }
1561
1562 /**
1563 * Get an option as an integer value from the source string.
1564 * @param string $oname The option to check
1565 * @param int $default Optional value to return if option is unset/blank.
1566 * @return int
1567 */
1568 function getIntOption( $oname, $default=0 ) {
1569 $val = $this->getOption( $oname );
1570 if( $val == '' ) {
1571 $val = $default;
1572 }
1573 return intval( $val );
1574 }
1575
1576 function setOption( $oname, $val ) {
1577 $this->load();
1578 if ( is_null( $this->mOptions ) ) {
1579 $this->mOptions = User::getDefaultOptions();
1580 }
1581 if ( $oname == 'skin' ) {
1582 # Clear cached skin, so the new one displays immediately in Special:Preferences
1583 unset( $this->mSkin );
1584 }
1585 // Filter out any newlines that may have passed through input validation.
1586 // Newlines are used to separate items in the options blob.
1587 $val = str_replace( "\r\n", "\n", $val );
1588 $val = str_replace( "\r", "\n", $val );
1589 $val = str_replace( "\n", " ", $val );
1590 $this->mOptions[$oname] = $val;
1591 }
1592
1593 function getRights() {
1594 if ( is_null( $this->mRights ) ) {
1595 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
1596 }
1597 return $this->mRights;
1598 }
1599
1600 /**
1601 * Get the list of explicit group memberships this user has.
1602 * The implicit * and user groups are not included.
1603 * @return array of strings
1604 */
1605 function getGroups() {
1606 $this->load();
1607 return $this->mGroups;
1608 }
1609
1610 /**
1611 * Get the list of implicit group memberships this user has.
1612 * This includes all explicit groups, plus 'user' if logged in
1613 * and '*' for all accounts.
1614 * @param boolean $recache Don't use the cache
1615 * @return array of strings
1616 */
1617 function getEffectiveGroups( $recache = false ) {
1618 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
1619 $this->load();
1620 $this->mEffectiveGroups = $this->mGroups;
1621 $this->mEffectiveGroups[] = '*';
1622 if( $this->mId ) {
1623 $this->mEffectiveGroups[] = 'user';
1624
1625 global $wgAutoConfirmAge, $wgAutoConfirmCount;
1626
1627 $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
1628 if( $accountAge >= $wgAutoConfirmAge && $this->getEditCount() >= $wgAutoConfirmCount ) {
1629 $this->mEffectiveGroups[] = 'autoconfirmed';
1630 }
1631 # Implicit group for users whose email addresses are confirmed
1632 global $wgEmailAuthentication;
1633 if( self::isValidEmailAddr( $this->mEmail ) ) {
1634 if( $wgEmailAuthentication ) {
1635 if( $this->mEmailAuthenticated )
1636 $this->mEffectiveGroups[] = 'emailconfirmed';
1637 } else {
1638 $this->mEffectiveGroups[] = 'emailconfirmed';
1639 }
1640 }
1641 }
1642 }
1643 return $this->mEffectiveGroups;
1644 }
1645
1646 /* Return the edit count for the user. This is where User::edits should have been */
1647 function getEditCount() {
1648 if ($this->mId) {
1649 if ( !isset( $this->mEditCount ) ) {
1650 /* Populate the count, if it has not been populated yet */
1651 $this->mEditCount = User::edits($this->mId);
1652 }
1653 return $this->mEditCount;
1654 } else {
1655 /* nil */
1656 return null;
1657 }
1658 }
1659
1660 /**
1661 * Add the user to the given group.
1662 * This takes immediate effect.
1663 * @param string $group
1664 */
1665 function addGroup( $group ) {
1666 $this->load();
1667 $dbw = wfGetDB( DB_MASTER );
1668 if( $this->getId() ) {
1669 $dbw->insert( 'user_groups',
1670 array(
1671 'ug_user' => $this->getID(),
1672 'ug_group' => $group,
1673 ),
1674 'User::addGroup',
1675 array( 'IGNORE' ) );
1676 }
1677
1678 $this->mGroups[] = $group;
1679 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1680
1681 $this->invalidateCache();
1682 }
1683
1684 /**
1685 * Remove the user from the given group.
1686 * This takes immediate effect.
1687 * @param string $group
1688 */
1689 function removeGroup( $group ) {
1690 $this->load();
1691 $dbw = wfGetDB( DB_MASTER );
1692 $dbw->delete( 'user_groups',
1693 array(
1694 'ug_user' => $this->getID(),
1695 'ug_group' => $group,
1696 ),
1697 'User::removeGroup' );
1698
1699 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1700 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1701
1702 $this->invalidateCache();
1703 }
1704
1705
1706 /**
1707 * A more legible check for non-anonymousness.
1708 * Returns true if the user is not an anonymous visitor.
1709 *
1710 * @return bool
1711 */
1712 function isLoggedIn() {
1713 return( $this->getID() != 0 );
1714 }
1715
1716 /**
1717 * A more legible check for anonymousness.
1718 * Returns true if the user is an anonymous visitor.
1719 *
1720 * @return bool
1721 */
1722 function isAnon() {
1723 return !$this->isLoggedIn();
1724 }
1725
1726 /**
1727 * Whether the user is a bot
1728 * @deprecated
1729 */
1730 function isBot() {
1731 return $this->isAllowed( 'bot' );
1732 }
1733
1734 /**
1735 * Check if user is allowed to access a feature / make an action
1736 * @param string $action Action to be checked
1737 * @return boolean True: action is allowed, False: action should not be allowed
1738 */
1739 function isAllowed($action='') {
1740 if ( $action === '' )
1741 // In the spirit of DWIM
1742 return true;
1743
1744 return in_array( $action, $this->getRights() );
1745 }
1746
1747 /**
1748 * Load a skin if it doesn't exist or return it
1749 * @todo FIXME : need to check the old failback system [AV]
1750 */
1751 function &getSkin() {
1752 global $wgRequest;
1753 if ( ! isset( $this->mSkin ) ) {
1754 wfProfileIn( __METHOD__ );
1755
1756 # get the user skin
1757 $userSkin = $this->getOption( 'skin' );
1758 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1759
1760 $this->mSkin =& Skin::newFromKey( $userSkin );
1761 wfProfileOut( __METHOD__ );
1762 }
1763 return $this->mSkin;
1764 }
1765
1766 /**#@+
1767 * @param string $title Article title to look at
1768 */
1769
1770 /**
1771 * Check watched status of an article
1772 * @return bool True if article is watched
1773 */
1774 function isWatched( $title ) {
1775 $wl = WatchedItem::fromUserTitle( $this, $title );
1776 return $wl->isWatched();
1777 }
1778
1779 /**
1780 * Watch an article
1781 */
1782 function addWatch( $title ) {
1783 $wl = WatchedItem::fromUserTitle( $this, $title );
1784 $wl->addWatch();
1785 $this->invalidateCache();
1786 }
1787
1788 /**
1789 * Stop watching an article
1790 */
1791 function removeWatch( $title ) {
1792 $wl = WatchedItem::fromUserTitle( $this, $title );
1793 $wl->removeWatch();
1794 $this->invalidateCache();
1795 }
1796
1797 /**
1798 * Clear the user's notification timestamp for the given title.
1799 * If e-notif e-mails are on, they will receive notification mails on
1800 * the next change of the page if it's watched etc.
1801 */
1802 function clearNotification( &$title ) {
1803 global $wgUser, $wgUseEnotif;
1804
1805 # Do nothing if the database is locked to writes
1806 if( wfReadOnly() ) {
1807 return;
1808 }
1809
1810 if ($title->getNamespace() == NS_USER_TALK &&
1811 $title->getText() == $this->getName() ) {
1812 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1813 return;
1814 $this->setNewtalk( false );
1815 }
1816
1817 if( !$wgUseEnotif ) {
1818 return;
1819 }
1820
1821 if( $this->isAnon() ) {
1822 // Nothing else to do...
1823 return;
1824 }
1825
1826 // Only update the timestamp if the page is being watched.
1827 // The query to find out if it is watched is cached both in memcached and per-invocation,
1828 // and when it does have to be executed, it can be on a slave
1829 // If this is the user's newtalk page, we always update the timestamp
1830 if ($title->getNamespace() == NS_USER_TALK &&
1831 $title->getText() == $wgUser->getName())
1832 {
1833 $watched = true;
1834 } elseif ( $this->getID() == $wgUser->getID() ) {
1835 $watched = $title->userIsWatching();
1836 } else {
1837 $watched = true;
1838 }
1839
1840 // If the page is watched by the user (or may be watched), update the timestamp on any
1841 // any matching rows
1842 if ( $watched ) {
1843 $dbw = wfGetDB( DB_MASTER );
1844 $dbw->update( 'watchlist',
1845 array( /* SET */
1846 'wl_notificationtimestamp' => NULL
1847 ), array( /* WHERE */
1848 'wl_title' => $title->getDBkey(),
1849 'wl_namespace' => $title->getNamespace(),
1850 'wl_user' => $this->getID()
1851 ), 'User::clearLastVisited'
1852 );
1853 }
1854 }
1855
1856 /**#@-*/
1857
1858 /**
1859 * Resets all of the given user's page-change notification timestamps.
1860 * If e-notif e-mails are on, they will receive notification mails on
1861 * the next change of any watched page.
1862 *
1863 * @param int $currentUser user ID number
1864 * @public
1865 */
1866 function clearAllNotifications( $currentUser ) {
1867 global $wgUseEnotif;
1868 if ( !$wgUseEnotif ) {
1869 $this->setNewtalk( false );
1870 return;
1871 }
1872 if( $currentUser != 0 ) {
1873
1874 $dbw = wfGetDB( DB_MASTER );
1875 $dbw->update( 'watchlist',
1876 array( /* SET */
1877 'wl_notificationtimestamp' => NULL
1878 ), array( /* WHERE */
1879 'wl_user' => $currentUser
1880 ), 'UserMailer::clearAll'
1881 );
1882
1883 # we also need to clear here the "you have new message" notification for the own user_talk page
1884 # This is cleared one page view later in Article::viewUpdates();
1885 }
1886 }
1887
1888 /**
1889 * @private
1890 * @return string Encoding options
1891 */
1892 function encodeOptions() {
1893 $this->load();
1894 if ( is_null( $this->mOptions ) ) {
1895 $this->mOptions = User::getDefaultOptions();
1896 }
1897 $a = array();
1898 foreach ( $this->mOptions as $oname => $oval ) {
1899 array_push( $a, $oname.'='.$oval );
1900 }
1901 $s = implode( "\n", $a );
1902 return $s;
1903 }
1904
1905 /**
1906 * @private
1907 */
1908 function decodeOptions( $str ) {
1909 $this->mOptions = array();
1910 $a = explode( "\n", $str );
1911 foreach ( $a as $s ) {
1912 $m = array();
1913 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1914 $this->mOptions[$m[1]] = $m[2];
1915 }
1916 }
1917 }
1918
1919 function setCookies() {
1920 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1921 $this->load();
1922 if ( 0 == $this->mId ) return;
1923 $exp = time() + $wgCookieExpiration;
1924
1925 $_SESSION['wsUserID'] = $this->mId;
1926 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1927
1928 $_SESSION['wsUserName'] = $this->getName();
1929 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1930
1931 $_SESSION['wsToken'] = $this->mToken;
1932 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1933 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1934 } else {
1935 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1936 }
1937 }
1938
1939 /**
1940 * Logout user
1941 * Clears the cookies and session, resets the instance cache
1942 */
1943 function logout() {
1944 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1945 $this->clearInstanceCache( 'defaults' );
1946
1947 $_SESSION['wsUserID'] = 0;
1948
1949 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1950 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1951
1952 # Remember when user logged out, to prevent seeing cached pages
1953 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1954 }
1955
1956 /**
1957 * Save object settings into database
1958 * @todo Only rarely do all these fields need to be set!
1959 */
1960 function saveSettings() {
1961 $this->load();
1962 if ( wfReadOnly() ) { return; }
1963 if ( 0 == $this->mId ) { return; }
1964
1965 $this->mTouched = self::newTouchedTimestamp();
1966
1967 $dbw = wfGetDB( DB_MASTER );
1968 $dbw->update( 'user',
1969 array( /* SET */
1970 'user_name' => $this->mName,
1971 'user_password' => $this->mPassword,
1972 'user_newpassword' => $this->mNewpassword,
1973 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
1974 'user_real_name' => $this->mRealName,
1975 'user_email' => $this->mEmail,
1976 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1977 'user_options' => $this->encodeOptions(),
1978 'user_touched' => $dbw->timestamp($this->mTouched),
1979 'user_token' => $this->mToken
1980 ), array( /* WHERE */
1981 'user_id' => $this->mId
1982 ), __METHOD__
1983 );
1984 $this->clearSharedCache();
1985 }
1986
1987
1988 /**
1989 * Checks if a user with the given name exists, returns the ID
1990 */
1991 function idForName() {
1992 $s = trim( $this->getName() );
1993 if ( 0 == strcmp( '', $s ) ) return 0;
1994
1995 $dbr = wfGetDB( DB_SLAVE );
1996 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
1997 if ( $id === false ) {
1998 $id = 0;
1999 }
2000 return $id;
2001 }
2002
2003 /**
2004 * Add a user to the database, return the user object
2005 *
2006 * @param string $name The user's name
2007 * @param array $params Associative array of non-default parameters to save to the database:
2008 * password The user's password. Password logins will be disabled if this is omitted.
2009 * newpassword A temporary password mailed to the user
2010 * email The user's email address
2011 * email_authenticated The email authentication timestamp
2012 * real_name The user's real name
2013 * options An associative array of non-default options
2014 * token Random authentication token. Do not set.
2015 * registration Registration timestamp. Do not set.
2016 *
2017 * @return User object, or null if the username already exists
2018 */
2019 static function createNew( $name, $params = array() ) {
2020 $user = new User;
2021 $user->load();
2022 if ( isset( $params['options'] ) ) {
2023 $user->mOptions = $params['options'] + $user->mOptions;
2024 unset( $params['options'] );
2025 }
2026 $dbw = wfGetDB( DB_MASTER );
2027 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2028 $fields = array(
2029 'user_id' => $seqVal,
2030 'user_name' => $name,
2031 'user_password' => $user->mPassword,
2032 'user_newpassword' => $user->mNewpassword,
2033 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2034 'user_email' => $user->mEmail,
2035 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2036 'user_real_name' => $user->mRealName,
2037 'user_options' => $user->encodeOptions(),
2038 'user_token' => $user->mToken,
2039 'user_registration' => $dbw->timestamp( $user->mRegistration ),
2040 'user_editcount' => 0,
2041 );
2042 foreach ( $params as $name => $value ) {
2043 $fields["user_$name"] = $value;
2044 }
2045 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2046 if ( $dbw->affectedRows() ) {
2047 $newUser = User::newFromId( $dbw->insertId() );
2048 } else {
2049 $newUser = null;
2050 }
2051 return $newUser;
2052 }
2053
2054 /**
2055 * Add an existing user object to the database
2056 */
2057 function addToDatabase() {
2058 $this->load();
2059 $dbw = wfGetDB( DB_MASTER );
2060 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2061 $dbw->insert( 'user',
2062 array(
2063 'user_id' => $seqVal,
2064 'user_name' => $this->mName,
2065 'user_password' => $this->mPassword,
2066 'user_newpassword' => $this->mNewpassword,
2067 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2068 'user_email' => $this->mEmail,
2069 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2070 'user_real_name' => $this->mRealName,
2071 'user_options' => $this->encodeOptions(),
2072 'user_token' => $this->mToken,
2073 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2074 'user_editcount' => 0,
2075 ), __METHOD__
2076 );
2077 $this->mId = $dbw->insertId();
2078
2079 # Clear instance cache other than user table data, which is already accurate
2080 $this->clearInstanceCache();
2081 }
2082
2083 /**
2084 * If the (non-anonymous) user is blocked, this function will block any IP address
2085 * that they successfully log on from.
2086 */
2087 function spreadBlock() {
2088 wfDebug( __METHOD__."()\n" );
2089 $this->load();
2090 if ( $this->mId == 0 ) {
2091 return;
2092 }
2093
2094 $userblock = Block::newFromDB( '', $this->mId );
2095 if ( !$userblock ) {
2096 return;
2097 }
2098
2099 $userblock->doAutoblock( wfGetIp() );
2100
2101 }
2102
2103 /**
2104 * Generate a string which will be different for any combination of
2105 * user options which would produce different parser output.
2106 * This will be used as part of the hash key for the parser cache,
2107 * so users will the same options can share the same cached data
2108 * safely.
2109 *
2110 * Extensions which require it should install 'PageRenderingHash' hook,
2111 * which will give them a chance to modify this key based on their own
2112 * settings.
2113 *
2114 * @return string
2115 */
2116 function getPageRenderingHash() {
2117 global $wgContLang, $wgUseDynamicDates, $wgLang;
2118 if( $this->mHash ){
2119 return $this->mHash;
2120 }
2121
2122 // stubthreshold is only included below for completeness,
2123 // it will always be 0 when this function is called by parsercache.
2124
2125 $confstr = $this->getOption( 'math' );
2126 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2127 if ( $wgUseDynamicDates ) {
2128 $confstr .= '!' . $this->getDatePreference();
2129 }
2130 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2131 $confstr .= '!' . $wgLang->getCode();
2132 $confstr .= '!' . $this->getOption( 'thumbsize' );
2133 // add in language specific options, if any
2134 $extra = $wgContLang->getExtraHashOptions();
2135 $confstr .= $extra;
2136
2137 // Give a chance for extensions to modify the hash, if they have
2138 // extra options or other effects on the parser cache.
2139 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2140
2141 // Make it a valid memcached key fragment
2142 $confstr = str_replace( ' ', '_', $confstr );
2143 $this->mHash = $confstr;
2144 return $confstr;
2145 }
2146
2147 function isBlockedFromCreateAccount() {
2148 $this->getBlockedStatus();
2149 return $this->mBlock && $this->mBlock->mCreateAccount;
2150 }
2151
2152 function isAllowedToCreateAccount() {
2153 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2154 }
2155
2156 /**
2157 * @deprecated
2158 */
2159 function setLoaded( $loaded ) {}
2160
2161 /**
2162 * Get this user's personal page title.
2163 *
2164 * @return Title
2165 * @public
2166 */
2167 function getUserPage() {
2168 return Title::makeTitle( NS_USER, $this->getName() );
2169 }
2170
2171 /**
2172 * Get this user's talk page title.
2173 *
2174 * @return Title
2175 * @public
2176 */
2177 function getTalkPage() {
2178 $title = $this->getUserPage();
2179 return $title->getTalkPage();
2180 }
2181
2182 /**
2183 * @static
2184 */
2185 function getMaxID() {
2186 static $res; // cache
2187
2188 if ( isset( $res ) )
2189 return $res;
2190 else {
2191 $dbr = wfGetDB( DB_SLAVE );
2192 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2193 }
2194 }
2195
2196 /**
2197 * Determine whether the user is a newbie. Newbies are either
2198 * anonymous IPs, or the most recently created accounts.
2199 * @return bool True if it is a newbie.
2200 */
2201 function isNewbie() {
2202 return !$this->isAllowed( 'autoconfirmed' );
2203 }
2204
2205 /**
2206 * Check to see if the given clear-text password is one of the accepted passwords
2207 * @param string $password User password.
2208 * @return bool True if the given password is correct otherwise False.
2209 */
2210 function checkPassword( $password ) {
2211 global $wgAuth;
2212 $this->load();
2213
2214 // Even though we stop people from creating passwords that
2215 // are shorter than this, doesn't mean people wont be able
2216 // to. Certain authentication plugins do NOT want to save
2217 // domain passwords in a mysql database, so we should
2218 // check this (incase $wgAuth->strict() is false).
2219 if( !$this->isValidPassword( $password ) ) {
2220 return false;
2221 }
2222
2223 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2224 return true;
2225 } elseif( $wgAuth->strict() ) {
2226 /* Auth plugin doesn't allow local authentication */
2227 return false;
2228 }
2229 $ep = $this->encryptPassword( $password );
2230 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
2231 return true;
2232 } elseif ( function_exists( 'iconv' ) ) {
2233 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2234 # Check for this with iconv
2235 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ) );
2236 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
2237 return true;
2238 }
2239 }
2240 return false;
2241 }
2242
2243 /**
2244 * Check if the given clear-text password matches the temporary password
2245 * sent by e-mail for password reset operations.
2246 * @return bool
2247 */
2248 function checkTemporaryPassword( $plaintext ) {
2249 $hash = $this->encryptPassword( $plaintext );
2250 return $hash === $this->mNewpassword;
2251 }
2252
2253 /**
2254 * Initialize (if necessary) and return a session token value
2255 * which can be used in edit forms to show that the user's
2256 * login credentials aren't being hijacked with a foreign form
2257 * submission.
2258 *
2259 * @param mixed $salt - Optional function-specific data for hash.
2260 * Use a string or an array of strings.
2261 * @return string
2262 * @public
2263 */
2264 function editToken( $salt = '' ) {
2265 if( !isset( $_SESSION['wsEditToken'] ) ) {
2266 $token = $this->generateToken();
2267 $_SESSION['wsEditToken'] = $token;
2268 } else {
2269 $token = $_SESSION['wsEditToken'];
2270 }
2271 if( is_array( $salt ) ) {
2272 $salt = implode( '|', $salt );
2273 }
2274 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2275 }
2276
2277 /**
2278 * Generate a hex-y looking random token for various uses.
2279 * Could be made more cryptographically sure if someone cares.
2280 * @return string
2281 */
2282 function generateToken( $salt = '' ) {
2283 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2284 return md5( $token . $salt );
2285 }
2286
2287 /**
2288 * Check given value against the token value stored in the session.
2289 * A match should confirm that the form was submitted from the
2290 * user's own login session, not a form submission from a third-party
2291 * site.
2292 *
2293 * @param string $val - the input value to compare
2294 * @param string $salt - Optional function-specific data for hash
2295 * @return bool
2296 * @public
2297 */
2298 function matchEditToken( $val, $salt = '' ) {
2299 global $wgMemc;
2300 $sessionToken = $this->editToken( $salt );
2301 if ( $val != $sessionToken ) {
2302 wfDebug( "User::matchEditToken: broken session data\n" );
2303 }
2304 return $val == $sessionToken;
2305 }
2306
2307 /**
2308 * Generate a new e-mail confirmation token and send a confirmation
2309 * mail to the user's given address.
2310 *
2311 * @return mixed True on success, a WikiError object on failure.
2312 */
2313 function sendConfirmationMail() {
2314 global $wgContLang;
2315 $expiration = null; // gets passed-by-ref and defined in next line.
2316 $url = $this->confirmationTokenUrl( $expiration );
2317 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2318 wfMsg( 'confirmemail_body',
2319 wfGetIP(),
2320 $this->getName(),
2321 $url,
2322 $wgContLang->timeanddate( $expiration, false ) ) );
2323 }
2324
2325 /**
2326 * Send an e-mail to this user's account. Does not check for
2327 * confirmed status or validity.
2328 *
2329 * @param string $subject
2330 * @param string $body
2331 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
2332 * @return mixed True on success, a WikiError object on failure.
2333 */
2334 function sendMail( $subject, $body, $from = null ) {
2335 if( is_null( $from ) ) {
2336 global $wgPasswordSender;
2337 $from = $wgPasswordSender;
2338 }
2339
2340 require_once( 'UserMailer.php' );
2341 $to = new MailAddress( $this );
2342 $sender = new MailAddress( $from );
2343 $error = userMailer( $to, $sender, $subject, $body );
2344
2345 if( $error == '' ) {
2346 return true;
2347 } else {
2348 return new WikiError( $error );
2349 }
2350 }
2351
2352 /**
2353 * Generate, store, and return a new e-mail confirmation code.
2354 * A hash (unsalted since it's used as a key) is stored.
2355 * @param &$expiration mixed output: accepts the expiration time
2356 * @return string
2357 * @private
2358 */
2359 function confirmationToken( &$expiration ) {
2360 $now = time();
2361 $expires = $now + 7 * 24 * 60 * 60;
2362 $expiration = wfTimestamp( TS_MW, $expires );
2363
2364 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2365 $hash = md5( $token );
2366
2367 $dbw = wfGetDB( DB_MASTER );
2368 $dbw->update( 'user',
2369 array( 'user_email_token' => $hash,
2370 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
2371 array( 'user_id' => $this->mId ),
2372 __METHOD__ );
2373
2374 return $token;
2375 }
2376
2377 /**
2378 * Generate and store a new e-mail confirmation token, and return
2379 * the URL the user can use to confirm.
2380 * @param &$expiration mixed output: accepts the expiration time
2381 * @return string
2382 * @private
2383 */
2384 function confirmationTokenUrl( &$expiration ) {
2385 $token = $this->confirmationToken( $expiration );
2386 $title = SpecialPage::getTitleFor( 'Confirmemail', $token );
2387 return $title->getFullUrl();
2388 }
2389
2390 /**
2391 * Mark the e-mail address confirmed and save.
2392 */
2393 function confirmEmail() {
2394 $this->load();
2395 $this->mEmailAuthenticated = wfTimestampNow();
2396 $this->saveSettings();
2397 return true;
2398 }
2399
2400 /**
2401 * Is this user allowed to send e-mails within limits of current
2402 * site configuration?
2403 * @return bool
2404 */
2405 function canSendEmail() {
2406 return $this->isEmailConfirmed();
2407 }
2408
2409 /**
2410 * Is this user allowed to receive e-mails within limits of current
2411 * site configuration?
2412 * @return bool
2413 */
2414 function canReceiveEmail() {
2415 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
2416 }
2417
2418 /**
2419 * Is this user's e-mail address valid-looking and confirmed within
2420 * limits of the current site configuration?
2421 *
2422 * If $wgEmailAuthentication is on, this may require the user to have
2423 * confirmed their address by returning a code or using a password
2424 * sent to the address from the wiki.
2425 *
2426 * @return bool
2427 */
2428 function isEmailConfirmed() {
2429 global $wgEmailAuthentication;
2430 $this->load();
2431 $confirmed = true;
2432 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2433 if( $this->isAnon() )
2434 return false;
2435 if( !self::isValidEmailAddr( $this->mEmail ) )
2436 return false;
2437 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
2438 return false;
2439 return true;
2440 } else {
2441 return $confirmed;
2442 }
2443 }
2444
2445 /**
2446 * Return true if there is an outstanding request for e-mail confirmation.
2447 * @return bool
2448 */
2449 function isEmailConfirmationPending() {
2450 global $wgEmailAuthentication;
2451 return $wgEmailAuthentication &&
2452 !$this->isEmailConfirmed() &&
2453 $this->mEmailToken &&
2454 $this->mEmailTokenExpires > wfTimestamp();
2455 }
2456
2457 /**
2458 * @param array $groups list of groups
2459 * @return array list of permission key names for given groups combined
2460 * @static
2461 */
2462 static function getGroupPermissions( $groups ) {
2463 global $wgGroupPermissions;
2464 $rights = array();
2465 foreach( $groups as $group ) {
2466 if( isset( $wgGroupPermissions[$group] ) ) {
2467 $rights = array_merge( $rights,
2468 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2469 }
2470 }
2471 return $rights;
2472 }
2473
2474 /**
2475 * @param string $group key name
2476 * @return string localized descriptive name for group, if provided
2477 * @static
2478 */
2479 static function getGroupName( $group ) {
2480 MessageCache::loadAllMessages();
2481 $key = "group-$group";
2482 $name = wfMsg( $key );
2483 return $name == '' || wfEmptyMsg( $key, $name )
2484 ? $group
2485 : $name;
2486 }
2487
2488 /**
2489 * @param string $group key name
2490 * @return string localized descriptive name for member of a group, if provided
2491 * @static
2492 */
2493 static function getGroupMember( $group ) {
2494 MessageCache::loadAllMessages();
2495 $key = "group-$group-member";
2496 $name = wfMsg( $key );
2497 return $name == '' || wfEmptyMsg( $key, $name )
2498 ? $group
2499 : $name;
2500 }
2501
2502 /**
2503 * Return the set of defined explicit groups.
2504 * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
2505 * groups are not included, as they are defined
2506 * automatically, not in the database.
2507 * @return array
2508 * @static
2509 */
2510 static function getAllGroups() {
2511 global $wgGroupPermissions;
2512 return array_diff(
2513 array_keys( $wgGroupPermissions ),
2514 array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) );
2515 }
2516
2517 /**
2518 * Get the title of a page describing a particular group
2519 *
2520 * @param $group Name of the group
2521 * @return mixed
2522 */
2523 static function getGroupPage( $group ) {
2524 MessageCache::loadAllMessages();
2525 $page = wfMsgForContent( 'grouppage-' . $group );
2526 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
2527 $title = Title::newFromText( $page );
2528 if( is_object( $title ) )
2529 return $title;
2530 }
2531 return false;
2532 }
2533
2534 /**
2535 * Create a link to the group in HTML, if available
2536 *
2537 * @param $group Name of the group
2538 * @param $text The text of the link
2539 * @return mixed
2540 */
2541 static function makeGroupLinkHTML( $group, $text = '' ) {
2542 if( $text == '' ) {
2543 $text = self::getGroupName( $group );
2544 }
2545 $title = self::getGroupPage( $group );
2546 if( $title ) {
2547 global $wgUser;
2548 $sk = $wgUser->getSkin();
2549 return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
2550 } else {
2551 return $text;
2552 }
2553 }
2554
2555 /**
2556 * Create a link to the group in Wikitext, if available
2557 *
2558 * @param $group Name of the group
2559 * @param $text The text of the link (by default, the name of the group)
2560 * @return mixed
2561 */
2562 static function makeGroupLinkWiki( $group, $text = '' ) {
2563 if( $text == '' ) {
2564 $text = self::getGroupName( $group );
2565 }
2566 $title = self::getGroupPage( $group );
2567 if( $title ) {
2568 $page = $title->getPrefixedText();
2569 return "[[$page|$text]]";
2570 } else {
2571 return $text;
2572 }
2573 }
2574
2575 /**
2576 * Increment the user's edit-count field.
2577 * Will have no effect for anonymous users.
2578 */
2579 function incEditCount() {
2580 if( !$this->isAnon() ) {
2581 $dbw = wfGetDB( DB_MASTER );
2582 $dbw->update( 'user',
2583 array( 'user_editcount=user_editcount+1' ),
2584 array( 'user_id' => $this->getId() ),
2585 __METHOD__ );
2586
2587 // Lazy initialization check...
2588 if( $dbw->affectedRows() == 0 ) {
2589 // Pull from a slave to be less cruel to servers
2590 // Accuracy isn't the point anyway here
2591 $dbr = wfGetDB( DB_SLAVE );
2592 $count = $dbr->selectField( 'revision',
2593 'COUNT(rev_user)',
2594 array( 'rev_user' => $this->getId() ),
2595 __METHOD__ );
2596
2597 // Now here's a goddamn hack...
2598 if( $dbr !== $dbw ) {
2599 // If we actually have a slave server, the count is
2600 // at least one behind because the current transaction
2601 // has not been committed and replicated.
2602 $count++;
2603 } else {
2604 // But if DB_SLAVE is selecting the master, then the
2605 // count we just read includes the revision that was
2606 // just added in the working transaction.
2607 }
2608
2609 $dbw->update( 'user',
2610 array( 'user_editcount' => $count ),
2611 array( 'user_id' => $this->getId() ),
2612 __METHOD__ );
2613 }
2614 }
2615 // edit count in user cache too
2616 $this->invalidateCache();
2617 }
2618 }
2619
2620 ?>