/** * @param array $lbConf Config for LBFactory::__construct() * @param Config $mainConfig Main config object from MediaWikiServices * @return array */ public static function applyDefaultConfig(array $lbConf, Config $mainConfig) { global $wgCommandLineMode; $lbConf += ['localDomain' => new DatabaseDomain($mainConfig->get('DBname'), null, $mainConfig->get('DBprefix')), 'profiler' => Profiler::instance(), 'trxProfiler' => Profiler::instance()->getTransactionProfiler(), 'replLogger' => LoggerFactory::getInstance('DBReplication'), 'queryLogger' => LoggerFactory::getInstance('DBQuery'), 'connLogger' => LoggerFactory::getInstance('DBConnection'), 'perfLogger' => LoggerFactory::getInstance('DBPerformance'), 'errorLogger' => [MWExceptionHandler::class, 'logException'], 'cliMode' => $wgCommandLineMode, 'hostname' => wfHostname(), 'readOnlyReason' => wfConfiguredReadOnlyReason()]; if ($lbConf['class'] === 'LBFactorySimple') { if (isset($lbConf['servers'])) { // Server array is already explicitly configured; leave alone } elseif (is_array($mainConfig->get('DBservers'))) { foreach ($mainConfig->get('DBservers') as $i => $server) { if ($server['type'] === 'sqlite') { $server += ['dbDirectory' => $mainConfig->get('SQLiteDataDir')]; } elseif ($server['type'] === 'postgres') { $server += ['port' => $mainConfig->get('DBport')]; } $lbConf['servers'][$i] = $server + ['schema' => $mainConfig->get('DBmwschema'), 'tablePrefix' => $mainConfig->get('DBprefix'), 'flags' => DBO_DEFAULT, 'sqlMode' => $mainConfig->get('SQLMode'), 'utf8Mode' => $mainConfig->get('DBmysql5')]; } } else { $flags = DBO_DEFAULT; $flags |= $mainConfig->get('DebugDumpSql') ? DBO_DEBUG : 0; $flags |= $mainConfig->get('DBssl') ? DBO_SSL : 0; $flags |= $mainConfig->get('DBcompress') ? DBO_COMPRESS : 0; $server = ['host' => $mainConfig->get('DBserver'), 'user' => $mainConfig->get('DBuser'), 'password' => $mainConfig->get('DBpassword'), 'dbname' => $mainConfig->get('DBname'), 'schema' => $mainConfig->get('DBmwschema'), 'tablePrefix' => $mainConfig->get('DBprefix'), 'type' => $mainConfig->get('DBtype'), 'load' => 1, 'flags' => $flags, 'sqlMode' => $mainConfig->get('SQLMode'), 'utf8Mode' => $mainConfig->get('DBmysql5')]; if ($server['type'] === 'sqlite') { $server['dbDirectory'] = $mainConfig->get('SQLiteDataDir'); } elseif ($server['type'] === 'postgres') { $server['port'] = $mainConfig->get('DBport'); } $lbConf['servers'] = [$server]; } if (!isset($lbConf['externalClusters'])) { $lbConf['externalClusters'] = $mainConfig->get('ExternalServers'); } } elseif ($lbConf['class'] === 'LBFactoryMulti') { if (isset($lbConf['serverTemplate'])) { $lbConf['serverTemplate']['schema'] = $mainConfig->get('DBmwschema'); $lbConf['serverTemplate']['sqlMode'] = $mainConfig->get('SQLMode'); $lbConf['serverTemplate']['utf8Mode'] = $mainConfig->get('DBmysql5'); } } // Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804) $sCache = MediaWikiServices::getInstance()->getLocalServerObjectCache(); if ($sCache->getQoS($sCache::ATTR_EMULATION) > $sCache::QOS_EMULATION_SQL) { $lbConf['srvCache'] = $sCache; } $cCache = ObjectCache::getLocalClusterInstance(); if ($cCache->getQoS($cCache::ATTR_EMULATION) > $cCache::QOS_EMULATION_SQL) { $lbConf['memCache'] = $cCache; } $wCache = MediaWikiServices::getInstance()->getMainWANObjectCache(); if ($wCache->getQoS($wCache::ATTR_EMULATION) > $wCache::QOS_EMULATION_SQL) { $lbConf['wanCache'] = $wCache; } return $lbConf; }
/** * @param array $conditions An array of arrays describing throttling conditions. * Defaults to $wgPasswordAttemptThrottle. See documentation of that variable for format. * @param array $params Parameters (all optional): * - type: throttle type, used as a namespace for counters, * - cache: a BagOStuff object where throttle counters are stored. * - warningLimit: the log level will be raised to warning when rejecting an attempt after * no less than this many failures. */ public function __construct(array $conditions = null, array $params = []) { $invalidParams = array_diff_key($params, array_fill_keys(['type', 'cache', 'warningLimit'], true)); if ($invalidParams) { throw new \InvalidArgumentException('unrecognized parameters: ' . implode(', ', array_keys($invalidParams))); } if ($conditions === null) { $config = \ConfigFactory::getDefaultInstance()->makeConfig('main'); $conditions = $config->get('PasswordAttemptThrottle'); $params += ['type' => 'password', 'cache' => \ObjectCache::getLocalClusterInstance(), 'warningLimit' => 50]; } else { $params += ['type' => 'custom', 'cache' => \ObjectCache::getLocalClusterInstance(), 'warningLimit' => INF]; } $this->type = $params['type']; $this->conditions = static::normalizeThrottleConditions($conditions); $this->cache = $params['cache']; $this->warningLimit = $params['warningLimit']; $this->setLogger(LoggerFactory::getInstance('throttler')); }
public function testAutoCreateUser() { global $wgGroupPermissions; $that = $this; \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff(); $this->setMwGlobals(array('wgMainCacheType' => __METHOD__)); $this->stashMwGlobals(array('wgGroupPermissions')); $wgGroupPermissions['*']['createaccount'] = true; $wgGroupPermissions['*']['autocreateaccount'] = false; // Replace the global singleton with one configured for testing $manager = $this->getManager(); $reset = TestUtils::setSessionManagerSingleton($manager); $logger = new \TestLogger(true, function ($m) { if (substr($m, 0, 15) === 'SessionBackend ') { // Don't care. return null; } $m = str_replace('MediaWiki\\Session\\SessionManager::autoCreateUser: '******'', $m); $m = preg_replace('/ - from: .*$/', ' - from: XXX', $m); return $m; }); $manager->setLogger($logger); $session = SessionManager::getGlobalSession(); // Can't create an already-existing user $user = User::newFromName('UTSysop'); $id = $user->getId(); $this->assertFalse($manager->autoCreateUser($user)); $this->assertSame($id, $user->getId()); $this->assertSame('UTSysop', $user->getName()); $this->assertSame(array(), $logger->getBuffer()); $logger->clearBuffer(); // Sanity check that creation works at all $user = User::newFromName('UTSessionAutoCreate1'); $this->assertSame(0, $user->getId(), 'sanity check'); $this->assertTrue($manager->autoCreateUser($user)); $this->assertNotEquals(0, $user->getId()); $this->assertSame('UTSessionAutoCreate1', $user->getName()); $this->assertEquals($user->getId(), User::idFromName('UTSessionAutoCreate1', User::READ_LATEST)); $this->assertSame(array(array(LogLevel::INFO, 'creating new user (UTSessionAutoCreate1) - from: XXX')), $logger->getBuffer()); $logger->clearBuffer(); // Check lack of permissions $wgGroupPermissions['*']['createaccount'] = false; $wgGroupPermissions['*']['autocreateaccount'] = false; $user = User::newFromName('UTDoesNotExist'); $this->assertFalse($manager->autoCreateUser($user)); $this->assertSame(0, $user->getId()); $this->assertNotSame('UTDoesNotExist', $user->getName()); $this->assertEquals(0, User::idFromName('UTDoesNotExist', User::READ_LATEST)); $session->clear(); $this->assertSame(array(array(LogLevel::DEBUG, 'user is blocked from this wiki, blacklisting')), $logger->getBuffer()); $logger->clearBuffer(); // Check other permission $wgGroupPermissions['*']['createaccount'] = false; $wgGroupPermissions['*']['autocreateaccount'] = true; $user = User::newFromName('UTSessionAutoCreate2'); $this->assertSame(0, $user->getId(), 'sanity check'); $this->assertTrue($manager->autoCreateUser($user)); $this->assertNotEquals(0, $user->getId()); $this->assertSame('UTSessionAutoCreate2', $user->getName()); $this->assertEquals($user->getId(), User::idFromName('UTSessionAutoCreate2', User::READ_LATEST)); $this->assertSame(array(array(LogLevel::INFO, 'creating new user (UTSessionAutoCreate2) - from: XXX')), $logger->getBuffer()); $logger->clearBuffer(); // Test account-creation block $anon = new User(); $block = new \Block(array('address' => $anon->getName(), 'user' => $id, 'reason' => __METHOD__, 'expiry' => time() + 100500, 'createAccount' => true)); $block->insert(); $this->assertInstanceOf('Block', $anon->isBlockedFromCreateAccount(), 'sanity check'); $reset2 = new \ScopedCallback(array($block, 'delete')); $user = User::newFromName('UTDoesNotExist'); $this->assertFalse($manager->autoCreateUser($user)); $this->assertSame(0, $user->getId()); $this->assertNotSame('UTDoesNotExist', $user->getName()); $this->assertEquals(0, User::idFromName('UTDoesNotExist', User::READ_LATEST)); \ScopedCallback::consume($reset2); $session->clear(); $this->assertSame(array(array(LogLevel::DEBUG, 'user is blocked from this wiki, blacklisting')), $logger->getBuffer()); $logger->clearBuffer(); // Sanity check that creation still works $user = User::newFromName('UTSessionAutoCreate3'); $this->assertSame(0, $user->getId(), 'sanity check'); $this->assertTrue($manager->autoCreateUser($user)); $this->assertNotEquals(0, $user->getId()); $this->assertSame('UTSessionAutoCreate3', $user->getName()); $this->assertEquals($user->getId(), User::idFromName('UTSessionAutoCreate3', User::READ_LATEST)); $this->assertSame(array(array(LogLevel::INFO, 'creating new user (UTSessionAutoCreate3) - from: XXX')), $logger->getBuffer()); $logger->clearBuffer(); // Test prevention by AuthPlugin global $wgAuth; $oldWgAuth = $wgAuth; $mockWgAuth = $this->getMock('AuthPlugin', array('autoCreate')); $mockWgAuth->expects($this->once())->method('autoCreate')->will($this->returnValue(false)); $this->setMwGlobals(array('wgAuth' => $mockWgAuth)); $user = User::newFromName('UTDoesNotExist'); $this->assertFalse($manager->autoCreateUser($user)); $this->assertSame(0, $user->getId()); $this->assertNotSame('UTDoesNotExist', $user->getName()); $this->assertEquals(0, User::idFromName('UTDoesNotExist', User::READ_LATEST)); $this->setMwGlobals(array('wgAuth' => $oldWgAuth)); $session->clear(); $this->assertSame(array(array(LogLevel::DEBUG, 'denied by AuthPlugin')), $logger->getBuffer()); $logger->clearBuffer(); // Test prevention by wfReadOnly() $this->setMwGlobals(array('wgReadOnly' => 'Because')); $user = User::newFromName('UTDoesNotExist'); $this->assertFalse($manager->autoCreateUser($user)); $this->assertSame(0, $user->getId()); $this->assertNotSame('UTDoesNotExist', $user->getName()); $this->assertEquals(0, User::idFromName('UTDoesNotExist', User::READ_LATEST)); $this->setMwGlobals(array('wgReadOnly' => false)); $session->clear(); $this->assertSame(array(array(LogLevel::DEBUG, 'denied by wfReadOnly()')), $logger->getBuffer()); $logger->clearBuffer(); // Test prevention by a previous session $session->set('MWSession::AutoCreateBlacklist', 'test'); $user = User::newFromName('UTDoesNotExist'); $this->assertFalse($manager->autoCreateUser($user)); $this->assertSame(0, $user->getId()); $this->assertNotSame('UTDoesNotExist', $user->getName()); $this->assertEquals(0, User::idFromName('UTDoesNotExist', User::READ_LATEST)); $session->clear(); $this->assertSame(array(array(LogLevel::DEBUG, 'blacklisted in session (test)')), $logger->getBuffer()); $logger->clearBuffer(); // Test uncreatable name $user = User::newFromName('UTDoesNotExist@'); $this->assertFalse($manager->autoCreateUser($user)); $this->assertSame(0, $user->getId()); $this->assertNotSame('UTDoesNotExist@', $user->getName()); $this->assertEquals(0, User::idFromName('UTDoesNotExist', User::READ_LATEST)); $session->clear(); $this->assertSame(array(array(LogLevel::DEBUG, 'Invalid username, blacklisting')), $logger->getBuffer()); $logger->clearBuffer(); // Test AbortAutoAccount hook $mock = $this->getMock(__CLASS__, array('onAbortAutoAccount')); $mock->expects($this->once())->method('onAbortAutoAccount')->will($this->returnCallback(function (User $user, &$msg) { $msg = 'No way!'; return false; })); $this->mergeMwGlobalArrayValue('wgHooks', array('AbortAutoAccount' => array($mock))); $user = User::newFromName('UTDoesNotExist'); $this->assertFalse($manager->autoCreateUser($user)); $this->assertSame(0, $user->getId()); $this->assertNotSame('UTDoesNotExist', $user->getName()); $this->assertEquals(0, User::idFromName('UTDoesNotExist', User::READ_LATEST)); $this->mergeMwGlobalArrayValue('wgHooks', array('AbortAutoAccount' => array())); $session->clear(); $this->assertSame(array(array(LogLevel::DEBUG, 'denied by hook: No way!')), $logger->getBuffer()); $logger->clearBuffer(); // Test AbortAutoAccount hook screwing up the name $mock = $this->getMock('stdClass', array('onAbortAutoAccount')); $mock->expects($this->once())->method('onAbortAutoAccount')->will($this->returnCallback(function (User $user) { $user->setName('UTDoesNotExistEither'); })); $this->mergeMwGlobalArrayValue('wgHooks', array('AbortAutoAccount' => array($mock))); try { $user = User::newFromName('UTDoesNotExist'); $manager->autoCreateUser($user); $this->fail('Expected exception not thrown'); } catch (\UnexpectedValueException $ex) { $this->assertSame('AbortAutoAccount hook tried to change the user name', $ex->getMessage()); } $this->assertSame(0, $user->getId()); $this->assertNotSame('UTDoesNotExist', $user->getName()); $this->assertNotSame('UTDoesNotExistEither', $user->getName()); $this->assertEquals(0, User::idFromName('UTDoesNotExist', User::READ_LATEST)); $this->assertEquals(0, User::idFromName('UTDoesNotExistEither', User::READ_LATEST)); $this->mergeMwGlobalArrayValue('wgHooks', array('AbortAutoAccount' => array())); $session->clear(); $this->assertSame(array(), $logger->getBuffer()); $logger->clearBuffer(); // Test for "exception backoff" $user = User::newFromName('UTDoesNotExist'); $cache = \ObjectCache::getLocalClusterInstance(); $backoffKey = wfMemcKey('MWSession', 'autocreate-failed', md5($user->getName())); $cache->set($backoffKey, 1, 60 * 10); $this->assertFalse($manager->autoCreateUser($user)); $this->assertSame(0, $user->getId()); $this->assertNotSame('UTDoesNotExist', $user->getName()); $this->assertEquals(0, User::idFromName('UTDoesNotExist', User::READ_LATEST)); $cache->delete($backoffKey); $session->clear(); $this->assertSame(array(array(LogLevel::DEBUG, 'denied by prior creation attempt failures')), $logger->getBuffer()); $logger->clearBuffer(); // Sanity check that creation still works, and test completion hook $cb = $this->callback(function (User $user) use($that) { $that->assertNotEquals(0, $user->getId()); $that->assertSame('UTSessionAutoCreate4', $user->getName()); $that->assertEquals($user->getId(), User::idFromName('UTSessionAutoCreate4', User::READ_LATEST)); return true; }); $mock = $this->getMock('stdClass', array('onAuthPluginAutoCreate', 'onLocalUserCreated')); $mock->expects($this->once())->method('onAuthPluginAutoCreate')->with($cb); $mock->expects($this->once())->method('onLocalUserCreated')->with($cb, $this->identicalTo(true)); $this->mergeMwGlobalArrayValue('wgHooks', array('AuthPluginAutoCreate' => array($mock), 'LocalUserCreated' => array($mock))); $user = User::newFromName('UTSessionAutoCreate4'); $this->assertSame(0, $user->getId(), 'sanity check'); $this->assertTrue($manager->autoCreateUser($user)); $this->assertNotEquals(0, $user->getId()); $this->assertSame('UTSessionAutoCreate4', $user->getName()); $this->assertEquals($user->getId(), User::idFromName('UTSessionAutoCreate4', User::READ_LATEST)); $this->mergeMwGlobalArrayValue('wgHooks', array('AuthPluginAutoCreate' => array(), 'LocalUserCreated' => array())); $this->assertSame(array(array(LogLevel::INFO, 'creating new user (UTSessionAutoCreate4) - from: XXX')), $logger->getBuffer()); $logger->clearBuffer(); }
/** * Check that a prepared edit is in cache and still up-to-date * * This method blocks if the prepared edit is already being rendered, * waiting until rendering finishes before doing final validity checks. * * The cache is rejected if template or file changes are detected. * Note that foreign template or file transclusions are not checked. * * The result is a map (pstContent,output,timestamp) with fields * extracted directly from WikiPage::prepareContentForEdit(). * * @param Title $title * @param Content $content * @param User $user User to get parser options from * @return stdClass|bool Returns false on cache miss */ public static function checkCache(Title $title, Content $content, User $user) { if ($user->isBot()) { return false; // bots never stash - don't pollute stats } $cache = ObjectCache::getLocalClusterInstance(); $logger = LoggerFactory::getInstance('StashEdit'); $stats = RequestContext::getMain()->getStats(); $key = self::getStashKey($title, $content, $user); $editInfo = $cache->get($key); if (!is_object($editInfo)) { $start = microtime(true); // We ignore user aborts and keep parsing. Block on any prior parsing // so as to use its results and make use of the time spent parsing. // Skip this logic if there no master connection in case this method // is called on an HTTP GET request for some reason. $lb = wfGetLB(); $dbw = $lb->getAnyOpenConnection($lb->getWriterIndex()); if ($dbw && $dbw->lock($key, __METHOD__, 30)) { $editInfo = $cache->get($key); $dbw->unlock($key, __METHOD__); } $timeMs = 1000 * max(0, microtime(true) - $start); $stats->timing('editstash.lock_wait_time', $timeMs); } if (!is_object($editInfo) || !$editInfo->output) { $stats->increment('editstash.cache_misses.no_stash'); $logger->debug("No cache value for key '{$key}'."); return false; } $age = time() - wfTimestamp(TS_UNIX, $editInfo->output->getCacheTime()); if ($age <= self::PRESUME_FRESH_TTL_SEC) { $stats->increment('editstash.cache_hits.presumed_fresh'); $logger->debug("Timestamp-based cache hit for key '{$key}' (age: {$age} sec)."); return $editInfo; // assume nothing changed } elseif (isset($editInfo->edits) && $editInfo->edits === $user->getEditCount()) { // Logged-in user made no local upload/template edits in the meantime $stats->increment('editstash.cache_hits.presumed_fresh'); $logger->debug("Edit count based cache hit for key '{$key}' (age: {$age} sec)."); return $editInfo; } elseif ($user->isAnon() && self::lastEditTime($user) < $editInfo->output->getCacheTime()) { // Logged-out user made no local upload/template edits in the meantime $stats->increment('editstash.cache_hits.presumed_fresh'); $logger->debug("Edit check based cache hit for key '{$key}' (age: {$age} sec)."); return $editInfo; } $dbr = wfGetDB(DB_SLAVE); $templates = []; // conditions to find changes/creations $templateUses = 0; // expected existing templates foreach ($editInfo->output->getTemplateIds() as $ns => $stuff) { foreach ($stuff as $dbkey => $revId) { $templates[(string) $ns][$dbkey] = (int) $revId; ++$templateUses; } } // Check that no templates used in the output changed... if (count($templates)) { $res = $dbr->select('page', ['ns' => 'page_namespace', 'dbk' => 'page_title', 'page_latest'], $dbr->makeWhereFrom2d($templates, 'page_namespace', 'page_title'), __METHOD__); $changed = false; foreach ($res as $row) { $changed = $changed || $row->page_latest != $templates[$row->ns][$row->dbk]; } if ($changed || $res->numRows() != $templateUses) { $stats->increment('editstash.cache_misses.proven_stale'); $logger->info("Stale cache for key '{$key}'; template changed. (age: {$age} sec)"); return false; } } $files = []; // conditions to find changes/creations foreach ($editInfo->output->getFileSearchOptions() as $name => $options) { $files[$name] = (string) $options['sha1']; } // Check that no files used in the output changed... if (count($files)) { $res = $dbr->select('image', ['name' => 'img_name', 'img_sha1'], ['img_name' => array_keys($files)], __METHOD__); $changed = false; foreach ($res as $row) { $changed = $changed || $row->img_sha1 != $files[$row->name]; } if ($changed || $res->numRows() != count($files)) { $stats->increment('editstash.cache_misses.proven_stale'); $logger->info("Stale cache for key '{$key}'; file changed. (age: {$age} sec)"); return false; } } $stats->increment('editstash.cache_hits.proven_fresh'); $logger->debug("Verified cache hit for key '{$key}' (age: {$age} sec)."); return $editInfo; }
/** * @see FileBackendStore::__construct() * Additional $config params include: * - swiftAuthUrl : Swift authentication server URL * - swiftUser : Swift user used by MediaWiki (account:username) * - swiftKey : Swift authentication key for the above user * - swiftAuthTTL : Swift authentication TTL (seconds) * - swiftTempUrlKey : Swift "X-Account-Meta-Temp-URL-Key" value on the account. * Do not set this until it has been set in the backend. * - shardViaHashLevels : Map of container names to sharding config with: * - base : base of hash characters, 16 or 36 * - levels : the number of hash levels (and digits) * - repeat : hash subdirectories are prefixed with all the * parent hash directory names (e.g. "a/ab/abc") * - cacheAuthInfo : Whether to cache authentication tokens in APC, XCache, ect. * If those are not available, then the main cache will be used. * This is probably insecure in shared hosting environments. * - rgwS3AccessKey : Rados Gateway S3 "access key" value on the account. * Do not set this until it has been set in the backend. * This is used for generating expiring pre-authenticated URLs. * Only use this when using rgw and to work around * http://tracker.newdream.net/issues/3454. * - rgwS3SecretKey : Rados Gateway S3 "secret key" value on the account. * Do not set this until it has been set in the backend. * This is used for generating expiring pre-authenticated URLs. * Only use this when using rgw and to work around * http://tracker.newdream.net/issues/3454. */ public function __construct(array $config) { parent::__construct($config); // Required settings $this->swiftAuthUrl = $config['swiftAuthUrl']; $this->swiftUser = $config['swiftUser']; $this->swiftKey = $config['swiftKey']; // Optional settings $this->authTTL = isset($config['swiftAuthTTL']) ? $config['swiftAuthTTL'] : 15 * 60; // some sane number $this->swiftTempUrlKey = isset($config['swiftTempUrlKey']) ? $config['swiftTempUrlKey'] : ''; $this->shardViaHashLevels = isset($config['shardViaHashLevels']) ? $config['shardViaHashLevels'] : ''; $this->rgwS3AccessKey = isset($config['rgwS3AccessKey']) ? $config['rgwS3AccessKey'] : ''; $this->rgwS3SecretKey = isset($config['rgwS3SecretKey']) ? $config['rgwS3SecretKey'] : ''; // HTTP helper client $this->http = new MultiHttpClient(array()); // Cache container information to mask latency if (isset($config['wanCache']) && $config['wanCache'] instanceof WANObjectCache) { $this->memCache = $config['wanCache']; } // Process cache for container info $this->containerStatCache = new ProcessCacheLRU(300); // Cache auth token information to avoid RTTs if (!empty($config['cacheAuthInfo'])) { if (PHP_SAPI === 'cli') { // Preferrably memcached $this->srvCache = ObjectCache::getLocalClusterInstance(); } else { // Look for APC, XCache, WinCache, ect... $this->srvCache = ObjectCache::getLocalServerInstance(CACHE_NONE); } } else { $this->srvCache = new EmptyBagOStuff(); } }
/** * Primitive rate limits: enforce maximum actions per time period * to put a brake on flooding. * * The method generates both a generic profiling point and a per action one * (suffix being "-$action". * * @note When using a shared cache like memcached, IP-address * last-hit counters will be shared across wikis. * * @param string $action Action to enforce; 'edit' if unspecified * @param int $incrBy Positive amount to increment counter by [defaults to 1] * @return bool True if a rate limiter was tripped */ public function pingLimiter($action = 'edit', $incrBy = 1) { // Call the 'PingLimiter' hook $result = false; if (!Hooks::run('PingLimiter', array(&$this, $action, &$result, $incrBy))) { return $result; } global $wgRateLimits; if (!isset($wgRateLimits[$action])) { return false; } // Some groups shouldn't trigger the ping limiter, ever if (!$this->isPingLimitable()) { return false; } $limits = $wgRateLimits[$action]; $keys = array(); $id = $this->getId(); $userLimit = false; if (isset($limits['anon']) && $id == 0) { $keys[wfMemcKey('limiter', $action, 'anon')] = $limits['anon']; } if (isset($limits['user']) && $id != 0) { $userLimit = $limits['user']; } if ($this->isNewbie()) { if (isset($limits['newbie']) && $id != 0) { $keys[wfMemcKey('limiter', $action, 'user', $id)] = $limits['newbie']; } if (isset($limits['ip'])) { $ip = $this->getRequest()->getIP(); $keys["mediawiki:limiter:{$action}:ip:{$ip}"] = $limits['ip']; } if (isset($limits['subnet'])) { $ip = $this->getRequest()->getIP(); $matches = array(); $subnet = false; if (IP::isIPv6($ip)) { $parts = IP::parseRange("{$ip}/64"); $subnet = $parts[0]; } elseif (preg_match('/^(\\d+\\.\\d+\\.\\d+)\\.\\d+$/', $ip, $matches)) { // IPv4 $subnet = $matches[1]; } if ($subnet !== false) { $keys["mediawiki:limiter:{$action}:subnet:{$subnet}"] = $limits['subnet']; } } } // Check for group-specific permissions // If more than one group applies, use the group with the highest limit foreach ($this->getGroups() as $group) { if (isset($limits[$group])) { if ($userLimit === false || $limits[$group][0] / $limits[$group][1] > $userLimit[0] / $userLimit[1]) { $userLimit = $limits[$group]; } } } // Set the user limit key if ($userLimit !== false) { list($max, $period) = $userLimit; wfDebug(__METHOD__ . ": effective user limit: {$max} in {$period}s\n"); $keys[wfMemcKey('limiter', $action, 'user', $id)] = $userLimit; } $cache = ObjectCache::getLocalClusterInstance(); $triggered = false; foreach ($keys as $key => $limit) { list($max, $period) = $limit; $summary = "(limit {$max} in {$period}s)"; $count = $cache->get($key); // Already pinged? if ($count) { if ($count >= $max) { wfDebugLog('ratelimit', "User '{$this->getName()}' " . "(IP {$this->getRequest()->getIP()}) tripped {$key} at {$count} {$summary}"); $triggered = true; } else { wfDebug(__METHOD__ . ": ok. {$key} at {$count} {$summary}\n"); } } else { wfDebug(__METHOD__ . ": adding record for {$key} {$summary}\n"); if ($incrBy > 0) { $cache->add($key, 0, intval($period)); // first ping } } if ($incrBy > 0) { $cache->incr($key, $incrBy); } } return $triggered; }
public function testAutoAccountCreation() { global $wgGroupPermissions, $wgHooks; // PHPUnit seems to have a bug where it will call the ->with() // callbacks for our hooks again after the test is run (WTF?), which // breaks here because $username no longer matches $user by the end of // the testing. $workaroundPHPUnitBug = false; $username = self::usernameForCreation(); $this->initializeManager(); $this->stashMwGlobals(['wgGroupPermissions']); $wgGroupPermissions['*']['createaccount'] = true; $wgGroupPermissions['*']['autocreateaccount'] = false; \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff(); $this->setMwGlobals(['wgMainCacheType' => __METHOD__]); // Set up lots of mocks... $mocks = []; foreach (['pre', 'primary', 'secondary'] as $key) { $class = ucfirst($key) . 'AuthenticationProvider'; $mocks[$key] = $this->getMockForAbstractClass("MediaWiki\\Auth\\{$class}", [], "Mock{$class}"); $mocks[$key]->expects($this->any())->method('getUniqueId')->will($this->returnValue($key)); } $good = StatusValue::newGood(); $callback = $this->callback(function ($user) use(&$username, &$workaroundPHPUnitBug) { return $workaroundPHPUnitBug || $user->getName() === $username; }); $mocks['pre']->expects($this->exactly(12))->method('testUserForCreation')->with($callback, $this->identicalTo(AuthManager::AUTOCREATE_SOURCE_SESSION))->will($this->onConsecutiveCalls(StatusValue::newFatal('ok'), StatusValue::newFatal('ok'), StatusValue::newFatal('fail-in-pre'), $good, $good, $good, $good, $good, $good, $good, $good, $good)); $mocks['primary']->expects($this->any())->method('accountCreationType')->will($this->returnValue(PrimaryAuthenticationProvider::TYPE_CREATE)); $mocks['primary']->expects($this->any())->method('testUserExists')->will($this->returnValue(true)); $mocks['primary']->expects($this->exactly(9))->method('testUserForCreation')->with($callback, $this->identicalTo(AuthManager::AUTOCREATE_SOURCE_SESSION))->will($this->onConsecutiveCalls(StatusValue::newFatal('fail-in-primary'), $good, $good, $good, $good, $good, $good, $good, $good)); $mocks['primary']->expects($this->exactly(3))->method('autoCreatedAccount')->with($callback, $this->identicalTo(AuthManager::AUTOCREATE_SOURCE_SESSION)); $mocks['secondary']->expects($this->exactly(8))->method('testUserForCreation')->with($callback, $this->identicalTo(AuthManager::AUTOCREATE_SOURCE_SESSION))->will($this->onConsecutiveCalls(StatusValue::newFatal('fail-in-secondary'), $good, $good, $good, $good, $good, $good, $good)); $mocks['secondary']->expects($this->exactly(3))->method('autoCreatedAccount')->with($callback, $this->identicalTo(AuthManager::AUTOCREATE_SOURCE_SESSION)); $this->preauthMocks = [$mocks['pre']]; $this->primaryauthMocks = [$mocks['primary']]; $this->secondaryauthMocks = [$mocks['secondary']]; $this->initializeManager(true); $session = $this->request->getSession(); $logger = new \TestLogger(true, function ($m) { $m = str_replace('MediaWiki\\Auth\\AuthManager::autoCreateUser: '******'', $m); return $m; }); $this->manager->setLogger($logger); try { $user = \User::newFromName('UTSysop'); $this->manager->autoCreateUser($user, 'InvalidSource', true); $this->fail('Expected exception not thrown'); } catch (\InvalidArgumentException $ex) { $this->assertSame('Unknown auto-creation source: InvalidSource', $ex->getMessage()); } // First, check an existing user $session->clear(); $user = \User::newFromName('UTSysop'); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $expect = \Status::newGood(); $expect->warning('userexists'); $this->assertEquals($expect, $ret); $this->assertNotEquals(0, $user->getId()); $this->assertSame('UTSysop', $user->getName()); $this->assertEquals($user->getId(), $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, '{username} already exists locally']], $logger->getBuffer()); $logger->clearBuffer(); $session->clear(); $user = \User::newFromName('UTSysop'); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, false); $this->unhook('LocalUserCreated'); $expect = \Status::newGood(); $expect->warning('userexists'); $this->assertEquals($expect, $ret); $this->assertNotEquals(0, $user->getId()); $this->assertSame('UTSysop', $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, '{username} already exists locally']], $logger->getBuffer()); $logger->clearBuffer(); // Wiki is read-only $session->clear(); $this->setMwGlobals(['wgReadOnly' => 'Because']); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('readonlytext', 'Because'), $ret); $this->assertEquals(0, $user->getId()); $this->assertNotEquals($username, $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, 'denied by wfReadOnly(): {reason}']], $logger->getBuffer()); $logger->clearBuffer(); $this->setMwGlobals(['wgReadOnly' => false]); // Session blacklisted $session->clear(); $session->set('AuthManager::AutoCreateBlacklist', 'test'); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('test'), $ret); $this->assertEquals(0, $user->getId()); $this->assertNotEquals($username, $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, 'blacklisted in session {sessionid}']], $logger->getBuffer()); $logger->clearBuffer(); $session->clear(); $session->set('AuthManager::AutoCreateBlacklist', StatusValue::newFatal('test2')); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('test2'), $ret); $this->assertEquals(0, $user->getId()); $this->assertNotEquals($username, $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, 'blacklisted in session {sessionid}']], $logger->getBuffer()); $logger->clearBuffer(); // Uncreatable name $session->clear(); $user = \User::newFromName($username . '@'); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('noname'), $ret); $this->assertEquals(0, $user->getId()); $this->assertNotEquals($username . '@', $user->getId()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, 'name "{username}" is not creatable']], $logger->getBuffer()); $logger->clearBuffer(); $this->assertSame('noname', $session->get('AuthManager::AutoCreateBlacklist')); // IP unable to create accounts $wgGroupPermissions['*']['createaccount'] = false; $wgGroupPermissions['*']['autocreateaccount'] = false; $session->clear(); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('authmanager-autocreate-noperm'), $ret); $this->assertEquals(0, $user->getId()); $this->assertNotEquals($username, $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, 'IP lacks the ability to create or autocreate accounts']], $logger->getBuffer()); $logger->clearBuffer(); $this->assertSame('authmanager-autocreate-noperm', $session->get('AuthManager::AutoCreateBlacklist')); // Test that both permutations of permissions are allowed // (this hits the two "ok" entries in $mocks['pre']) $wgGroupPermissions['*']['createaccount'] = false; $wgGroupPermissions['*']['autocreateaccount'] = true; $session->clear(); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('ok'), $ret); $wgGroupPermissions['*']['createaccount'] = true; $wgGroupPermissions['*']['autocreateaccount'] = false; $session->clear(); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('ok'), $ret); $logger->clearBuffer(); // Test lock fail $session->clear(); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->never()); $cache = \ObjectCache::getLocalClusterInstance(); $lock = $cache->getScopedLock($cache->makeGlobalKey('account', md5($username))); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); unset($lock); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('usernameinprogress'), $ret); $this->assertEquals(0, $user->getId()); $this->assertNotEquals($username, $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, 'Could not acquire account creation lock']], $logger->getBuffer()); $logger->clearBuffer(); // Test pre-authentication provider fail $session->clear(); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('fail-in-pre'), $ret); $this->assertEquals(0, $user->getId()); $this->assertNotEquals($username, $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}']], $logger->getBuffer()); $logger->clearBuffer(); $this->assertEquals(StatusValue::newFatal('fail-in-pre'), $session->get('AuthManager::AutoCreateBlacklist')); $session->clear(); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('fail-in-primary'), $ret); $this->assertEquals(0, $user->getId()); $this->assertNotEquals($username, $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}']], $logger->getBuffer()); $logger->clearBuffer(); $this->assertEquals(StatusValue::newFatal('fail-in-primary'), $session->get('AuthManager::AutoCreateBlacklist')); $session->clear(); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('fail-in-secondary'), $ret); $this->assertEquals(0, $user->getId()); $this->assertNotEquals($username, $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}']], $logger->getBuffer()); $logger->clearBuffer(); $this->assertEquals(StatusValue::newFatal('fail-in-secondary'), $session->get('AuthManager::AutoCreateBlacklist')); // Test backoff $cache = \ObjectCache::getLocalClusterInstance(); $backoffKey = wfMemcKey('AuthManager', 'autocreate-failed', md5($username)); $cache->set($backoffKey, true); $session->clear(); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->never()); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newFatal('authmanager-autocreate-exception'), $ret); $this->assertEquals(0, $user->getId()); $this->assertNotEquals($username, $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::DEBUG, '{username} denied by prior creation attempt failures']], $logger->getBuffer()); $logger->clearBuffer(); $this->assertSame(null, $session->get('AuthManager::AutoCreateBlacklist')); $cache->delete($backoffKey); // Test addToDatabase fails $session->clear(); $user = $this->getMock('User', ['addToDatabase']); $user->expects($this->once())->method('addToDatabase')->will($this->returnValue(\Status::newFatal('because'))); $user->setName($username); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->assertEquals(\Status::newFatal('because'), $ret); $this->assertEquals(0, $user->getId()); $this->assertNotEquals($username, $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::INFO, 'creating new user ({username}) - from: {from}'], [LogLevel::ERROR, '{username} failed with message {message}']], $logger->getBuffer()); $logger->clearBuffer(); $this->assertSame(null, $session->get('AuthManager::AutoCreateBlacklist')); // Test addToDatabase throws an exception $cache = \ObjectCache::getLocalClusterInstance(); $backoffKey = wfMemcKey('AuthManager', 'autocreate-failed', md5($username)); $this->assertFalse($cache->get($backoffKey), 'sanity check'); $session->clear(); $user = $this->getMock('User', ['addToDatabase']); $user->expects($this->once())->method('addToDatabase')->will($this->throwException(new \Exception('Excepted'))); $user->setName($username); try { $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->fail('Expected exception not thrown'); } catch (\Exception $ex) { $this->assertSame('Excepted', $ex->getMessage()); } $this->assertEquals(0, $user->getId()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::INFO, 'creating new user ({username}) - from: {from}'], [LogLevel::ERROR, '{username} failed with exception {exception}']], $logger->getBuffer()); $logger->clearBuffer(); $this->assertSame(null, $session->get('AuthManager::AutoCreateBlacklist')); $this->assertNotEquals(false, $cache->get($backoffKey)); $cache->delete($backoffKey); // Test addToDatabase fails because the user already exists. $session->clear(); $user = $this->getMock('User', ['addToDatabase']); $user->expects($this->once())->method('addToDatabase')->will($this->returnCallback(function () use($username) { $status = \User::newFromName($username)->addToDatabase(); $this->assertTrue($status->isOK(), 'sanity check'); return \Status::newFatal('userexists'); })); $user->setName($username); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $expect = \Status::newGood(); $expect->warning('userexists'); $this->assertEquals($expect, $ret); $this->assertNotEquals(0, $user->getId()); $this->assertEquals($username, $user->getName()); $this->assertEquals($user->getId(), $session->getUser()->getId()); $this->assertSame([[LogLevel::INFO, 'creating new user ({username}) - from: {from}'], [LogLevel::INFO, '{username} already exists locally (race)']], $logger->getBuffer()); $logger->clearBuffer(); $this->assertSame(null, $session->get('AuthManager::AutoCreateBlacklist')); // Success! $session->clear(); $username = self::usernameForCreation(); $user = \User::newFromName($username); $this->hook('AuthPluginAutoCreate', $this->once())->with($callback); $this->hideDeprecated('AuthPluginAutoCreate hook (used in ' . get_class($wgHooks['AuthPluginAutoCreate'][0]) . '::onAuthPluginAutoCreate)'); $this->hook('LocalUserCreated', $this->once())->with($callback, $this->equalTo(true)); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, true); $this->unhook('LocalUserCreated'); $this->unhook('AuthPluginAutoCreate'); $this->assertEquals(\Status::newGood(), $ret); $this->assertNotEquals(0, $user->getId()); $this->assertEquals($username, $user->getName()); $this->assertEquals($user->getId(), $session->getUser()->getId()); $this->assertSame([[LogLevel::INFO, 'creating new user ({username}) - from: {from}']], $logger->getBuffer()); $logger->clearBuffer(); $dbw = wfGetDB(DB_MASTER); $maxLogId = $dbw->selectField('logging', 'MAX(log_id)', ['log_type' => 'newusers']); $session->clear(); $username = self::usernameForCreation(); $user = \User::newFromName($username); $this->hook('LocalUserCreated', $this->once())->with($callback, $this->equalTo(true)); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, false); $this->unhook('LocalUserCreated'); $this->assertEquals(\Status::newGood(), $ret); $this->assertNotEquals(0, $user->getId()); $this->assertEquals($username, $user->getName()); $this->assertEquals(0, $session->getUser()->getId()); $this->assertSame([[LogLevel::INFO, 'creating new user ({username}) - from: {from}']], $logger->getBuffer()); $logger->clearBuffer(); $this->assertSame($maxLogId, $dbw->selectField('logging', 'MAX(log_id)', ['log_type' => 'newusers'])); $this->config->set('NewUserLog', true); $session->clear(); $username = self::usernameForCreation(); $user = \User::newFromName($username); $ret = $this->manager->autoCreateUser($user, AuthManager::AUTOCREATE_SOURCE_SESSION, false); $this->assertEquals(\Status::newGood(), $ret); $logger->clearBuffer(); $data = \DatabaseLogEntry::getSelectQueryData(); $rows = iterator_to_array($dbw->select($data['tables'], $data['fields'], ['log_id > ' . (int) $maxLogId, 'log_type' => 'newusers'] + $data['conds'], __METHOD__, $data['options'], $data['join_conds'])); $this->assertCount(1, $rows); $entry = \DatabaseLogEntry::newFromRow(reset($rows)); $this->assertSame('autocreate', $entry->getSubtype()); $this->assertSame($user->getId(), $entry->getPerformer()->getId()); $this->assertSame($user->getName(), $entry->getPerformer()->getName()); $this->assertSame($user->getUserPage()->getFullText(), $entry->getTarget()->getFullText()); $this->assertSame(['4::userid' => $user->getId()], $entry->getParameters()); $workaroundPHPUnitBug = true; }
/** * Make a cache key with database-agnostic prefix. * * Doesn't have a wiki-specific namespace. Uses a generic 'global' prefix * instead. Must have a prefix as otherwise keys that use a database name * in the first segment will clash with wfMemcKey/wfForeignMemcKey. * * @since 1.26 * @param string $args,... * @return string */ function wfGlobalCacheKey() { return call_user_func_array(array(ObjectCache::getLocalClusterInstance(), 'makeGlobalKey'), func_get_args()); }
/** * Roughly gets the cache misses in the last hour by unique visitors * @return int */ public function getMissesRecent() { $cache = ObjectCache::getLocalClusterInstance(); return self::MISS_FACTOR * $cache->get($this->cacheMissKey()); }
/** * @param string $name * @return mixed */ private function getCachedConfigVar($name) { global $wgConf; if ($this->wiki === wfWikiID()) { return $GLOBALS[$name]; // common case } else { $cache = ObjectCache::getLocalClusterInstance(); list($db, $prefix) = wfSplitWikiID($this->wiki); $key = wfForeignMemcKey($db, $prefix, 'configvalue', $name); $value = $cache->get($key); // ('v' => ...) or false if (is_array($value)) { return $value['v']; } else { $value = $wgConf->getConfig($this->wiki, $name); $cache->set($key, array('v' => $value), $cache::TTL_DAY + mt_rand(0, $cache::TTL_DAY)); return $value; } } }
/** * Actually try to generate a new thumbnail * * @param File $file * @param array $params * @param string $thumbName * @param string $thumbPath * @return array (MediaTransformOutput|bool, string|bool error message HTML) */ function wfGenerateThumbnail(File $file, array $params, $thumbName, $thumbPath) { global $wgAttemptFailureEpoch; $cache = ObjectCache::getLocalClusterInstance(); $key = $cache->makeKey('attempt-failures', $wgAttemptFailureEpoch, $file->getRepo()->getName(), $file->getSha1(), md5($thumbName)); // Check if this file keeps failing to render if ($cache->get($key) >= 4) { return array(false, wfMessage('thumbnail_image-failure-limit', 4)); } $done = false; // Record failures on PHP fatals in addition to caching exceptions register_shutdown_function(function () use($cache, &$done, $key) { if (!$done) { // transform() gave a fatal // Randomize TTL to reduce stampedes $cache->incrWithInit($key, $cache::TTL_HOUR + mt_rand(0, 300)); } }); $thumb = false; $errorHtml = false; // guard thumbnail rendering with PoolCounter to avoid stampedes // expensive files use a separate PoolCounter config so it is possible // to set up a global limit on them if ($file->isExpensiveToThumbnail()) { $poolCounterType = 'FileRenderExpensive'; } else { $poolCounterType = 'FileRender'; } // Thumbnail isn't already there, so create the new thumbnail... try { $work = new PoolCounterWorkViaCallback($poolCounterType, sha1($file->getName()), array('doWork' => function () use($file, $params) { return $file->transform($params, File::RENDER_NOW); }, 'doCachedWork' => function () use($file, $params, $thumbPath) { // If the worker that finished made this thumbnail then use it. // Otherwise, it probably made a different thumbnail for this file. return $file->getRepo()->fileExists($thumbPath) ? $file->transform($params, File::RENDER_NOW) : false; // retry once more in exclusive mode }, 'error' => function (Status $status) { return wfMessage('generic-pool-error')->parse() . '<hr>' . $status->getHTML(); })); $result = $work->execute(); if ($result instanceof MediaTransformOutput) { $thumb = $result; } elseif (is_string($result)) { // error $errorHtml = $result; } } catch (Exception $e) { // Tried to select a page on a non-paged file? } /** @noinspection PhpUnusedLocalVariableInspection */ $done = true; // no PHP fatal occured if (!$thumb || $thumb->isError()) { // Randomize TTL to reduce stampedes $cache->incrWithInit($key, $cache::TTL_HOUR + mt_rand(0, 300)); } return array($thumb, $errorHtml); }
/** * @param array $params * - accountCreationThrottle: (array) Condition array for the account creation throttle; an array * of arrays in a format like $wgPasswordAttemptThrottle, passed to the Throttler constructor. * - passwordAttemptThrottle: (array) Condition array for the password attempt throttle, in the * same format as accountCreationThrottle. * - cache: (BagOStuff) Where to store the throttle, defaults to the local cluster instance. */ public function __construct($params = []) { $this->throttleSettings = array_intersect_key($params, ['accountCreationThrottle' => true, 'passwordAttemptThrottle' => true]); $this->cache = isset($params['cache']) ? $params['cache'] : \ObjectCache::getLocalClusterInstance(); }
/** * Auto-create an account, and log into that account * @param User $user User to auto-create * @param string $source What caused the auto-creation? This must be the ID * of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION. * @param bool $login Whether to also log the user in * @return Status Good if user was created, Ok if user already existed, otherwise Fatal */ public function autoCreateUser(User $user, $source, $login = true) { if ($source !== self::AUTOCREATE_SOURCE_SESSION && !$this->getAuthenticationProvider($source) instanceof PrimaryAuthenticationProvider) { throw new \InvalidArgumentException("Unknown auto-creation source: {$source}"); } $username = $user->getName(); // Try the local user from the slave DB $localId = User::idFromName($username); $flags = User::READ_NORMAL; // Fetch the user ID from the master, so that we don't try to create the user // when they already exist, due to replication lag // @codeCoverageIgnoreStart if (!$localId && wfGetLB()->getReaderIndex() != 0) { $localId = User::idFromName($username, User::READ_LATEST); $flags = User::READ_LATEST; } // @codeCoverageIgnoreEnd if ($localId) { $this->logger->debug(__METHOD__ . ': {username} already exists locally', ['username' => $username]); $user->setId($localId); $user->loadFromId($flags); if ($login) { $this->setSessionDataForUser($user); } $status = Status::newGood(); $status->warning('userexists'); return $status; } // Wiki is read-only? if (wfReadOnly()) { $this->logger->debug(__METHOD__ . ': denied by wfReadOnly(): {reason}', ['username' => $username, 'reason' => wfReadOnlyReason()]); $user->setId(0); $user->loadFromId(); return Status::newFatal('readonlytext', wfReadOnlyReason()); } // Check the session, if we tried to create this user already there's // no point in retrying. $session = $this->request->getSession(); if ($session->get('AuthManager::AutoCreateBlacklist')) { $this->logger->debug(__METHOD__ . ': blacklisted in session {sessionid}', ['username' => $username, 'sessionid' => $session->getId()]); $user->setId(0); $user->loadFromId(); $reason = $session->get('AuthManager::AutoCreateBlacklist'); if ($reason instanceof StatusValue) { return Status::wrap($reason); } else { return Status::newFatal($reason); } } // Is the username creatable? if (!User::isCreatableName($username)) { $this->logger->debug(__METHOD__ . ': name "{username}" is not creatable', ['username' => $username]); $session->set('AuthManager::AutoCreateBlacklist', 'noname', 600); $user->setId(0); $user->loadFromId(); return Status::newFatal('noname'); } // Is the IP user able to create accounts? $anon = new User(); if (!$anon->isAllowedAny('createaccount', 'autocreateaccount')) { $this->logger->debug(__METHOD__ . ': IP lacks the ability to create or autocreate accounts', ['username' => $username, 'ip' => $anon->getName()]); $session->set('AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm', 600); $session->persist(); $user->setId(0); $user->loadFromId(); return Status::newFatal('authmanager-autocreate-noperm'); } // Avoid account creation races on double submissions $cache = \ObjectCache::getLocalClusterInstance(); $lock = $cache->getScopedLock($cache->makeGlobalKey('account', md5($username))); if (!$lock) { $this->logger->debug(__METHOD__ . ': Could not acquire account creation lock', ['user' => $username]); $user->setId(0); $user->loadFromId(); return Status::newFatal('usernameinprogress'); } // Denied by providers? $providers = $this->getPreAuthenticationProviders() + $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders(); foreach ($providers as $provider) { $status = $provider->testUserForCreation($user, $source); if (!$status->isGood()) { $ret = Status::wrap($status); $this->logger->debug(__METHOD__ . ': Provider denied creation of {username}: {reason}', ['username' => $username, 'reason' => $ret->getWikiText(null, null, 'en')]); $session->set('AuthManager::AutoCreateBlacklist', $status, 600); $user->setId(0); $user->loadFromId(); return $ret; } } // Ignore warnings about master connections/writes...hard to avoid here \Profiler::instance()->getTransactionProfiler()->resetExpectations(); $backoffKey = wfMemcKey('AuthManager', 'autocreate-failed', md5($username)); if ($cache->get($backoffKey)) { $this->logger->debug(__METHOD__ . ': {username} denied by prior creation attempt failures', ['username' => $username]); $user->setId(0); $user->loadFromId(); return Status::newFatal('authmanager-autocreate-exception'); } // Checks passed, create the user... $from = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : 'CLI'; $this->logger->info(__METHOD__ . ': creating new user ({username}) - from: {from}', ['username' => $username, 'from' => $from]); try { $status = $user->addToDatabase(); if (!$status->isOk()) { // double-check for a race condition (T70012) $localId = User::idFromName($username, User::READ_LATEST); if ($localId) { $this->logger->info(__METHOD__ . ': {username} already exists locally (race)', ['username' => $username]); $user->setId($localId); $user->loadFromId(User::READ_LATEST); if ($login) { $this->setSessionDataForUser($user); } $status = Status::newGood(); $status->warning('userexists'); } else { $this->logger->error(__METHOD__ . ': {username} failed with message {message}', ['username' => $username, 'message' => $status->getWikiText(null, null, 'en')]); $user->setId(0); $user->loadFromId(); } return $status; } } catch (\Exception $ex) { $this->logger->error(__METHOD__ . ': {username} failed with exception {exception}', ['username' => $username, 'exception' => $ex]); // Do not keep throwing errors for a while $cache->set($backoffKey, 1, 600); // Bubble up error; which should normally trigger DB rollbacks throw $ex; } $this->setDefaultUserOptions($user, true); // Inform the providers $this->callMethodOnProviders(6, 'autoCreatedAccount', [$user, $source]); \Hooks::run('AuthPluginAutoCreate', [$user], '1.27'); \Hooks::run('LocalUserCreated', [$user, true]); $user->saveSettings(); // Update user count \DeferredUpdates::addUpdate(new \SiteStatsUpdate(0, 0, 0, 0, 1)); // Watch user's userpage and talk page $user->addWatch($user->getUserPage(), User::IGNORE_USER_RIGHTS); // Log the creation if ($this->config->get('NewUserLog')) { $logEntry = new \ManualLogEntry('newusers', 'autocreate'); $logEntry->setPerformer($user); $logEntry->setTarget($user->getUserPage()); $logEntry->setComment(''); $logEntry->setParameters(['4::userid' => $user->getId()]); $logid = $logEntry->insert(); } if ($login) { $this->setSessionDataForUser($user); } return Status::newGood(); }
/** * Primitive rate limits: enforce maximum actions per time period * to put a brake on flooding. * * The method generates both a generic profiling point and a per action one * (suffix being "-$action". * * @note When using a shared cache like memcached, IP-address * last-hit counters will be shared across wikis. * * @param string $action Action to enforce; 'edit' if unspecified * @param int $incrBy Positive amount to increment counter by [defaults to 1] * @return bool True if a rate limiter was tripped */ public function pingLimiter($action = 'edit', $incrBy = 1) { // Call the 'PingLimiter' hook $result = false; if (!Hooks::run('PingLimiter', [&$this, $action, &$result, $incrBy])) { return $result; } global $wgRateLimits; if (!isset($wgRateLimits[$action])) { return false; } // Some groups shouldn't trigger the ping limiter, ever if (!$this->isPingLimitable()) { return false; } $limits = $wgRateLimits[$action]; $keys = []; $id = $this->getId(); $userLimit = false; $isNewbie = $this->isNewbie(); if ($id == 0) { // limits for anons if (isset($limits['anon'])) { $keys[wfMemcKey('limiter', $action, 'anon')] = $limits['anon']; } } else { // limits for logged-in users if (isset($limits['user'])) { $userLimit = $limits['user']; } // limits for newbie logged-in users if ($isNewbie && isset($limits['newbie'])) { $keys[wfMemcKey('limiter', $action, 'user', $id)] = $limits['newbie']; } } // limits for anons and for newbie logged-in users if ($isNewbie) { // ip-based limits if (isset($limits['ip'])) { $ip = $this->getRequest()->getIP(); $keys["mediawiki:limiter:{$action}:ip:{$ip}"] = $limits['ip']; } // subnet-based limits if (isset($limits['subnet'])) { $ip = $this->getRequest()->getIP(); $subnet = IP::getSubnet($ip); if ($subnet !== false) { $keys["mediawiki:limiter:{$action}:subnet:{$subnet}"] = $limits['subnet']; } } } // Check for group-specific permissions // If more than one group applies, use the group with the highest limit ratio (max/period) foreach ($this->getGroups() as $group) { if (isset($limits[$group])) { if ($userLimit === false || $limits[$group][0] / $limits[$group][1] > $userLimit[0] / $userLimit[1]) { $userLimit = $limits[$group]; } } } // Set the user limit key if ($userLimit !== false) { list($max, $period) = $userLimit; wfDebug(__METHOD__ . ": effective user limit: {$max} in {$period}s\n"); $keys[wfMemcKey('limiter', $action, 'user', $id)] = $userLimit; } // ip-based limits for all ping-limitable users if (isset($limits['ip-all'])) { $ip = $this->getRequest()->getIP(); // ignore if user limit is more permissive if ($isNewbie || $userLimit === false || $limits['ip-all'][0] / $limits['ip-all'][1] > $userLimit[0] / $userLimit[1]) { $keys["mediawiki:limiter:{$action}:ip-all:{$ip}"] = $limits['ip-all']; } } // subnet-based limits for all ping-limitable users if (isset($limits['subnet-all'])) { $ip = $this->getRequest()->getIP(); $subnet = IP::getSubnet($ip); if ($subnet !== false) { // ignore if user limit is more permissive if ($isNewbie || $userLimit === false || $limits['ip-all'][0] / $limits['ip-all'][1] > $userLimit[0] / $userLimit[1]) { $keys["mediawiki:limiter:{$action}:subnet-all:{$subnet}"] = $limits['subnet-all']; } } } $cache = ObjectCache::getLocalClusterInstance(); $triggered = false; foreach ($keys as $key => $limit) { list($max, $period) = $limit; $summary = "(limit {$max} in {$period}s)"; $count = $cache->get($key); // Already pinged? if ($count) { if ($count >= $max) { wfDebugLog('ratelimit', "User '{$this->getName()}' " . "(IP {$this->getRequest()->getIP()}) tripped {$key} at {$count} {$summary}"); $triggered = true; } else { wfDebug(__METHOD__ . ": ok. {$key} at {$count} {$summary}\n"); } } else { wfDebug(__METHOD__ . ": adding record for {$key} {$summary}\n"); if ($incrBy > 0) { $cache->add($key, 0, intval($period)); // first ping } } if ($incrBy > 0) { $cache->incr($key, $incrBy); } } return $triggered; }
/** * Get the temporary prepared edit stash key for a user * * This key can be used for caching prepared edits provided: * - a) The $user was used for PST options * - b) The parser output was made from the PST using cannonical matching options * * @param Title $title * @param string $contentHash Result of getContentHash() * @param User $user User to get parser options from * @return string */ private static function getStashKey(Title $title, $contentHash, User $user) { return ObjectCache::getLocalClusterInstance()->makeKey('prepared-edit', md5($title->getPrefixedDBkey()), $contentHash, md5($user->getId() . "\n" . $user->getName())); }
/** * Acquire lock for sending a pingback * * This ensures only one thread can attempt to send a pingback at any given * time and that we wait an hour before retrying failed attempts. * * @return bool Whether lock was acquired */ private function acquireLock() { $cache = ObjectCache::getLocalClusterInstance(); if (!$cache->add($this->key, 1, 60 * 60)) { return false; // throttled } $dbw = wfGetDB(DB_MASTER); if (!$dbw->lock($this->key, __METHOD__, 0)) { return false; // already in progress } return true; }
/** * Check that a prepared edit is in cache and still up-to-date * * This method blocks if the prepared edit is already being rendered, * waiting until rendering finishes before doing final validity checks. * * The cache is rejected if template or file changes are detected. * Note that foreign template or file transclusions are not checked. * * The result is a map (pstContent,output,timestamp) with fields * extracted directly from WikiPage::prepareContentForEdit(). * * @param Title $title * @param Content $content * @param User $user User to get parser options from * @return stdClass|bool Returns false on cache miss */ public static function checkCache(Title $title, Content $content, User $user) { $cache = ObjectCache::getLocalClusterInstance(); $key = self::getStashKey($title, $content, $user); $editInfo = $cache->get($key); if (!is_object($editInfo)) { $start = microtime(true); // We ignore user aborts and keep parsing. Block on any prior parsing // so as to use it's results and make use of the time spent parsing. if ($cache->lock($key, 30, 30)) { $editInfo = $cache->get($key); $cache->unlock($key); } $sec = microtime(true) - $start; if ($sec > 0.01) { wfDebugLog('StashEdit', "Waited {$sec} seconds on '{$key}'."); } } if (!is_object($editInfo) || !$editInfo->output) { wfDebugLog('StashEdit', "No cache value for key '{$key}'."); return false; } $time = wfTimestamp(TS_UNIX, $editInfo->output->getTimestamp()); if (time() - $time <= 3) { wfDebugLog('StashEdit', "Timestamp-based cache hit for key '{$key}'."); return $editInfo; // assume nothing changed } $dbr = wfGetDB(DB_SLAVE); $templates = array(); // conditions to find changes/creations $templateUses = 0; // expected existing templates foreach ($editInfo->output->getTemplateIds() as $ns => $stuff) { foreach ($stuff as $dbkey => $revId) { $templates[(string) $ns][$dbkey] = (int) $revId; ++$templateUses; } } // Check that no templates used in the output changed... if (count($templates)) { $res = $dbr->select('page', array('ns' => 'page_namespace', 'dbk' => 'page_title', 'page_latest'), $dbr->makeWhereFrom2d($templates, 'page_namespace', 'page_title'), __METHOD__); $changed = false; foreach ($res as $row) { $changed = $changed || $row->page_latest != $templates[$row->ns][$row->dbk]; } if ($changed || $res->numRows() != $templateUses) { wfDebugLog('StashEdit', "Stale cache for key '{$key}'; template changed."); return false; } } $files = array(); // conditions to find changes/creations foreach ($editInfo->output->getFileSearchOptions() as $name => $options) { $files[$name] = (string) $options['sha1']; } // Check that no files used in the output changed... if (count($files)) { $res = $dbr->select('image', array('name' => 'img_name', 'img_sha1'), array('img_name' => array_keys($files)), __METHOD__); $changed = false; foreach ($res as $row) { $changed = $changed || $row->img_sha1 != $files[$row->name]; } if ($changed || $res->numRows() != count($files)) { wfDebugLog('StashEdit', "Stale cache for key '{$key}'; file changed."); return false; } } wfDebugLog('StashEdit', "Cache hit for key '{$key}'."); return $editInfo; }
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])); }
public function testWfGlobalCacheKey() { $cache = ObjectCache::getLocalClusterInstance(); $this->assertEquals($cache->makeGlobalKey('foo', 123, 'bar'), wfGlobalCacheKey('foo', 123, 'bar')); }
public function __construct($parent) { $this->parent = $parent; $this->srvCache = ObjectCache::getLocalServerInstance('hash'); $this->mainCache = ObjectCache::getLocalClusterInstance(); }
/** * Check if there are any queues with jobs (this is cached) * * @param int $type JobQueueGroup::TYPE_* constant * @return bool * @since 1.23 */ public function queuesHaveJobs($type = self::TYPE_ANY) { $key = wfMemcKey('jobqueue', 'queueshavejobs', $type); $cache = ObjectCache::getLocalClusterInstance(); $value = $cache->get($key); if ($value === false) { $queues = $this->getQueuesWithJobs(); if ($type == self::TYPE_DEFAULT) { $queues = array_intersect($queues, $this->getDefaultQueueTypes()); } $value = count($queues) ? 'true' : 'false'; $cache->add($key, $value, 15); } return $value === 'true'; }
/** * 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); } } }
/** * Check that a prepared edit is in cache and still up-to-date * * This method blocks if the prepared edit is already being rendered, * waiting until rendering finishes before doing final validity checks. * * The cache is rejected if template or file changes are detected. * Note that foreign template or file transclusions are not checked. * * The result is a map (pstContent,output,timestamp) with fields * extracted directly from WikiPage::prepareContentForEdit(). * * @param Title $title * @param Content $content * @param User $user User to get parser options from * @return stdClass|bool Returns false on cache miss */ public static function checkCache(Title $title, Content $content, User $user) { $cache = ObjectCache::getLocalClusterInstance(); $logger = LoggerFactory::getInstance('StashEdit'); $stats = RequestContext::getMain()->getStats(); $key = self::getStashKey($title, $content, $user); $editInfo = $cache->get($key); if (!is_object($editInfo)) { $start = microtime(true); // We ignore user aborts and keep parsing. Block on any prior parsing // so as to use its results and make use of the time spent parsing. // Skip this logic if there no master connection in case this method // is called on an HTTP GET request for some reason. $lb = wfGetLB(); $dbw = $lb->getAnyOpenConnection($lb->getWriterIndex()); if ($dbw && $dbw->lock($key, __METHOD__, 30)) { $editInfo = $cache->get($key); $dbw->unlock($key, __METHOD__); } $timeMs = 1000 * max(0, microtime(true) - $start); $stats->timing('editstash.lock-wait-time', $timeMs); } if (!is_object($editInfo) || !$editInfo->output) { $stats->increment('editstash.cache-misses'); $logger->debug("No cache value for key '{$key}'."); return false; } $time = wfTimestamp(TS_UNIX, $editInfo->output->getTimestamp()); if (time() - $time <= 3) { $stats->increment('editstash.cache-hits'); $logger->debug("Timestamp-based cache hit for key '{$key}'."); return $editInfo; // assume nothing changed } $dbr = wfGetDB(DB_SLAVE); $templates = array(); // conditions to find changes/creations $templateUses = 0; // expected existing templates foreach ($editInfo->output->getTemplateIds() as $ns => $stuff) { foreach ($stuff as $dbkey => $revId) { $templates[(string) $ns][$dbkey] = (int) $revId; ++$templateUses; } } // Check that no templates used in the output changed... if (count($templates)) { $res = $dbr->select('page', array('ns' => 'page_namespace', 'dbk' => 'page_title', 'page_latest'), $dbr->makeWhereFrom2d($templates, 'page_namespace', 'page_title'), __METHOD__); $changed = false; foreach ($res as $row) { $changed = $changed || $row->page_latest != $templates[$row->ns][$row->dbk]; } if ($changed || $res->numRows() != $templateUses) { $stats->increment('editstash.cache-misses'); $logger->info("Stale cache for key '{$key}'; template changed."); return false; } } $files = array(); // conditions to find changes/creations foreach ($editInfo->output->getFileSearchOptions() as $name => $options) { $files[$name] = (string) $options['sha1']; } // Check that no files used in the output changed... if (count($files)) { $res = $dbr->select('image', array('name' => 'img_name', 'img_sha1'), array('img_name' => array_keys($files)), __METHOD__); $changed = false; foreach ($res as $row) { $changed = $changed || $row->img_sha1 != $files[$row->name]; } if ($changed || $res->numRows() != count($files)) { $stats->increment('editstash.cache-misses'); $logger->info("Stale cache for key '{$key}'; file changed."); return false; } } $stats->increment('editstash.cache-hits'); $logger->debug("Cache hit for key '{$key}'."); return $editInfo; }
/** * Validate a given script file; if valid returns the original source. * If invalid, returns replacement JS source that throws an exception. * * @param string $fileName * @param string $contents * @return string JS with the original, or a replacement error */ protected function validateScriptFile($fileName, $contents) { if ($this->getConfig()->get('ResourceLoaderValidateJS')) { // Try for cache hit $cache = ObjectCache::getLocalClusterInstance(); $key = $cache->makeKey('resourceloader', 'jsparse', self::$parseCacheVersion, md5($contents)); $cacheEntry = $cache->get($key); if (is_string($cacheEntry)) { return $cacheEntry; } $parser = self::javaScriptParser(); try { $parser->parse($contents, $fileName, 1); $result = $contents; } catch (Exception $e) { // We'll save this to cache to avoid having to validate broken JS over and over... $err = $e->getMessage(); $result = "mw.log.error(" . Xml::encodeJsVar("JavaScript parse error: {$err}") . ");"; } $cache->set($key, $result); return $result; } else { return $contents; } }
/** * Opportunistically enqueue link update jobs given fresh parser output if useful * * @param ParserOutput $parserOutput Current version page output * @since 1.25 */ public function triggerOpportunisticLinksUpdate(ParserOutput $parserOutput) { if (wfReadOnly()) { return; } if (!Hooks::run('OpportunisticLinksUpdate', [$this, $this->mTitle, $parserOutput])) { return; } $config = RequestContext::getMain()->getConfig(); $params = ['isOpportunistic' => true, 'rootJobTimestamp' => $parserOutput->getCacheTime()]; if ($this->mTitle->areRestrictionsCascading()) { // If the page is cascade protecting, the links should really be up-to-date JobQueueGroup::singleton()->lazyPush(RefreshLinksJob::newPrioritized($this->mTitle, $params)); } elseif (!$config->get('MiserMode') && $parserOutput->hasDynamicContent()) { // Assume the output contains "dynamic" time/random based magic words. // Only update pages that expired due to dynamic content and NOT due to edits // to referenced templates/files. When the cache expires due to dynamic content, // page_touched is unchanged. We want to avoid triggering redundant jobs due to // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the // template/file edit already triggered recursive RefreshLinksJob jobs. if ($this->getLinksTimestamp() > $this->getTouched()) { // If a page is uncacheable, do not keep spamming a job for it. // Although it would be de-duplicated, it would still waste I/O. $cache = ObjectCache::getLocalClusterInstance(); $key = $cache->makeKey('dynamic-linksupdate', 'last', $this->getId()); $ttl = max($parserOutput->getCacheExpiry(), 3600); if ($cache->add($key, time(), $ttl)) { JobQueueGroup::singleton()->lazyPush(RefreshLinksJob::newDynamic($this->mTitle, $params)); } } } }
/** * Set the files this module depends on indirectly for a given skin. * * @since 1.27 * @param ResourceLoaderContext $context * @param array $localFileRefs List of files */ protected function saveFileDependencies(ResourceLoaderContext $context, $localFileRefs) { // Normalise array $localFileRefs = array_values(array_unique($localFileRefs)); sort($localFileRefs); try { // If the list has been modified since last time we cached it, update the cache if ($localFileRefs !== $this->getFileDependencies($context)) { $cache = ObjectCache::getLocalClusterInstance(); $key = $cache->makeKey(__METHOD__, $this->getName()); $scopeLock = $cache->getScopedLock($key, 0); if (!$scopeLock) { return; // T124649; avoid write slams } $vary = $context->getSkin() . '|' . $context->getLanguage(); $dbw = wfGetDB(DB_MASTER); $dbw->replace('module_deps', [['md_module', 'md_skin']], ['md_module' => $this->getName(), 'md_skin' => $vary, 'md_deps' => FormatJson::encode(self::getRelativePaths($localFileRefs))]); $dbw->onTransactionIdle(function () use(&$scopeLock) { ScopedCallback::consume($scopeLock); // release after commit }); } } catch (Exception $e) { wfDebugLog('resourceloader', __METHOD__ . ": failed to update DB: {$e}"); } }
/** * Auto-create the given user, if necessary * @private Don't call this yourself. Let Setup.php do it for you at the right time. * @note This more properly belongs in AuthManager, but we need it now. * When AuthManager comes, this will be deprecated and will pass-through * to the corresponding AuthManager method. * @param User $user User to auto-create * @return bool Success */ public static function autoCreateUser(User $user) { global $wgAuth; $logger = self::singleton()->logger; // Much of this code is based on that in CentralAuth // Try the local user from the slave DB $localId = User::idFromName($user->getName()); // Fetch the user ID from the master, so that we don't try to create the user // when they already exist, due to replication lag // @codeCoverageIgnoreStart if (!$localId && wfGetLB()->getReaderIndex() != 0) { $localId = User::idFromName($user->getName(), User::READ_LATEST); } // @codeCoverageIgnoreEnd if ($localId) { // User exists after all. $user->setId($localId); $user->loadFromId(); return false; } // Denied by AuthPlugin? But ignore AuthPlugin itself. if (get_class($wgAuth) !== 'AuthPlugin' && !$wgAuth->autoCreate()) { $logger->debug(__METHOD__ . ': denied by AuthPlugin'); $user->setId(0); $user->loadFromId(); return false; } // Wiki is read-only? if (wfReadOnly()) { $logger->debug(__METHOD__ . ': denied by wfReadOnly()'); $user->setId(0); $user->loadFromId(); return false; } $userName = $user->getName(); // Check the session, if we tried to create this user already there's // no point in retrying. $session = self::getGlobalSession(); $reason = $session->get('MWSession::AutoCreateBlacklist'); if ($reason) { $logger->debug(__METHOD__ . ": blacklisted in session ({$reason})"); $user->setId(0); $user->loadFromId(); return false; } // Is the IP user able to create accounts? $anon = new User(); if (!$anon->isAllowedAny('createaccount', 'autocreateaccount') || $anon->isBlockedFromCreateAccount()) { // Blacklist the user to avoid repeated DB queries subsequently $logger->debug(__METHOD__ . ': user is blocked from this wiki, blacklisting'); $session->set('MWSession::AutoCreateBlacklist', 'blocked', 600); $session->persist(); $user->setId(0); $user->loadFromId(); return false; } // Check for validity of username if (!User::isCreatableName($userName)) { $logger->debug(__METHOD__ . ': Invalid username, blacklisting'); $session->set('MWSession::AutoCreateBlacklist', 'invalid username', 600); $session->persist(); $user->setId(0); $user->loadFromId(); return false; } // Give other extensions a chance to stop auto creation. $user->loadDefaults($userName); $abortMessage = ''; if (!\Hooks::run('AbortAutoAccount', array($user, &$abortMessage))) { // In this case we have no way to return the message to the user, // but we can log it. $logger->debug(__METHOD__ . ": denied by hook: {$abortMessage}"); $session->set('MWSession::AutoCreateBlacklist', "hook aborted: {$abortMessage}", 600); $session->persist(); $user->setId(0); $user->loadFromId(); return false; } // Make sure the name has not been changed if ($user->getName() !== $userName) { $user->setId(0); $user->loadFromId(); throw new \UnexpectedValueException('AbortAutoAccount hook tried to change the user name'); } // Ignore warnings about master connections/writes...hard to avoid here \Profiler::instance()->getTransactionProfiler()->resetExpectations(); $cache = \ObjectCache::getLocalClusterInstance(); $backoffKey = wfMemcKey('MWSession', 'autocreate-failed', md5($userName)); if ($cache->get($backoffKey)) { $logger->debug(__METHOD__ . ': denied by prior creation attempt failures'); $user->setId(0); $user->loadFromId(); return false; } // Checks passed, create the user... $from = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : 'CLI'; $logger->info(__METHOD__ . ": creating new user ({$userName}) - from: {$from}"); try { // Insert the user into the local DB master $status = $user->addToDatabase(); if (!$status->isOK()) { // @codeCoverageIgnoreStart $logger->error(__METHOD__ . ': failed with message ' . $status->getWikiText()); $user->setId(0); $user->loadFromId(); return false; // @codeCoverageIgnoreEnd } } catch (\Exception $ex) { // @codeCoverageIgnoreStart $logger->error(__METHOD__ . ': failed with exception ' . $ex->getMessage()); // Do not keep throwing errors for a while $cache->set($backoffKey, 1, 600); // Bubble up error; which should normally trigger DB rollbacks throw $ex; // @codeCoverageIgnoreEnd } # Notify hooks (e.g. Newuserlog) \Hooks::run('AuthPluginAutoCreate', array($user)); \Hooks::run('LocalUserCreated', array($user, true)); # Notify AuthPlugin too $tmpUser = $user; $wgAuth->initUser($tmpUser, true); if ($tmpUser !== $user) { $logger->warning(__METHOD__ . ': ' . get_class($wgAuth) . '::initUser() replaced the user object'); } $user->saveSettings(); # Update user count \DeferredUpdates::addUpdate(new \SiteStatsUpdate(0, 0, 0, 0, 1)); # Watch user's userpage and talk page $user->addWatch($user->getUserPage(), \WatchedItem::IGNORE_USER_RIGHTS); return true; }
return new CryptRand(['wfHostname', 'wfWikiID', function () use($secretKey) { return $secretKey ?: ''; }], defined('MW_CONFIG_FILE') ? [MW_CONFIG_FILE] : [], LoggerFactory::getInstance('CryptRand')); }, 'CryptHKDF' => function (MediaWikiServices $services) { $config = $services->getMainConfig(); $secret = $config->get('HKDFSecret') ?: $config->get('SecretKey'); if (!$secret) { throw new RuntimeException("Cannot use MWCryptHKDF without a secret."); } // In HKDF, the context can be known to the attacker, but this will // keep simultaneous runs from producing the same output. $context = [microtime(), getmypid(), gethostname()]; // Setup salt cache. Use APC, or fallback to the main cache if it isn't setup $cache = $services->getLocalServerObjectCache(); if ($cache instanceof EmptyBagOStuff) { $cache = ObjectCache::getLocalClusterInstance(); } return new CryptHKDF($secret, $config->get('HKDFAlgorithm'), $cache, $context, $services->getCryptRand()); }, 'MediaHandlerFactory' => function (MediaWikiServices $services) { return new MediaHandlerFactory($services->getMainConfig()->get('MediaHandlers')); }, 'MimeAnalyzer' => function (MediaWikiServices $services) { return new MimeMagic(MimeMagic::applyDefaultParameters([], $services->getMainConfig())); }, 'ProxyLookup' => function (MediaWikiServices $services) { $mainConfig = $services->getMainConfig(); return new ProxyLookup($mainConfig->get('SquidServers'), $mainConfig->get('SquidServersNoPurge')); }, 'Parser' => function (MediaWikiServices $services) { $conf = $services->getMainConfig()->get('ParserConf'); return ObjectFactory::constructClassInstance($conf['class'], [$conf]); }, 'LinkCache' => function (MediaWikiServices $services) { return new LinkCache($services->getTitleFormatter(), ObjectCache::getMainWANInstance()); }, 'LinkRendererFactory' => function (MediaWikiServices $services) {