/** * Get a hash of a file's contents, either by retrieving a previously- * computed hash from the cache, or by computing a hash from the file. * * @private * @param string $filePath Full path to the file. * @param string $algo Name of selected hashing algorithm. * @return string|bool Hash of file contents, or false if the file could not be read. */ public function getFileContentsHashInternal($filePath, $algo = 'md4') { $mtime = MediaWiki\quietCall('filemtime', $filePath); if ($mtime === false) { return false; } $cacheKey = wfGlobalCacheKey(__CLASS__, $filePath, $mtime, $algo); $hash = $this->cache->get($cacheKey); if ($hash) { return $hash; } $contents = MediaWiki\quietCall('file_get_contents', $filePath); if ($contents === false) { return false; } $hash = hash($algo, $contents); $this->cache->set($cacheKey, $hash, 60 * 60 * 24); // 24h return $hash; }
public function getApproximateLagStatus() { if ($this->lagDetectionMethod === 'pt-heartbeat') { // Disable caching since this is fast enough and we don't wan't // to be *too* pessimistic by having both the cache TTL and the // pt-heartbeat interval count as lag in getSessionLagStatus() return parent::getApproximateLagStatus(); } $key = wfGlobalCacheKey('mysql-lag', $this->getServer()); $approxLag = $this->srvCache->get($key); if (!$approxLag) { $approxLag = parent::getApproximateLagStatus(); $this->srvCache->set($key, $approxLag, 1); } return $approxLag; }
/** * Run JavaScript or CSS data through a filter, caching the filtered result for future calls. * * Available filters are: * * - minify-js \see JavaScriptMinifier::minify * - minify-css \see CSSMin::minify * * If $data is empty, only contains whitespace or the filter was unknown, * $data is returned unmodified. * * @param string $filter Name of filter to run * @param string $data Text to filter, such as JavaScript or CSS text * @param array $options For back-compat, can also be the boolean value for "cacheReport". Keys: * - (bool) cache: Whether to allow caching this data. Default: true. * - (bool) cacheReport: Whether to include the "cache key" report comment. Default: false. * @return string Filtered data, or a comment containing an error message */ public function filter($filter, $data, $options = array()) { // Back-compat if (is_bool($options)) { $options = array('cacheReport' => $options); } // Defaults $options += array('cache' => true, 'cacheReport' => false); $stats = RequestContext::getMain()->getStats(); // Don't filter empty content if (trim($data) === '') { return $data; } if (!in_array($filter, array('minify-js', 'minify-css'))) { $this->logger->warning('Invalid filter {filter}', array('filter' => $filter)); return $data; } if (!$options['cache']) { $result = self::applyFilter($filter, $data, $this->config); } else { $key = wfGlobalCacheKey('resourceloader', 'filter', $filter, self::$filterCacheVersion, md5($data)); $cache = wfGetCache(wfIsHHVM() ? CACHE_ACCEL : CACHE_ANYTHING); $cacheEntry = $cache->get($key); if (is_string($cacheEntry)) { $stats->increment("resourceloader_cache.{$filter}.hit"); return $cacheEntry; } $result = ''; try { $statStart = microtime(true); $result = self::applyFilter($filter, $data, $this->config); $statTiming = microtime(true) - $statStart; $stats->increment("resourceloader_cache.{$filter}.miss"); $stats->timing("resourceloader_cache.{$filter}.timing", 1000 * $statTiming); if ($options['cacheReport']) { $result .= "\n/* cache key: {$key} */"; } // Set a TTL since HHVM's APC doesn't have any limitation or eviction logic. $cache->set($key, $result, 24 * 3600); } catch (Exception $e) { MWExceptionHandler::logException($e); $this->logger->warning('Minification failed: {exception}', array('exception' => $e)); $this->errors[] = self::formatExceptionNoComment($e); } } return $result; }
/** * Run JavaScript or CSS data through a filter, caching the filtered result for future calls. * * Available filters are: * * - minify-js \see JavaScriptMinifier::minify * - minify-css \see CSSMin::minify * * If $data is empty, only contains whitespace or the filter was unknown, * $data is returned unmodified. * * @param string $filter Name of filter to run * @param string $data Text to filter, such as JavaScript or CSS text * @param array $options Keys: * - (bool) cache: Whether to allow caching this data. Default: true. * @return string Filtered data, or a comment containing an error message */ public static function filter($filter, $data, array $options = array()) { if (strpos($data, ResourceLoader::FILTER_NOMIN) !== false) { return $data; } if (isset($options['cache']) && $options['cache'] === false) { return self::applyFilter($filter, $data); } $stats = RequestContext::getMain()->getStats(); $cache = ObjectCache::newAccelerator(CACHE_ANYTHING); $key = wfGlobalCacheKey('resourceloader', 'filter', $filter, self::$filterCacheVersion, md5($data)); $result = $cache->get($key); if ($result === false) { $stats->increment("resourceloader_cache.{$filter}.miss"); $result = self::applyFilter($filter, $data); $cache->set($key, $result, 24 * 3600); } else { $stats->increment("resourceloader_cache.{$filter}.hit"); } if ($result === null) { // Cached failure $result = $data; } return $result; }
public function testWfGlobalCacheKey() { $this->setMwGlobals(array('wgCachePrefix' => 'ignored', 'wgDBname' => 'example', 'wgDBprefix' => '')); $one = wfGlobalCacheKey('some', 'thing'); $this->assertEquals($one, 'global:some:thing'); $this->setMwGlobals(array('wgDBname' => 'other', 'wgDBprefix' => 'mw_')); $two = wfGlobalCacheKey('some', 'thing'); $this->assertEquals($one, $two, 'Not fragmented by wiki id'); }
/** * Clear the throttle counter. * * Should be called after a successful authentication attempt. * * @param string|null $username * @param string|null $ip * @throws \MWException */ public function clear($username = null, $ip = null) { $userKey = $username ? md5($username) : null; foreach ($this->conditions as $index => $specificThrottle) { $ipKey = isset($specificThrottle['allIPs']) ? null : $ip; $throttleKey = wfGlobalCacheKey('throttler', $this->type, $index, $ipKey, $userKey); $this->cache->delete($throttleKey); } }
/** * Construct a cache key for the results of a Pygments invocation. * * @param string $code Code to be highlighted. * @param string $lexer Lexer name. * @param array $options Options array. * @return string Cache key. */ private static function makeCacheKey($code, $lexer, $options) { $optionString = FormatJson::encode($options, false, FormatJson::ALL_OK); $hash = md5("{$code}|{$lexer}|{$optionString}|" . self::CACHE_VERSION); if (function_exists('wfGlobalCacheKey')) { return wfGlobalCacheKey('highlight', $hash); } else { return 'highlight:' . $hash; } }
/** * Make a new user account using the loaded data. * @private * @throws PermissionsError|ReadOnlyError * @return Status */ public function addNewAccountInternal() { global $wgAuth, $wgMemc, $wgAccountCreationThrottle, $wgEmailConfirmToEdit; // If the user passes an invalid domain, something is fishy if (!$wgAuth->validDomain($this->mDomain)) { return Status::newFatal('wrongpassword'); } // If we are not allowing users to login locally, we should be checking // to see if the user is actually able to authenticate to the authenti- // cation server before they create an account (otherwise, they can // create a local account and login as any domain user). We only need // to check this for domains that aren't local. if ('local' != $this->mDomain && $this->mDomain != '') { if (!$wgAuth->canCreateAccounts() && (!$wgAuth->userExists($this->mUsername) || !$wgAuth->authenticate($this->mUsername, $this->mPassword))) { return Status::newFatal('wrongpassword'); } } if (wfReadOnly()) { throw new ReadOnlyError(); } # Request forgery checks. if (!self::getCreateaccountToken()) { self::setCreateaccountToken(); return Status::newFatal('nocookiesfornew'); } # The user didn't pass a createaccount token if (!$this->mToken) { return Status::newFatal('sessionfailure'); } # Validate the createaccount token if ($this->mToken !== self::getCreateaccountToken()) { return Status::newFatal('sessionfailure'); } # Check permissions $currentUser = $this->getUser(); $creationBlock = $currentUser->isBlockedFromCreateAccount(); if (!$currentUser->isAllowed('createaccount')) { throw new PermissionsError('createaccount'); } elseif ($creationBlock instanceof Block) { // Throws an ErrorPageError. $this->userBlockedMessage($creationBlock); // This should never be reached. return false; } # Include checks that will include GlobalBlocking (Bug 38333) $permErrors = $this->getPageTitle()->getUserPermissionsErrors('createaccount', $currentUser, true); if (count($permErrors)) { throw new PermissionsError('createaccount', $permErrors); } $ip = $this->getRequest()->getIP(); if ($currentUser->isDnsBlacklisted($ip, true)) { return Status::newFatal('sorbs_create_account_reason'); } # Now create a dummy user ($u) and check if it is valid $u = User::newFromName($this->mUsername, 'creatable'); if (!$u) { return Status::newFatal('noname'); } # Make sure the user does not exist already $lock = $wgMemc->getScopedLock(wfGlobalCacheKey('account', md5($this->mUsername))); if (!$lock) { return Status::newFatal('usernameinprogress'); } elseif ($u->idForName(User::READ_LOCKING)) { return Status::newFatal('userexists'); } if ($this->mCreateaccountMail) { # do not force a password for account creation by email # set invalid password, it will be replaced later by a random generated password $this->mPassword = null; } else { if ($this->mPassword !== $this->mRetype) { return Status::newFatal('badretype'); } # check for password validity, return a fatal Status if invalid $validity = $u->checkPasswordValidity($this->mPassword, 'create'); if (!$validity->isGood()) { $validity->ok = false; // make sure this Status is fatal return $validity; } } # if you need a confirmed email address to edit, then obviously you # need an email address. if ($wgEmailConfirmToEdit && strval($this->mEmail) === '') { return Status::newFatal('noemailtitle'); } if (strval($this->mEmail) !== '' && !Sanitizer::validateEmail($this->mEmail)) { return Status::newFatal('invalidemailaddress'); } # Set some additional data so the AbortNewAccount hook can be used for # more than just username validation $u->setEmail($this->mEmail); $u->setRealName($this->mRealName); $abortError = ''; $abortStatus = null; if (!Hooks::run('AbortNewAccount', array($u, &$abortError, &$abortStatus))) { // Hook point to add extra creation throttles and blocks wfDebug("LoginForm::addNewAccountInternal: a hook blocked creation\n"); if ($abortStatus === null) { // Report back the old string as a raw message status. // This will report the error back as 'createaccount-hook-aborted' // with the given string as the message. // To return a different error code, return a Status object. $abortError = new Message('createaccount-hook-aborted', array($abortError)); $abortError->text(); return Status::newFatal($abortError); } else { // For MediaWiki 1.23+ and updated hooks, return the Status object // returned from the hook. return $abortStatus; } } // Hook point to check for exempt from account creation throttle if (!Hooks::run('ExemptFromAccountCreationThrottle', array($ip))) { wfDebug("LoginForm::exemptFromAccountCreationThrottle: a hook " . "allowed account creation w/o throttle\n"); } else { if ($wgAccountCreationThrottle && $currentUser->isPingLimitable()) { $key = wfMemcKey('acctcreate', 'ip', $ip); $value = $wgMemc->get($key); if (!$value) { $wgMemc->set($key, 0, 86400); } if ($value >= $wgAccountCreationThrottle) { return Status::newFatal('acct_creation_throttle_hit', $wgAccountCreationThrottle); } $wgMemc->incr($key); } } if (!$wgAuth->addUser($u, $this->mPassword, $this->mEmail, $this->mRealName)) { return Status::newFatal('externaldberror'); } self::clearCreateaccountToken(); return $this->initUser($u, false); }
public function testWfGlobalCacheKey() { $cache = ObjectCache::getLocalClusterInstance(); $this->assertEquals($cache->makeGlobalKey('foo', 123, 'bar'), wfGlobalCacheKey('foo', 123, 'bar')); }
/** * Clear the login attempt throttle hit count for the (username,current IP) tuple. * @param string $username The user name * @return void */ public static function clearLoginThrottle($username) { global $wgRequest, $wgPasswordAttemptThrottle; $canUsername = User::getCanonicalName($username, 'usable'); $username = $canUsername !== false ? $canUsername : $username; if (is_array($wgPasswordAttemptThrottle)) { $throttleConfig = $wgPasswordAttemptThrottle; if (isset($wgPasswordAttemptThrottle['count'])) { // old style. Convert for backwards compat. $throttleConfig = [$wgPasswordAttemptThrottle]; } foreach ($throttleConfig as $index => $specificThrottle) { if (isset($specificThrottle['allIPs'])) { $ip = 'All'; } else { $ip = $wgRequest->getIP(); } $throttleKey = wfGlobalCacheKey('password-throttle', $index, $ip, md5($username)); ObjectCache::getLocalClusterInstance()->delete($throttleKey); } } }
/** * Compute a non-cryptographic string hash of a file's contents. * If the file does not exist or cannot be read, returns an empty string. * * @since 1.26 Uses MD4 instead of SHA1. * @param string $filePath File path * @return string Hash */ protected static function safeFileHash($filePath) { static $cache; if (!$cache) { $cache = ObjectCache::newAccelerator(CACHE_NONE); } MediaWiki\suppressWarnings(); $mtime = filemtime($filePath); MediaWiki\restoreWarnings(); if (!$mtime) { return ''; } $cacheKey = wfGlobalCacheKey('resourceloader', __METHOD__, $filePath); $cachedHash = $cache->get($cacheKey); if (isset($cachedHash['mtime']) && $cachedHash['mtime'] === $mtime) { return $cachedHash['hash']; } MediaWiki\suppressWarnings(); $contents = file_get_contents($filePath); MediaWiki\restoreWarnings(); if (!$contents) { return ''; } $hash = hash('md4', $contents); $cache->set($cacheKey, array('mtime' => $mtime, 'hash' => $hash), 60 * 60 * 24); return $hash; }
private function getLagTimeCacheKey() { # Lag is per-server, not per-DB, so key on the master DB name return wfGlobalCacheKey('lag-times', $this->parent->getServerName(0)); }
/** * Compile a LESS file into CSS. * * Keeps track of all used files and adds them to localFileRefs. * * @since 1.22 * @since 1.26 Added $context paramter. * @throws Exception If less.php encounters a parse error * @param string $fileName File path of LESS source * @param ResourceLoaderContext $context Context in which to generate script * @return string CSS source */ protected function compileLessFile($fileName, ResourceLoaderContext $context) { static $cache; if (!$cache) { $cache = ObjectCache::newAccelerator(CACHE_ANYTHING); } // Construct a cache key from the LESS file name and a hash digest // of the LESS variables used for compilation. $vars = $this->getLessVars($context); ksort($vars); $varsHash = hash('md4', serialize($vars)); $cacheKey = wfGlobalCacheKey('LESS', $fileName, $varsHash); $cachedCompile = $cache->get($cacheKey); // If we got a cached value, we have to validate it by getting a // checksum of all the files that were loaded by the parser and // ensuring it matches the cached entry's. if (isset($cachedCompile['hash'])) { $contentHash = FileContentsHasher::getFileContentsHash($cachedCompile['files']); if ($contentHash === $cachedCompile['hash']) { $this->localFileRefs = array_merge($this->localFileRefs, $cachedCompile['files']); return $cachedCompile['css']; } } $compiler = ResourceLoader::getLessCompiler($this->getConfig(), $vars); $css = $compiler->parseFile($fileName)->getCss(); $files = $compiler->AllParsedFiles(); $this->localFileRefs = array_merge($this->localFileRefs, $files); $cache->set($cacheKey, array('css' => $css, 'files' => $files, 'hash' => FileContentsHasher::getFileContentsHash($files)), 60 * 60 * 24); // 86400 seconds, or 24 hours. return $css; }
function processLogin() { global $wgLang, $wgSecureLogin, $wgPasswordAttemptThrottle, $wgInvalidPasswordReset; $cache = ObjectCache::getLocalClusterInstance(); $authRes = $this->authenticateUserData(); switch ($authRes) { case self::SUCCESS: # We've verified now, update the real record $user = $this->getUser(); $user->touch(); if ($user->requiresHTTPS()) { $this->mStickHTTPS = true; } if ($wgSecureLogin && !$this->mStickHTTPS) { $user->setCookies($this->mRequest, false, $this->mRemember); } else { $user->setCookies($this->mRequest, null, $this->mRemember); } self::clearLoginToken(); // Reset the throttle $request = $this->getRequest(); $key = wfGlobalCacheKey('password-throttle', $request->getIP(), md5($this->mUsername)); $cache->delete($key); if ($this->hasSessionCookie() || $this->mSkipCookieCheck) { /* Replace the language object to provide user interface in * correct language immediately on this first page load. */ $code = $request->getVal('uselang', $user->getOption('language')); $userLang = Language::factory($code); $wgLang = $userLang; $this->getContext()->setLanguage($userLang); // Reset SessionID on Successful login (bug 40995) $this->renewSessionId(); if ($this->checkUserPasswordExpired($this->getUser()) == 'soft') { $this->resetLoginForm($this->msg('resetpass-expired-soft')); } elseif ($wgInvalidPasswordReset && !$user->isValidPassword($this->mPassword)) { $status = $user->checkPasswordValidity($this->mPassword, 'login'); $this->resetLoginForm($status->getMessage('resetpass-validity-soft')); } else { $this->successfulLogin(); } } else { $this->cookieRedirectCheck('login'); } break; case self::NEED_TOKEN: $error = $this->mAbortLoginErrorMsg ?: 'nocookiesforlogin'; $this->mainLoginForm($this->msg($error)->parse()); break; case self::WRONG_TOKEN: $error = $this->mAbortLoginErrorMsg ?: 'sessionfailure'; $this->mainLoginForm($this->msg($error)->text()); break; case self::NO_NAME: case self::ILLEGAL: $error = $this->mAbortLoginErrorMsg ?: 'noname'; $this->mainLoginForm($this->msg($error)->text()); break; case self::WRONG_PLUGIN_PASS: $error = $this->mAbortLoginErrorMsg ?: 'wrongpassword'; $this->mainLoginForm($this->msg($error)->text()); break; case self::NOT_EXISTS: if ($this->getUser()->isAllowed('createaccount')) { $error = $this->mAbortLoginErrorMsg ?: 'nosuchuser'; $this->mainLoginForm($this->msg($error, wfEscapeWikiText($this->mUsername))->parse()); } else { $error = $this->mAbortLoginErrorMsg ?: 'nosuchusershort'; $this->mainLoginForm($this->msg($error, wfEscapeWikiText($this->mUsername))->text()); } break; case self::WRONG_PASS: $error = $this->mAbortLoginErrorMsg ?: 'wrongpassword'; $this->mainLoginForm($this->msg($error)->text()); break; case self::EMPTY_PASS: $error = $this->mAbortLoginErrorMsg ?: 'wrongpasswordempty'; $this->mainLoginForm($this->msg($error)->text()); break; case self::RESET_PASS: $error = $this->mAbortLoginErrorMsg ?: 'resetpass_announce'; $this->resetLoginForm($this->msg($error)); break; case self::CREATE_BLOCKED: $this->userBlockedMessage($this->getUser()->isBlockedFromCreateAccount()); break; case self::THROTTLED: $error = $this->mAbortLoginErrorMsg ?: 'login-throttled'; $this->mainLoginForm($this->msg($error)->params($this->getLanguage()->formatDuration($wgPasswordAttemptThrottle['seconds']))->text()); break; case self::USER_BLOCKED: $error = $this->mAbortLoginErrorMsg ?: 'login-userblocked'; $this->mainLoginForm($this->msg($error, $this->mUsername)->escaped()); break; case self::ABORTED: $error = $this->mAbortLoginErrorMsg ?: 'login-abort-generic'; $this->mainLoginForm($this->msg($error, wfEscapeWikiText($this->mUsername))->text()); break; case self::USER_MIGRATED: $error = $this->mAbortLoginErrorMsg ?: 'login-migrated-generic'; $params = array(); if (is_array($error)) { $error = array_shift($this->mAbortLoginErrorMsg); $params = $this->mAbortLoginErrorMsg; } $this->mainLoginForm($this->msg($error, $params)->text()); break; default: throw new MWException('Unhandled case value'); } LoggerFactory::getInstance('authmanager')->info('Login attempt', array('event' => 'login', 'successful' => $authRes === self::SUCCESS, 'status' => LoginForm::$statusCodes[$authRes])); }
/** * Load a library from the given file and execute it in the base environment. * @param string File name/path to load * @return mixed the export list, or null if there isn't one. */ protected function loadLibraryFromFile($fileName) { static $cache = null; if (!$cache) { $cache = ObjectCache::newAccelerator(array(), 'hash'); } $mtime = filemtime($fileName); if ($mtime === false) { throw new MWException('Lua file does not exist: ' . $fileName); } $cacheKey = wfGlobalCacheKey(__CLASS__, $fileName); $fileData = $cache->get($cacheKey); $code = false; if ($fileData) { list($code, $cachedMtime) = $fileData; if ($cachedMtime < $mtime) { $code = false; } } if (!$code) { $code = file_get_contents($fileName); if ($code === false) { throw new MWException('Lua file does not exist: ' . $fileName); } $cache->set($cacheKey, array($code, $mtime), 60 * 5); } # Prepending an "@" to the chunk name makes Lua think it is a filename $module = $this->getInterpreter()->loadString($code, '@' . basename($fileName)); $ret = $this->getInterpreter()->callFunction($module); return isset($ret[0]) ? $ret[0] : null; }
/** * Clear the login attempt throttle hit count for the (username,current IP) tuple. * @param string $username The user name * @return void */ public static function clearLoginThrottle($username) { global $wgMemc, $wgRequest; $canUsername = User::getCanonicalName($username, 'usable'); $username = $canUsername !== false ? $canUsername : $username; $throttleKey = wfGlobalCacheKey('password-throttle', $wgRequest->getIP(), md5($username)); $wgMemc->delete($throttleKey); }