Return nothing on empty math tags instead of char encoding
[lhc/web/wiklou.git] / includes / User.php
index 4fac12a..c66c2d7 100644 (file)
@@ -11,6 +11,18 @@ define( 'USER_TOKEN_LENGTH', 32 );
 # Serialized record version
 define( 'MW_USER_VERSION', 4 );
 
+# Some punctuation to prevent editing from broken text-mangling proxies.
+# FIXME: this is embedded unescaped into HTML attributes in various
+# places, so we can't safely include ' or " even though we really should.
+define( 'EDIT_TOKEN_SUFFIX', '\\' );
+
+/**
+ * Thrown by User::setPassword() on error
+ */
+class PasswordError extends MWException {
+       // NOP
+}
+
 /**
  *
  * @package MediaWiki
@@ -38,6 +50,8 @@ class User {
                'editwidth',
                'watchcreations',
                'watchdefault',
+               'watchmoves',
+               'watchdeletion',
                'minordefault',
                'previewontop',
                'previewonfirst',
@@ -52,10 +66,11 @@ class User {
                'externaldiff',
                'showjumplinks',
                'uselivepreview',
-               'autopatrol',
                'forceeditsummary',
                'watchlisthideown',
                'watchlisthidebots',
+               'watchlisthideminor',
+               'ccmeonemails',
        );
 
        /**
@@ -70,6 +85,7 @@ class User {
                'mRealName',
                'mPassword',
                'mNewpassword',
+               'mNewpassTime',
                'mEmail',
                'mOptions',
                'mTouched',
@@ -86,9 +102,9 @@ class User {
        /**
         * The cache variable declarations
         */
-       var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mEmail, $mOptions
-               $mTouched, $mToken, $mEmailAuthenticated, $mEmailToken, $mEmailTokenExpires,
-               $mRegistration, $mGroups;
+       var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime
+               $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated, 
+               $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
 
        /**
         * Whether the cache variables have been loaded
@@ -343,7 +359,7 @@ class User {
         * @return bool
         */
        static function isIP( $name ) {
-               return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/",$name);
+               return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name);
                /*return preg_match("/^
                        (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
                        (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
@@ -574,6 +590,7 @@ class User {
                $this->mName = $name;
                $this->mRealName = '';
                $this->mPassword = $this->mNewpassword = '';
+               $this->mNewpassTime = null;
                $this->mEmail = '';
                $this->mOptions = null; # Defer init
 
@@ -682,7 +699,7 @@ class User {
                        return false;
                }
 
-               $dbr =& wfGetDB( DB_SLAVE );
+               $dbr =& wfGetDB( DB_MASTER );
                $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
 
                if ( $s !== false ) {
@@ -691,6 +708,7 @@ class User {
                        $this->mRealName = $s->user_real_name;
                        $this->mPassword = $s->user_password;
                        $this->mNewpassword = $s->user_newpassword;
+                       $this->mNewpassTime = wfTimestampOrNull( TS_MW, $s->user_newpass_time );
                        $this->mEmail = $s->user_email;
                        $this->decodeOptions( $s->user_options );
                        $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
@@ -858,9 +876,10 @@ class User {
        }
 
        function inSorbsBlacklist( $ip ) {
-               global $wgEnableSorbs;
+               global $wgEnableSorbs, $wgSorbsUrl;
+
                return $wgEnableSorbs &&
-                       $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
+                       $this->inDnsBlacklist( $ip, $wgSorbsUrl );
        }
 
        function inDnsBlacklist( $ip, $base ) {
@@ -869,6 +888,7 @@ class User {
                $found = false;
                $host = '';
 
+               $m = array();
                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-- ) {
@@ -902,6 +922,13 @@ class User {
         * @public
         */
        function pingLimiter( $action='edit' ) {
+       
+               # Call the 'PingLimiter' hook
+               $result = false;
+               if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
+                       return $result;
+               }
+               
                global $wgRateLimits, $wgRateLimitsExcludedGroups;
                if( !isset( $wgRateLimits[$action] ) ) {
                        return false;
@@ -935,6 +962,7 @@ class User {
                        if( isset( $limits['ip'] ) ) {
                                $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
                        }
+                       $matches = array();
                        if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
                                $subnet = $matches[1];
                                $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
@@ -1278,13 +1306,51 @@ class User {
        }
 
        /**
-        *  Set the password and reset the random token
+        * Set the password and reset the random token
+        * Calls through to authentication plugin if necessary;
+        * will have no effect if the auth plugin refuses to
+        * pass the change through or if the legal password
+        * checks fail.
+        *
+        * As a special case, setting the password to null
+        * wipes it, so the account cannot be logged in until
+        * a new password is set, for instance via e-mail.
+        *
+        * @param string $str
+        * @throws PasswordError on failure
         */
        function setPassword( $str ) {
+               global $wgAuth;
+               
+               if( $str !== null ) {
+                       if( !$wgAuth->allowPasswordChange() ) {
+                               throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
+                       }
+               
+                       if( !$this->isValidPassword( $str ) ) {
+                               global $wgMinimalPasswordLength;
+                               throw new PasswordError( wfMsg( 'passwordtooshort',
+                                       $wgMinimalPasswordLength ) );
+                       }
+               }
+               
+               if( !$wgAuth->setPassword( $this, $str ) ) {
+                       throw new PasswordError( wfMsg( 'externaldberror' ) );
+               }
+               
                $this->load();
                $this->setToken();
-               $this->mPassword = $this->encryptPassword( $str );
+               
+               if( $str === null ) {
+                       // Save an invalid hash...
+                       $this->mPassword = '';
+               } else {
+                       $this->mPassword = $this->encryptPassword( $str );
+               }
                $this->mNewpassword = '';
+               $this->mNewpassTime = null;
+               
+               return true;
        }
 
        /**
@@ -1314,11 +1380,32 @@ class User {
                $this->mCookiePassword = md5( $str );
        }
 
-       function setNewpassword( $str ) {
+       /**
+        * Set the password for a password reminder or new account email
+        * Sets the user_newpass_time field if $throttle is true
+        */
+       function setNewpassword( $str, $throttle = true ) {
                $this->load();
                $this->mNewpassword = $this->encryptPassword( $str );
+               if ( $throttle ) {
+                       $this->mNewpassTime = wfTimestampNow();
+               }
        }
 
+       /**
+        * Returns true if a password reminder email has already been sent within
+        * the last $wgPasswordReminderResendTime hours
+        */
+       function isPasswordReminderThrottled() {
+               global $wgPasswordReminderResendTime;
+               $this->load();
+               if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
+                       return false;
+               }
+               $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
+               return time() < $expiry;
+       }
+       
        function getEmail() {
                $this->load();
                return $this->mEmail;
@@ -1346,17 +1433,23 @@ class User {
 
        /**
         * @param string $oname The option to check
+        * @param string $defaultOverride A default value returned if the option does not exist
         * @return string
         */
-       function getOption( $oname ) {
+       function getOption( $oname, $defaultOverride = '' ) {
                $this->load();
+
                if ( is_null( $this->mOptions ) ) {
+                       if($defaultOverride != '') {
+                               return $defaultOverride;
+                       }
                        $this->mOptions = User::getDefaultOptions();
                }
+
                if ( array_key_exists( $oname, $this->mOptions ) ) {
                        return trim( $this->mOptions[$oname] );
                } else {
-                       return '';
+                       return $defaultOverride;
                }
        }
 
@@ -1559,7 +1652,7 @@ class User {
         * @todo FIXME : need to check the old failback system [AV]
         */
        function &getSkin() {
-               global $IP, $wgRequest;
+               global $wgRequest;
                if ( ! isset( $this->mSkin ) ) {
                        wfProfileIn( __METHOD__ );
 
@@ -1612,6 +1705,11 @@ class User {
        function clearNotification( &$title ) {
                global $wgUser, $wgUseEnotif;
 
+               # Do nothing if the database is locked to writes
+               if( wfReadOnly() ) {
+                       return;
+               }
+
                if ($title->getNamespace() == NS_USER_TALK &&
                        $title->getText() == $this->getName() ) {
                        if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
@@ -1646,7 +1744,7 @@ class User {
                // any matching rows
                if ( $watched ) {
                        $dbw =& wfGetDB( DB_MASTER );
-                       $success = $dbw->update( 'watchlist',
+                       $dbw->update( 'watchlist',
                                        array( /* SET */
                                                'wl_notificationtimestamp' => NULL
                                        ), array( /* WHERE */
@@ -1677,7 +1775,7 @@ class User {
                if( $currentUser != 0 )  {
 
                        $dbw =& wfGetDB( DB_MASTER );
-                       $success = $dbw->update( 'watchlist',
+                       $dbw->update( 'watchlist',
                                array( /* SET */
                                        'wl_notificationtimestamp' => NULL
                                ), array( /* WHERE */
@@ -1714,6 +1812,7 @@ class User {
                $this->mOptions = array();
                $a = explode( "\n", $str );
                foreach ( $a as $s ) {
+                       $m = array();
                        if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
                                $this->mOptions[$m[1]] = $m[2];
                        }
@@ -1774,6 +1873,7 @@ class User {
                                'user_name' => $this->mName,
                                'user_password' => $this->mPassword,
                                'user_newpassword' => $this->mNewpassword,
+                               'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
                                'user_real_name' => $this->mRealName,
                                'user_email' => $this->mEmail,
                                'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
@@ -1792,7 +1892,6 @@ class User {
         * Checks if a user with the given name exists, returns the ID
         */
        function idForName() {
-               $gotid = 0;
                $s = trim( $this->getName() );
                if ( 0 == strcmp( '', $s ) ) return 0;
 
@@ -1834,12 +1933,14 @@ class User {
                        'user_name' => $name,
                        'user_password' => $user->mPassword,
                        'user_newpassword' => $user->mNewpassword,
+                       'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
                        'user_email' => $user->mEmail,
                        'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
                        'user_real_name' => $user->mRealName,
                        'user_options' => $user->encodeOptions(),
                        'user_token' => $user->mToken,
                        'user_registration' => $dbw->timestamp( $user->mRegistration ),
+                       'user_editcount' => 0,
                );
                foreach ( $params as $name => $value ) {
                        $fields["user_$name"] = $value;
@@ -1866,12 +1967,14 @@ class User {
                                'user_name' => $this->mName,
                                'user_password' => $this->mPassword,
                                'user_newpassword' => $this->mNewpassword,
+                               'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
                                'user_email' => $this->mEmail,
                                'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
                                'user_real_name' => $this->mRealName,
                                'user_options' => $this->encodeOptions(),
                                'user_token' => $this->mToken,
                                'user_registration' => $dbw->timestamp( $this->mRegistration ),
+                               'user_editcount' => 0,
                        ), __METHOD__
                );
                $this->mId = $dbw->insertId();
@@ -1896,41 +1999,7 @@ class User {
                        return;
                }
 
-               # Check if this IP address is already blocked
-               $ipblock = Block::newFromDB( wfGetIP() );
-               if ( $ipblock ) {
-                       # If the user is already blocked. Then check if the autoblock would
-                       # excede the user block. If it would excede, then do nothing, else
-                       # prolong block time
-                       if ($userblock->mExpiry &&
-                               ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
-                               return;
-                       }
-                       # Just update the timestamp
-                       $ipblock->updateTimestamp();
-                       return;
-               } else {
-                       $ipblock = new Block;
-               }
-
-               # Make a new block object with the desired properties
-               wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
-               $ipblock->mAddress = wfGetIP();
-               $ipblock->mUser = 0;
-               $ipblock->mBy = $userblock->mBy;
-               $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
-               $ipblock->mTimestamp = wfTimestampNow();
-               $ipblock->mAuto = 1;
-               # If the user is already blocked with an expiry date, we don't
-               # want to pile on top of that!
-               if($userblock->mExpiry) {
-                       $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
-               } else {
-                       $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
-               }
-
-               # Insert it
-               $ipblock->insert();
+               $userblock->doAutoblock( wfGetIp() );
 
        }
 
@@ -1948,7 +2017,7 @@ class User {
         * @return string
         */
        function getPageRenderingHash() {
-               global $wgContLang, $wgUseDynamicDates;
+               global $wgContLang, $wgUseDynamicDates, $wgLang;
                if( $this->mHash ){
                        return $this->mHash;
                }
@@ -1962,7 +2031,7 @@ class User {
                        $confstr .= '!' . $this->getDatePreference();
                }
                $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
-               $confstr .= '!' . $this->getOption( 'language' );
+               $confstr .= '!' . $wgLang->getCode();
                $confstr .= '!' . $this->getOption( 'thumbsize' );
                // add in language specific options, if any
                $extra = $wgContLang->getExtraHashOptions();
@@ -2040,7 +2109,7 @@ class User {
         * @return bool True if the given password is correct otherwise False.
         */
        function checkPassword( $password ) {
-               global $wgAuth, $wgMinimalPasswordLength;
+               global $wgAuth;
                $this->load();
 
                // Even though we stop people from creating passwords that
@@ -2048,7 +2117,7 @@ class User {
                // to. Certain authentication plugins do NOT want to save
                // domain passwords in a mysql database, so we should
                // check this (incase $wgAuth->strict() is false).
-               if( strlen( $password ) < $wgMinimalPasswordLength ) {
+               if( !$this->isValidPassword( $password ) ) {
                        return false;
                }
 
@@ -2061,8 +2130,6 @@ class User {
                $ep = $this->encryptPassword( $password );
                if ( 0 == strcmp( $ep, $this->mPassword ) ) {
                        return true;
-               } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
-                       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
@@ -2073,6 +2140,16 @@ class User {
                }
                return false;
        }
+       
+       /**
+        * Check if the given clear-text password matches the temporary password
+        * sent by e-mail for password reset operations.
+        * @return bool
+        */
+       function checkTemporaryPassword( $plaintext ) {
+               $hash = $this->encryptPassword( $plaintext );
+               return $hash === $this->mNewpassword;
+       }
 
        /**
         * Initialize (if necessary) and return a session token value
@@ -2095,7 +2172,7 @@ class User {
                if( is_array( $salt ) ) {
                        $salt = implode( '|', $salt );
                }
-               return md5( $token . $salt );
+               return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
        }
 
        /**
@@ -2136,6 +2213,7 @@ class User {
         */
        function sendConfirmationMail() {
                global $wgContLang;
+               $expiration = null; // gets passed-by-ref and defined in next line.
                $url = $this->confirmationTokenUrl( $expiration );
                return $this->sendMail( wfMsg( 'confirmemail_subject' ),
                        wfMsg( 'confirmemail_body',
@@ -2206,7 +2284,7 @@ class User {
         */
        function confirmationTokenUrl( &$expiration ) {
                $token = $this->confirmationToken( $expiration );
-               $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
+               $title = SpecialPage::getTitleFor( 'Confirmemail', $token );
                return $title->getFullUrl();
        }
 
@@ -2264,6 +2342,18 @@ class User {
                        return $confirmed;
                }
        }
+       
+       /**
+        * Return true if there is an outstanding request for e-mail confirmation.
+        * @return bool
+        */
+       function isEmailConfirmationPending() {
+               global $wgEmailAuthentication;
+               return $wgEmailAuthentication &&
+                       !$this->isEmailConfirmed() &&
+                       $this->mEmailToken &&
+                       $this->mEmailTokenExpires > wfTimestamp();
+       }
 
        /**
         * @param array $groups list of groups
@@ -2383,6 +2473,48 @@ class User {
                        return $text;
                }
        }
+       
+       /**
+        * Increment the user's edit-count field.
+        * Will have no effect for anonymous users.
+        */
+       function incEditCount() {
+               if( !$this->isAnon() ) {
+                       $dbw = wfGetDB( DB_MASTER );
+                       $dbw->update( 'user',
+                               array( 'user_editcount=user_editcount+1' ),
+                               array( 'user_id' => $this->getId() ),
+                               __METHOD__ );
+                       
+                       // Lazy initialization check...
+                       if( $dbw->affectedRows() == 0 ) {
+                               // Pull from a slave to be less cruel to servers
+                               // Accuracy isn't the point anyway here
+                               $dbr = wfGetDB( DB_SLAVE );
+                               $count = $dbr->selectField( 'revision',
+                                       'COUNT(rev_user)',
+                                       array( 'rev_user' => $this->getId() ),
+                                       __METHOD__ );
+                               
+                               // Now here's a goddamn hack...
+                               if( $dbr !== $dbw ) {
+                                       // If we actually have a slave server, the count is
+                                       // at least one behind because the current transaction
+                                       // has not been committed and replicated.
+                                       $count++;
+                               } else {
+                                       // But if DB_SLAVE is selecting the master, then the
+                                       // count we just read includes the revision that was
+                                       // just added in the working transaction.
+                               }
+                               
+                               $dbw->update( 'user',
+                                       array( 'user_editcount' => $count ),
+                                       array( 'user_id' => $this->getId() ),
+                                       __METHOD__ );
+                       }
+               }
+       }
 }
 
 ?>