/** * @dataProvider provideCategoryContent * @covers WikiCategoryPage::isHidden */ public function testHiddenCategory_PropertyIsSet($isHidden) { $categoryTitle = Title::makeTitle(NS_CATEGORY, 'CategoryPage'); $categoryPage = WikiCategoryPage::factory($categoryTitle); $pageProps = $this->getMockPageProps(); $pageProps->expects($this->once())->method('getProperties')->with($categoryTitle, 'hiddencat')->will($this->returnValue($isHidden ? [$categoryTitle->getArticleID() => ''] : [])); $scopedOverride = PageProps::overrideInstance($pageProps); $this->assertEquals($isHidden, $categoryPage->isHidden()); ScopedCallback::consume($scopedOverride); }
public function run() { $scope = RequestContext::importScopedSession($this->params['session']); $this->addTeardownCallback(function () use(&$scope) { ScopedCallback::consume($scope); // T126450 }); $context = RequestContext::getMain(); $user = $context->getUser(); try { if (!$user->isLoggedIn()) { $this->setLastError("Could not load the author user from session."); return false; } UploadBase::setSessionStatus($user, $this->params['filekey'], ['result' => 'Poll', 'stage' => 'publish', 'status' => Status::newGood()]); $upload = new UploadFromStash($user); // @todo initialize() causes a GET, ideally we could frontload the antivirus // checks and anything else to the stash stage (which includes concatenation and // the local file is thus already there). That way, instead of GET+PUT, there could // just be a COPY operation from the stash to the public zone. $upload->initialize($this->params['filekey'], $this->params['filename']); // Check if the local file checks out (this is generally a no-op) $verification = $upload->verifyUpload(); if ($verification['status'] !== UploadBase::OK) { $status = Status::newFatal('verification-error'); $status->value = ['verification' => $verification]; UploadBase::setSessionStatus($user, $this->params['filekey'], ['result' => 'Failure', 'stage' => 'publish', 'status' => $status]); $this->setLastError("Could not verify upload."); return false; } // Upload the stashed file to a permanent location $status = $upload->performUpload($this->params['comment'], $this->params['text'], $this->params['watch'], $user, isset($this->params['tags']) ? $this->params['tags'] : []); if (!$status->isGood()) { UploadBase::setSessionStatus($user, $this->params['filekey'], ['result' => 'Failure', 'stage' => 'publish', 'status' => $status]); $this->setLastError($status->getWikiText(false, false, 'en')); return false; } // Build the image info array while we have the local reference handy $apiMain = new ApiMain(); // dummy object (XXX) $imageInfo = $upload->getImageInfo($apiMain->getResult()); // Cleanup any temporary local file $upload->cleanupTempFile(); // Cache the info so the user doesn't have to wait forever to get the final info UploadBase::setSessionStatus($user, $this->params['filekey'], ['result' => 'Success', 'stage' => 'publish', 'filename' => $upload->getLocalFile()->getName(), 'imageinfo' => $imageInfo, 'status' => Status::newGood()]); } catch (Exception $e) { UploadBase::setSessionStatus($user, $this->params['filekey'], ['result' => 'Failure', 'stage' => 'publish', 'status' => Status::newFatal('api-error-publishfailed')]); $this->setLastError(get_class($e) . ": " . $e->getMessage()); // To prevent potential database referential integrity issues. // See bug 32551. MWExceptionHandler::rollbackMasterChangesAndLog($e); return false; } return true; }
public function run() { $page = WikiPage::newFromID($this->params['pageId'], WikiPage::READ_LATEST); if (!$page) { $this->setLastError("Could not find page #{$this->params['pageId']}"); return false; // deleted? } $dbw = wfGetDB(DB_MASTER); // Use a named lock so that jobs for this page see each others' changes $fname = __METHOD__; $lockKey = "CategoryMembershipUpdates:{$page->getId()}"; if (!$dbw->lock($lockKey, $fname, 10)) { $this->setLastError("Could not acquire lock '{$lockKey}'"); return false; } $unlocker = new ScopedCallback(function () use($dbw, $lockKey, $fname) { $dbw->unlock($lockKey, $fname); }); // Sanity: clear any DB transaction snapshot $dbw->commit(__METHOD__, 'flush'); $cutoffUnix = wfTimestamp(TS_UNIX, $this->params['revTimestamp']); // Using ENQUEUE_FUDGE_SEC handles jobs inserted out of revision order due to the delay // between COMMIT and actual enqueueing of the CategoryMembershipChangeJob job. $cutoffUnix -= self::ENQUEUE_FUDGE_SEC; // Get the newest revision that has a SRC_CATEGORIZE row... $row = $dbw->selectRow(array('revision', 'recentchanges'), array('rev_timestamp', 'rev_id'), array('rev_page' => $page->getId(), 'rev_timestamp >= ' . $dbw->addQuotes($dbw->timestamp($cutoffUnix))), __METHOD__, array('ORDER BY' => 'rev_timestamp DESC, rev_id DESC'), array('recentchanges' => array('INNER JOIN', array('rc_this_oldid = rev_id', 'rc_source' => RecentChange::SRC_CATEGORIZE, 'rc_cur_id = rev_page', 'rc_timestamp >= rev_timestamp')))); // Only consider revisions newer than any such revision if ($row) { $cutoffUnix = wfTimestamp(TS_UNIX, $row->rev_timestamp); $lastRevId = (int) $row->rev_id; } else { $lastRevId = 0; } // Find revisions to this page made around and after this revision which lack category // notifications in recent changes. This lets jobs pick up were the last one left off. $encCutoff = $dbw->addQuotes($dbw->timestamp($cutoffUnix)); $res = $dbw->select('revision', Revision::selectFields(), array('rev_page' => $page->getId(), "rev_timestamp > {$encCutoff}" . " OR (rev_timestamp = {$encCutoff} AND rev_id > {$lastRevId})"), __METHOD__, array('ORDER BY' => 'rev_timestamp ASC, rev_id ASC')); // Apply all category updates in revision timestamp order foreach ($res as $row) { $this->notifyUpdatesForRevision($page, Revision::newFromRow($row)); } ScopedCallback::consume($unlocker); return true; }
public function run() { $scope = RequestContext::importScopedSession($this->params['session']); $this->addTeardownCallback(function () use(&$scope) { ScopedCallback::consume($scope); // T126450 }); $context = RequestContext::getMain(); $user = $context->getUser(); try { if (!$user->isLoggedIn()) { $this->setLastError("Could not load the author user from session."); return false; } UploadBase::setSessionStatus($user, $this->params['filekey'], ['result' => 'Poll', 'stage' => 'assembling', 'status' => Status::newGood()]); $upload = new UploadFromChunks($user); $upload->continueChunks($this->params['filename'], $this->params['filekey'], new WebRequestUpload($context->getRequest(), 'null')); // Combine all of the chunks into a local file and upload that to a new stash file $status = $upload->concatenateChunks(); if (!$status->isGood()) { UploadBase::setSessionStatus($user, $this->params['filekey'], ['result' => 'Failure', 'stage' => 'assembling', 'status' => $status]); $this->setLastError($status->getWikiText(false, false, 'en')); return false; } // We have a new filekey for the fully concatenated file $newFileKey = $upload->getLocalFile()->getFileKey(); // Remove the old stash file row and first chunk file $upload->stash->removeFileNoAuth($this->params['filekey']); // Build the image info array while we have the local reference handy $apiMain = new ApiMain(); // dummy object (XXX) $imageInfo = $upload->getImageInfo($apiMain->getResult()); // Cleanup any temporary local file $upload->cleanupTempFile(); // Cache the info so the user doesn't have to wait forever to get the final info UploadBase::setSessionStatus($user, $this->params['filekey'], ['result' => 'Success', 'stage' => 'assembling', 'filekey' => $newFileKey, 'imageinfo' => $imageInfo, 'status' => Status::newGood()]); } catch (Exception $e) { UploadBase::setSessionStatus($user, $this->params['filekey'], ['result' => 'Failure', 'stage' => 'assembling', 'status' => Status::newFatal('api-error-stashfailed')]); $this->setLastError(get_class($e) . ": " . $e->getMessage()); // To be extra robust. MWExceptionHandler::rollbackMasterChangesAndLog($e); return false; } return true; }
/** * Reset the session id * * @deprecated since 1.27, use MediaWiki\\Session\\SessionManager instead * @since 1.22 */ function wfResetSessionID() { wfDeprecated(__FUNCTION__, '1.27'); $session = SessionManager::getGlobalSession(); $delay = $session->delaySave(); $session->resetId(); // Make sure a session is started, since that's what the old // wfResetSessionID() did. if (session_id() !== $session->getId()) { wfSetupSession($session->getId()); } ScopedCallback::consume($delay); }
/** * Clear the user's session, and reset the instance cache. * @see logout() */ public function doLogout() { $session = $this->getRequest()->getSession(); if (!$session->canSetUser()) { \MediaWiki\Logger\LoggerFactory::getInstance('session')->warning(__METHOD__ . ": Cannot log out of an immutable session"); $error = 'immutable'; } elseif (!$session->getUser()->equals($this)) { \MediaWiki\Logger\LoggerFactory::getInstance('session')->warning(__METHOD__ . ": Cannot log user \"{$this}\" out of a user \"{$session->getUser()}\"'s session"); // But we still may as well make this user object anon $this->clearInstanceCache('defaults'); $error = 'wronguser'; } else { $this->clearInstanceCache('defaults'); $delay = $session->delaySave(); $session->unpersist(); // Clear cookies (T127436) $session->setLoggedOutTimestamp(time()); $session->setUser(new User()); $session->set('wsUserID', 0); // Other code expects this ScopedCallback::consume($delay); $error = false; } \MediaWiki\Logger\LoggerFactory::getInstance('authmanager')->info('Logout', ['event' => 'logout', 'successful' => $error === false, 'status' => $error ?: 'success']); }
/** * Updates cache as necessary when message page is changed * * @param string|bool $title Name of the page changed (false if deleted) * @param mixed $text New contents of the page. */ public function replace($title, $text) { global $wgMaxMsgCacheEntrySize, $wgContLang, $wgLanguageCode; if ($this->mDisable) { return; } list($msg, $code) = $this->figureMessage($title); if (strpos($title, '/') !== false && $code === $wgLanguageCode) { // Content language overrides do not use the /<code> suffix return; } // Note that if the cache is volatile, load() may trigger a DB fetch. // In that case we reenter/reuse the existing cache key lock to avoid // a self-deadlock. This is safe as no reads happen *directly* in this // method between getReentrantScopedLock() and load() below. There is // no risk of data "changing under our feet" for replace(). $cacheKey = wfMemcKey('messages', $code); $scopedLock = $this->getReentrantScopedLock($cacheKey); $this->load($code, self::FOR_UPDATE); $titleKey = wfMemcKey('messages', 'individual', $title); if ($text === false) { // Article was deleted $this->mCache[$code][$title] = '!NONEXISTENT'; $this->wanCache->delete($titleKey); } elseif (strlen($text) > $wgMaxMsgCacheEntrySize) { // Check for size $this->mCache[$code][$title] = '!TOO BIG'; $this->wanCache->set($titleKey, ' ' . $text, $this->mExpiry); } else { $this->mCache[$code][$title] = ' ' . $text; $this->wanCache->delete($titleKey); } // Mark this cache as definitely "latest" (non-volatile) so // load() calls do try to refresh the cache with slave data $this->mCache[$code]['LATEST'] = time(); // Update caches if the lock was acquired if ($scopedLock) { $this->saveToCaches($this->mCache[$code], 'all', $code); } ScopedCallback::consume($scopedLock); // Relay the purge to APC and other DCs $this->wanCache->touchCheckKey(wfMemcKey('messages', $code)); // Also delete cached sidebar... just in case it is affected $codes = array($code); if ($code === 'en') { // Delete all sidebars, like for example on action=purge on the // sidebar messages $codes = array_keys(Language::fetchLanguageNames()); } foreach ($codes as $code) { $sidebarKey = wfMemcKey('sidebar', $code); $this->wanCache->delete($sidebarKey, 5); } // Update the message in the message blob store $blobStore = new MessageBlobStore(); $blobStore->updateMessage($wgContLang->lcfirst($msg)); Hooks::run('MessageCacheReplace', array($title, $text)); }
public function testResetNotificationTimestamp_futureNotificationTimestampNotForced() { $user = $this->getMockNonAnonUserWithId(1); $oldid = 22; $title = $this->getMockTitle('SomeDbKey'); $title->expects($this->once())->method('getNextRevisionID')->with($oldid)->will($this->returnValue(33)); $mockDb = $this->getMockDb(); $mockDb->expects($this->once())->method('selectRow')->with('watchlist', 'wl_notificationtimestamp', ['wl_user' => 1, 'wl_namespace' => 0, 'wl_title' => 'SomeDbKey'])->will($this->returnValue($this->getFakeRow(['wl_notificationtimestamp' => '30151212010101']))); $mockCache = $this->getMockCache(); $mockDb->expects($this->never())->method('get'); $mockDb->expects($this->never())->method('set'); $mockDb->expects($this->never())->method('delete'); $store = $this->newWatchedItemStore($this->getMockLoadBalancer($mockDb), $mockCache); $addUpdateCallCounter = 0; $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(function ($callable) use(&$addUpdateCallCounter, $title, $user) { $addUpdateCallCounter++; $this->verifyCallbackJob($callable, $title, $user->getId(), function ($time) { return $time === false; }); }); $getTimestampCallCounter = 0; $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(function ($titleParam, $oldidParam) use(&$getTimestampCallCounter, $title, $oldid) { $getTimestampCallCounter++; $this->assertEquals($title, $titleParam); $this->assertEquals($oldid, $oldidParam); }); $this->assertTrue($store->resetNotificationTimestamp($user, $title, '', $oldid)); $this->assertEquals(1, $addUpdateCallCounter); $this->assertEquals(1, $getTimestampCallCounter); ScopedCallback::consume($scopedOverrideDeferred); ScopedCallback::consume($scopedOverrideRevision); }
/** * Get the rendered text for previewing. * @throws MWException * @return string */ function getPreviewText() { global $wgOut, $wgUser, $wgRawHtml, $wgLang; global $wgAllowUserCss, $wgAllowUserJs; $stats = $wgOut->getContext()->getStats(); if ($wgRawHtml && !$this->mTokenOk) { // Could be an offsite preview attempt. This is very unsafe if // HTML is enabled, as it could be an attack. $parsedNote = ''; if ($this->textbox1 !== '') { // Do not put big scary notice, if previewing the empty // string, which happens when you initially edit // a category page, due to automatic preview-on-open. $parsedNote = $wgOut->parse("<div class='previewnote'>" . wfMessage('session_fail_preview_html')->text() . "</div>", true, true); } $stats->increment('edit.failures.session_loss'); return $parsedNote; } $note = ''; try { $content = $this->toEditContent($this->textbox1); $previewHTML = ''; if (!Hooks::run('AlternateEditPreview', array($this, &$content, &$previewHTML, &$this->mParserOutput))) { return $previewHTML; } # provide a anchor link to the editform $continueEditing = '<span class="mw-continue-editing">' . '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage('continue-editing')->text() . ']]</span>'; if ($this->mTriedSave && !$this->mTokenOk) { if ($this->mTokenOkExceptSuffix) { $note = wfMessage('token_suffix_mismatch')->plain(); $stats->increment('edit.failures.bad_token'); } else { $note = wfMessage('session_fail_preview')->plain(); $stats->increment('edit.failures.session_loss'); } } elseif ($this->incompleteForm) { $note = wfMessage('edit_form_incomplete')->plain(); if ($this->mTriedSave) { $stats->increment('edit.failures.incomplete_form'); } } else { $note = wfMessage('previewnote')->plain() . ' ' . $continueEditing; } $parserOptions = $this->page->makeParserOptions($this->mArticle->getContext()); $parserOptions->setIsPreview(true); $parserOptions->setIsSectionPreview(!is_null($this->section) && $this->section !== ''); # don't parse non-wikitext pages, show message about preview if ($this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage()) { if ($this->mTitle->isCssJsSubpage()) { $level = 'user'; } elseif ($this->mTitle->isCssOrJsPage()) { $level = 'site'; } else { $level = false; } if ($content->getModel() == CONTENT_MODEL_CSS) { $format = 'css'; if ($level === 'user' && !$wgAllowUserCss) { $format = false; } } elseif ($content->getModel() == CONTENT_MODEL_JAVASCRIPT) { $format = 'js'; if ($level === 'user' && !$wgAllowUserJs) { $format = false; } } else { $format = false; } # Used messages to make sure grep find them: # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview if ($level && $format) { $note = "<div id='mw-{$level}{$format}preview'>" . wfMessage("{$level}{$format}preview")->text() . ' ' . $continueEditing . "</div>"; } } # If we're adding a comment, we need to show the # summary as the headline if ($this->section === "new" && $this->summary !== "") { $content = $content->addSectionHeader($this->summary); } $hook_args = array($this, &$content); ContentHandler::runLegacyHooks('EditPageGetPreviewText', $hook_args); Hooks::run('EditPageGetPreviewContent', $hook_args); $parserOptions->enableLimitReport(); # For CSS/JS pages, we should have called the ShowRawCssJs hook here. # But it's now deprecated, so never mind $pstContent = $content->preSaveTransform($this->mTitle, $wgUser, $parserOptions); $scopedCallback = $parserOptions->setupFakeRevision($this->mTitle, $pstContent, $wgUser); $parserOutput = $pstContent->getParserOutput($this->mTitle, null, $parserOptions); # Try to stash the edit for the final submission step # @todo: different date format preferences cause cache misses ApiStashEdit::stashEditFromPreview($this->getArticle(), $content, $pstContent, $parserOutput, $parserOptions, $parserOptions, wfTimestampNow()); $parserOutput->setEditSectionTokens(false); // no section edit links $previewHTML = $parserOutput->getText(); $this->mParserOutput = $parserOutput; $wgOut->addParserOutputMetadata($parserOutput); if (count($parserOutput->getWarnings())) { $note .= "\n\n" . implode("\n\n", $parserOutput->getWarnings()); } ScopedCallback::consume($scopedCallback); } catch (MWContentSerializationException $ex) { $m = wfMessage('content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage()); $note .= "\n\n" . $m->parse(); $previewHTML = ''; } if ($this->isConflict) { $conflict = '<h2 id="mw-previewconflict">' . wfMessage('previewconflict')->escaped() . "</h2>\n"; } else { $conflict = '<hr />'; } $previewhead = "<div class='previewnote'>\n" . '<h2 id="mw-previewheader">' . wfMessage('preview')->escaped() . "</h2>" . $wgOut->parse($note, true, true) . $conflict . "</div>\n"; $pageViewLang = $this->mTitle->getPageViewLanguage(); $attribs = array('lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(), 'class' => 'mw-content-' . $pageViewLang->getDir()); $previewHTML = Html::rawElement('div', $attribs, $previewHTML); return $previewhead . $previewHTML . $this->previewTextAfterContent; }
/** * @param string $code * @param array $where List of wfDebug() comments * @return bool Lock acquired and loadFromDB() called */ protected function loadFromDBWithLock($code, array &$where) { global $wgUseLocalMessageCache; $memCache = $this->mMemc; $statusKey = wfMemcKey('messages', $code, 'status'); if (!$memCache->add($statusKey, 'loading', MSG_LOAD_TIMEOUT)) { return false; // could not acquire lock } # Unlock the status key if there is an exception $statusUnlocker = new ScopedCallback(function () use($memCache, $statusKey) { $memCache->delete($statusKey); }); # Now let's regenerate $where[] = 'loading from database'; $cacheKey = wfMemcKey('messages', $code); # Lock the cache to prevent conflicting writes # If this lock fails, it doesn't really matter, it just means the # write is potentially non-atomic, e.g. the results of a replace() # may be discarded. if ($this->lock($cacheKey)) { $that = $this; $mainUnlocker = new ScopedCallback(function () use($that, $cacheKey) { $that->unlock($cacheKey); }); } else { $mainUnlocker = null; $where[] = 'could not acquire main lock'; } $cache = $this->loadFromDB($code); $this->mCache[$code] = $cache; $saveSuccess = $this->saveToCaches($cache, 'all', $code); # Unlock ScopedCallback::consume($mainUnlocker); ScopedCallback::consume($statusUnlocker); if (!$saveSuccess) { # Cache save has failed. # There are two main scenarios where this could be a problem: # # - The cache is more than the maximum size (typically # 1MB compressed). # # - Memcached has no space remaining in the relevant slab # class. This is unlikely with recent versions of # memcached. # # Either way, if there is a local cache, nothing bad will # happen. If there is no local cache, disabling the message # cache for all requests avoids incurring a loadFromDB() # overhead on every request, and thus saves the wiki from # complete downtime under moderate traffic conditions. if (!$wgUseLocalMessageCache) { $memCache->set($statusKey, 'error', 60 * 5); $where[] = 'could not save cache, disabled globally for 5 minutes'; } else { $where[] = "could not save global cache"; } } return true; }
/** * @param User $user * @param bool|null $remember */ private function setSessionDataForUser($user, $remember = null) { $session = $this->request->getSession(); $delay = $session->delaySave(); $session->resetId(); if ($session->canSetUser()) { $session->setUser($user); } if ($remember !== null) { $session->setRememberUser($remember); } $session->set('AuthManager:lastAuthId', $user->getId()); $session->set('AuthManager:lastAuthTimestamp', time()); $session->persist(); \ScopedCallback::consume($delay); \Hooks::run('UserLoggedIn', [$user]); }
/** * Clear the user's session, and reset the instance cache. * @see logout() */ public function doLogout() { $session = $this->getRequest()->getSession(); if (!$session->canSetUser()) { \MediaWiki\Logger\LoggerFactory::getInstance('session')->warning(__METHOD__ . ": Cannot log out of an immutable session"); } elseif (!$session->getUser()->equals($this)) { \MediaWiki\Logger\LoggerFactory::getInstance('session')->warning(__METHOD__ . ": Cannot log user \"{$this}\" out of a user \"{$session->getUser()}\"'s session"); // But we still may as well make this user object anon $this->clearInstanceCache('defaults'); } else { $this->clearInstanceCache('defaults'); $delay = $session->delaySave(); $session->setLoggedOutTimestamp(time()); $session->setUser(new User()); $session->set('wsUserID', 0); // Other code expects this ScopedCallback::consume($delay); } }
/** * @dataProvider provideCategoryContent * @covers RecentChange::newForCategorization */ public function testHiddenCategoryChange($isHidden) { $categoryTitle = Title::newFromText('CategoryPage', NS_CATEGORY); $pageProps = $this->getMockPageProps(); $pageProps->expects($this->once())->method('getProperties')->with($categoryTitle, 'hiddencat')->will($this->returnValue($isHidden ? [$categoryTitle->getArticleID() => ''] : [])); $scopedOverride = PageProps::overrideInstance($pageProps); $rc = RecentChange::newForCategorization('0', $categoryTitle, $this->user, $this->user_comment, $this->title, $categoryTitle->getLatestRevID(), $categoryTitle->getLatestRevID(), '0', false); $this->assertEquals($isHidden, $rc->getParam('hidden-cat')); ScopedCallback::consume($scopedOverride); }
/** * 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}"); } }
public function doUpdate() { # Page may already be deleted, so don't just getId() $id = $this->pageId; // Make sure all links update threads see the changes of each other. // This handles the case when updates have to batched into several COMMITs. $scopedLock = LinksUpdate::acquirePageLock($this->mDb, $id); # Delete restrictions for it $this->mDb->delete('page_restrictions', ['pr_page' => $id], __METHOD__); # Fix category table counts $cats = $this->mDb->selectFieldValues('categorylinks', 'cl_to', ['cl_from' => $id], __METHOD__); $this->page->updateCategoryCounts([], $cats); # If using cascading deletes, we can skip some explicit deletes if (!$this->mDb->cascadingDeletes()) { # Delete outgoing links $this->mDb->delete('pagelinks', ['pl_from' => $id], __METHOD__); $this->mDb->delete('imagelinks', ['il_from' => $id], __METHOD__); $this->mDb->delete('categorylinks', ['cl_from' => $id], __METHOD__); $this->mDb->delete('templatelinks', ['tl_from' => $id], __METHOD__); $this->mDb->delete('externallinks', ['el_from' => $id], __METHOD__); $this->mDb->delete('langlinks', ['ll_from' => $id], __METHOD__); $this->mDb->delete('iwlinks', ['iwl_from' => $id], __METHOD__); $this->mDb->delete('redirect', ['rd_from' => $id], __METHOD__); $this->mDb->delete('page_props', ['pp_page' => $id], __METHOD__); } # If using cleanup triggers, we can skip some manual deletes if (!$this->mDb->cleanupTriggers()) { $title = $this->page->getTitle(); # Find recentchanges entries to clean up... $rcIdsForTitle = $this->mDb->selectFieldValues('recentchanges', 'rc_id', ['rc_type != ' . RC_LOG, 'rc_namespace' => $title->getNamespace(), 'rc_title' => $title->getDBkey()], __METHOD__); $rcIdsForPage = $this->mDb->selectFieldValues('recentchanges', 'rc_id', ['rc_type != ' . RC_LOG, 'rc_cur_id' => $id], __METHOD__); # T98706: delete PK to avoid lock contention with RC delete log insertions $rcIds = array_merge($rcIdsForTitle, $rcIdsForPage); if ($rcIds) { $this->mDb->delete('recentchanges', ['rc_id' => $rcIds], __METHOD__); } } $this->mDb->onTransactionIdle(function () use(&$scopedLock) { // Release the lock *after* the final COMMIT for correctness ScopedCallback::consume($scopedLock); }); }
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(); }
/** * @param SectionProfiler $profiler * @param string $section */ public function __construct(SectionProfiler $profiler, $section) { parent::__construct(null); $this->profiler = $profiler; $this->section = $section; }
public function testLogin() { // Test failure when bot passwords aren't enabled $this->setMwGlobals('wgEnableBotPasswords', false); $status = BotPassword::login("{$this->testUserName}@BotPassword", 'foobaz', new FauxRequest()); $this->assertEquals(Status::newFatal('botpasswords-disabled'), $status); $this->setMwGlobals('wgEnableBotPasswords', true); // Test failure when BotPasswordSessionProvider isn't configured $manager = new SessionManager(['logger' => new Psr\Log\NullLogger(), 'store' => new EmptyBagOStuff()]); $reset = MediaWiki\Session\TestUtils::setSessionManagerSingleton($manager); $this->assertNull($manager->getProvider(MediaWiki\Session\BotPasswordSessionProvider::class), 'sanity check'); $status = BotPassword::login("{$this->testUserName}@BotPassword", 'foobaz', new FauxRequest()); $this->assertEquals(Status::newFatal('botpasswords-no-provider'), $status); ScopedCallback::consume($reset); // Now configure BotPasswordSessionProvider for further tests... $mainConfig = RequestContext::getMain()->getConfig(); $config = new HashConfig(['SessionProviders' => $mainConfig->get('SessionProviders') + [MediaWiki\Session\BotPasswordSessionProvider::class => ['class' => MediaWiki\Session\BotPasswordSessionProvider::class, 'args' => [['priority' => 40]]]]]); $manager = new SessionManager(['config' => new MultiConfig([$config, RequestContext::getMain()->getConfig()]), 'logger' => new Psr\Log\NullLogger(), 'store' => new EmptyBagOStuff()]); $reset = MediaWiki\Session\TestUtils::setSessionManagerSingleton($manager); // No "@"-thing in the username $status = BotPassword::login($this->testUserName, 'foobaz', new FauxRequest()); $this->assertEquals(Status::newFatal('botpasswords-invalid-name', '@'), $status); // No base user $status = BotPassword::login('UTDummy@BotPassword', 'foobaz', new FauxRequest()); $this->assertEquals(Status::newFatal('nosuchuser', 'UTDummy'), $status); // No bot password $status = BotPassword::login("{$this->testUserName}@DoesNotExist", 'foobaz', new FauxRequest()); $this->assertEquals(Status::newFatal('botpasswords-not-exist', $this->testUserName, 'DoesNotExist'), $status); // Failed restriction $request = $this->getMock('FauxRequest', ['getIP']); $request->expects($this->any())->method('getIP')->will($this->returnValue('10.0.0.1')); $status = BotPassword::login("{$this->testUserName}@BotPassword", 'foobaz', $request); $this->assertEquals(Status::newFatal('botpasswords-restriction-failed'), $status); // Wrong password $status = BotPassword::login("{$this->testUserName}@BotPassword", $this->testUser->password, new FauxRequest()); $this->assertEquals(Status::newFatal('wrongpassword'), $status); // Success! $request = new FauxRequest(); $this->assertNotInstanceOf(MediaWiki\Session\BotPasswordSessionProvider::class, $request->getSession()->getProvider(), 'sanity check'); $status = BotPassword::login("{$this->testUserName}@BotPassword", 'foobaz', $request); $this->assertInstanceOf('Status', $status); $this->assertTrue($status->isGood()); $session = $status->getValue(); $this->assertInstanceOf(MediaWiki\Session\Session::class, $session); $this->assertInstanceOf(MediaWiki\Session\BotPasswordSessionProvider::class, $session->getProvider()); $this->assertSame($session->getId(), $request->getSession()->getId()); ScopedCallback::consume($reset); }
public function testAccountCreationEmail() { $creator = \User::newFromName('Foo'); $user = \User::newFromName('UTSysop'); $reset = new \ScopedCallback(function ($email) use($user) { $user->setEmail($email); $user->saveSettings(); }, [$user->getEmail()]); $user->setEmail(null); $req = TemporaryPasswordAuthenticationRequest::newRandom(); $req->username = $user->getName(); $req->mailpassword = true; $provider = $this->getProvider(['emailEnabled' => false]); $status = $provider->testForAccountCreation($user, $creator, [$req]); $this->assertEquals(\StatusValue::newFatal('emaildisabled'), $status); $req->hasBackchannel = true; $status = $provider->testForAccountCreation($user, $creator, [$req]); $this->assertFalse($status->hasMessage('emaildisabled')); $req->hasBackchannel = false; $provider = $this->getProvider(['emailEnabled' => true]); $status = $provider->testForAccountCreation($user, $creator, [$req]); $this->assertEquals(\StatusValue::newFatal('noemailcreate'), $status); $req->hasBackchannel = true; $status = $provider->testForAccountCreation($user, $creator, [$req]); $this->assertFalse($status->hasMessage('noemailcreate')); $req->hasBackchannel = false; $user->setEmail('*****@*****.**'); $status = $provider->testForAccountCreation($user, $creator, [$req]); $this->assertEquals(\StatusValue::newGood(), $status); $mailed = false; $resetMailer = $this->hookMailer(function ($headers, $to, $from, $subject, $body) use(&$mailed, $req) { $mailed = true; $this->assertSame('*****@*****.**', $to[0]->address); $this->assertContains($req->password, $body); return false; }); $expect = AuthenticationResponse::newPass('UTSysop'); $expect->createRequest = clone $req; $expect->createRequest->username = '******'; $res = $provider->beginPrimaryAccountCreation($user, $creator, [$req]); $this->assertEquals($expect, $res); $this->assertTrue($this->manager->getAuthenticationSessionData('no-email')); $this->assertFalse($mailed); $this->assertSame('byemail', $provider->finishAccountCreation($user, $creator, $res)); $this->assertTrue($mailed); \ScopedCallback::consume($resetMailer); $this->assertTrue($mailed); }
/** * Run an SQL query and return the result. Normally throws a DBQueryError * on failure. If errors are ignored, returns false instead. * * In new code, the query wrappers select(), insert(), update(), delete(), * etc. should be used where possible, since they give much better DBMS * independence and automatically quote or validate user input in a variety * of contexts. This function is generally only useful for queries which are * explicitly DBMS-dependent and are unsupported by the query wrappers, such * as CREATE TABLE. * * However, the query wrappers themselves should call this function. * * @param string $sql SQL query * @param string $fname Name of the calling function, for profiling/SHOW PROCESSLIST * comment (you can use __METHOD__ or add some extra info) * @param bool $tempIgnore Whether to avoid throwing an exception on errors... * maybe best to catch the exception instead? * @throws MWException * @return bool|ResultWrapper True for a successful write query, ResultWrapper object * for a successful read query, or false on failure if $tempIgnore set */ public function query($sql, $fname = __METHOD__, $tempIgnore = false) { global $wgUser; $this->mLastQuery = $sql; $isWriteQuery = $this->isWriteQuery($sql); if ($isWriteQuery) { $reason = $this->getReadOnlyReason(); if ($reason !== false) { throw new DBReadOnlyError($this, "Database is read-only: {$reason}"); } # Set a flag indicating that writes have been done $this->mDoneWrites = microtime(true); } # Add a comment for easy SHOW PROCESSLIST interpretation if (is_object($wgUser) && $wgUser->isItemLoaded('name')) { $userName = $wgUser->getName(); if (mb_strlen($userName) > 15) { $userName = mb_substr($userName, 0, 15) . '...'; } $userName = str_replace('/', '', $userName); } else { $userName = ''; } // Add trace comment to the begin of the sql string, right after the operator. // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598) $commentedSql = preg_replace('/\\s|$/', " /* {$fname} {$userName} */ ", $sql, 1); if (!$this->mTrxLevel && $this->getFlag(DBO_TRX) && $this->isTransactableQuery($sql)) { $this->begin(__METHOD__ . " ({$fname})"); $this->mTrxAutomatic = true; } # Keep track of whether the transaction has write queries pending if ($this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery) { $this->mTrxDoneWrites = true; $this->getTransactionProfiler()->transactionWritingIn($this->mServer, $this->mDBname, $this->mTrxShortId); } $isMaster = !is_null($this->getLBInfo('master')); # generalizeSQL will probably cut down the query to reasonable # logging size most of the time. The substr is really just a sanity check. if ($isMaster) { $queryProf = 'query-m: ' . substr(DatabaseBase::generalizeSQL($sql), 0, 255); $totalProf = 'DatabaseBase::query-master'; } else { $queryProf = 'query: ' . substr(DatabaseBase::generalizeSQL($sql), 0, 255); $totalProf = 'DatabaseBase::query'; } # Include query transaction state $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : ""; $profiler = Profiler::instance(); if (!$profiler instanceof ProfilerStub) { $totalProfSection = $profiler->scopedProfileIn($totalProf); $queryProfSection = $profiler->scopedProfileIn($queryProf); } if ($this->debug()) { wfDebugLog('queries', sprintf("%s: %s", $this->mDBname, $sql)); } $queryId = MWDebug::query($sql, $fname, $isMaster); # Avoid fatals if close() was called $this->assertOpen(); # Do the query and handle errors $startTime = microtime(true); $ret = $this->doQuery($commentedSql); $queryRuntime = microtime(true) - $startTime; # Log the query time and feed it into the DB trx profiler $this->getTransactionProfiler()->recordQueryCompletion($queryProf, $startTime, $isWriteQuery, $this->affectedRows()); MWDebug::queryTime($queryId); # Try reconnecting if the connection was lost if (false === $ret && $this->wasErrorReissuable()) { # Transaction is gone, like it or not $hadTrx = $this->mTrxLevel; // possible lost transaction $this->mTrxLevel = 0; $this->mTrxIdleCallbacks = array(); // bug 65263 $this->mTrxPreCommitCallbacks = array(); // bug 65263 wfDebug("Connection lost, reconnecting...\n"); # Stash the last error values since ping() might clear them $lastError = $this->lastError(); $lastErrno = $this->lastErrno(); if ($this->ping()) { wfDebug("Reconnected\n"); $server = $this->getServer(); $msg = __METHOD__ . ": lost connection to {$server}; reconnected"; wfDebugLog('DBPerformance', "{$msg}:\n" . wfBacktrace(true)); if ($hadTrx) { # Leave $ret as false and let an error be reported. # Callers may catch the exception and continue to use the DB. $this->reportQueryError($lastError, $lastErrno, $sql, $fname, $tempIgnore); } else { # Should be safe to silently retry (no trx and thus no callbacks) $startTime = microtime(true); $ret = $this->doQuery($commentedSql); $queryRuntime = microtime(true) - $startTime; # Log the query time and feed it into the DB trx profiler $this->getTransactionProfiler()->recordQueryCompletion($queryProf, $startTime, $isWriteQuery, $this->affectedRows()); } } else { wfDebug("Failed\n"); } } if (false === $ret) { $this->reportQueryError($this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore); } $res = $this->resultObject($ret); // Destroy profile sections in the opposite order to their creation ScopedCallback::consume($queryProfSection); ScopedCallback::consume($totalProfSection); if ($isWriteQuery && $this->mTrxLevel) { $this->mTrxWriteDuration += $queryRuntime; } return $res; }
/** * Loads messages from caches or from database in this order: * (1) local message cache (if $wgUseLocalMessageCache is enabled) * (2) memcached * (3) from the database. * * When succesfully loading from (2) or (3), all higher level caches are * updated for the newest version. * * Nothing is loaded if member variable mDisable is true, either manually * set by calling code or if message loading fails (is this possible?). * * Returns true if cache is already populated or it was succesfully populated, * or false if populating empty cache fails. Also returns true if MessageCache * is disabled. * * @param bool|string $code Language to which load messages * @throws MWException * @return bool */ function load($code = false) { global $wgUseLocalMessageCache; if (!is_string($code)) { # This isn't really nice, so at least make a note about it and try to # fall back wfDebug(__METHOD__ . " called without providing a language code\n"); $code = 'en'; } # Don't do double loading... if (isset($this->mLoadedLanguages[$code])) { return true; } # 8 lines of code just to say (once) that message cache is disabled if ($this->mDisable) { static $shownDisabled = false; if (!$shownDisabled) { wfDebug(__METHOD__ . ": disabled\n"); $shownDisabled = true; } return true; } # Loading code starts wfProfileIn(__METHOD__); $success = false; # Keep track of success $staleCache = false; # a cache array with expired data, or false if none has been loaded $where = array(); # Debug info, delayed to avoid spamming debug log too much $cacheKey = wfMemcKey('messages', $code); # Key in memc for messages # Local cache # Hash of the contents is stored in memcache, to detect if local cache goes # out of date (e.g. due to replace() on some other server) if ($wgUseLocalMessageCache) { wfProfileIn(__METHOD__ . '-fromlocal'); $hash = $this->mMemc->get(wfMemcKey('messages', $code, 'hash')); if ($hash) { $cache = $this->getLocalCache($hash, $code); if (!$cache) { $where[] = 'local cache is empty or has the wrong hash'; } elseif ($this->isCacheExpired($cache)) { $where[] = 'local cache is expired'; $staleCache = $cache; } else { $where[] = 'got from local cache'; $success = true; $this->mCache[$code] = $cache; } } wfProfileOut(__METHOD__ . '-fromlocal'); } if (!$success) { # Try the global cache. If it is empty, try to acquire a lock. If # the lock can't be acquired, wait for the other thread to finish # and then try the global cache a second time. for ($failedAttempts = 0; $failedAttempts < 2; $failedAttempts++) { wfProfileIn(__METHOD__ . '-fromcache'); $cache = $this->mMemc->get($cacheKey); if (!$cache) { $where[] = 'global cache is empty'; } elseif ($this->isCacheExpired($cache)) { $where[] = 'global cache is expired'; $staleCache = $cache; } else { $where[] = 'got from global cache'; $this->mCache[$code] = $cache; $this->saveToCaches($cache, 'local-only', $code); $success = true; } wfProfileOut(__METHOD__ . '-fromcache'); if ($success) { # Done, no need to retry break; } # We need to call loadFromDB. Limit the concurrency to a single # process. This prevents the site from going down when the cache # expires. $statusKey = wfMemcKey('messages', $code, 'status'); $acquired = $this->mMemc->add($statusKey, 'loading', MSG_LOAD_TIMEOUT); if ($acquired) { # Unlock the status key if there is an exception $that = $this; $statusUnlocker = new ScopedCallback(function () use($that, $statusKey) { $that->mMemc->delete($statusKey); }); # Now let's regenerate $where[] = 'loading from database'; # Lock the cache to prevent conflicting writes # If this lock fails, it doesn't really matter, it just means the # write is potentially non-atomic, e.g. the results of a replace() # may be discarded. if ($this->lock($cacheKey)) { $mainUnlocker = new ScopedCallback(function () use($that, $cacheKey) { $that->unlock($cacheKey); }); } else { $mainUnlocker = null; $where[] = 'could not acquire main lock'; } $cache = $this->loadFromDB($code); $this->mCache[$code] = $cache; $success = true; $saveSuccess = $this->saveToCaches($cache, 'all', $code); # Unlock ScopedCallback::consume($mainUnlocker); ScopedCallback::consume($statusUnlocker); if (!$saveSuccess) { # Cache save has failed. # There are two main scenarios where this could be a problem: # # - The cache is more than the maximum size (typically # 1MB compressed). # # - Memcached has no space remaining in the relevant slab # class. This is unlikely with recent versions of # memcached. # # Either way, if there is a local cache, nothing bad will # happen. If there is no local cache, disabling the message # cache for all requests avoids incurring a loadFromDB() # overhead on every request, and thus saves the wiki from # complete downtime under moderate traffic conditions. if (!$wgUseLocalMessageCache) { $this->mMemc->set($statusKey, 'error', 60 * 5); $where[] = 'could not save cache, disabled globally for 5 minutes'; } else { $where[] = "could not save global cache"; } } # Load from DB complete, no need to retry break; } elseif ($staleCache) { # Use the stale cache while some other thread constructs the new one $where[] = 'using stale cache'; $this->mCache[$code] = $staleCache; $success = true; break; } elseif ($failedAttempts > 0) { # Already retried once, still failed, so don't do another lock/unlock cycle # This case will typically be hit if memcached is down, or if # loadFromDB() takes longer than MSG_WAIT_TIMEOUT $where[] = "could not acquire status key."; break; } else { $status = $this->mMemc->get($statusKey); if ($status === 'error') { # Disable cache break; } else { # Wait for the other thread to finish, then retry $where[] = 'waited for other thread to complete'; $this->lock($cacheKey); $this->unlock($cacheKey); } } } } if (!$success) { $where[] = 'loading FAILED - cache is disabled'; $this->mDisable = true; $this->mCache = false; # This used to throw an exception, but that led to nasty side effects like # the whole wiki being instantly down if the memcached server died } else { # All good, just record the success $this->mLoadedLanguages[$code] = true; } $info = implode(', ', $where); wfDebug(__METHOD__ . ": Loading {$code}... {$info}\n"); wfProfileOut(__METHOD__); return $success; }
public function testCheckAccountCreatePermissions() { global $wgGroupPermissions; $this->stashMwGlobals(['wgGroupPermissions']); $this->initializeManager(true); $wgGroupPermissions['*']['createaccount'] = true; $this->assertEquals(\Status::newGood(), $this->manager->checkAccountCreatePermissions(new \User())); $this->setMwGlobals(['wgReadOnly' => 'Because']); $this->assertEquals(\Status::newFatal('readonlytext', 'Because'), $this->manager->checkAccountCreatePermissions(new \User())); $this->setMwGlobals(['wgReadOnly' => false]); $wgGroupPermissions['*']['createaccount'] = false; $status = $this->manager->checkAccountCreatePermissions(new \User()); $this->assertFalse($status->isOK()); $this->assertTrue($status->hasMessage('badaccess-groups')); $wgGroupPermissions['*']['createaccount'] = true; $user = \User::newFromName('UTBlockee'); if ($user->getID() == 0) { $user->addToDatabase(); \TestUser::setPasswordForUser($user, 'UTBlockeePassword'); $user->saveSettings(); } $oldBlock = \Block::newFromTarget('UTBlockee'); if ($oldBlock) { // An old block will prevent our new one from saving. $oldBlock->delete(); } $blockOptions = ['address' => 'UTBlockee', 'user' => $user->getID(), 'reason' => __METHOD__, 'expiry' => time() + 100500, 'createAccount' => true]; $block = new \Block($blockOptions); $block->insert(); $status = $this->manager->checkAccountCreatePermissions($user); $this->assertFalse($status->isOK()); $this->assertTrue($status->hasMessage('cantcreateaccount-text')); $blockOptions = ['address' => '127.0.0.0/24', 'reason' => __METHOD__, 'expiry' => time() + 100500, 'createAccount' => true]; $block = new \Block($blockOptions); $block->insert(); $scopeVariable = new \ScopedCallback([$block, 'delete']); $status = $this->manager->checkAccountCreatePermissions(new \User()); $this->assertFalse($status->isOK()); $this->assertTrue($status->hasMessage('cantcreateaccount-range-text')); \ScopedCallback::consume($scopeVariable); $this->setMwGlobals(['wgEnableDnsBlacklist' => true, 'wgDnsBlacklistUrls' => ['local.wmftest.net'], 'wgProxyWhitelist' => []]); $status = $this->manager->checkAccountCreatePermissions(new \User()); $this->assertFalse($status->isOK()); $this->assertTrue($status->hasMessage('sorbs_create_account_reason')); $this->setMwGlobals('wgProxyWhitelist', ['127.0.0.1']); $status = $this->manager->checkAccountCreatePermissions(new \User()); $this->assertTrue($status->isGood()); }
public function testDelaySave() { $this->mergeMwGlobalArrayValue('wgHooks', array('SessionMetadata' => array($this))); $backend = $this->getBackend(); $priv = \TestingAccessWrapper::newFromObject($backend); $priv->persist = true; // Saves happen normally when no delay is in effect $this->onSessionMetadataCalled = false; $priv->metaDirty = true; $backend->save(); $this->assertTrue($this->onSessionMetadataCalled, 'sanity check'); $this->onSessionMetadataCalled = false; $priv->metaDirty = true; $priv->autosave(); $this->assertTrue($this->onSessionMetadataCalled, 'sanity check'); $delay = $backend->delaySave(); // Autosave doesn't happen when no delay is in effect $this->onSessionMetadataCalled = false; $priv->metaDirty = true; $priv->autosave(); $this->assertFalse($this->onSessionMetadataCalled); // Save still does happen when no delay is in effect $priv->save(); $this->assertTrue($this->onSessionMetadataCalled); // Save happens when delay is consumed $this->onSessionMetadataCalled = false; $priv->metaDirty = true; \ScopedCallback::consume($delay); $this->assertTrue($this->onSessionMetadataCalled); // Test multiple delays $delay1 = $backend->delaySave(); $delay2 = $backend->delaySave(); $delay3 = $backend->delaySave(); $this->onSessionMetadataCalled = false; $priv->metaDirty = true; $priv->autosave(); $this->assertFalse($this->onSessionMetadataCalled); \ScopedCallback::consume($delay3); $this->assertFalse($this->onSessionMetadataCalled); \ScopedCallback::consume($delay1); $this->assertFalse($this->onSessionMetadataCalled); \ScopedCallback::consume($delay2); $this->assertTrue($this->onSessionMetadataCalled); }
/** * Create a session corresponding to the passed SessionInfo * @private For use by a SessionProvider that needs to specially create its * own session. * @param SessionInfo $info * @param WebRequest $request * @return Session */ public function getSessionFromInfo(SessionInfo $info, WebRequest $request) { $id = $info->getId(); if (!isset($this->allSessionBackends[$id])) { if (!isset($this->allSessionIds[$id])) { $this->allSessionIds[$id] = new SessionId($id); } $backend = new SessionBackend($this->allSessionIds[$id], $info, $this->store, $this->logger, $this->config->get('ObjectCacheSessionExpiry')); $this->allSessionBackends[$id] = $backend; $delay = $backend->delaySave(); } else { $backend = $this->allSessionBackends[$id]; $delay = $backend->delaySave(); if ($info->wasPersisted()) { $backend->persist(); } if ($info->wasRemembered()) { $backend->setRememberUser(true); } } $request->setSessionId($backend->getSessionId()); $session = $backend->getSession($request); if (!$info->isIdSafe()) { $session->resetId(); } \ScopedCallback::consume($delay); return $session; }
/** * Update link tables with outgoing links from an updated article * * @note: this is managed by DeferredUpdates::execute(). Do not run this in a transaction. */ public function doUpdate() { // Make sure all links update threads see the changes of each other. // This handles the case when updates have to batched into several COMMITs. $scopedLock = self::acquirePageLock($this->mDb, $this->mId); Hooks::run('LinksUpdate', [&$this]); $this->doIncrementalUpdate(); $this->mDb->onTransactionIdle(function () use(&$scopedLock) { Hooks::run('LinksUpdateComplete', [&$this]); // Release the lock *after* the final COMMIT for correctness ScopedCallback::consume($scopedLock); }); }