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