Ejemplo n.º 1
0
 /**
  * @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);
 }
Ejemplo n.º 2
0
 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;
 }
Ejemplo n.º 5
0
/**
 * 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);
}
Ejemplo n.º 6
0
 /**
  * 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']);
 }
Ejemplo n.º 7
0
 /**
  * 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);
 }
Ejemplo n.º 9
0
 /**
  * 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;
 }
Ejemplo n.º 10
0
 /**
  * @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;
 }
Ejemplo n.º 11
0
 /**
  * @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]);
 }
Ejemplo n.º 12
0
Archivo: User.php Proyecto: paladox/2
 /**
  * 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);
     }
 }
Ejemplo n.º 13
0
 /**
  * @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);
 }
Ejemplo n.º 14
0
 /**
  * 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}");
     }
 }
Ejemplo n.º 15
0
 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);
     });
 }
Ejemplo n.º 16
0
 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;
 }
Ejemplo n.º 18
0
 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);
 }
Ejemplo n.º 20
0
 /**
  * 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;
 }
Ejemplo n.º 21
0
 /**
  * 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;
 }
Ejemplo n.º 22
0
 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());
 }
Ejemplo n.º 23
0
 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);
 }
Ejemplo n.º 24
0
 /**
  * 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;
 }
Ejemplo n.º 25
0
 /**
  * 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);
     });
 }