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