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