Beispiel #1
0
 /**
  * @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;
 }
Beispiel #2
0
 /**
  * @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();
 }
Beispiel #4
0
 /**
  * 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;
 }
Beispiel #5
0
 /**
  * @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();
     }
 }
Beispiel #6
0
 /**
  * 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;
 }
Beispiel #8
0
/**
 * 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());
}
Beispiel #9
0
 /**
  * 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());
 }
Beispiel #10
0
 /**
  * @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;
         }
     }
 }
Beispiel #11
0
/**
 * 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();
 }
Beispiel #13
0
 /**
  * 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();
 }
Beispiel #14
0
 /**
  * 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;
 }
Beispiel #15
0
 /**
  * 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()));
 }
Beispiel #16
0
 /**
  * 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;
 }
Beispiel #17
0
 /**
  * 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;
 }
Beispiel #18
0
 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]));
 }
Beispiel #19
0
 public function testWfGlobalCacheKey()
 {
     $cache = ObjectCache::getLocalClusterInstance();
     $this->assertEquals($cache->makeGlobalKey('foo', 123, 'bar'), wfGlobalCacheKey('foo', 123, 'bar'));
 }
Beispiel #20
0
 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);
         }
     }
 }
Beispiel #23
0
 /**
  * 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;
     }
 }
Beispiel #25
0
 /**
  * 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}");
     }
 }
Beispiel #27
0
 /**
  * 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;
 }
Beispiel #28
0
    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) {