public function testExecute_email() { $config = new HashConfig(['PasswordResetRoutes' => ['username' => true, 'email' => true], 'EnableEmail' => true]); $authManager = $this->getMockBuilder(AuthManager::class)->disableOriginalConstructor()->getMock(); $authManager->expects($this->any())->method('allowsAuthenticationDataChange')->willReturn(Status::newGood()); $authManager->expects($this->exactly(2))->method('changeAuthenticationData'); $request = new FauxRequest(); $request->setIP('1.2.3.4'); $performingUser = $this->getMock(User::class); $performingUser->expects($this->any())->method('getRequest')->willReturn($request); $performingUser->expects($this->any())->method('isAllowed')->willReturn(true); $targetUser1 = $this->getMock(User::class); $targetUser2 = $this->getMock(User::class); $targetUser1->expects($this->any())->method('getName')->willReturn('User1'); $targetUser2->expects($this->any())->method('getName')->willReturn('User2'); $targetUser1->expects($this->any())->method('getId')->willReturn(1); $targetUser2->expects($this->any())->method('getId')->willReturn(2); $targetUser1->expects($this->any())->method('getEmail')->willReturn('*****@*****.**'); $targetUser2->expects($this->any())->method('getEmail')->willReturn('*****@*****.**'); $passwordReset = $this->getMockBuilder(PasswordReset::class)->setMethods(['getUsersByEmail'])->setConstructorArgs([$config, $authManager])->getMock(); $passwordReset->expects($this->any())->method('getUsersByEmail')->with('*****@*****.**')->willReturn([$targetUser1, $targetUser2]); $status = $passwordReset->isAllowed($performingUser); $this->assertTrue($status->isGood()); $status = $passwordReset->execute($performingUser, null, '*****@*****.**'); $this->assertTrue($status->isGood()); }
/** * Import an client IP address, HTTP headers, user ID, and session ID * * This sets the current session, $wgUser, and $wgRequest from $params. * Once the return value falls out of scope, the old context is restored. * This method should only be called in contexts where there is no session * ID or end user receiving the response (CLI or HTTP job runners). This * is partly enforced, and is done so to avoid leaking cookies if certain * error conditions arise. * * This is useful when background scripts inherit context when acting on * behalf of a user. In general the 'sessionId' parameter should be set * to an empty string unless session importing is *truly* needed. This * feature is somewhat deprecated. * * @note suhosin.session.encrypt may interfere with this method. * * @param array $params Result of RequestContext::exportSession() * @return ScopedCallback * @throws MWException * @since 1.21 */ public static function importScopedSession(array $params) { if (strlen($params['sessionId']) && MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent()) { // Sanity check to avoid sending random cookies for the wrong users. // This method should only called by CLI scripts or by HTTP job runners. throw new MWException("Sessions can only be imported when none is active."); } elseif (!IP::isValid($params['ip'])) { throw new MWException("Invalid client IP address '{$params['ip']}'."); } if ($params['userId']) { // logged-in user $user = User::newFromId($params['userId']); $user->load(); if (!$user->getId()) { throw new MWException("No user with ID '{$params['userId']}'."); } } else { // anon user $user = User::newFromName($params['ip'], false); } $importSessionFunc = function (User $user, array $params) { global $wgRequest, $wgUser; $context = RequestContext::getMain(); // Commit and close any current session if (MediaWiki\Session\PHPSessionHandler::isEnabled()) { session_write_close(); // persist session_id(''); // detach $_SESSION = []; // clear in-memory array } // Get new session, if applicable $session = null; if (strlen($params['sessionId'])) { // don't make a new random ID $manager = MediaWiki\Session\SessionManager::singleton(); $session = $manager->getSessionById($params['sessionId'], true) ?: $manager->getEmptySession(); } // Remove any user IP or agent information, and attach the request // with the new session. $context->setRequest(new FauxRequest([], false, $session)); $wgRequest = $context->getRequest(); // b/c // Now that all private information is detached from the user, it should // be safe to load the new user. If errors occur or an exception is thrown // and caught (leaving the main context in a mixed state), there is no risk // of the User object being attached to the wrong IP, headers, or session. $context->setUser($user); $wgUser = $context->getUser(); // b/c if ($session && MediaWiki\Session\PHPSessionHandler::isEnabled()) { session_id($session->getId()); MediaWiki\quietCall('session_start'); } $request = new FauxRequest([], false, $session); $request->setIP($params['ip']); foreach ($params['headers'] as $name => $value) { $request->setHeader($name, $value); } // Set the current context to use the new WebRequest $context->setRequest($request); $wgRequest = $context->getRequest(); // b/c }; // Stash the old session and load in the new one $oUser = self::getMain()->getUser(); $oParams = self::getMain()->exportSession(); $oRequest = self::getMain()->getRequest(); $importSessionFunc($user, $params); // Set callback to save and close the new session and reload the old one return new ScopedCallback(function () use($importSessionFunc, $oUser, $oParams, $oRequest) { global $wgRequest; $importSessionFunc($oUser, $oParams); // Restore the exact previous Request object (instead of leaving FauxRequest) RequestContext::getMain()->setRequest($oRequest); $wgRequest = RequestContext::getMain()->getRequest(); // b/c }); }
/** * Import the resolved user IP, HTTP headers, user ID, and session ID. * This sets the current session and sets $wgUser and $wgRequest. * Once the return value falls out of scope, the old context is restored. * This function can only be called within CLI mode scripts. * * This will setup the session from the given ID. This is useful when * background scripts inherit context when acting on behalf of a user. * * $param array $params Result of RequestContext::exportSession() * @return ScopedCallback * @throws MWException * @since 1.21 */ public static function importScopedSession(array $params) { if (PHP_SAPI !== 'cli') { // Don't send random private cookies or turn $wgRequest into FauxRequest throw new MWException("Sessions can only be imported in cli mode."); } elseif (!strlen($params['sessionId'])) { throw new MWException("No session ID was specified."); } if ($params['userId']) { // logged-in user $user = User::newFromId($params['userId']); if (!$user) { throw new MWException("No user with ID '{$params['userId']}'."); } } elseif (!IP::isValid($params['ip'])) { throw new MWException("Could not load user '{$params['ip']}'."); } else { // anon user $user = User::newFromName($params['ip'], false); } $importSessionFunction = function (User $user, array $params) { global $wgRequest, $wgUser; $context = RequestContext::getMain(); // Commit and close any current session session_write_close(); // persist session_id(''); // detach $_SESSION = array(); // clear in-memory array // Remove any user IP or agent information $context->setRequest(new FauxRequest()); $wgRequest = $context->getRequest(); // b/c // Now that all private information is detached from the user, it should // be safe to load the new user. If errors occur or an exception is thrown // and caught (leaving the main context in a mixed state), there is no risk // of the User object being attached to the wrong IP, headers, or session. $context->setUser($user); $wgUser = $context->getUser(); // b/c if (strlen($params['sessionId'])) { // don't make a new random ID wfSetupSession($params['sessionId']); // sets $_SESSION } $request = new FauxRequest(array(), false, $_SESSION); $request->setIP($params['ip']); foreach ($params['headers'] as $name => $value) { $request->setHeader($name, $value); } // Set the current context to use the new WebRequest $context->setRequest($request); $wgRequest = $context->getRequest(); // b/c }; // Stash the old session and load in the new one $oUser = self::getMain()->getUser(); $oParams = self::getMain()->exportSession(); $importSessionFunction($user, $params); // Set callback to save and close the new session and reload the old one return new ScopedCallback(function () use($importSessionFunction, $oUser, $oParams) { $importSessionFunction($oUser, $oParams); }); }
/** * Execute the requested Api actions. * @todo: Write some unit tests for API results */ public function execute() { // Logged-in users' parser options depend on preferences $this->getMain()->setCacheMode('anon-public-user-private'); // Enough '*' keys in JSON!!! $isXml = $this->getMain()->isInternalMode() || $this->getMain()->getPrinter()->getFormat() == 'XML'; $textElement = $isXml ? '*' : 'text'; $params = $this->extractRequestParams(); $prop = array_flip($params['prop']); $sectionProp = array_flip($params['sectionprop']); $this->variant = $params['variant']; $this->followRedirects = $params['redirect'] == 'yes'; $this->noHeadings = $params['noheadings']; $this->noTransform = $params['notransform']; $onlyRequestedSections = $params['onlyrequestedsections']; $this->offset = $params['offset']; $this->maxlen = $params['maxlen']; if ($this->offset === 0 && $this->maxlen === 0) { $this->offset = -1; // Disable text splitting } elseif ($this->maxlen === 0) { $this->maxlen = PHP_INT_MAX; } $title = $this->makeTitle($params['page']); // See whether the actual page (or if enabled, the redirect target) is the main page $this->mainPage = $this->isMainPage($title); if ($this->mainPage && $this->noHeadings) { $this->noHeadings = false; $this->setWarning("``noheadings'' makes no sense on the main page, ignoring"); } if (isset($prop['normalizedtitle']) && $title->getPrefixedText() != $params['page']) { $this->getResult()->addValue(null, $this->getModuleName(), array('normalizedtitle' => $title->getPageLanguage()->convert($title->getPrefixedText()))); } $data = $this->getData($title, $params['noimages']); // Bug 73109: #getData will return an empty array if the title redirects to // a page in a virtual namespace (NS_SPECIAL, NS_MEDIA), so make sure that // the requested data exists too. if (isset($prop['lastmodified']) && isset($data['lastmodified'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('lastmodified' => $data['lastmodified'])); } if (isset($prop['lastmodifiedby']) && isset($data['lastmodifiedby'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('lastmodifiedby' => $data['lastmodifiedby'])); } if (isset($prop['revision']) && isset($data['revision'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('revision' => $data['revision'])); } if (isset($prop['id']) && isset($data['id'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('id' => $data['id'])); } if (isset($prop['languagecount']) && isset($data['languagecount'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('languagecount' => $data['languagecount'])); } if (isset($prop['hasvariants']) && isset($data['hasvariants'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('hasvariants' => $data['hasvariants'])); } if (isset($prop['displaytitle']) && isset($data['displaytitle'])) { $this->getResult()->addValue(null, $this->getModuleName(), array('displaytitle' => $data['displaytitle'])); } if (isset($prop['pageprops'])) { $propNames = $params['pageprops']; if ($propNames == '*' && isset($data['pageprops'])) { $pageProps = $data['pageprops']; } else { $propNames = explode('|', $propNames); $pageProps = array_intersect_key($data['pageprops'], array_flip($propNames)); } $this->getResult()->addValue(null, $this->getModuleName(), array('pageprops' => $pageProps)); } if (isset($prop['description']) && isset($data['pageprops']['wikibase_item'])) { $desc = ExtMobileFrontend::getWikibaseDescription($data['pageprops']['wikibase_item']); if ($desc) { $this->getResult()->addValue(null, $this->getModuleName(), array('description' => $desc)); } } if ($this->usePageImages) { $this->addPageImage($data, $params, $prop); } $result = array(); $missingSections = array(); if ($this->mainPage) { if ($onlyRequestedSections) { $requestedSections = self::parseSections($params['sections'], $data, $missingSections); } else { $requestedSections = array(0); } $this->getResult()->addValue(null, $this->getModuleName(), array('mainpage' => '')); } elseif (isset($params['sections'])) { $requestedSections = self::parseSections($params['sections'], $data, $missingSections); } else { $requestedSections = array(); } if (isset($data['sections'])) { if (isset($prop['sections'])) { $sectionCount = count($data['sections']); for ($i = 0; $i <= $sectionCount; $i++) { if (!isset($requestedSections[$i]) && $onlyRequestedSections) { continue; } $section = array(); if ($i > 0) { $section = array_intersect_key($data['sections'][$i - 1], $sectionProp); } $section['id'] = $i; if (isset($prop['text']) && isset($requestedSections[$i]) && isset($data['text'][$i])) { $section[$textElement] = $this->stringSplitter($this->prepareSection($data['text'][$i])); unset($requestedSections[$i]); } if (isset($data['refsections'][$i])) { $section['references'] = ''; } $result[] = $section; } $missingSections = array_keys($requestedSections); } else { foreach (array_keys($requestedSections) as $index) { $section = array('id' => $index); if (isset($data['text'][$index])) { $section[$textElement] = $this->stringSplitter($this->prepareSection($data['text'][$index])); } else { $missingSections[] = $index; } $result[] = $section; } } $this->getResult()->setIndexedTagName($result, 'section'); $this->getResult()->addValue(null, $this->getModuleName(), array('sections' => $result)); } if (isset($prop['protection'])) { $this->addProtection($title); } if (isset($prop['editable'])) { $user = $this->getUser(); if ($user->isAnon()) { // HACK: Anons receive cached information, so don't check blocked status for them // to avoid them receiving false positives. Currently there is no way to check // all permissions except blocked status from the Title class. $req = new FauxRequest(); $req->setIP('127.0.0.1'); $user = User::newFromSession($req); } $editable = $title->quickUserCan('edit', $user); if ($isXml) { $editable = intval($editable); } $this->getResult()->addValue(null, $this->getModuleName(), array('editable' => $editable)); } // https://bugzilla.wikimedia.org/show_bug.cgi?id=51586 // Inform ppl if the page is infested with LiquidThreads but that's the // only thing we support about it. if (class_exists('LqtDispatch') && LqtDispatch::isLqtPage($title)) { $this->getResult()->addValue(null, $this->getModuleName(), array('liquidthreads' => '')); } if (count($missingSections) && isset($prop['text'])) { $this->setWarning('Section(s) ' . implode(', ', $missingSections) . ' not found'); } if ($this->maxlen < 0) { // There is more data available $this->getResult()->addValue(null, $this->getModuleName(), array('continue-offset' => $params['offset'] + $params['maxlen'])); } }
/** * @dataProvider provideCheckIpLimits */ public function testCheckIpLimits($ip, $sessionData, $userData, $logLevel1, $logLevel2) { $this->setMwGlobals(array('wgSuspiciousIpPerSessionLimit' => 5, 'wgSuspiciousIpPerUserLimit' => 10, 'wgSuspiciousIpExpiry' => 600, 'wgSquidServers' => array('11.22.33.44'))); $manager = new SessionManager(); $logger = $this->getMock('\\Psr\\Log\\LoggerInterface'); $this->setLogger('session-ip', $logger); $request = new \FauxRequest(); $request->setIP($ip); $session = $manager->getSessionForRequest($request); /** @var SessionBackend $backend */ $backend = \TestingAccessWrapper::newFromObject($session)->backend; $data =& $backend->getData(); $data = array('SessionManager-ip' => $sessionData); $backend->setUser(User::newFromName('UTSysop')); $manager = \TestingAccessWrapper::newFromObject($manager); $manager->store->set('SessionManager-ip:' . md5('UTSysop'), $userData); $logger->expects($this->exactly(isset($logLevel1) + isset($logLevel2)))->method('log'); if ($logLevel1) { $logger->expects($this->at(0))->method('log')->with($logLevel1, 'Same session used from {count} IPs', $this->isType('array')); } if ($logLevel2) { $logger->expects($this->at(isset($logLevel1)))->method('log')->with($logLevel2, 'Same user had sessions from {count} IPs', $this->isType('array')); } $manager->checkIpLimits($session); }