X-Git-Url: http://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FUser.php;h=477571396eb7211aff110c186a352be623f0418f;hb=0af46ac5e13577b469727da03a7794343ed70018;hp=250235de13f84e405d3f9c9eb2028bd51bae6f3b;hpb=8d9271fac61db6f7094f1975211d4a52a2223152;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/User.php b/includes/User.php index 250235de13..477571396e 100644 --- a/includes/User.php +++ b/includes/User.php @@ -1,46 +1,95 @@ loadDefaults(); } - # Static factory method - # + /** + * Static factory method + * @static + * @param string $name Username, validated by Title:newFromText() + */ function newFromName( $name ) { $u = new User(); # Clean up name according to title rules $t = Title::newFromText( $name ); - $u->setName( $t->getText() ); - return $u; + if( is_null( $t ) ) { + return NULL; + } else { + $u->setName( $t->getText() ); + $u->setId( $u->idFromName( $t->getText() ) ); + return $u; + } } - /* static */ function whoIs( $id ) { + /** + * Get username given an id. + * @param integer $id Database user id + * @return string Nickname of a user + * @static + */ + function whoIs( $id ) { $dbr =& wfGetDB( DB_SLAVE ); - return $dbr->getField( 'user', 'user_name', array( 'user_id' => $id ) ); + return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ) ); } - /* static */ function whoIsReal( $id ) { + /** + * Get real username given an id. + * @param integer $id Database user id + * @return string Realname of a user + * @static + */ + function whoIsReal( $id ) { $dbr =& wfGetDB( DB_SLAVE ); - return $dbr->getField( 'user', 'user_real_name', array( 'user_id' => $id ) ); + return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ) ); } - /* static */ function idFromName( $name ) { + /** + * Get database id given a user name + * @param string $name Nickname of a user + * @return integer|null Database user id (null: if non existent + * @static + */ + function idFromName( $name ) { $fname = "User::idFromName"; $nt = Title::newFromText( $name ); @@ -49,7 +98,7 @@ class User { return null; } $dbr =& wfGetDB( DB_SLAVE ); - $s = $dbr->getArray( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname ); + $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname ); if ( $s === false ) { return 0; @@ -58,13 +107,31 @@ class User { } } - # does the string match an anonymous user IP address? - /* static */ function isIP( $name ) { + /** + * does the string match an anonymous user IP address? + * @param string $name Nickname of a user + * @static + */ + function isIP( $name ) { return preg_match("/^\d{1,3}\.\d{1,3}.\d{1,3}\.\d{1,3}$/",$name); + } + /** + * does the string match roughly an email address ? + * @param string $addr email address + * @static + */ + function isValidEmailAddr ( $addr ) { + return preg_match( '/^([a-z0-9_.-]+([a-z0-9_.-]+)*\@[a-z0-9_-]+([a-z0-9_.-]+)*([a-z.]{2,})+)$/', strtolower($addr)); } - /* static */ function randomPassword() { + /** + * probably return a random password + * @return string probably a random password + * @static + * @todo Check what is doing really [AV] + */ + function randomPassword() { $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz'; $l = strlen( $pwchars ) - 1; @@ -75,33 +142,117 @@ class User { return $np; } + /** + * Set properties to default + * Used at construction. It will load per language default settings only + * if we have an available language object. + */ function loadDefaults() { - global $wgLang, $wgIP; + static $n=0; + $n++; + $fname = 'User::loadDefaults' . $n; + wfProfileIn( $fname ); + + global $wgContLang, $wgIP, $wgDBname; global $wgNamespacesToBeSearchedDefault; - $this->mId = $this->mNewtalk = 0; + $this->mId = 0; + $this->mNewtalk = -1; $this->mName = $wgIP; $this->mRealName = $this->mEmail = ''; + $this->mEmailAuthenticationtimestamp = 0; $this->mPassword = $this->mNewpassword = ''; $this->mRights = array(); - $defOpt = $wgLang->getDefaultUserOptions() ; - foreach ( $defOpt as $oname => $val ) { - $this->mOptions[$oname] = $val; + $this->mGroups = array(); + // Getting user defaults only if we have an available language + if( isset( $wgContLang ) ) { + $this->loadDefaultFromLanguage(); } - foreach ($wgNamespacesToBeSearchedDefault as $nsnum => $val) { + + foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) { $this->mOptions['searchNs'.$nsnum] = $val; } unset( $this->mSkin ); $this->mDataLoaded = false; $this->mBlockedby = -1; # Unset - $this->mTouched = '0'; # Allow any pages to be cached - $this->cookiePassword = ''; + $this->setToken(); # Random $this->mHash = false; + + if ( isset( $_COOKIE[$wgDBname.'LoggedOut'] ) ) { + $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgDBname.'LoggedOut'] ); + } + else { + $this->mTouched = '0'; # Allow any pages to be cached + } + + wfProfileOut( $fname ); } - /* private */ function getBlockedStatus() - { - global $wgIP, $wgBlockCache, $wgProxyList; + /** + * Used to load user options from a language. + * This is not in loadDefault() cause we sometime create user before having + * a language object. + */ + function loadDefaultFromLanguage(){ + $this->mOptions = User::getDefaultOptions(); + } + + /** + * Combine the language default options with any site-specific options + * and add the default language variants. + * + * @return array + * @static + * @access private + */ + function getDefaultOptions() { + /** + * Site defaults will override the global/language defaults + */ + global $wgContLang, $wgDefaultUserOptions; + $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptions(); + + /** + * default language setting + */ + $variant = $wgContLang->getPreferredVariant(); + $defOpt['variant'] = $variant; + $defOpt['language'] = $variant; + + return $defOpt; + } + + /** + * Get a given default option value. + * + * @param string $opt + * @return string + * @static + * @access public + */ + function getDefaultOption( $opt ) { + $defOpts = User::getDefaultOptions(); + if( isset( $defOpts[$opt] ) ) { + return $defOpts[$opt]; + } else { + return ''; + } + } + + /** + * Get blocking information + * @access private + * @param bool $bFromSlave Specify whether to check slave or master. To improve performance, + * non-critical checks are done against slaves. Check when actually saving should be done against + * master. + * + * Note that even if $bFromSlave is false, the check is done first against slave, then master. + * The logic is that if blocked on slave, we'll assume it's either blocked on master or + * just slightly outta sync and soon corrected - safer to block slightly more that less. + * And it's cheaper to check slave first, then master if needed, than master always. + */ + function getBlockedStatus() { + global $wgIP, $wgBlockCache, $wgProxyList, $wgEnableSorbs, $bFromSlave; if ( -1 != $this->mBlockedby ) { return; } @@ -110,15 +261,24 @@ class User { # User blocking if ( $this->mId ) { $block = new Block(); - if ( $block->load( $wgIP , $this->mId ) ) { + $block->forUpdate( $bFromSlave ); + if ( $block->load( $wgIP , $this->mId ) ) { $this->mBlockedby = $block->mBy; $this->mBlockreason = $block->mReason; + $this->spreadBlock(); } } # IP/range blocking if ( !$this->mBlockedby ) { - $block = $wgBlockCache->get( $wgIP ); + # Check first against slave, and optionally from master. + $block = $wgBlockCache->get( $wgIP, true ); + if ( !$block && !$bFromSlave ) + { + # Not blocked: check against master, to make sure. + $wgBlockCache->clearLocal( ); + $block = $wgBlockCache->get( $wgIP, false ); + } if ( $block !== false ) { $this->mBlockedby = $block->mBy; $this->mBlockreason = $block->mReason; @@ -128,41 +288,100 @@ class User { # Proxy blocking if ( !$this->mBlockedby ) { if ( array_key_exists( $wgIP, $wgProxyList ) ) { + $this->mBlockedby = wfMsg( 'proxyblocker' ); $this->mBlockreason = wfMsg( 'proxyblockreason' ); - $this->mBlockedby = "Proxy blocker"; } } + + # DNSBL + if ( !$this->mBlockedby && $wgEnableSorbs ) { + if ( $this->inSorbsBlacklist( $wgIP ) ) { + $this->mBlockedby = wfMsg( 'sorbs' ); + $this->mBlockreason = wfMsg( 'sorbsreason' ); + } + } + } - function isBlocked() - { - $this->getBlockedStatus(); + function inSorbsBlacklist( $ip ) { + $fname = 'User::inSorbsBlacklist'; + wfProfileIn( $fname ); + + $found = false; + $host = ''; + + if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) { + # Make hostname + for ( $i=4; $i>=1; $i-- ) { + $host .= $m[$i] . '.'; + } + $host .= 'http.dnsbl.sorbs.net.'; + + # Send query + $ipList = gethostbynamel( $host ); + + if ( $ipList ) { + wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy!\n" ); + $found = true; + } else { + wfDebug( "Requested $host, not found.\n" ); + } + } + + wfProfileOut( $fname ); + return $found; + } + + /** + * Check if user is blocked + * @return bool True if blocked, false otherwise + */ + function isBlocked( $bFromSlave = false ) { + $this->getBlockedStatus( $bFromSlave ); if ( 0 === $this->mBlockedby ) { return false; } return true; } - + + /** + * Get name of blocker + * @return string name of blocker + */ function blockedBy() { $this->getBlockedStatus(); return $this->mBlockedby; } - + + /** + * Get blocking reason + * @return string Blocking reason + */ function blockedFor() { $this->getBlockedStatus(); return $this->mBlockreason; } + /** + * Initialise php session + */ function SetupSession() { global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain; if( $wgSessionsInMemcached ) { require_once( 'MemcachedSessions.php' ); + } elseif( 'files' != ini_get( 'session.save_handler' ) ) { + # If it's left on 'user' or another setting from another + # application, it will end up failing. Try to recover. + ini_set ( 'session.save_handler', 'files' ); } session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain ); session_cache_limiter( 'private, must-revalidate' ); @session_start(); } - /* static */ function loadFromSession() - { + /** + * Read datas from session + * @static + */ + function loadFromSession() { global $wgMemc, $wgDBname; if ( isset( $_SESSION['wsUserID'] ) ) { @@ -197,12 +416,10 @@ class User { wfDebug( "User::loadFromSession() got from cache!\n" ); } - if ( isset( $_SESSION['wsUserPassword'] ) ) { - $passwordCorrect = $_SESSION['wsUserPassword'] == $user->mPassword; - } else if ( isset( $_COOKIE["{$wgDBname}Password"] ) ) { - $user->mCookiePassword = $_COOKIE["{$wgDBname}Password"]; - $_SESSION['wsUserPassword'] = $user->addSalt( $user->mCookiePassword ); - $passwordCorrect = $_SESSION['wsUserPassword'] == $user->mPassword; + if ( isset( $_SESSION['wsToken'] ) ) { + $passwordCorrect = $_SESSION['wsToken'] == $user->mToken; + } else if ( isset( $_COOKIE["{$wgDBname}Token"] ) ) { + $passwordCorrect = $user->mToken == $_COOKIE["{$wgDBname}Token"]; } else { return new User(); # Can't log in from session } @@ -214,66 +431,81 @@ class User { else wfDebug( "User::loadFromSession() unable to save to memcached\n" ); } - $user->spreadBlock(); return $user; } return new User(); # Can't log in from session } - function loadFromDatabase() - { - global $wgCommandLineMode; + /** + * Load a user from the database + */ + function loadFromDatabase() { + global $wgCommandLineMode, $wgAnonGroupId, $wgLoggedInGroupId; $fname = "User::loadFromDatabase"; - if ( $this->mDataLoaded || $wgCommandLineMode ) { + + # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress + # loading in a command line script, don't assume all command line scripts need it like this + #if ( $this->mDataLoaded || $wgCommandLineMode ) { + if ( $this->mDataLoaded ) { return; } # Paranoia $this->mId = IntVal( $this->mId ); - # check in separate table if there are changes to the talk page - $this->mNewtalk=0; # reset talk page status - $dbr =& wfGetDB( DB_SLAVE ); - if($this->mId) { - $res = $dbr->select( 'user_newtalk', 1, array( 'user_id' => $this->mId ), $fname ); - - if ( $dbr->numRows($res)>0 ) { - $this->mNewtalk= 1; - } - $dbr->freeResult( $res ); - } else { - global $wgDBname, $wgMemc; - $key = "$wgDBname:newtalk:ip:{$this->mName}"; - $newtalk = $wgMemc->get( $key ); - if( ! is_integer( $newtalk ) ){ - $res = $dbr->select( 'user_newtalk', 1, array( 'user_ip' => $this->mName ), $fname ); - - $this->mNewtalk = $dbr->numRows( $res ) > 0 ? 1 : 0; - $dbr->freeResult( $res ); - - $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 ); - } else { - $this->mNewtalk = $newtalk ? 1 : 0; - } - } + /** Anonymous user */ if(!$this->mId) { + /** Get rights */ + $anong = Group::newFromId($wgAnonGroupId); + if (!$anong) + wfDebugDieBacktrace("Please update your database schema " + ."and populate initial group data from " + ."maintenance/archives patches"); + $anong->loadFromDatabase(); + $this->mRights = explode(',', $anong->getRights()); $this->mDataLoaded = true; return; } # the following stuff is for non-anonymous users only - - $s = $dbr->getArray( 'user', array( 'user_name','user_password','user_newpassword','user_email', - 'user_real_name','user_options','user_rights','user_touched' ), + + $dbr =& wfGetDB( DB_SLAVE ); + $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email', + 'user_emailauthenticationtimestamp', + 'user_real_name','user_options','user_touched', 'user_token' ), array( 'user_id' => $this->mId ), $fname ); - + if ( $s !== false ) { $this->mName = $s->user_name; $this->mEmail = $s->user_email; + $this->mEmailAuthenticationtimestamp = wfTimestamp(TS_MW,$s->user_emailauthenticationtimestamp); $this->mRealName = $s->user_real_name; $this->mPassword = $s->user_password; $this->mNewpassword = $s->user_newpassword; $this->decodeOptions( $s->user_options ); - $this->mRights = explode( ",", strtolower( $s->user_rights ) ); $this->mTouched = wfTimestamp(TS_MW,$s->user_touched); + $this->mToken = $s->user_token; + + // Get groups id + $res = $dbr->select( 'user_groups', array( 'ug_group' ), array( 'ug_user' => $this->mId ) ); + + while($group = $dbr->fetchRow($res)) { + $this->mGroups[] = $group[0]; + } + + // add the default group for logged in user + $this->mGroups[] = $wgLoggedInGroupId; + + $this->mRights = array(); + // now we merge groups rights to get this user rights + foreach($this->mGroups as $aGroupId) { + $g = Group::newFromId($aGroupId); + $g->loadFromDatabase(); + $this->mRights = array_merge($this->mRights, explode(',', $g->getRights())); + } + + // array merge duplicate rights which are part of several groups + $this->mRights = array_unique($this->mRights); + + $dbr->freeResult($res); } $this->mDataLoaded = true; @@ -295,13 +527,58 @@ class User { $this->mName = $str; } + + /** + * Return the title dbkey form of the name, for eg user pages. + * @return string + * @access public + */ + function getTitleKey() { + return str_replace( ' ', '_', $this->getName() ); + } + function getNewtalk() { + $fname = 'User::getNewtalk'; $this->loadFromDatabase(); + + # Load the newtalk status if it is unloaded (mNewtalk=-1) + if( $this->mNewtalk == -1 ) { + $this->mNewtalk = 0; # reset talk page status + + # Check memcached separately for anons, who have no + # entire User object stored in there. + if( !$this->mId ) { + global $wgDBname, $wgMemc; + $key = "$wgDBname:newtalk:ip:{$this->mName}"; + $newtalk = $wgMemc->get( $key ); + if( is_integer( $newtalk ) ) { + $this->mNewtalk = $newtalk ? 1 : 0; + return (bool)$this->mNewtalk; + } + } + + $dbr =& wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'watchlist', + array( 'wl_user' ), + array( 'wl_title' => $this->getTitleKey(), + 'wl_namespace' => NS_USER_TALK, + 'wl_user' => $this->mId, + 'wl_notificationtimestamp != 0' ), + 'User::getNewtalk' ); + if( $dbr->numRows($res) > 0 ) { + $this->mNewtalk = 1; + } + $dbr->freeResult( $res ); + + if( !$this->mId ) { + $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 ); + } + } + return ( 0 != $this->mNewtalk ); } - function setNewtalk( $val ) - { + function setNewtalk( $val ) { $this->loadFromDatabase(); $this->mNewtalk = $val; $this->invalidateCache(); @@ -319,6 +596,12 @@ class User { return ($timestamp >= $this->mTouched); } + /** + * Salt a password. + * Will only be salted if $wgPasswordSalt is true + * @param string Password. + * @return string Salted password or clear password. + */ function addSalt( $p ) { global $wgPasswordSalt; if($wgPasswordSalt) @@ -327,17 +610,42 @@ class User { return $p; } + /** + * Encrypt a password. + * It can eventuall salt a password @see User::addSalt() + * @param string $p clear Password. + * @param string Encrypted password. + */ function encryptPassword( $p ) { return $this->addSalt( md5( $p ) ); } + # Set the password and reset the random token function setPassword( $str ) { $this->loadFromDatabase(); - $this->setCookiePassword( $str ); + $this->setToken(); $this->mPassword = $this->encryptPassword( $str ); $this->mNewpassword = ''; } + # Set the random token (used for persistent authentication) + function setToken( $token = false ) { + global $wgSecretKey, $wgProxyKey, $wgDBname; + if ( !$token ) { + if ( $wgSecretKey ) { + $key = $wgSecretKey; + } elseif ( $wgProxyKey ) { + $key = $wgProxyKey; + } else { + $key = microtime(); + } + $this->mToken = md5( $wgSecretKey . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId ); + } else { + $this->mToken = $token; + } + } + + function setCookiePassword( $str ) { $this->loadFromDatabase(); $this->mCookiePassword = md5( $str ); @@ -353,6 +661,11 @@ class User { return $this->mEmail; } + function getEmailAuthenticationtimestamp() { + $this->loadFromDatabase(); + return $this->mEmailAuthenticationtimestamp; + } + function setEmail( $str ) { $this->loadFromDatabase(); $this->mEmail = $str; @@ -391,34 +704,67 @@ class User { $this->loadFromDatabase(); return $this->mRights; } - + function addRight( $rname ) { $this->loadFromDatabase(); array_push( $this->mRights, $rname ); $this->invalidateCache(); } - function isSysop() { + function getGroups() { $this->loadFromDatabase(); - if ( 0 == $this->mId ) { return false; } - - return in_array( 'sysop', $this->mRights ); + return $this->mGroups; } - function isDeveloper() { + function setGroups($groups) { $this->loadFromDatabase(); - if ( 0 == $this->mId ) { return false; } + $this->mGroups = $groups; + $this->invalidateCache(); + } + + /** + * A more legible check for non-anonymousness. + * Returns true if the user is not an anonymous visitor. + * + * @return bool + */ + function isLoggedIn() { + return( $this->getID() != 0 ); + } + + /** + * A more legible check for anonymousness. + * Returns true if the user is an anonymous visitor. + * + * @return bool + */ + function isAnon() { + return !$this->isLoggedIn(); + } + + /** + * Check if a user is sysop + * Die with backtrace. Use User:isAllowed() instead. + * @deprecated + */ + function isSysop() { + wfDebugDieBacktrace("User::isSysop() is deprecated. Use User::isAllowed() instead"); + } - return in_array( 'developer', $this->mRights ); + /** @deprecated */ + function isDeveloper() { + wfDebugDieBacktrace("User::isDeveloper() is deprecated. Use User::isAllowed() instead"); } + /** @deprecated */ function isBureaucrat() { - $this->loadFromDatabase(); - if ( 0 == $this->mId ) { return false; } - - return in_array( 'bureaucrat', $this->mRights ); + wfDebugDieBacktrace("User::isBureaucrat() is deprecated. Use User::isAllowed() instead"); } + /** + * Whether the user is a bot + * @todo need to be migrated to the new user level management sytem + */ function isBot() { $this->loadFromDatabase(); @@ -428,10 +774,29 @@ class User { return in_array( 'bot', $this->mRights ); } + /** + * Check if user is allowed to access a feature / make an action + * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions). + * @return boolean True: action is allowed, False: action should not be allowed + */ + function isAllowed($action='') { + $this->loadFromDatabase(); + return in_array( $action , $this->mRights ); + } + + /** + * Load a skin if it doesn't exist or return it + * @todo FIXME : need to check the old failback system [AV] + */ function &getSkin() { + global $IP; if ( ! isset( $this->mSkin ) ) { - # get all skin names available from SkinNames.php + $fname = 'User::getSkin'; + wfProfileIn( $fname ); + + # get all skin names available $skinNames = Skin::getSkinNames(); + # get the user skin $userSkin = $this->getOption( 'skin' ); if ( $userSkin == '' ) { $userSkin = 'standard'; } @@ -439,65 +804,129 @@ class User { if ( !isset( $skinNames[$userSkin] ) ) { # in case the user skin could not be found find a replacement $fallback = array( - 0 => 'SkinStandard', - 1 => 'SkinNostalgia', - 2 => 'SkinCologneBlue'); - # if phptal is enabled we should have monobook skin that superseed - # the good old SkinStandard. + 0 => 'Standard', + 1 => 'Nostalgia', + 2 => 'CologneBlue'); + # if phptal is enabled we should have monobook skin that + # superseed the good old SkinStandard. if ( isset( $skinNames['monobook'] ) ) { - $fallback[0] = 'SkinMonoBook'; + $fallback[0] = 'MonoBook'; } if(is_numeric($userSkin) && isset( $fallback[$userSkin]) ){ $sn = $fallback[$userSkin]; } else { - $sn = 'SkinStandard'; + $sn = 'Standard'; } } else { # The user skin is available - $sn = 'Skin' . $skinNames[$userSkin]; + $sn = $skinNames[$userSkin]; } - # only require the needed stuff - switch($sn) { - case 'SkinMonoBook': - require_once( 'SkinPHPTal.php' ); - break; - case 'SkinStandard': - require_once( 'SkinStandard.php' ); - break; - case 'SkinNostalgia': - require_once( 'SkinNostalgia.php' ); - break; - case 'SkinCologneBlue': - require_once( 'SkinCologneBlue.php' ); - break; + # Grab the skin class and initialise it. Each skin checks for PHPTal + # and will not load if it's not enabled. + require_once( $IP.'/skins/'.$sn.'.php' ); + + # Check if we got if not failback to default skin + $className = 'Skin'.$sn; + if( !class_exists( $className ) ) { + # DO NOT die if the class isn't found. This breaks maintenance + # scripts and can cause a user account to be unrecoverable + # except by SQL manipulation if a previously valid skin name + # is no longer valid. + $className = 'SkinStandard'; + require_once( $IP.'/skins/Standard.php' ); } - # now we can create the skin object - $this->mSkin = new $sn; + $this->mSkin =& new $className; + wfProfileOut( $fname ); } return $this->mSkin; } + /**#@+ + * @param string $title Article title to look at + */ + + /** + * Check watched status of an article + * @return bool True if article is watched + */ function isWatched( $title ) { $wl = WatchedItem::fromUserTitle( $this, $title ); return $wl->isWatched(); } + /** + * Watch an article + */ function addWatch( $title ) { $wl = WatchedItem::fromUserTitle( $this, $title ); $wl->addWatch(); $this->invalidateCache(); } + /** + * Stop watching an article + */ function removeWatch( $title ) { $wl = WatchedItem::fromUserTitle( $this, $title ); $wl->removeWatch(); $this->invalidateCache(); } + /** + * Clear the user's notification timestamp for the given title. + * If e-notif e-mails are on, they will receive notification mails on + * the next change of the page if it's watched etc. + */ + function clearNotification( $title ) { + $userid = $this->getId(); + if ($userid==0) + return; + $dbw =& wfGetDB( DB_MASTER ); + $success = $dbw->update( 'watchlist', + array( /* SET */ + 'wl_notificationtimestamp' => 0 + ), array( /* WHERE */ + 'wl_title' => $title->getDBkey(), + 'wl_namespace' => $title->getNamespace(), + 'wl_user' => $this->getId() + ), 'User::clearLastVisited' + ); + } + + /**#@-*/ + + /** + * Resets all of the given user's page-change notification timestamps. + * If e-notif e-mails are on, they will receive notification mails on + * the next change of any watched page. + * + * @param int $currentUser user ID number + * @access public + */ + function clearAllNotifications( $currentUser ) { + if( $currentUser != 0 ) { + + $dbw =& wfGetDB( DB_MASTER ); + $success = $dbw->update( 'watchlist', + array( /* SET */ + 'wl_notificationtimestamp' => 0 + ), array( /* WHERE */ + 'wl_user' => $currentUser + ), 'UserMailer::clearAll' + ); + + # we also need to clear here the "you have new message" notification for the own user_talk page + # This is cleared one page view later in Article::viewUpdates(); + } + } - /* private */ function encodeOptions() { + /** + * @access private + * @return string Encoding options + */ + function encodeOptions() { $a = array(); foreach ( $this->mOptions as $oname => $oval ) { array_push( $a, $oname.'='.$oval ); @@ -506,7 +935,10 @@ class User { return $s; } - /* private */ function decodeOptions( $str ) { + /** + * @access private + */ + function decodeOptions( $str ) { $a = explode( "\n", $str ); foreach ( $a as $s ) { if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) { @@ -527,40 +959,56 @@ class User { $_SESSION['wsUserName'] = $this->mName; setcookie( $wgDBname.'UserName', $this->mName, $exp, $wgCookiePath, $wgCookieDomain ); - $_SESSION['wsUserPassword'] = $this->mPassword; + $_SESSION['wsToken'] = $this->mToken; if ( 1 == $this->getOption( 'rememberpassword' ) ) { - setcookie( $wgDBname.'Password', $this->mCookiePassword, $exp, $wgCookiePath, $wgCookieDomain ); + setcookie( $wgDBname.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain ); } else { - setcookie( $wgDBname.'Password', '', time() - 3600 ); + setcookie( $wgDBname.'Token', '', time() - 3600 ); } } + /** + * Logout user + * It will clean the session cookie + */ function logout() { - global $wgCookiePath, $wgCookieDomain, $wgDBname; - $this->mId = 0; + global $wgCookiePath, $wgCookieDomain, $wgDBname, $wgIP; + $this->loadDefaults(); + $this->setLoaded( true ); $_SESSION['wsUserID'] = 0; setcookie( $wgDBname.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain ); - setcookie( $wgDBname.'Password', '', time() - 3600, $wgCookiePath, $wgCookieDomain ); + setcookie( $wgDBname.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain ); + + # Remember when user logged out, to prevent seeing cached pages + setcookie( $wgDBname.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain ); } + /** + * Save object settings into database + */ function saveSettings() { global $wgMemc, $wgDBname; $fname = 'User::saveSettings'; $dbw =& wfGetDB( DB_MASTER ); - if ( ! $this->mNewtalk ) { - # Delete user_newtalk row - if( $this->mId ) { - $dbw->delete( 'user_newtalk', array( 'user_id' => $this->mId ), $fname ); - } else { - $dbw->delete( 'user_newtalk', array( 'user_ip' => $this->mName ), $fname ); + if ( ! $this->getNewtalk() ) { + # Delete the watchlist entry for user_talk page X watched by user X + $dbw->delete( 'watchlist', + array( 'wl_user' => $this->mId, + 'wl_title' => $this->getTitleKey(), + 'wl_namespace' => NS_USER_TALK ), + $fname ); + if( !$this->mId ) { + # Anon users have a separate memcache space for newtalk + # since they don't store their own info. Trim... $wgMemc->delete( "$wgDBname:newtalk:ip:{$this->mName}" ); } } - if ( 0 == $this->mId ) { return; } + if ( 0 == $this->mId ) { return; } + $dbw->update( 'user', array( /* SET */ 'user_name' => $this->mName, @@ -568,18 +1016,37 @@ class User { 'user_newpassword' => $this->mNewpassword, 'user_real_name' => $this->mRealName, 'user_email' => $this->mEmail, + 'user_emailauthenticationtimestamp' => $dbw->timestamp($this->mEmailAuthenticationtimestamp), 'user_options' => $this->encodeOptions(), - 'user_rights' => implode( ",", $this->mRights ), - 'user_touched' => $dbw->timestamp($this->mTouched) + 'user_touched' => $dbw->timestamp($this->mTouched), + 'user_token' => $this->mToken ), array( /* WHERE */ 'user_id' => $this->mId ), $fname ); + $dbw->set( 'user_rights', 'ur_rights', implode( ',', $this->mRights ), + 'ur_user='. $this->mId, $fname ); $wgMemc->delete( "$wgDBname:user:id:$this->mId" ); + + // delete old groups + $dbw->delete( 'user_groups', array( 'ug_user' => $this->mId), $fname); + + // save new ones + foreach ($this->mGroups as $group) { + $dbw->replace( 'user_groups', + array(array('ug_user','ug_group')), + array( + 'ug_user' => $this->mId, + 'ug_group' => $group + ), $fname + ); + } } - # Checks if a user with the given name exists, returns the ID - # + + /** + * Checks if a user with the given name exists, returns the ID + */ function idForName() { $fname = 'User::idForName'; @@ -595,6 +1062,9 @@ class User { return $id; } + /** + * Add user object to the database + */ function addToDatabase() { $fname = 'User::addToDatabase'; $dbw =& wfGetDB( DB_MASTER ); @@ -606,16 +1076,31 @@ class User { 'user_password' => $this->mPassword, 'user_newpassword' => $this->mNewpassword, 'user_email' => $this->mEmail, + 'user_emailauthenticationtimestamp' => $dbw->timestamp($this->mEmailAuthenticationtimestamp), 'user_real_name' => $this->mRealName, - 'user_rights' => implode( ',', $this->mRights ), - 'user_options' => $this->encodeOptions() + 'user_options' => $this->encodeOptions(), + 'user_token' => $this->mToken ), $fname ); $this->mId = $dbw->insertId(); + $dbw->insert( 'user_rights', + array( + 'ur_user' => $this->mId, + 'ur_rights' => implode( ',', $this->mRights ) + ), $fname + ); + + foreach ($this->mGroups as $group) { + $dbw->insert( 'user_groups', + array( + 'ug_user' => $this->mId, + 'ug_group' => $group + ), $fname + ); + } } - function spreadBlock() - { + function spreadBlock() { global $wgIP; # If the (non-anonymous) user is blocked, this function will block any IP address # that they successfully log on from. @@ -660,7 +1145,8 @@ class User { } - function getPageRenderingHash(){ + function getPageRenderingHash() { + global $wgContLang; if( $this->mHash ){ return $this->mHash; } @@ -669,12 +1155,15 @@ class User { // it will always be 0 when this function is called by parsercache. $confstr = $this->getOption( 'math' ); - $confstr .= '!' . $this->getOption( 'highlightbroken' ); $confstr .= '!' . $this->getOption( 'stubthreshold' ); $confstr .= '!' . $this->getOption( 'editsection' ); - $confstr .= '!' . $this->getOption( 'editsectiononrightclick' ); - $confstr .= '!' . $this->getOption( 'showtoc' ); $confstr .= '!' . $this->getOption( 'date' ); + $confstr .= '!' . $this->getOption( 'numberheadings' ); + $confstr .= '!' . $this->getOption( 'language' ); + $confstr .= '!' . $this->getOption( 'thumbsize' ); + // add in language specific options, if any + $extra = $wgContLang->getExtraHashOptions(); + $confstr .= $extra; $this->mHash = $confstr; return $confstr ; @@ -692,44 +1181,125 @@ class User { return $allowed; } - # Set mDataLoaded, return previous value - # Use this to prevent DB access in command-line scripts or similar situations - function setLoaded( $loaded ) - { + /** + * Set mDataLoaded, return previous value + * Use this to prevent DB access in command-line scripts or similar situations + */ + function setLoaded( $loaded ) { return wfSetVar( $this->mDataLoaded, $loaded ); } + /** + * Get this user's personal page title. + * + * @return Title + * @access public + */ function getUserPage() { return Title::makeTitle( NS_USER, $this->mName ); } + + /** + * Get this user's talk page title. + * + * @return Title + * @access public + */ + function getTalkPage() { + $title = $this->getUserPage(); + return $title->getTalkPage(); + } - /* static */ function getMaxID() { + /** + * @static + */ + function getMaxID() { $dbr =& wfGetDB( DB_SLAVE ); return $dbr->selectField( 'user', 'max(user_id)', false ); } + /** + * Determine whether the user is a newbie. Newbies are either + * anonymous IPs, or the 1% most recently created accounts. + * Bots and sysops are excluded. + * @return bool True if it is a newbie. + */ function isNewbie() { return $this->mId > User::getMaxID() * 0.99 && !$this->isSysop() && !$this->isBot() || $this->getID() == 0; } - # Check to see if the given clear-text password is one of the accepted passwords + /** + * Check to see if the given clear-text password is one of the accepted passwords + * @param string $password User password. + * @return bool True if the given password is correct otherwise False. + */ function checkPassword( $password ) { + global $wgAuth; $this->loadFromDatabase(); + + if( $wgAuth->authenticate( $this->getName(), $password ) ) { + return true; + } elseif( $wgAuth->strict() ) { + /* Auth plugin doesn't allow local authentication */ + return false; + } $ep = $this->encryptPassword( $password ); if ( 0 == strcmp( $ep, $this->mPassword ) ) { return true; - } elseif ( 0 == strcmp( $ep, $this->mNewpassword ) ) { + } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) { + $this->mEmailAuthenticationtimestamp = wfTimestampNow(); + $this->mNewpassword = ''; # use the temporary one-time password only once: clear it now ! + $this->saveSettings(); return true; } elseif ( function_exists( 'iconv' ) ) { # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted # Check for this with iconv -/* $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) ); + $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) ); if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) { return true; - }*/ + } } return false; } + + /** + * Initialize (if necessary) and return a session token value + * which can be used in edit forms to show that the user's + * login credentials aren't being hijacked with a foreign form + * submission. + * + * @param mixed $salt - Optional function-specific data for hash. + * Use a string or an array of strings. + * @return string + * @access public + */ + function editToken( $salt = '' ) { + if( !isset( $_SESSION['wsEditToken'] ) ) { + $token = dechex( mt_rand() ) . dechex( mt_rand() ); + $_SESSION['wsEditToken'] = $token; + } else { + $token = $_SESSION['wsEditToken']; + } + if( is_array( $salt ) ) { + $salt = implode( '|', $salt ); + } + return md5( $token . $salt ); + } + + /** + * Check given value against the token value stored in the session. + * A match should confirm that the form was submitted from the + * user's own login session, not a form submission from a third-party + * site. + * + * @param string $val - the input value to compare + * @param string $salt - Optional function-specific data for hash + * @return bool + * @access public + */ + function matchEditToken( $val, $salt = '' ) { + return ( $val == $this->editToken( $salt ) ); + } } ?>