/** * Get template and image versions from parsing a revision * @param Page $article * @param Revision $rev * @param User $user * @param string $regen use 'regen' to force regeneration * @return array( templateIds, fileSHA1Keys ) * templateIds like ParserOutput->mTemplateIds * fileSHA1Keys like ParserOutput->mImageTimeKeys */ public static function getRevIncludes(Page $article, Revision $rev, User $user, $regen = '') { global $wgMemc; wfProfileIn(__METHOD__); $key = self::getCacheKey($article->getTitle(), $rev->getId()); if ($regen === 'regen') { $versions = false; // skip cache } elseif ($rev->isCurrent()) { // Check cache entry against page_touched $versions = FlaggedRevs::getMemcValue($wgMemc->get($key), $article); } else { // Old revs won't always be invalidated with template/file changes. // Also, we don't care if page_touched changed due to a direct edit. $versions = FlaggedRevs::getMemcValue($wgMemc->get($key), $article, 'allowStale'); if (is_array($versions)) { // entry exists // Sanity check that the cache is reasonably up to date list($templates, $files) = $versions; if (self::templatesStale($templates) || self::filesStale($files)) { $versions = false; // no good } } } if (!is_array($versions)) { // cache miss $pOut = false; if ($rev->isCurrent()) { $parserCache = ParserCache::singleton(); # Try current version parser cache for this user... $pOut = $parserCache->get($article, $article->makeParserOptions($user)); if ($pOut == false) { # Try current version parser cache for the revision author... $optsUser = $rev->getUser() ? User::newFromId($rev->getUser()) : 'canonical'; $pOut = $parserCache->get($article, $article->makeParserOptions($optsUser)); } } // ParserOutput::mImageTimeKeys wasn't always there if ($pOut == false || !FlaggedRevs::parserOutputIsVersioned($pOut)) { $content = $rev->getContent(Revision::RAW); if (!$content) { // Just for extra sanity $pOut = new ParserOutput(); } else { $pOut = $content->getParserOutput($article->getTitle(), $rev->getId(), ParserOptions::newFromUser($user)); } } # Get the template/file versions used... $versions = array($pOut->getTemplateIds(), $pOut->getFileSearchOptions()); # Save to cache (check cache expiry for dynamic elements)... $data = FlaggedRevs::makeMemcObj($versions); $wgMemc->set($key, $data, $pOut->getCacheExpiry()); } wfProfileOut(__METHOD__); return $versions; }
/** * Lazy initialization of article text from DB */ protected function initText() { if (!isset($this->mText)) { if ($this->mRevision != null) { $this->mText = SearchEngine::create()->getTextFromContent($this->mTitle, $this->mRevision->getContent()); } else { // TODO: can we fetch raw wikitext for commons images? $this->mText = ''; } } }
/** * Load the text of the new revision, not the old one * * @return bool */ public function loadNewText() { if ($this->mTextLoaded >= 1) { return true; } $this->mTextLoaded = 1; if (!$this->loadRevisionData()) { return false; } $this->mNewContent = $this->mNewRev->getContent(Revision::FOR_THIS_USER, $this->getUser()); return true; }
/** * Callback function for each revision, preprocessToObj() * @param Revision $rev */ public function processRevision($rev) { $content = $rev->getContent(Revision::RAW); if ($content->getModel() !== CONTENT_MODEL_WIKITEXT) { return; } try { $this->mPreprocessor->preprocessToObj(strval($content->getNativeData()), 0); } catch (Exception $e) { $this->error("Caught exception " . $e->getMessage() . " in " . $rev->getTitle()->getPrefixedText()); } }
public static function runForTitleInternal(Title $title, Revision $revision, $fname) { global $wgContLang; wfProfileIn($fname . '-parse'); $options = ParserOptions::newFromUserAndLang(new User(), $wgContLang); $content = $revision->getContent(); $parserOutput = $content->getParserOutput($title, $revision->getId(), $options, false); wfProfileOut($fname . '-parse'); wfProfileIn($fname . '-update'); $updates = $content->getSecondaryDataUpdates($title, null, false, $parserOutput); DataUpdate::runUpdates($updates); wfProfileOut($fname . '-update'); }
/** * @param $title Title * @param $revision Revision * @param $fname string * @return void */ public static function runForTitleInternal(Title $title, Revision $revision, $fname) { wfProfileIn($fname); $content = $revision->getContent(Revision::RAW); if (!$content) { // if there is no content, pretend the content is empty $content = $revision->getContentHandler()->makeEmptyContent(); } // Revision ID must be passed to the parser output to get revision variables correct $parserOutput = $content->getParserOutput($title, $revision->getId(), null, false); $updates = $content->getSecondaryDataUpdates($title, null, false, $parserOutput); DataUpdate::runUpdates($updates); InfoAction::invalidateCache($title); wfProfileOut($fname); }
public function doDBUpdates() { $db = $this->getDB(DB_MASTER); if (!$db->tableExists('revision')) { $this->error("revision table does not exist", true); } else { if (!$db->fieldExists('revision', 'rev_sha1', __METHOD__)) { $this->output("rev_sha1 column does not exist\n\n", true); return false; } } $this->output("Populating rev_len column\n"); $start = $db->selectField('revision', 'MIN(rev_id)', false, __METHOD__); $end = $db->selectField('revision', 'MAX(rev_id)', false, __METHOD__); if (!$start || !$end) { $this->output("...revision table seems to be empty.\n"); return true; } # Do remaining chunks $blockStart = intval($start); $blockEnd = intval($start) + $this->mBatchSize - 1; $count = 0; $missing = 0; $fields = Revision::selectFields(); while ($blockStart <= $end) { $this->output("...doing rev_id from {$blockStart} to {$blockEnd}\n"); $res = $db->select('revision', $fields, array("rev_id >= {$blockStart}", "rev_id <= {$blockEnd}", "rev_len IS NULL"), __METHOD__); # Go through and update rev_len from these rows. foreach ($res as $row) { $rev = new Revision($row); $content = $rev->getContent(); if (!$content) { # This should not happen, but sometimes does (bug 20757) $this->output("Content of revision {$row->rev_id} unavailable!\n"); $missing++; } else { # Update the row... $db->update('revision', array('rev_len' => $content->getSize()), array('rev_id' => $row->rev_id), __METHOD__); $count++; } } $blockStart += $this->mBatchSize; $blockEnd += $this->mBatchSize; wfWaitForSlaves(); } $this->output("rev_len population complete ... {$count} rows changed ({$missing} missing)\n"); return true; }
/** * Do standard deferred updates after page edit. * Update links tables, site stats, search index and message cache. * Purges pages that include this page if the text was changed here. * Every 100th edit, prune the recent changes table. * * @param $revision Revision object * @param $user User object that did the revision * @param $options Array of options, following indexes are used: * - changed: boolean, whether the revision changed the content (default true) * - created: boolean, whether the revision created the page (default false) * - oldcountable: boolean or null (default null): * - boolean: whether the page was counted as an article before that * revision, only used in changed is true and created is false * - null: don't change the article count */ public function doEditUpdates(Revision $revision, User $user, array $options = array()) { global $wgEnableParserCache; wfProfileIn(__METHOD__); $options += array('changed' => true, 'created' => false, 'oldcountable' => null); $content = $revision->getContent(); # Parse the text # Be careful not to double-PST: $text is usually already PST-ed once if (!$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag('vary-revision')) { wfDebug(__METHOD__ . ": No prepared edit or vary-revision is set...\n"); $editInfo = $this->prepareContentForEdit($content, $revision->getId(), $user); } else { wfDebug(__METHOD__ . ": No vary-revision, using prepared edit...\n"); $editInfo = $this->mPreparedEdit; } # Save it to the parser cache if ($wgEnableParserCache) { $parserCache = ParserCache::singleton(); $parserCache->save($editInfo->output, $this, $editInfo->popts); } # Update the links tables and other secondary data $updates = $content->getSecondaryDataUpdates($this->getTitle(), null, true, $editInfo->output); DataUpdate::runUpdates($updates); wfRunHooks('ArticleEditUpdates', array(&$this, &$editInfo, $options['changed'])); if (wfRunHooks('ArticleEditUpdatesDeleteFromRecentchanges', array(&$this))) { if (0 == mt_rand(0, 99)) { // Flush old entries from the `recentchanges` table; we do this on // random requests so as to avoid an increase in writes for no good reason global $wgRCMaxAge; $dbw = wfGetDB(DB_MASTER); $cutoff = $dbw->timestamp(time() - $wgRCMaxAge); $dbw->delete('recentchanges', array("rc_timestamp < '{$cutoff}'"), __METHOD__); } } if (!$this->mTitle->exists()) { wfProfileOut(__METHOD__); return; } $id = $this->getId(); $title = $this->mTitle->getPrefixedDBkey(); $shortTitle = $this->mTitle->getDBkey(); if (!$options['changed']) { $good = 0; $total = 0; } elseif ($options['created']) { $good = (int) $this->isCountable($editInfo); $total = 1; } elseif ($options['oldcountable'] !== null) { $good = (int) $this->isCountable($editInfo) - (int) $options['oldcountable']; $total = 0; } else { $good = 0; $total = 0; } DeferredUpdates::addUpdate(new SiteStatsUpdate(0, 1, $good, $total)); DeferredUpdates::addUpdate(new SearchUpdate($id, $title, $content->getTextForSearchIndex())); #@TODO: let the search engine decide what to do with the content object # If this is another user's talk page, update newtalk. # Don't do this if $options['changed'] = false (null-edits) nor if # it's a minor edit and the user doesn't want notifications for those. if ($options['changed'] && $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $user->getTitleKey() && !($revision->isMinor() && $user->isAllowed('nominornewtalk'))) { if (wfRunHooks('ArticleEditUpdateNewTalk', array(&$this))) { $other = User::newFromName($shortTitle, false); if (!$other) { wfDebug(__METHOD__ . ": invalid username\n"); } elseif (User::isIP($shortTitle)) { // An anonymous user $other->setNewtalk(true, $revision); } elseif ($other->isLoggedIn()) { $other->setNewtalk(true, $revision); } else { wfDebug(__METHOD__ . ": don't need to notify a nonexistent user\n"); } } } if ($this->mTitle->getNamespace() == NS_MEDIAWIKI) { #XXX: could skip pseudo-messages like js/css here, based on content model. $msgtext = $content->getWikitextForTransclusion(); if ($msgtext === false || $msgtext === null) { $msgtext = ''; } MessageCache::singleton()->replace($shortTitle, $msgtext); } if ($options['created']) { self::onArticleCreate($this->mTitle); } else { self::onArticleEdit($this->mTitle); } wfProfileOut(__METHOD__); }
public function onSubmit(array $data) { global $wgContLang; if ($data['pagetitle'] === '') { // Initial form view of special page, pass return false; } // At this point, it has to be a POST request. This is enforced by HTMLForm, // but lets be safe verify that. if (!$this->getRequest()->wasPosted()) { throw new RuntimeException("Form submission was not POSTed"); } $this->title = Title::newFromText($data['pagetitle']); $user = $this->getUser(); // Check permissions and make sure the user has permission to edit the specific page $errors = $this->title->getUserPermissionsErrors('editcontentmodel', $user); $errors = wfMergeErrorArrays($errors, $this->title->getUserPermissionsErrors('edit', $user)); if ($errors) { $out = $this->getOutput(); $wikitext = $out->formatPermissionsErrorMessage($errors); // Hack to get our wikitext parsed return Status::newFatal(new RawMessage('$1', array($wikitext))); } $page = WikiPage::factory($this->title); if ($this->oldRevision === null) { $this->oldRevision = $page->getRevision() ?: false; } $oldModel = $this->title->getContentModel(); if ($this->oldRevision) { $oldContent = $this->oldRevision->getContent(); try { $newContent = ContentHandler::makeContent($oldContent->getNativeData(), $this->title, $data['model']); } catch (MWException $e) { return Status::newFatal($this->msg('changecontentmodel-cannot-convert')->params($this->title->getPrefixedText(), ContentHandler::getLocalizedName($data['model']))); } } else { // Page doesn't exist, create an empty content object $newContent = ContentHandler::getForModelID($data['model'])->makeEmptyContent(); } $flags = $this->oldRevision ? EDIT_UPDATE : EDIT_NEW; if ($user->isAllowed('bot')) { $flags |= EDIT_FORCE_BOT; } $log = new ManualLogEntry('contentmodel', 'change'); $log->setPerformer($user); $log->setTarget($this->title); $log->setComment($data['reason']); $log->setParameters(array('4::oldmodel' => $oldModel, '5::newmodel' => $data['model'])); $formatter = LogFormatter::newFromEntry($log); $formatter->setContext(RequestContext::newExtraneousContext($this->title)); $reason = $formatter->getPlainActionText(); if ($data['reason'] !== '') { $reason .= $this->msg('colon-separator')->inContentLanguage()->text() . $data['reason']; } # Truncate for whole multibyte characters. $reason = $wgContLang->truncate($reason, 255); $status = $page->doEditContent($newContent, $reason, $flags, $this->oldRevision ? $this->oldRevision->getId() : false, $user); if (!$status->isOK()) { return $status; } $logid = $log->insert(); $log->publish($logid); return $status; }
/** * @covers Revision::getContent */ public function testGetContent_failure() { $rev = new Revision(['page' => $this->the_page->getId(), 'content_model' => $this->the_page->getContentModel(), 'text_id' => 123456789]); $this->assertNull($rev->getContent(), "getContent() should return null if the revision's text blob could not be loaded."); // NOTE: check this twice, once for lazy initialization, and once with the cached value. $this->assertNull($rev->getContent(), "getContent() should return null if the revision's text blob could not be loaded."); }
/** * @param Title $title * @param Revision $rev * @param string $parseTimestamp TS_MW * * @return string[] category names */ private function getCategoriesAtRev(Title $title, Revision $rev, $parseTimestamp) { $content = $rev->getContent(); $options = $content->getContentHandler()->makeParserOptions('canonical'); $options->setTimestamp($parseTimestamp); // This could possibly use the parser cache if it checked the revision ID, // but that's more complicated than it's worth. $output = $content->getParserOutput($title, $rev->getId(), $options); // array keys will cast numeric category names to ints // so we need to cast them back to strings to avoid breaking things! return array_map('strval', array_keys($output->getCategories())); }
/** * Callback function for each revision, parse with both parsers and compare * @param Revision $rev */ public function processRevision($rev) { $title = $rev->getTitle(); $parser1Name = $this->getOption('parser1'); $parser2Name = $this->getOption('parser2'); self::checkParserLocally($parser1Name); self::checkParserLocally($parser2Name); $parser1 = new $parser1Name(); $parser2 = new $parser2Name(); $content = $rev->getContent(); if ($content->getModel() !== CONTENT_MODEL_WIKITEXT) { $this->error("Page {$title->getPrefixedText()} does not contain wikitext " . "but {$content->getModel()}\n"); return; } $text = strval($content->getNativeData()); $output1 = $parser1->parse($text, $title, $this->options); $output2 = $parser2->parse($text, $title, $this->options); if ($output1->getText() != $output2->getText()) { $this->failed++; $this->error("Parsing for {$title->getPrefixedText()} differs\n"); if ($this->saveFailed) { file_put_contents($this->saveFailed . '/' . rawurlencode($title->getPrefixedText()) . ".txt", $text); } if ($this->showDiff) { $this->output(wfDiff($this->stripParameters($output1->getText()), $this->stripParameters($output2->getText()), '')); } } else { $this->output($title->getPrefixedText() . "\tOK\n"); if ($this->showParsedOutput) { $this->output($this->stripParameters($output1->getText())); } } }
/** * Do standard deferred updates after page edit. * Update links tables, site stats, search index and message cache. * Purges pages that include this page if the text was changed here. * Every 100th edit, prune the recent changes table. * * @param Revision $revision * @param User $user User object that did the revision * @param array $options Array of options, following indexes are used: * - changed: boolean, whether the revision changed the content (default true) * - created: boolean, whether the revision created the page (default false) * - moved: boolean, whether the page was moved (default false) * - oldcountable: boolean, null, or string 'no-change' (default null): * - boolean: whether the page was counted as an article before that * revision, only used in changed is true and created is false * - null: if created is false, don't update the article count; if created * is true, do update the article count * - 'no-change': don't update the article count, ever */ public function doEditUpdates(Revision $revision, User $user, array $options = array()) { $options += array('changed' => true, 'created' => false, 'moved' => false, 'oldcountable' => null); $content = $revision->getContent(); // Parse the text // Be careful not to do pre-save transform twice: $text is usually // already pre-save transformed once. if (!$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag('vary-revision')) { wfDebug(__METHOD__ . ": No prepared edit or vary-revision is set...\n"); $editInfo = $this->prepareContentForEdit($content, $revision, $user); } else { wfDebug(__METHOD__ . ": No vary-revision, using prepared edit...\n"); $editInfo = $this->mPreparedEdit; } // Save it to the parser cache. // Make sure the cache time matches page_touched to avoid double parsing. ParserCache::singleton()->save($editInfo->output, $this, $editInfo->popts, $revision->getTimestamp(), $editInfo->revid); // Update the links tables and other secondary data if ($content) { $recursive = $options['changed']; // bug 50785 $updates = $content->getSecondaryDataUpdates($this->getTitle(), null, $recursive, $editInfo->output); foreach ($updates as $update) { if ($update instanceof LinksUpdate) { $update->setRevision($revision); $update->setTriggeringUser($user); } DeferredUpdates::addUpdate($update); } } Hooks::run('ArticleEditUpdates', array(&$this, &$editInfo, $options['changed'])); if (Hooks::run('ArticleEditUpdatesDeleteFromRecentchanges', array(&$this))) { // Flush old entries from the `recentchanges` table if (mt_rand(0, 9) == 0) { JobQueueGroup::singleton()->lazyPush(RecentChangesUpdateJob::newPurgeJob()); } } if (!$this->exists()) { return; } $id = $this->getId(); $title = $this->mTitle->getPrefixedDBkey(); $shortTitle = $this->mTitle->getDBkey(); if ($options['oldcountable'] === 'no-change' || !$options['changed'] && !$options['moved']) { $good = 0; } elseif ($options['created']) { $good = (int) $this->isCountable($editInfo); } elseif ($options['oldcountable'] !== null) { $good = (int) $this->isCountable($editInfo) - (int) $options['oldcountable']; } else { $good = 0; } $edits = $options['changed'] ? 1 : 0; $total = $options['created'] ? 1 : 0; DeferredUpdates::addUpdate(new SiteStatsUpdate(0, $edits, $good, $total)); DeferredUpdates::addUpdate(new SearchUpdate($id, $title, $content)); // If this is another user's talk page, update newtalk. // Don't do this if $options['changed'] = false (null-edits) nor if // it's a minor edit and the user doesn't want notifications for those. if ($options['changed'] && $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $user->getTitleKey() && !($revision->isMinor() && $user->isAllowed('nominornewtalk'))) { $recipient = User::newFromName($shortTitle, false); if (!$recipient) { wfDebug(__METHOD__ . ": invalid username\n"); } else { // Allow extensions to prevent user notification // when a new message is added to their talk page if (Hooks::run('ArticleEditUpdateNewTalk', array(&$this, $recipient))) { if (User::isIP($shortTitle)) { // An anonymous user $recipient->setNewtalk(true, $revision); } elseif ($recipient->isLoggedIn()) { $recipient->setNewtalk(true, $revision); } else { wfDebug(__METHOD__ . ": don't need to notify a nonexistent user\n"); } } } } if ($this->mTitle->getNamespace() == NS_MEDIAWIKI) { // XXX: could skip pseudo-messages like js/css here, based on content model. $msgtext = $content ? $content->getWikitextForTransclusion() : null; if ($msgtext === false || $msgtext === null) { $msgtext = ''; } MessageCache::singleton()->replace($shortTitle, $msgtext); } if ($options['created']) { self::onArticleCreate($this->mTitle); } elseif ($options['changed']) { // bug 50785 self::onArticleEdit($this->mTitle, $revision); } }
/** * Build a diff display between this and the previous either deleted * or non-deleted edit. * * @param Revision $previousRev * @param Revision $currentRev * @return string HTML */ function showDiff($previousRev, $currentRev) { $diffContext = clone $this->getContext(); $diffContext->setTitle($currentRev->getTitle()); $diffContext->setWikiPage(WikiPage::factory($currentRev->getTitle())); $diffEngine = $currentRev->getContentHandler()->createDifferenceEngine($diffContext); $diffEngine->showDiffStyle(); $formattedDiff = $diffEngine->generateContentDiffBody($previousRev->getContent(Revision::FOR_THIS_USER, $this->getUser()), $currentRev->getContent(Revision::FOR_THIS_USER, $this->getUser())); $formattedDiff = $diffEngine->addHeader($formattedDiff, $this->diffHeader($previousRev, 'o'), $this->diffHeader($currentRev, 'n')); $this->getOutput()->addHTML("<div>{$formattedDiff}</div>\n"); }
/** * Do standard deferred updates after page edit. * Update links tables, site stats, search index and message cache. * Purges pages that include this page if the text was changed here. * Every 100th edit, prune the recent changes table. * * @param Revision $revision * @param User $user User object that did the revision * @param array $options Array of options, following indexes are used: * - changed: boolean, whether the revision changed the content (default true) * - created: boolean, whether the revision created the page (default false) * - moved: boolean, whether the page was moved (default false) * - restored: boolean, whether the page was undeleted (default false) * - oldrevision: Revision object for the pre-update revision (default null) * - oldcountable: boolean, null, or string 'no-change' (default null): * - boolean: whether the page was counted as an article before that * revision, only used in changed is true and created is false * - null: if created is false, don't update the article count; if created * is true, do update the article count * - 'no-change': don't update the article count, ever */ public function doEditUpdates(Revision $revision, User $user, array $options = []) { global $wgRCWatchCategoryMembership, $wgContLang; $options += ['changed' => true, 'created' => false, 'moved' => false, 'restored' => false, 'oldrevision' => null, 'oldcountable' => null]; $content = $revision->getContent(); $logger = LoggerFactory::getInstance('SaveParse'); // See if the parser output before $revision was inserted is still valid $editInfo = false; if (!$this->mPreparedEdit) { $logger->debug(__METHOD__ . ": No prepared edit...\n"); } elseif ($this->mPreparedEdit->output->getFlag('vary-revision')) { $logger->info(__METHOD__ . ": Prepared edit has vary-revision...\n"); } elseif ($this->mPreparedEdit->output->getFlag('vary-revision-id') && $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->getId()) { $logger->info(__METHOD__ . ": Prepared edit has vary-revision-id with wrong ID...\n"); } elseif ($this->mPreparedEdit->output->getFlag('vary-user') && !$options['changed']) { $logger->info(__METHOD__ . ": Prepared edit has vary-user and is null...\n"); } else { wfDebug(__METHOD__ . ": Using prepared edit...\n"); $editInfo = $this->mPreparedEdit; } if (!$editInfo) { // Parse the text again if needed. Be careful not to do pre-save transform twice: // $text is usually already pre-save transformed once. Avoid using the edit stash // as any prepared content from there or in doEditContent() was already rejected. $editInfo = $this->prepareContentForEdit($content, $revision, $user, null, false); } // Save it to the parser cache. // Make sure the cache time matches page_touched to avoid double parsing. ParserCache::singleton()->save($editInfo->output, $this, $editInfo->popts, $revision->getTimestamp(), $editInfo->revid); // Update the links tables and other secondary data if ($content) { $recursive = $options['changed']; // bug 50785 $updates = $content->getSecondaryDataUpdates($this->getTitle(), null, $recursive, $editInfo->output); foreach ($updates as $update) { if ($update instanceof LinksUpdate) { $update->setRevision($revision); $update->setTriggeringUser($user); } DeferredUpdates::addUpdate($update); } if ($wgRCWatchCategoryMembership && $this->getContentHandler()->supportsCategories() === true && ($options['changed'] || $options['created']) && !$options['restored']) { // Note: jobs are pushed after deferred updates, so the job should be able to see // the recent change entry (also done via deferred updates) and carry over any // bot/deletion/IP flags, ect. JobQueueGroup::singleton()->lazyPush(new CategoryMembershipChangeJob($this->getTitle(), ['pageId' => $this->getId(), 'revTimestamp' => $revision->getTimestamp()])); } } Hooks::run('ArticleEditUpdates', [&$this, &$editInfo, $options['changed']]); if (Hooks::run('ArticleEditUpdatesDeleteFromRecentchanges', [&$this])) { // Flush old entries from the `recentchanges` table if (mt_rand(0, 9) == 0) { JobQueueGroup::singleton()->lazyPush(RecentChangesUpdateJob::newPurgeJob()); } } if (!$this->exists()) { return; } $id = $this->getId(); $title = $this->mTitle->getPrefixedDBkey(); $shortTitle = $this->mTitle->getDBkey(); if ($options['oldcountable'] === 'no-change' || !$options['changed'] && !$options['moved']) { $good = 0; } elseif ($options['created']) { $good = (int) $this->isCountable($editInfo); } elseif ($options['oldcountable'] !== null) { $good = (int) $this->isCountable($editInfo) - (int) $options['oldcountable']; } else { $good = 0; } $edits = $options['changed'] ? 1 : 0; $total = $options['created'] ? 1 : 0; DeferredUpdates::addUpdate(new SiteStatsUpdate(0, $edits, $good, $total)); DeferredUpdates::addUpdate(new SearchUpdate($id, $title, $content)); // If this is another user's talk page, update newtalk. // Don't do this if $options['changed'] = false (null-edits) nor if // it's a minor edit and the user doesn't want notifications for those. if ($options['changed'] && $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $user->getTitleKey() && !($revision->isMinor() && $user->isAllowed('nominornewtalk'))) { $recipient = User::newFromName($shortTitle, false); if (!$recipient) { wfDebug(__METHOD__ . ": invalid username\n"); } else { // Allow extensions to prevent user notification // when a new message is added to their talk page if (Hooks::run('ArticleEditUpdateNewTalk', [&$this, $recipient])) { if (User::isIP($shortTitle)) { // An anonymous user $recipient->setNewtalk(true, $revision); } elseif ($recipient->isLoggedIn()) { $recipient->setNewtalk(true, $revision); } else { wfDebug(__METHOD__ . ": don't need to notify a nonexistent user\n"); } } } } if ($this->mTitle->getNamespace() == NS_MEDIAWIKI) { // XXX: could skip pseudo-messages like js/css here, based on content model. $msgtext = $content ? $content->getWikitextForTransclusion() : null; if ($msgtext === false || $msgtext === null) { $msgtext = ''; } MessageCache::singleton()->replace($shortTitle, $msgtext); if ($wgContLang->hasVariants()) { $wgContLang->updateConversionTable($this->mTitle); } } if ($options['created']) { self::onArticleCreate($this->mTitle); } elseif ($options['changed']) { // bug 50785 self::onArticleEdit($this->mTitle, $revision); } ResourceLoaderWikiModule::invalidateModuleCache($this->mTitle, $options['oldrevision'], $revision, wfWikiID()); }
/** * Get the Content object that needs to be saved in order to undo all revisions * between $undo and $undoafter. Revisions must belong to the same page, * must exist and must not be deleted. * * @since 1.21 * * @param Revision $current The current text * @param Revision $undo The revision to undo * @param Revision $undoafter Must be an earlier revision than $undo * * @return mixed String on success, false on failure */ public function getUndoContent(Revision $current, Revision $undo, Revision $undoafter) { $cur_content = $current->getContent(); if (empty($cur_content)) { return false; // no page } $undo_content = $undo->getContent(); $undoafter_content = $undoafter->getContent(); if (!$undo_content || !$undoafter_content) { return false; // no content to undo } $this->checkModelID($cur_content->getModel()); $this->checkModelID($undo_content->getModel()); $this->checkModelID($undoafter_content->getModel()); if ($cur_content->equals($undo_content)) { // No use doing a merge if it's just a straight revert. return $undoafter_content; } $undone_content = $this->merge3($undo_content, $undoafter_content, $cur_content); return $undone_content; }
/** * Automatically review an revision and add a log entry in the review log. * * This is called during edit operations after the new revision is added * and the page tables updated, but before LinksUpdate is called. * * $auto is here for revisions checked off to be reviewed. Auto-review * triggers on edit, but we don't want those to count as just automatic. * This also makes it so the user's name shows up in the page history. * * If $flags is given, then they will be the review tags. If not, the one * from the stable version will be used or minimal tags if that's not possible. * If no appropriate tags can be found, then the review will abort. */ public static function autoReviewEdit(WikiPage $article, $user, Revision $rev, array $flags = null, $auto = true) { wfProfileIn(__METHOD__); $title = $article->getTitle(); // convenience # Get current stable version ID (for logging) $oldSv = FlaggedRevision::newFromStable($title, FR_MASTER); $oldSvId = $oldSv ? $oldSv->getRevId() : 0; # Set the auto-review tags from the prior stable version. # Normally, this should already be done and given here... if (!is_array($flags)) { if ($oldSv) { # Use the last stable version if $flags not given if ($user->isAllowed('bot')) { $flags = $oldSv->getTags(); // no change for bot edits } else { # Account for perms/tags... $flags = self::getAutoReviewTags($user, $oldSv->getTags()); } } else { // new page? $flags = self::quickTags(FR_CHECKED); // use minimal level } if (!is_array($flags)) { wfProfileOut(__METHOD__); return false; // can't auto-review this revision } } # Get review property flags $propFlags = $auto ? array('auto') : array(); # Note: this needs to match the prepareContentForEdit() call WikiPage::doEditContent. # This is for consistency and also to avoid triggering a second parse otherwise. $editInfo = $article->prepareContentForEdit($rev->getContent(), null, $user, $rev->getContentFormat()); $poutput = $editInfo->output; // revision HTML output # Get the "review time" versions of templates and files. # This tries to make sure each template/file version either came from the stable # version of that template/file or was a "review time" version used in the stable # version of this page. If a pending version of a template/file is currently vandalism, # we try to avoid storing its ID as the "review time" version so it won't show up when # someone views the page. If not possible, this stores the current template/file. if (FlaggedRevs::inclusionSetting() === FR_INCLUDES_CURRENT) { $tVersions = $poutput->getTemplateIds(); $fVersions = $poutput->getFileSearchOptions(); } else { $tVersions = $oldSv ? $oldSv->getTemplateVersions() : array(); $fVersions = $oldSv ? $oldSv->getFileVersions() : array(); foreach ($poutput->getTemplateIds() as $ns => $pages) { foreach ($pages as $dbKey => $revId) { if (!isset($tVersions[$ns][$dbKey])) { $srev = FlaggedRevision::newFromStable(Title::makeTitle($ns, $dbKey)); if ($srev) { // use stable $tVersions[$ns][$dbKey] = $srev->getRevId(); } else { // use current $tVersions[$ns][$dbKey] = $revId; } } } } foreach ($poutput->getFileSearchOptions() as $dbKey => $info) { if (!isset($fVersions[$dbKey])) { $srev = FlaggedRevision::newFromStable(Title::makeTitle(NS_FILE, $dbKey)); if ($srev && $srev->getFileTimestamp()) { // use stable $fVersions[$dbKey]['time'] = $srev->getFileTimestamp(); $fVersions[$dbKey]['sha1'] = $srev->getFileSha1(); } else { // use current $fVersions[$dbKey]['time'] = $info['time']; $fVersions[$dbKey]['sha1'] = $info['sha1']; } } } } # If this is an image page, get the corresponding file version info... $fileData = array('name' => null, 'timestamp' => null, 'sha1' => null); if ($title->getNamespace() == NS_FILE) { # We must use WikiFilePage process cache on upload or get bitten by slave lag $file = $article instanceof WikiFilePage || $article instanceof ImagePage ? $article->getFile() : wfFindFile($title, array('bypassCache' => true)); // skip cache; bug 31056 if (is_object($file) && $file->exists()) { $fileData['name'] = $title->getDBkey(); $fileData['timestamp'] = $file->getTimestamp(); $fileData['sha1'] = $file->getSha1(); } } # Our review entry $flaggedRevision = new FlaggedRevision(array('rev' => $rev, 'user_id' => $user->getId(), 'timestamp' => $rev->getTimestamp(), 'quality' => FlaggedRevs::getQualityTier($flags, 0), 'tags' => FlaggedRevision::flattenRevisionTags($flags), 'img_name' => $fileData['name'], 'img_timestamp' => $fileData['timestamp'], 'img_sha1' => $fileData['sha1'], 'templateVersions' => $tVersions, 'fileVersions' => $fVersions, 'flags' => implode(',', $propFlags))); $flaggedRevision->insert(); # Update the article review log FlaggedRevsLog::updateReviewLog($title, $flags, array(), '', $rev->getId(), $oldSvId, true, $auto); # Update page and tracking tables and clear cache FlaggedRevs::stableVersionUpdates($article); wfProfileOut(__METHOD__); return true; }
/** * Callback function for each revision, turn into HTML and save * @param Revision $rev */ public function handleRevision($rev) { $title = $rev->getTitle(); if (!$title) { $this->error("Got bogus revision with null title!"); return; } $display = $title->getPrefixedText(); $this->count++; $sanitized = rawurlencode($display); $filename = sprintf("%s/%s-%07d-%s.html", $this->outputDirectory, $this->prefix, $this->count, $sanitized); $this->output(sprintf("%s\n", $filename, $display)); $user = new User(); $options = ParserOptions::newFromUser($user); $content = $rev->getContent(); $output = $content->getParserOutput($title, null, $options); file_put_contents($filename, "<!DOCTYPE html>\n" . "<html lang=\"en\" dir=\"ltr\">\n" . "<head>\n" . "<meta charset=\"UTF-8\" />\n" . "<title>" . htmlspecialchars($display) . "</title>\n" . "</head>\n" . "<body>\n" . $output->getText() . "</body>\n" . "</html>"); }
/** * Get the Content object that needs to be saved in order to undo all revisions * between $undo and $undoafter. Revisions must belong to the same page, * must exist and must not be deleted. * * @since 1.21 * * @param Revision $current The current text * @param Revision $undo The revision to undo * @param Revision $undoafter Must be an earlier revision than $undo * * @return mixed String on success, false on failure */ public function getUndoContent(Revision $current, Revision $undo, Revision $undoafter) { $cur_content = $current->getContent(); if (empty($cur_content)) { return false; // no page } $undo_content = $undo->getContent(); $undoafter_content = $undoafter->getContent(); if (!$undo_content || !$undoafter_content) { return false; // no content to undo } try { $this->checkModelID($cur_content->getModel()); $this->checkModelID($undo_content->getModel()); if ($current->getId() !== $undo->getId()) { // If we are undoing the most recent revision, // its ok to revert content model changes. However // if we are undoing a revision in the middle, then // doing that will be confusing. $this->checkModelID($undoafter_content->getModel()); } } catch (MWException $e) { // If the revisions have different content models // just return false return false; } if ($cur_content->equals($undo_content)) { // No use doing a merge if it's just a straight revert. return $undoafter_content; } $undone_content = $this->merge3($undo_content, $undoafter_content, $cur_content); return $undone_content; }
/** * Parse the text from a given Revision * * @param Revision $revision */ function runParser(Revision $revision) { $content = $revision->getContent(); $content->getParserOutput($revision->getTitle(), $revision->getId()); if ($this->clearLinkCache) { $this->linkCache->clear(); } }
/** * Extract information from the Revision * * @param Revision $revision * @param object $row Should have a field 'ts_tags' if $this->fld_tags is set * @return array */ protected function extractRevisionInfo(Revision $revision, $row) { $title = $revision->getTitle(); $user = $this->getUser(); $vals = array(); $anyHidden = false; if ($this->fld_ids) { $vals['revid'] = intval($revision->getId()); if (!is_null($revision->getParentId())) { $vals['parentid'] = intval($revision->getParentId()); } } if ($this->fld_flags) { $vals['minor'] = $revision->isMinor(); } if ($this->fld_user || $this->fld_userid) { if ($revision->isDeleted(Revision::DELETED_USER)) { $vals['userhidden'] = true; $anyHidden = true; } if ($revision->userCan(Revision::DELETED_USER, $user)) { if ($this->fld_user) { $vals['user'] = $revision->getUserText(Revision::RAW); } $userid = $revision->getUser(Revision::RAW); if (!$userid) { $vals['anon'] = true; } if ($this->fld_userid) { $vals['userid'] = $userid; } } } if ($this->fld_timestamp) { $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $revision->getTimestamp()); } if ($this->fld_size) { if (!is_null($revision->getSize())) { $vals['size'] = intval($revision->getSize()); } else { $vals['size'] = 0; } } if ($this->fld_sha1) { if ($revision->isDeleted(Revision::DELETED_TEXT)) { $vals['sha1hidden'] = true; $anyHidden = true; } if ($revision->userCan(Revision::DELETED_TEXT, $user)) { if ($revision->getSha1() != '') { $vals['sha1'] = wfBaseConvert($revision->getSha1(), 36, 16, 40); } else { $vals['sha1'] = ''; } } } if ($this->fld_contentmodel) { $vals['contentmodel'] = $revision->getContentModel(); } if ($this->fld_comment || $this->fld_parsedcomment) { if ($revision->isDeleted(Revision::DELETED_COMMENT)) { $vals['commenthidden'] = true; $anyHidden = true; } if ($revision->userCan(Revision::DELETED_COMMENT, $user)) { $comment = $revision->getComment(Revision::RAW); if ($this->fld_comment) { $vals['comment'] = $comment; } if ($this->fld_parsedcomment) { $vals['parsedcomment'] = Linker::formatComment($comment, $title); } } } if ($this->fld_tags) { if ($row->ts_tags) { $tags = explode(',', $row->ts_tags); ApiResult::setIndexedTagName($tags, 'tag'); $vals['tags'] = $tags; } else { $vals['tags'] = array(); } } $content = null; global $wgParser; if ($this->fetchContent) { $content = $revision->getContent(Revision::FOR_THIS_USER, $this->getUser()); // Expand templates after getting section content because // template-added sections don't count and Parser::preprocess() // will have less input if ($content && $this->section !== false) { $content = $content->getSection($this->section, false); if (!$content) { $this->dieUsage("There is no section {$this->section} in r" . $revision->getId(), 'nosuchsection'); } } if ($revision->isDeleted(Revision::DELETED_TEXT)) { $vals['texthidden'] = true; $anyHidden = true; } elseif (!$content) { $vals['textmissing'] = true; } } if ($this->fld_content && $content) { $text = null; if ($this->generateXML) { if ($content->getModel() === CONTENT_MODEL_WIKITEXT) { $t = $content->getNativeData(); # note: don't set $text $wgParser->startExternalParse($title, ParserOptions::newFromContext($this->getContext()), Parser::OT_PREPROCESS); $dom = $wgParser->preprocessToDom($t); if (is_callable(array($dom, 'saveXML'))) { $xml = $dom->saveXML(); } else { $xml = $dom->__toString(); } $vals['parsetree'] = $xml; } else { $vals['badcontentformatforparsetree'] = true; $this->setWarning("Conversion to XML is supported for wikitext only, " . $title->getPrefixedDBkey() . " uses content model " . $content->getModel()); } } if ($this->expandTemplates && !$this->parseContent) { #XXX: implement template expansion for all content types in ContentHandler? if ($content->getModel() === CONTENT_MODEL_WIKITEXT) { $text = $content->getNativeData(); $text = $wgParser->preprocess($text, $title, ParserOptions::newFromContext($this->getContext())); } else { $this->setWarning("Template expansion is supported for wikitext only, " . $title->getPrefixedDBkey() . " uses content model " . $content->getModel()); $vals['badcontentformat'] = true; $text = false; } } if ($this->parseContent) { $po = $content->getParserOutput($title, $revision->getId(), ParserOptions::newFromContext($this->getContext())); $text = $po->getText(); } if ($text === null) { $format = $this->contentFormat ? $this->contentFormat : $content->getDefaultFormat(); $model = $content->getModel(); if (!$content->isSupportedFormat($format)) { $name = $title->getPrefixedDBkey(); $this->setWarning("The requested format {$this->contentFormat} is not " . "supported for content model {$model} used by {$name}"); $vals['badcontentformat'] = true; $text = false; } else { $text = $content->serialize($format); // always include format and model. // Format is needed to deserialize, model is needed to interpret. $vals['contentformat'] = $format; $vals['contentmodel'] = $model; } } if ($text !== false) { ApiResult::setContentValue($vals, 'content', $text); } } if ($content && (!is_null($this->diffto) || !is_null($this->difftotext))) { static $n = 0; // Number of uncached diffs we've had if ($n < $this->getConfig()->get('APIMaxUncachedDiffs')) { $vals['diff'] = array(); $context = new DerivativeContext($this->getContext()); $context->setTitle($title); $handler = $revision->getContentHandler(); if (!is_null($this->difftotext)) { $model = $title->getContentModel(); if ($this->contentFormat && !ContentHandler::getForModelID($model)->isSupportedFormat($this->contentFormat)) { $name = $title->getPrefixedDBkey(); $this->setWarning("The requested format {$this->contentFormat} is not " . "supported for content model {$model} used by {$name}"); $vals['diff']['badcontentformat'] = true; $engine = null; } else { $difftocontent = ContentHandler::makeContent($this->difftotext, $title, $model, $this->contentFormat); $engine = $handler->createDifferenceEngine($context); $engine->setContent($content, $difftocontent); } } else { $engine = $handler->createDifferenceEngine($context, $revision->getID(), $this->diffto); $vals['diff']['from'] = $engine->getOldid(); $vals['diff']['to'] = $engine->getNewid(); } if ($engine) { $difftext = $engine->getDiffBody(); ApiResult::setContentValue($vals['diff'], 'body', $difftext); if (!$engine->wasCacheHit()) { $n++; } } } else { $vals['diff']['notcached'] = true; } } if ($anyHidden && $revision->isDeleted(Revision::DELETED_RESTRICTED)) { $vals['suppressed'] = true; } return $vals; }
/** * Lazy initialization of article text from DB */ protected function initText() { if (!isset($this->mText)) { if ($this->mRevision != null) { //TODO: if we could plug in some code that knows about special content models *and* about // special features of the search engine, the search could benefit. $content = $this->mRevision->getContent(); $this->mText = $content ? $content->getTextForSearchIndex() : ''; } else { // TODO: can we fetch raw wikitext for commons images? $this->mText = ''; } } }
/** * Look up some text of a revision from its revision id * * Note that this is really *some* text, we do not make *any* guarantee * that this text will be even close to what the user actually sees, or * that the form is fit for any intended purpose. * * Note also that if the revision for any reason is not an Revision * the function returns with an empty string. * * @param Revision $revision a valid revision * @param $audience Integer: one of: * Revision::FOR_PUBLIC to be displayed to all users * Revision::FOR_THIS_USER to be displayed to the given user * Revision::RAW get the text regardless of permissions * @return string|null the content of the revision as some kind of string, * or an empty string if it can not be found */ static function revisionToString($revision, $audience = Revision::FOR_THIS_USER) { if (!$revision instanceof Revision) { return ''; } if (defined('MW_SUPPORTS_CONTENTHANDLER')) { $content = $revision->getContent($audience); if ($content === null) { return ''; } $result = self::contentToString($content); } else { // For MediaWiki without contenthandler support (< 1.21) $result = $revision->getText(); } return $result; }
/** * Parse the text from a given Revision * * @param Revision $revision */ function runParser(Revision $revision) { $content = $revision->getContent(); $content->getParserOutput($revision->getTitle(), $revision->getId()); }
/** * @param Revision $revision * @return string */ protected function feedItemDesc($revision) { if ($revision) { $msg = wfMessage('colon-separator')->inContentLanguage()->text(); $content = $revision->getContent(); if ($content instanceof TextContent) { // only textual content has a "source view". $html = nl2br(htmlspecialchars($content->getNativeData())); } else { //XXX: we could get an HTML representation of the content via getParserOutput, but that may // contain JS magic and generally may not be suitable for inclusion in a feed. // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. //Compare also FeedUtils::formatDiffRow. $html = ''; } return '<p>' . htmlspecialchars($revision->getUserText()) . $msg . htmlspecialchars(FeedItem::stripComment($revision->getComment())) . "</p>\n<hr />\n<div>" . $html . "</div>"; } return ''; }
/** * Add a wiki page * * @return void */ function add() { if (!WikiPage::canAdd(logged_user(), active_project())) { flash_error(lang('no access permissions')); $this->redirectTo('wiki'); } //if //Here we will edit a wiki page $preview = false; $data = array_var($_POST, 'wiki', false); if (false !== $data) { $preview = array_key_exists('preview', $data); } if (!$preview && $data) { //Make a new wiki page $page = new WikiPage(); //Set the Id for this project $page->setProjectId(active_project()->getId()); $page->setProjectIndex(logged_user()->isMemberOfOwnerCompany() ? $data['project_index'] : 0); $page->setPublish(logged_user()->isMemberOfOwnerCompany() ? $data['publish'] : 0); $page->setParentId($data['parent_id']); $page->setProjectSidebar(logged_user()->isMemberOfOwnerCompany() ? $data['project_sidebar'] : 0); //Make a new revision of this page $revision = $page->makeRevision(); // Check to see if we want to lock this page if (isset($data['locked'])) { if ($data['locked'] == 1 && $page->canLock(logged_user())) { // If we want to lock this page and the user has permissions to lock it, and the page is not already locked $page->setLocked(true); $page->setLockedById(logged_user()->getId()); $page->setLockedOn(DateTimeValueLib::now()); } // if } // if //Set attributes from form $revision->setFromAttributes($data); //Set user ID and project ID $revision->setCreatedbyId(logged_user()->getId()); try { //Start the db transaction DB::beginWork(); //Save the page $page->save(); //Make a log entry ApplicationLogs::createLog($page, active_project(), ApplicationLogs::ACTION_ADD); if (plugin_active('tags')) { //Add page tags $page->setTagsFromCSV($data['tags']); } //Commit changed DB::commit(); //Tell the user they made a page flash_success(lang('success add wiki page')); //Redirect $this->redirectToUrl($page->getViewUrl()); } catch (Exception $e) { DB::rollback(); tpl_assign('error', $e); } //try } // if if (!isset($page) || !instance_of($page, 'WikiPage')) { $page = new WikiPage(); $page->setProjectId(active_project()->getId()); } // if $revision = new Revision(); if (!$data) { // there was no input POSTed $data['content'] = $revision->getContent(); } $data['preview_content'] = do_textile($data['content']); //Assign revision object tpl_assign('data', $data); tpl_assign('page', $page); tpl_assign('revision', $revision); tpl_assign('tags', ''); $this->setTemplate('edit'); $this->setSidebar(get_template_path('textile_help_sidebar')); }
/** * Populates the search index with content from all pages */ protected function populateSearchIndex() { $res = $this->db->select('page', 'MAX(page_id) AS count'); $s = $this->db->fetchObject($res); $count = $s->count; $this->output("Rebuilding index fields for {$count} pages...\n"); $n = 0; $fields = array_merge(Revision::selectPageFields(), Revision::selectFields(), Revision::selectTextFields()); while ($n < $count) { if ($n) { $this->output($n . "\n"); } $end = $n + self::RTI_CHUNK_SIZE - 1; $res = $this->db->select(['page', 'revision', 'text'], $fields, ["page_id BETWEEN {$n} AND {$end}", 'page_latest = rev_id', 'rev_text_id = old_id'], __METHOD__); foreach ($res as $s) { try { $title = Title::makeTitle($s->page_namespace, $s->page_title); $rev = new Revision($s); $content = $rev->getContent(); $u = new SearchUpdate($s->page_id, $title, $content); $u->doUpdate(); } catch (MWContentSerializationException $ex) { $this->output("Failed to deserialize content of revision {$s->rev_id} of page " . "`" . $title->getPrefixedDBkey() . "`!\n"); } } $n += self::RTI_CHUNK_SIZE; } }
/** * Tests whether $rev->getContent() returns a clone when needed. * * @group Database */ function testGetContentClone() { $content = new RevisionTestModifyableContent("foo"); $rev = new Revision(array('id' => 42, 'page' => 23, 'title' => Title::newFromText("testGetContentClone_dummy"), 'content' => $content, 'length' => $content->getSize(), 'comment' => "testing", 'minor_edit' => false)); $content = $rev->getContent(Revision::RAW); $content->setText("bar"); $content2 = $rev->getContent(Revision::RAW); $this->assertNotSame($content, $content2, "expected a clone"); // content is mutable, expect clone $this->assertEquals("foo", $content2->getText()); // clone should contain the original text $content2->setText("bla bla"); $this->assertEquals("bar", $content->getText()); // clones should be independent }
/** * Do standard deferred updates after page edit. * Update links tables, site stats, search index and message cache. * Purges pages that include this page if the text was changed here. * Every 100th edit, prune the recent changes table. * * @param $revision Revision object * @param $user User object that did the revision * @param array $options of options, following indexes are used: * - changed: boolean, whether the revision changed the content (default true) * - created: boolean, whether the revision created the page (default false) * - oldcountable: boolean or null (default null): * - boolean: whether the page was counted as an article before that * revision, only used in changed is true and created is false * - null: don't change the article count */ public function doEditUpdates( Revision $revision, User $user, array $options = array() ) { global $wgEnableParserCache; wfProfileIn( __METHOD__ ); $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null ); $content = $revision->getContent(); // Parse the text // Be careful not to do pre-save transform twice: $text is usually // already pre-save transformed once. if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user ); } else { wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); $editInfo = $this->mPreparedEdit; } // Save it to the parser cache if ( $wgEnableParserCache ) { $parserCache = ParserCache::singleton(); $parserCache->save( $editInfo->output, $this, $editInfo->popts ); } // Update the links tables and other secondary data if ( $content ) { $recursive = $options['changed']; // bug 50785 $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, $recursive, $editInfo->output ); DataUpdate::runUpdates( $updates ); } wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { if ( 0 == mt_rand( 0, 99 ) ) { // Flush old entries from the `recentchanges` table; we do this on // random requests so as to avoid an increase in writes for no good reason RecentChange::purgeExpiredChanges(); } } if ( !$this->exists() ) { wfProfileOut( __METHOD__ ); return; } $id = $this->getId(); $title = $this->mTitle->getPrefixedDBkey(); $shortTitle = $this->mTitle->getDBkey(); if ( !$options['changed'] ) { $good = 0; $total = 0; } elseif ( $options['created'] ) { $good = (int)$this->isCountable( $editInfo ); $total = 1; } elseif ( $options['oldcountable'] !== null ) { $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable']; $total = 0; } else { $good = 0; $total = 0; } DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) ); DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) ); // If this is another user's talk page, update newtalk. // Don't do this if $options['changed'] = false (null-edits) nor if // it's a minor edit and the user doesn't want notifications for those. if ( $options['changed'] && $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $user->getTitleKey() && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) ) ) { $recipient = User::newFromName( $shortTitle, false ); if ( !$recipient ) { wfDebug( __METHOD__ . ": invalid username\n" ); } else { // Allow extensions to prevent user notification when a new message is added to their talk page if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this, $recipient ) ) ) { if ( User::isIP( $shortTitle ) ) { // An anonymous user $recipient->setNewtalk( true, $revision ); } elseif ( $recipient->isLoggedIn() ) { $recipient->setNewtalk( true, $revision ); } else { wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" ); } } } } if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { // XXX: could skip pseudo-messages like js/css here, based on content model. $msgtext = $content ? $content->getWikitextForTransclusion() : null; if ( $msgtext === false || $msgtext === null ) { $msgtext = ''; } MessageCache::singleton()->replace( $shortTitle, $msgtext ); } if ( $options['created'] ) { self::onArticleCreate( $this->mTitle ); } else { self::onArticleEdit( $this->mTitle ); } wfProfileOut( __METHOD__ ); }
/** * Get text content object * Does *NOT* follow redirects. * TODO: when is this null? * * @note code that wants to retrieve page content from the database should use WikiPage::getContent(). * * @return Content|null * * @since 1.21 */ protected function fetchContentObject() { if ($this->mContentLoaded) { return $this->mContentObject; } wfProfileIn(__METHOD__); $this->mContentLoaded = true; $this->mContent = null; $oldid = $this->getOldID(); # Pre-fill content with error message so that if something # fails we'll have something telling us what we intended. //XXX: this isn't page content but a UI message. horrible. $this->mContentObject = new MessageContent('missing-revision', array($oldid), array()); if ($oldid) { # $this->mRevision might already be fetched by getOldIDFromRequest() if (!$this->mRevision) { $this->mRevision = Revision::newFromId($oldid); if (!$this->mRevision) { wfDebug(__METHOD__ . " failed to retrieve specified revision, id {$oldid}\n"); wfProfileOut(__METHOD__); return false; } } } else { if (!$this->mPage->getLatest()) { wfDebug(__METHOD__ . " failed to find page data for title " . $this->getTitle()->getPrefixedText() . "\n"); wfProfileOut(__METHOD__); return false; } $this->mRevision = $this->mPage->getRevision(); if (!$this->mRevision) { wfDebug(__METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n"); wfProfileOut(__METHOD__); return false; } } // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks. // We should instead work with the Revision object when we need it... $this->mContentObject = $this->mRevision->getContent(Revision::FOR_THIS_USER, $this->getContext()->getUser()); // Loads if user is allowed $this->mRevIdFetched = $this->mRevision->getId(); wfRunHooks('ArticleAfterFetchContentObject', array(&$this, &$this->mContentObject)); wfProfileOut(__METHOD__); return $this->mContentObject; }