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; }