/** * Loads everything except the text * This isn't necessary for all uses, so it's only done if needed. */ protected function loadLastEdit() { if ($this->mLastRevision !== null) { return; // already loaded } $latest = $this->getLatest(); if (!$latest) { return; // page doesn't exist or is missing page_latest info } if ($this->mDataLoadedFrom == self::READ_LOCKING) { // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always // includes the latest changes committed. This is true even within REPEATABLE-READ // transactions, where S1 normally only sees changes committed before the first S1 // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it // may not find it since a page row UPDATE and revision row INSERT by S2 may have // happened after the first S1 SELECT. // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read $flags = Revision::READ_LOCKING; $revision = Revision::newFromPageId($this->getId(), $latest, $flags); } elseif ($this->mDataLoadedFrom == self::READ_LATEST) { // Bug T93976: if page_latest was loaded from the master, fetch the // revision from there as well, as it may not exist yet on a replica DB. // Also, this keeps the queries in the same REPEATABLE-READ snapshot. $flags = Revision::READ_LATEST; $revision = Revision::newFromPageId($this->getId(), $latest, $flags); } else { $dbr = wfGetDB(DB_REPLICA); $revision = Revision::newKnownCurrent($dbr, $this->getId(), $latest); } if ($revision) { // sanity $this->setLastEdit($revision); } }
/** * Loads everything except the text * This isn't necessary for all uses, so it's only done if needed. */ protected function loadLastEdit() { if ( $this->mLastRevision !== null ) { return; // already loaded } $latest = $this->getLatest(); if ( !$latest ) { return; // page doesn't exist or is missing page_latest info } // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the // latest changes committed. This is true even within REPEATABLE-READ transactions, where // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT. // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read. $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0; $revision = Revision::newFromPageId( $this->getId(), $latest, $flags ); if ( $revision ) { // sanity $this->setLastEdit( $revision ); } }
public function execute() { global $wgMemc; $user = $this->getUser(); $params = $this->extractRequestParams(); $page = $this->getTitleOrPageId($params); $title = $page->getTitle(); if (!ContentHandler::getForModelID($params['contentmodel'])->isSupportedFormat($params['contentformat'])) { $this->dieUsage("Unsupported content model/format", 'badmodelformat'); } // Trim and fix newlines so the key SHA1's match (see RequestContext::getText()) $text = rtrim(str_replace("\r\n", "\n", $params['text'])); $textContent = ContentHandler::makeContent($text, $title, $params['contentmodel'], $params['contentformat']); $page = WikiPage::factory($title); if ($page->exists()) { // Page exists: get the merged content with the proposed change $baseRev = Revision::newFromPageId($page->getId(), $params['baserevid']); if (!$baseRev) { $this->dieUsage("No revision ID {$params['baserevid']}", 'missingrev'); } $currentRev = $page->getRevision(); if (!$currentRev) { $this->dieUsage("No current revision of page ID {$page->getId()}", 'missingrev'); } // Merge in the new version of the section to get the proposed version $editContent = $page->replaceSectionAtRev($params['section'], $textContent, $params['sectiontitle'], $baseRev->getId()); if (!$editContent) { $this->dieUsage("Could not merge updated section.", 'replacefailed'); } if ($currentRev->getId() == $baseRev->getId()) { // Base revision was still the latest; nothing to merge $content = $editContent; } else { // Merge the edit into the current version $baseContent = $baseRev->getContent(); $currentContent = $currentRev->getContent(); if (!$baseContent || !$currentContent) { $this->dieUsage("Missing content for page ID {$page->getId()}", 'missingrev'); } $handler = ContentHandler::getForModelID($baseContent->getModel()); $content = $handler->merge3($baseContent, $editContent, $currentContent); } } else { // New pages: use the user-provided content model $content = $textContent; } if (!$content) { // merge3() failed $this->getResult()->addValue(null, $this->getModuleName(), array('status' => 'editconflict')); return; } // The user will abort the AJAX request by pressing "save", so ignore that ignore_user_abort(true); // Get a key based on the source text, format, and user preferences $key = self::getStashKey($title, $content, $user); // De-duplicate requests on the same key if ($user->pingLimiter('stashedit')) { $status = 'ratelimited'; } elseif ($wgMemc->lock($key, 0, 30)) { /** @noinspection PhpUnusedLocalVariableInspection */ $unlocker = new ScopedCallback(function () use($key) { global $wgMemc; $wgMemc->unlock($key); }); $status = self::parseAndStash($page, $content, $user); } else { $status = 'busy'; } $this->getResult()->addValue(null, $this->getModuleName(), array('status' => $status)); }
/** * Determine if we can merge a page. * We check if an inaccessible revision would become the latest and * deny the merge if so -- it's theoretically possible to update the * latest revision, but opens a can of worms -- search engine updates, * recentchanges review, etc. * * @param integer $id The page_id * @param Title $newTitle The new title * @param string $logStatus This is set to the log status message on failure * @return bool */ private function canMerge($id, Title $newTitle, &$logStatus) { $latestDest = Revision::newFromTitle($newTitle, 0, Revision::READ_LATEST); $latestSource = Revision::newFromPageId($id, 0, Revision::READ_LATEST); if ($latestSource->getTimestamp() > $latestDest->getTimestamp()) { $logStatus = 'cannot merge since source is later'; return false; } else { return true; } }
/** * Loads everything except the text * This isn't necessary for all uses, so it's only done if needed. */ protected function loadLastEdit() { if ($this->mLastRevision !== null) { return; // already loaded } $latest = $this->getLatest(); if (!$latest) { return; // page doesn't exist or is missing page_latest info } $revision = Revision::newFromPageId($this->getId(), $latest); if ($revision) { // sanity $this->setLastEdit($revision); } }
public function execute() { $user = $this->getUser(); $params = $this->extractRequestParams(); if ($user->isBot()) { // sanity $this->dieUsage('This interface is not supported for bots', 'botsnotsupported'); } $cache = ObjectCache::getLocalClusterInstance(); $page = $this->getTitleOrPageId($params); $title = $page->getTitle(); if (!ContentHandler::getForModelID($params['contentmodel'])->isSupportedFormat($params['contentformat'])) { $this->dieUsage('Unsupported content model/format', 'badmodelformat'); } $text = null; $textHash = null; if (strlen($params['stashedtexthash'])) { // Load from cache since the client indicates the text is the same as last stash $textHash = $params['stashedtexthash']; $textKey = $cache->makeKey('stashedit', 'text', $textHash); $text = $cache->get($textKey); if (!is_string($text)) { $this->dieUsage('No stashed text found with the given hash', 'missingtext'); } } elseif ($params['text'] !== null) { // Trim and fix newlines so the key SHA1's match (see WebRequest::getText()) $text = rtrim(str_replace("\r\n", "\n", $params['text'])); $textHash = sha1($text); } else { $this->dieUsage('The text or stashedtexthash parameter must be given', 'missingtextparam'); } $textContent = ContentHandler::makeContent($text, $title, $params['contentmodel'], $params['contentformat']); $page = WikiPage::factory($title); if ($page->exists()) { // Page exists: get the merged content with the proposed change $baseRev = Revision::newFromPageId($page->getId(), $params['baserevid']); if (!$baseRev) { $this->dieUsage("No revision ID {$params['baserevid']}", 'missingrev'); } $currentRev = $page->getRevision(); if (!$currentRev) { $this->dieUsage("No current revision of page ID {$page->getId()}", 'missingrev'); } // Merge in the new version of the section to get the proposed version $editContent = $page->replaceSectionAtRev($params['section'], $textContent, $params['sectiontitle'], $baseRev->getId()); if (!$editContent) { $this->dieUsage('Could not merge updated section.', 'replacefailed'); } if ($currentRev->getId() == $baseRev->getId()) { // Base revision was still the latest; nothing to merge $content = $editContent; } else { // Merge the edit into the current version $baseContent = $baseRev->getContent(); $currentContent = $currentRev->getContent(); if (!$baseContent || !$currentContent) { $this->dieUsage("Missing content for page ID {$page->getId()}", 'missingrev'); } $handler = ContentHandler::getForModelID($baseContent->getModel()); $content = $handler->merge3($baseContent, $editContent, $currentContent); } } else { // New pages: use the user-provided content model $content = $textContent; } if (!$content) { // merge3() failed $this->getResult()->addValue(null, $this->getModuleName(), ['status' => 'editconflict']); return; } // The user will abort the AJAX request by pressing "save", so ignore that ignore_user_abort(true); if ($user->pingLimiter('stashedit')) { $status = 'ratelimited'; } else { $status = self::parseAndStash($page, $content, $user, $params['summary']); $textKey = $cache->makeKey('stashedit', 'text', $textHash); $cache->set($textKey, $text, self::MAX_CACHE_TTL); } $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); $stats->increment("editstash.cache_stores.{$status}"); $this->getResult()->addValue(null, $this->getModuleName(), ['status' => $status, 'texthash' => $textHash]); }
public function execute() { $user = $this->getUser(); $params = $this->extractRequestParams(); if ($user->isBot()) { // sanity $this->dieUsage('This interface is not supported for bots', 'botsnotsupported'); } $page = $this->getTitleOrPageId($params); $title = $page->getTitle(); if (!ContentHandler::getForModelID($params['contentmodel'])->isSupportedFormat($params['contentformat'])) { $this->dieUsage('Unsupported content model/format', 'badmodelformat'); } // Trim and fix newlines so the key SHA1's match (see RequestContext::getText()) $text = rtrim(str_replace("\r\n", "\n", $params['text'])); $textContent = ContentHandler::makeContent($text, $title, $params['contentmodel'], $params['contentformat']); $page = WikiPage::factory($title); if ($page->exists()) { // Page exists: get the merged content with the proposed change $baseRev = Revision::newFromPageId($page->getId(), $params['baserevid']); if (!$baseRev) { $this->dieUsage("No revision ID {$params['baserevid']}", 'missingrev'); } $currentRev = $page->getRevision(); if (!$currentRev) { $this->dieUsage("No current revision of page ID {$page->getId()}", 'missingrev'); } // Merge in the new version of the section to get the proposed version $editContent = $page->replaceSectionAtRev($params['section'], $textContent, $params['sectiontitle'], $baseRev->getId()); if (!$editContent) { $this->dieUsage('Could not merge updated section.', 'replacefailed'); } if ($currentRev->getId() == $baseRev->getId()) { // Base revision was still the latest; nothing to merge $content = $editContent; } else { // Merge the edit into the current version $baseContent = $baseRev->getContent(); $currentContent = $currentRev->getContent(); if (!$baseContent || !$currentContent) { $this->dieUsage("Missing content for page ID {$page->getId()}", 'missingrev'); } $handler = ContentHandler::getForModelID($baseContent->getModel()); $content = $handler->merge3($baseContent, $editContent, $currentContent); } } else { // New pages: use the user-provided content model $content = $textContent; } if (!$content) { // merge3() failed $this->getResult()->addValue(null, $this->getModuleName(), ['status' => 'editconflict']); return; } // The user will abort the AJAX request by pressing "save", so ignore that ignore_user_abort(true); // Use the master DB for fast blocking locks $dbw = wfGetDB(DB_MASTER); // Get a key based on the source text, format, and user preferences $key = self::getStashKey($title, $content, $user); // De-duplicate requests on the same key if ($user->pingLimiter('stashedit')) { $status = 'ratelimited'; } elseif ($dbw->lock($key, __METHOD__, 1)) { $status = self::parseAndStash($page, $content, $user); $dbw->unlock($key, __METHOD__); } else { $status = 'busy'; } $this->getStats()->increment("editstash.cache_stores.{$status}"); $this->getResult()->addValue(null, $this->getModuleName(), ['status' => $status]); }