/** * @see Content::replaceSection() */ public function replaceSection($section, Content $with, $sectionTitle = '') { wfProfileIn(__METHOD__); $myModelId = $this->getModel(); $sectionModelId = $with->getModel(); if ($sectionModelId != $myModelId) { wfProfileOut(__METHOD__); throw new MWException("Incompatible content model for section: " . "document uses {$myModelId} but " . "section uses {$sectionModelId}."); } $oldtext = $this->getNativeData(); $text = $with->getNativeData(); if ($section === '') { wfProfileOut(__METHOD__); return $with; # XXX: copy first? } if ($section == 'new') { # Inserting a new section $subject = $sectionTitle ? wfMessage('newsectionheaderdefaultlevel')->rawParams($sectionTitle)->inContentLanguage()->text() . "\n\n" : ''; if (wfRunHooks('PlaceNewSection', array($this, $oldtext, $subject, &$text))) { $text = strlen(trim($oldtext)) > 0 ? "{$oldtext}\n\n{$subject}{$text}" : "{$subject}{$text}"; } } else { # Replacing an existing section; roll out the big guns global $wgParser; $text = $wgParser->replaceSection($oldtext, $section, $text); } $newContent = new WikitextContent($text); wfProfileOut(__METHOD__); return $newContent; }
/** * Attempts to merge differences between three versions. Returns a new * Content object for a clean merge and false for failure or a conflict. * * All three Content objects passed as parameters must have the same * content model. * * This text-based implementation uses wfMerge(). * * @param Content $oldContent The page's previous content. * @param Content $myContent One of the page's conflicting contents. * @param Content $yourContent One of the page's conflicting contents. * * @return Content|bool */ public function merge3(Content $oldContent, Content $myContent, Content $yourContent) { $this->checkModelID($oldContent->getModel()); $this->checkModelID($myContent->getModel()); $this->checkModelID($yourContent->getModel()); $format = $this->getDefaultFormat(); $old = $this->serializeContent($oldContent, $format); $mine = $this->serializeContent($myContent, $format); $yours = $this->serializeContent($yourContent, $format); $ok = wfMerge($old, $mine, $yours, $result); if (!$ok) { return false; } if (!$result) { return $this->makeEmptyContent(); } $mergedContent = $this->unserializeContent($result, $format); return $mergedContent; }
public function execute() { // The data is hot but user-dependent, like page views, so we set vary cookies $this->getMain()->setCacheMode('anon-public-user-private'); // Get parameters $params = $this->extractRequestParams(); $text = $params['text']; $title = $params['title']; $page = $params['page']; $pageid = $params['pageid']; $oldid = $params['oldid']; $model = $params['contentmodel']; $format = $params['contentformat']; if (!is_null($page) && (!is_null($text) || $title != 'API')) { $this->dieUsage('The page parameter cannot be used together with the text and title parameters', 'params'); } $prop = array_flip($params['prop']); if (isset($params['section'])) { $this->section = $params['section']; } else { $this->section = false; } // The parser needs $wgTitle to be set, apparently the // $title parameter in Parser::parse isn't enough *sigh* // TODO: Does this still need $wgTitle? global $wgParser, $wgTitle; // Currently unnecessary, code to act as a safeguard against any change in current behaviour of uselang breaks $oldLang = null; if (isset($params['uselang']) && $params['uselang'] != $this->getContext()->getLanguage()->getCode()) { $oldLang = $this->getContext()->getLanguage(); // Backup language $this->getContext()->setLanguage(Language::factory($params['uselang'])); } $redirValues = null; // Return result $result = $this->getResult(); if (!is_null($oldid) || !is_null($pageid) || !is_null($page)) { if (!is_null($oldid)) { // Don't use the parser cache $rev = Revision::newFromID($oldid); if (!$rev) { $this->dieUsage("There is no revision ID {$oldid}", 'missingrev'); } if (!$rev->userCan(Revision::DELETED_TEXT, $this->getUser())) { $this->dieUsage("You don't have permission to view deleted revisions", 'permissiondenied'); } $titleObj = $rev->getTitle(); $wgTitle = $titleObj; $pageObj = WikiPage::factory($titleObj); $popts = $pageObj->makeParserOptions($this->getContext()); $popts->enableLimitReport(!$params['disablepp']); // If for some reason the "oldid" is actually the current revision, it may be cached if ($rev->isCurrent()) { // May get from/save to parser cache $p_result = $this->getParsedContent($pageObj, $popts, $pageid, isset($prop['wikitext'])); } else { // This is an old revision, so get the text differently $this->content = $rev->getContent(Revision::FOR_THIS_USER, $this->getUser()); if ($this->section !== false) { $this->content = $this->getSectionContent($this->content, 'r' . $rev->getId()); } // Should we save old revision parses to the parser cache? $p_result = $this->content->getParserOutput($titleObj, $rev->getId(), $popts); } } else { // Not $oldid, but $pageid or $page if ($params['redirects']) { $reqParams = array('action' => 'query', 'redirects' => ''); if (!is_null($pageid)) { $reqParams['pageids'] = $pageid; } else { // $page $reqParams['titles'] = $page; } $req = new FauxRequest($reqParams); $main = new ApiMain($req); $main->execute(); $data = $main->getResultData(); $redirValues = isset($data['query']['redirects']) ? $data['query']['redirects'] : array(); $to = $page; foreach ((array) $redirValues as $r) { $to = $r['to']; } $pageParams = array('title' => $to); } elseif (!is_null($pageid)) { $pageParams = array('pageid' => $pageid); } else { // $page $pageParams = array('title' => $page); } $pageObj = $this->getTitleOrPageId($pageParams, 'fromdb'); $titleObj = $pageObj->getTitle(); if (!$titleObj || !$titleObj->exists()) { $this->dieUsage("The page you specified doesn't exist", 'missingtitle'); } $wgTitle = $titleObj; if (isset($prop['revid'])) { $oldid = $pageObj->getLatest(); } $popts = $pageObj->makeParserOptions($this->getContext()); $popts->enableLimitReport(!$params['disablepp']); // Potentially cached $p_result = $this->getParsedContent($pageObj, $popts, $pageid, isset($prop['wikitext'])); } } else { // Not $oldid, $pageid, $page. Hence based on $text $titleObj = Title::newFromText($title); if (!$titleObj) { $this->dieUsageMsg(array('invalidtitle', $title)); } if (!$titleObj->canExist()) { $this->dieUsage("Namespace doesn't allow actual pages", 'pagecannotexist'); } $wgTitle = $titleObj; $pageObj = WikiPage::factory($titleObj); $popts = $pageObj->makeParserOptions($this->getContext()); $popts->enableLimitReport(!$params['disablepp']); if (is_null($text)) { $this->dieUsage('The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params'); } try { $this->content = ContentHandler::makeContent($text, $titleObj, $model, $format); } catch (MWContentSerializationException $ex) { $this->dieUsage($ex->getMessage(), 'parseerror'); } if ($this->section !== false) { $this->content = $this->getSectionContent($this->content, $titleObj->getText()); } if ($params['pst'] || $params['onlypst']) { $this->pstContent = $this->content->preSaveTransform($titleObj, $this->getUser(), $popts); } if ($params['onlypst']) { // Build a result and bail out $result_array = array(); $result_array['text'] = array(); $result->setContent($result_array['text'], $this->pstContent->serialize($format)); if (isset($prop['wikitext'])) { $result_array['wikitext'] = array(); $result->setContent($result_array['wikitext'], $this->content->serialize($format)); } $result->addValue(null, $this->getModuleName(), $result_array); return; } // Not cached (save or load) if ($params['pst']) { $p_result = $this->pstContent->getParserOutput($titleObj, null, $popts); } else { $p_result = $this->content->getParserOutput($titleObj, null, $popts); } } $result_array = array(); $result_array['title'] = $titleObj->getPrefixedText(); if (!is_null($oldid)) { $result_array['revid'] = intval($oldid); } if ($params['redirects'] && !is_null($redirValues)) { $result_array['redirects'] = $redirValues; } if (isset($prop['text'])) { $result_array['text'] = array(); $result->setContent($result_array['text'], $p_result->getText()); } if (!is_null($params['summary'])) { $result_array['parsedsummary'] = array(); $result->setContent($result_array['parsedsummary'], Linker::formatComment($params['summary'], $titleObj)); } if (isset($prop['langlinks'])) { $result_array['langlinks'] = $this->formatLangLinks($p_result->getLanguageLinks()); } if (isset($prop['languageshtml'])) { $languagesHtml = $this->languagesHtml($p_result->getLanguageLinks()); $result_array['languageshtml'] = array(); $result->setContent($result_array['languageshtml'], $languagesHtml); } if (isset($prop['categories'])) { $result_array['categories'] = $this->formatCategoryLinks($p_result->getCategories()); } if (isset($prop['categorieshtml'])) { $categoriesHtml = $this->categoriesHtml($p_result->getCategories()); $result_array['categorieshtml'] = array(); $result->setContent($result_array['categorieshtml'], $categoriesHtml); } if (isset($prop['links'])) { $result_array['links'] = $this->formatLinks($p_result->getLinks()); } if (isset($prop['templates'])) { $result_array['templates'] = $this->formatLinks($p_result->getTemplates()); } if (isset($prop['images'])) { $result_array['images'] = array_keys($p_result->getImages()); } if (isset($prop['externallinks'])) { $result_array['externallinks'] = array_keys($p_result->getExternalLinks()); } if (isset($prop['sections'])) { $result_array['sections'] = $p_result->getSections(); } if (isset($prop['displaytitle'])) { $result_array['displaytitle'] = $p_result->getDisplayTitle() ? $p_result->getDisplayTitle() : $titleObj->getPrefixedText(); } if (isset($prop['headitems']) || isset($prop['headhtml'])) { $context = $this->getContext(); $context->setTitle($titleObj); $context->getOutput()->addParserOutputNoText($p_result); if (isset($prop['headitems'])) { $headItems = $this->formatHeadItems($p_result->getHeadItems()); $css = $this->formatCss($context->getOutput()->buildCssLinksArray()); $scripts = array($context->getOutput()->getHeadScripts()); $result_array['headitems'] = array_merge($headItems, $css, $scripts); } if (isset($prop['headhtml'])) { $result_array['headhtml'] = array(); $result->setContent($result_array['headhtml'], $context->getOutput()->headElement($context->getSkin())); } } if (isset($prop['iwlinks'])) { $result_array['iwlinks'] = $this->formatIWLinks($p_result->getInterwikiLinks()); } if (isset($prop['wikitext'])) { $result_array['wikitext'] = array(); $result->setContent($result_array['wikitext'], $this->content->serialize($format)); if (!is_null($this->pstContent)) { $result_array['psttext'] = array(); $result->setContent($result_array['psttext'], $this->pstContent->serialize($format)); } } if (isset($prop['properties'])) { $result_array['properties'] = $this->formatProperties($p_result->getProperties()); } if ($params['generatexml']) { if ($this->content->getModel() != CONTENT_MODEL_WIKITEXT) { $this->dieUsage("generatexml is only supported for wikitext content", "notwikitext"); } $wgParser->startExternalParse($titleObj, $popts, OT_PREPROCESS); $dom = $wgParser->preprocessToDom($this->content->getNativeData()); if (is_callable(array($dom, 'saveXML'))) { $xml = $dom->saveXML(); } else { $xml = $dom->__toString(); } $result_array['parsetree'] = array(); $result->setContent($result_array['parsetree'], $xml); } $result_mapping = array('redirects' => 'r', 'langlinks' => 'll', 'categories' => 'cl', 'links' => 'pl', 'templates' => 'tl', 'images' => 'img', 'externallinks' => 'el', 'iwlinks' => 'iw', 'sections' => 's', 'headitems' => 'hi', 'properties' => 'pp'); $this->setIndexedTagNames($result_array, $result_mapping); $result->addValue(null, $this->getModuleName(), $result_array); if (!is_null($oldLang)) { $this->getContext()->setLanguage($oldLang); // Reset language to $oldLang } }
/** * Change an existing article or create a new article. Updates RC and all necessary caches, * optionally via the deferred update array. * * @param $content Content: new content * @param string $summary edit summary * @param $flags Integer bitfield: * EDIT_NEW * Article is known or assumed to be non-existent, create a new one * EDIT_UPDATE * Article is known or assumed to be pre-existing, update it * EDIT_MINOR * Mark this edit minor, if the user is allowed to do so * EDIT_SUPPRESS_RC * Do not log the change in recentchanges * EDIT_FORCE_BOT * Mark the edit a "bot" edit regardless of user rights * EDIT_DEFER_UPDATES * Defer some of the updates until the end of index.php * EDIT_AUTOSUMMARY * Fill in blank summaries with generated text where possible * * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected. * If EDIT_UPDATE is specified and the article doesn't exist, the function will return an * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an * edit-already-exists error will be returned. These two conditions are also possible with * auto-detection due to MediaWiki's performance-optimised locking strategy. * * @param bool|int $baseRevId the revision ID this edit was based off, if any * @param $user User the user doing the edit * @param $serialisation_format String: format for storing the content in the database * * @throws MWException * @return Status object. Possible errors: * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't set the fatal flag of $status * edit-gone-missing: In update mode, but the article didn't exist * edit-conflict: In update mode, the article changed unexpectedly * edit-no-change: Warning that the text was the same as before * edit-already-exists: In creation mode, but the article already exists * * Extensions may define additional errors. * * $return->value will contain an associative array with members as follows: * new: Boolean indicating if the function attempted to create a new article * revision: The revision object for the inserted revision, or null * * @since 1.21 */ public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, User $user = null, $serialisation_format = null ) { global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; // Low-level sanity check if ( $this->mTitle->getText() === '' ) { throw new MWException( 'Something is trying to edit an article with an empty title' ); } wfProfileIn( __METHOD__ ); if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) { wfProfileOut( __METHOD__ ); return Status::newFatal( 'content-not-allowed-here', ContentHandler::getLocalizedName( $content->getModel() ), $this->getTitle()->getPrefixedText() ); } $user = is_null( $user ) ? $wgUser : $user; $status = Status::newGood( array() ); // Load the data from the master database if needed. // The caller may already loaded it from the master or even loaded it using // SELECT FOR UPDATE, so do not override that using clear(). $this->loadPageData( 'fromdbmaster' ); $flags = $this->checkFlags( $flags ); // handle hook $hook_args = array( &$this, &$user, &$content, &$summary, $flags & EDIT_MINOR, null, null, &$flags, &$status ); if ( !wfRunHooks( 'PageContentSave', $hook_args ) || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) { wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" ); if ( $status->isOK() ) { $status->fatal( 'edit-hook-aborted' ); } wfProfileOut( __METHOD__ ); return $status; } // Silently ignore EDIT_MINOR if not allowed $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); $bot = $flags & EDIT_FORCE_BOT; $old_content = $this->getContent( Revision::RAW ); // current revision's content $oldsize = $old_content ? $old_content->getSize() : 0; $oldid = $this->getLatest(); $oldIsRedirect = $this->isRedirect(); $oldcountable = $this->isCountable(); $handler = $content->getContentHandler(); // Provide autosummaries if one is not provided and autosummaries are enabled. if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { if ( !$old_content ) { $old_content = null; } $summary = $handler->getAutosummary( $old_content, $content, $flags ); } $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format ); $serialized = $editInfo->pst; /** * @var Content $content */ $content = $editInfo->pstContent; $newsize = $content->getSize(); $dbw = wfGetDB( DB_MASTER ); $now = wfTimestampNow(); $this->mTimestamp = $now; if ( $flags & EDIT_UPDATE ) { // Update article, but only if changed. $status->value['new'] = false; if ( !$oldid ) { // Article gone missing wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); $status->fatal( 'edit-gone-missing' ); wfProfileOut( __METHOD__ ); return $status; } elseif ( !$old_content ) { // Sanity check for bug 37225 wfProfileOut( __METHOD__ ); throw new MWException( "Could not find text for current revision {$oldid}." ); } $revision = new Revision( array( 'page' => $this->getId(), 'title' => $this->getTitle(), // for determining the default content model 'comment' => $summary, 'minor_edit' => $isminor, 'text' => $serialized, 'len' => $newsize, 'parent_id' => $oldid, 'user' => $user->getId(), 'user_text' => $user->getName(), 'timestamp' => $now, 'content_model' => $content->getModel(), 'content_format' => $serialisation_format, ) ); // XXX: pass content object?! $changed = !$content->equals( $old_content ); if ( $changed ) { if ( !$content->isValid() ) { wfProfileOut( __METHOD__ ); throw new MWException( "New content failed validity check!" ); } $dbw->begin( __METHOD__ ); $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); $status->merge( $prepStatus ); if ( !$status->isOK() ) { $dbw->rollback( __METHOD__ ); wfProfileOut( __METHOD__ ); return $status; } $revisionId = $revision->insertOn( $dbw ); // Update page // // Note that we use $this->mLatest instead of fetching a value from the master DB // during the course of this function. This makes sure that EditPage can detect // edit conflicts reliably, either by $ok here, or by $article->getTimestamp() // before this function is called. A previous function used a separate query, this // creates a window where concurrent edits can cause an ignored edit conflict. $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); if ( !$ok ) { // Belated edit conflict! Run away!! $status->fatal( 'edit-conflict' ); $dbw->rollback( __METHOD__ ); wfProfileOut( __METHOD__ ); return $status; } wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); // Update recentchanges if ( !( $flags & EDIT_SUPPRESS_RC ) ) { // Mark as patrolled if the user can do so $patrolled = $wgUseRCPatrol && !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); // Add RC row to the DB $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, $revisionId, $patrolled ); // Log auto-patrolled edits if ( $patrolled ) { PatrolLog::record( $rc, true, $user ); } } $user->incEditCount(); $dbw->commit( __METHOD__ ); } else { // Bug 32948: revision ID must be set to page {{REVISIONID}} and // related variables correctly $revision->setId( $this->getLatest() ); } // Update links tables, site stats, etc. $this->doEditUpdates( $revision, $user, array( 'changed' => $changed, 'oldcountable' => $oldcountable ) ); if ( !$changed ) { $status->warning( 'edit-no-change' ); $revision = null; // Update page_touched, this is usually implicit in the page update // Other cache updates are done in onArticleEdit() $this->mTitle->invalidateCache(); } } else { // Create new article $status->value['new'] = true; $dbw->begin( __METHOD__ ); $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); $status->merge( $prepStatus ); if ( !$status->isOK() ) { $dbw->rollback( __METHOD__ ); wfProfileOut( __METHOD__ ); return $status; } $status->merge( $prepStatus ); // Add the page record; stake our claim on this title! // This will return false if the article already exists $newid = $this->insertOn( $dbw ); if ( $newid === false ) { $dbw->rollback( __METHOD__ ); $status->fatal( 'edit-already-exists' ); wfProfileOut( __METHOD__ ); return $status; } // Save the revision text... $revision = new Revision( array( 'page' => $newid, 'title' => $this->getTitle(), // for determining the default content model 'comment' => $summary, 'minor_edit' => $isminor, 'text' => $serialized, 'len' => $newsize, 'user' => $user->getId(), 'user_text' => $user->getName(), 'timestamp' => $now, 'content_model' => $content->getModel(), 'content_format' => $serialisation_format, ) ); $revisionId = $revision->insertOn( $dbw ); // Bug 37225: use accessor to get the text as Revision may trim it $content = $revision->getContent(); // sanity; get normalized version if ( $content ) { $newsize = $content->getSize(); } // Update the page record with revision data $this->updateRevisionOn( $dbw, $revision, 0 ); wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); // Update recentchanges if ( !( $flags & EDIT_SUPPRESS_RC ) ) { // Mark as patrolled if the user can do so $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); // Add RC row to the DB $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, '', $newsize, $revisionId, $patrolled ); // Log auto-patrolled edits if ( $patrolled ) { PatrolLog::record( $rc, true, $user ); } } $user->incEditCount(); $dbw->commit( __METHOD__ ); // Update links, etc. $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); $hook_args = array( &$this, &$user, $content, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision ); ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args ); wfRunHooks( 'PageContentInsertComplete', $hook_args ); } // Do updates right now unless deferral was requested if ( !( $flags & EDIT_DEFER_UPDATES ) ) { DeferredUpdates::doUpdates(); } // Return the new revision (or null) to the caller $status->value['revision'] = $revision; $hook_args = array( &$this, &$user, $content, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ); ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args ); wfRunHooks( 'PageContentSaveComplete', $hook_args ); // Promote user to any groups they meet the criteria for $user->addAutopromoteOnceGroups( 'onEdit' ); wfProfileOut( __METHOD__ ); return $status; }
/** * @param Content $content Pre-save transform content * @param integer $flags * @param User $user * @param string $summary * @param array $meta * @return Status * @throws DBUnexpectedError * @throws Exception * @throws FatalError * @throws MWException */ private function doCreate(Content $content, $flags, User $user, $summary, array $meta) { global $wgUseRCPatrol, $wgUseNPPatrol; $status = Status::newGood(['new' => true, 'revision' => null]); $now = wfTimestampNow(); $newsize = $content->getSize(); $prepStatus = $content->prepareSave($this, $flags, $meta['oldId'], $user); $status->merge($prepStatus); if (!$status->isOK()) { return $status; } $dbw = wfGetDB(DB_MASTER); $dbw->startAtomic(__METHOD__); // Add the page record unless one already exists for the title $newid = $this->insertOn($dbw); if ($newid === false) { $dbw->endAtomic(__METHOD__); // nothing inserted $status->fatal('edit-already-exists'); return $status; // nothing done } // At this point we are now comitted to returning an OK // status unless some DB query error or other exception comes up. // This way callers don't have to call rollback() if $status is bad // unless they actually try to catch exceptions (which is rare). // @TODO: pass content object?! $revision = new Revision(['page' => $newid, 'title' => $this->mTitle, 'comment' => $summary, 'minor_edit' => $meta['minor'], 'text' => $meta['serialized'], 'len' => $newsize, 'user' => $user->getId(), 'user_text' => $user->getName(), 'timestamp' => $now, 'content_model' => $content->getModel(), 'content_format' => $meta['serialFormat']]); // Save the revision text... $revisionId = $revision->insertOn($dbw); // Update the page record with revision data if (!$this->updateRevisionOn($dbw, $revision, 0)) { throw new MWException("Failed to update page row to use new revision."); } Hooks::run('NewRevisionFromEditComplete', [$this, $revision, false, $user]); // Update recentchanges if (!($flags & EDIT_SUPPRESS_RC)) { // Mark as patrolled if the user can do so $patrolled = ($wgUseRCPatrol || $wgUseNPPatrol) && !count($this->mTitle->getUserPermissionsErrors('autopatrol', $user)); // Add RC row to the DB RecentChange::notifyNew($now, $this->mTitle, $revision->isMinor(), $user, $summary, $meta['bot'], '', $newsize, $revisionId, $patrolled, $meta['tags']); } $user->incEditCount(); $dbw->endAtomic(__METHOD__); $this->mTimestamp = $now; // Return the new revision to the caller $status->value['revision'] = $revision; // Do secondary updates once the main changes have been committed... DeferredUpdates::addUpdate(new AtomicSectionUpdate($dbw, __METHOD__, function () use($revision, &$user, $content, $summary, &$flags, $meta, &$status) { // Update links, etc. $this->doEditUpdates($revision, $user, ['created' => true]); // Trigger post-create hook $params = [&$this, &$user, $content, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision]; ContentHandler::runLegacyHooks('ArticleInsertComplete', $params, '1.21'); Hooks::run('PageContentInsertComplete', $params); // Trigger post-save hook $params = array_merge($params, [&$status, $meta['baseRevId']]); ContentHandler::runLegacyHooks('ArticleSaveComplete', $params, '1.21'); Hooks::run('PageContentSaveComplete', $params); }), DeferredUpdates::PRESEND); return $status; }
public function execute() { // The data is hot but user-dependent, like page views, so we set vary cookies $this->getMain()->setCacheMode('anon-public-user-private'); // Get parameters $params = $this->extractRequestParams(); $text = $params['text']; $title = $params['title']; if ($title === null) { $titleProvided = false; // A title is needed for parsing, so arbitrarily choose one $title = 'API'; } else { $titleProvided = true; } $page = $params['page']; $pageid = $params['pageid']; $oldid = $params['oldid']; $model = $params['contentmodel']; $format = $params['contentformat']; if (!is_null($page) && (!is_null($text) || $titleProvided)) { $this->dieUsage('The page parameter cannot be used together with the text and title parameters', 'params'); } $prop = array_flip($params['prop']); if (isset($params['section'])) { $this->section = $params['section']; if (!preg_match('/^((T-)?\\d+|new)$/', $this->section)) { $this->dieUsage("The section parameter must be a valid section id or 'new'", "invalidsection"); } } else { $this->section = false; } // The parser needs $wgTitle to be set, apparently the // $title parameter in Parser::parse isn't enough *sigh* // TODO: Does this still need $wgTitle? global $wgParser, $wgTitle; $redirValues = null; // Return result $result = $this->getResult(); if (!is_null($oldid) || !is_null($pageid) || !is_null($page)) { if ($this->section === 'new') { $this->dieUsage('section=new cannot be combined with oldid, pageid or page parameters. ' . 'Please use text', 'params'); } if (!is_null($oldid)) { // Don't use the parser cache $rev = Revision::newFromId($oldid); if (!$rev) { $this->dieUsage("There is no revision ID {$oldid}", 'missingrev'); } if (!$rev->userCan(Revision::DELETED_TEXT, $this->getUser())) { $this->dieUsage("You don't have permission to view deleted revisions", 'permissiondenied'); } $titleObj = $rev->getTitle(); $wgTitle = $titleObj; $pageObj = WikiPage::factory($titleObj); $popts = $this->makeParserOptions($pageObj, $params); // If for some reason the "oldid" is actually the current revision, it may be cached // Deliberately comparing $pageObj->getLatest() with $rev->getId(), rather than // checking $rev->isCurrent(), because $pageObj is what actually ends up being used, // and if its ->getLatest() is outdated, $rev->isCurrent() won't tell us that. if ($rev->getId() == $pageObj->getLatest()) { // May get from/save to parser cache $p_result = $this->getParsedContent($pageObj, $popts, $pageid, isset($prop['wikitext'])); } else { // This is an old revision, so get the text differently $this->content = $rev->getContent(Revision::FOR_THIS_USER, $this->getUser()); if ($this->section !== false) { $this->content = $this->getSectionContent($this->content, 'r' . $rev->getId()); } // Should we save old revision parses to the parser cache? $p_result = $this->content->getParserOutput($titleObj, $rev->getId(), $popts); } } else { // Not $oldid, but $pageid or $page if ($params['redirects']) { $reqParams = array('redirects' => ''); if (!is_null($pageid)) { $reqParams['pageids'] = $pageid; } else { // $page $reqParams['titles'] = $page; } $req = new FauxRequest($reqParams); $main = new ApiMain($req); $pageSet = new ApiPageSet($main); $pageSet->execute(); $redirValues = $pageSet->getRedirectTitlesAsResult($this->getResult()); $to = $page; foreach ($pageSet->getRedirectTitles() as $title) { $to = $title->getFullText(); } $pageParams = array('title' => $to); } elseif (!is_null($pageid)) { $pageParams = array('pageid' => $pageid); } else { // $page $pageParams = array('title' => $page); } $pageObj = $this->getTitleOrPageId($pageParams, 'fromdb'); $titleObj = $pageObj->getTitle(); if (!$titleObj || !$titleObj->exists()) { $this->dieUsage("The page you specified doesn't exist", 'missingtitle'); } $wgTitle = $titleObj; if (isset($prop['revid'])) { $oldid = $pageObj->getLatest(); } $popts = $this->makeParserOptions($pageObj, $params); // Don't pollute the parser cache when setting options that aren't // in ParserOptions::optionsHash() /// @todo: This should be handled closer to the actual cache instead of here, see T110269 $suppressCache = $params['disablepp'] || $params['disablelimitreport'] || $params['preview'] || $params['sectionpreview'] || $params['disabletidy']; if ($suppressCache) { $this->content = $this->getContent($pageObj, $pageid); $p_result = $this->content->getParserOutput($titleObj, null, $popts); } else { // Potentially cached $p_result = $this->getParsedContent($pageObj, $popts, $pageid, isset($prop['wikitext'])); } } } else { // Not $oldid, $pageid, $page. Hence based on $text $titleObj = Title::newFromText($title); if (!$titleObj || $titleObj->isExternal()) { $this->dieUsageMsg(array('invalidtitle', $title)); } $wgTitle = $titleObj; if ($titleObj->canExist()) { $pageObj = WikiPage::factory($titleObj); } else { // Do like MediaWiki::initializeArticle() $article = Article::newFromTitle($titleObj, $this->getContext()); $pageObj = $article->getPage(); } $popts = $this->makeParserOptions($pageObj, $params); $textProvided = !is_null($text); if (!$textProvided) { if ($titleProvided && ($prop || $params['generatexml'])) { $this->setWarning("'title' used without 'text', and parsed page properties were requested " . "(did you mean to use 'page' instead of 'title'?)"); } // Prevent warning from ContentHandler::makeContent() $text = ''; } // If we are parsing text, do not use the content model of the default // API title, but default to wikitext to keep BC. if ($textProvided && !$titleProvided && is_null($model)) { $model = CONTENT_MODEL_WIKITEXT; $this->setWarning("No 'title' or 'contentmodel' was given, assuming {$model}."); } try { $this->content = ContentHandler::makeContent($text, $titleObj, $model, $format); } catch (MWContentSerializationException $ex) { $this->dieUsage($ex->getMessage(), 'parseerror'); } if ($this->section !== false) { if ($this->section === 'new') { // Insert the section title above the content. if (!is_null($params['sectiontitle']) && $params['sectiontitle'] !== '') { $this->content = $this->content->addSectionHeader($params['sectiontitle']); } } else { $this->content = $this->getSectionContent($this->content, $titleObj->getPrefixedText()); } } if ($params['pst'] || $params['onlypst']) { $this->pstContent = $this->content->preSaveTransform($titleObj, $this->getUser(), $popts); } if ($params['onlypst']) { // Build a result and bail out $result_array = array(); $result_array['text'] = $this->pstContent->serialize($format); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text'; if (isset($prop['wikitext'])) { $result_array['wikitext'] = $this->content->serialize($format); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext'; } if (!is_null($params['summary']) || !is_null($params['sectiontitle']) && $this->section === 'new') { $result_array['parsedsummary'] = $this->formatSummary($titleObj, $params); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary'; } $result->addValue(null, $this->getModuleName(), $result_array); return; } // Not cached (save or load) if ($params['pst']) { $p_result = $this->pstContent->getParserOutput($titleObj, null, $popts); } else { $p_result = $this->content->getParserOutput($titleObj, null, $popts); } } $result_array = array(); $result_array['title'] = $titleObj->getPrefixedText(); $result_array['pageid'] = $pageid ? $pageid : $pageObj->getId(); if (!is_null($oldid)) { $result_array['revid'] = intval($oldid); } if ($params['redirects'] && !is_null($redirValues)) { $result_array['redirects'] = $redirValues; } if ($params['disabletoc']) { $p_result->setTOCEnabled(false); } if (isset($prop['text'])) { $result_array['text'] = $p_result->getText(); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text'; } if (!is_null($params['summary']) || !is_null($params['sectiontitle']) && $this->section === 'new') { $result_array['parsedsummary'] = $this->formatSummary($titleObj, $params); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary'; } if (isset($prop['langlinks'])) { $langlinks = $p_result->getLanguageLinks(); if ($params['effectivelanglinks']) { // Link flags are ignored for now, but may in the future be // included in the result. $linkFlags = array(); Hooks::run('LanguageLinks', array($titleObj, &$langlinks, &$linkFlags)); } } else { $langlinks = false; } if (isset($prop['langlinks'])) { $result_array['langlinks'] = $this->formatLangLinks($langlinks); } if (isset($prop['categories'])) { $result_array['categories'] = $this->formatCategoryLinks($p_result->getCategories()); } if (isset($prop['categorieshtml'])) { $result_array['categorieshtml'] = $this->categoriesHtml($p_result->getCategories()); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml'; } if (isset($prop['links'])) { $result_array['links'] = $this->formatLinks($p_result->getLinks()); } if (isset($prop['templates'])) { $result_array['templates'] = $this->formatLinks($p_result->getTemplates()); } if (isset($prop['images'])) { $result_array['images'] = array_keys($p_result->getImages()); } if (isset($prop['externallinks'])) { $result_array['externallinks'] = array_keys($p_result->getExternalLinks()); } if (isset($prop['sections'])) { $result_array['sections'] = $p_result->getSections(); } if (isset($prop['displaytitle'])) { $result_array['displaytitle'] = $p_result->getDisplayTitle() ? $p_result->getDisplayTitle() : $titleObj->getPrefixedText(); } if (isset($prop['headitems']) || isset($prop['headhtml'])) { $context = $this->getContext(); $context->setTitle($titleObj); $context->getOutput()->addParserOutputMetadata($p_result); if (isset($prop['headitems'])) { $headItems = $this->formatHeadItems($p_result->getHeadItems()); $css = $this->formatCss($context->getOutput()->buildCssLinksArray()); $scripts = array($context->getOutput()->getHeadScripts()); $result_array['headitems'] = array_merge($headItems, $css, $scripts); } if (isset($prop['headhtml'])) { $result_array['headhtml'] = $context->getOutput()->headElement($context->getSkin()); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml'; } } if (isset($prop['modules'])) { $result_array['modules'] = array_values(array_unique($p_result->getModules())); $result_array['modulescripts'] = array_values(array_unique($p_result->getModuleScripts())); $result_array['modulestyles'] = array_values(array_unique($p_result->getModuleStyles())); // To be removed in 1.27 $result_array['modulemessages'] = array(); $this->setWarning('modulemessages is deprecated since MediaWiki 1.26'); } if (isset($prop['jsconfigvars'])) { $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars($p_result->getJsConfigVars()); } if (isset($prop['encodedjsconfigvars'])) { $result_array['encodedjsconfigvars'] = FormatJson::encode($p_result->getJsConfigVars(), false, FormatJson::ALL_OK); $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars'; } if (isset($prop['modules']) && !isset($prop['jsconfigvars']) && !isset($prop['encodedjsconfigvars'])) { $this->setWarning("Property 'modules' was set but not 'jsconfigvars' " . "or 'encodedjsconfigvars'. Configuration variables are necessary " . "for proper module usage."); } if (isset($prop['indicators'])) { $result_array['indicators'] = (array) $p_result->getIndicators(); ApiResult::setArrayType($result_array['indicators'], 'BCkvp', 'name'); } if (isset($prop['iwlinks'])) { $result_array['iwlinks'] = $this->formatIWLinks($p_result->getInterwikiLinks()); } if (isset($prop['wikitext'])) { $result_array['wikitext'] = $this->content->serialize($format); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext'; if (!is_null($this->pstContent)) { $result_array['psttext'] = $this->pstContent->serialize($format); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext'; } } if (isset($prop['properties'])) { $result_array['properties'] = (array) $p_result->getProperties(); ApiResult::setArrayType($result_array['properties'], 'BCkvp', 'name'); } if (isset($prop['limitreportdata'])) { $result_array['limitreportdata'] = $this->formatLimitReportData($p_result->getLimitReportData()); } if (isset($prop['limitreporthtml'])) { $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport($p_result); $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml'; } if (isset($prop['parsetree']) || $params['generatexml']) { if ($this->content->getModel() != CONTENT_MODEL_WIKITEXT) { $this->dieUsage("parsetree is only supported for wikitext content", "notwikitext"); } $wgParser->startExternalParse($titleObj, $popts, Parser::OT_PREPROCESS); $dom = $wgParser->preprocessToDom($this->content->getNativeData()); if (is_callable(array($dom, 'saveXML'))) { $xml = $dom->saveXML(); } else { $xml = $dom->__toString(); } $result_array['parsetree'] = $xml; $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree'; } $result_mapping = array('redirects' => 'r', 'langlinks' => 'll', 'categories' => 'cl', 'links' => 'pl', 'templates' => 'tl', 'images' => 'img', 'externallinks' => 'el', 'iwlinks' => 'iw', 'sections' => 's', 'headitems' => 'hi', 'modules' => 'm', 'indicators' => 'ind', 'modulescripts' => 'm', 'modulestyles' => 'm', 'modulemessages' => 'm', 'properties' => 'pp', 'limitreportdata' => 'lr'); $this->setIndexedTagNames($result_array, $result_mapping); $result->addValue(null, $this->getModuleName(), $result_array); }
/** * Diff this content object with another content object. * * @since 1.21diff * * @param $that Content: The other content object to compare this content * object to. * @param $lang Language: The language object to use for text segmentation. * If not given, $wgContentLang is used. * * @return DiffResult: A diff representing the changes that would have to be * made to this content object to make it equal to $that. */ public function diff(Content $that, Language $lang = null) { global $wgContLang; $this->checkModelID($that->getModel()); # @todo: could implement this in DifferenceEngine and just delegate here? if (!$lang) { $lang = $wgContLang; } $otext = $this->getNativeData(); $ntext = $this->getNativeData(); # Note: Use native PHP diff, external engines don't give us abstract output $ota = explode("\n", $wgContLang->segmentForDiff($otext)); $nta = explode("\n", $wgContLang->segmentForDiff($ntext)); $diff = new Diff($ota, $nta); return $diff; }
/** * Returns the appropriate ContentHandler singleton for the given Content * object. * * @since 1.21 * * @param Content $content * * @return ContentHandler */ public static function getForContent(Content $content) { $modelId = $content->getModel(); return ContentHandler::getForModelID($modelId); }
/** * Hook into Content::getParserOutput to provide syntax highlighting for * script content. * * @return bool * @since MW 1.21 */ public static function onContentGetParserOutput(Content $content, Title $title, $revId, ParserOptions $options, $generateHtml, ParserOutput &$output) { global $wgParser, $wgTextModelsToParse; if (!$generateHtml) { // Nothing special for us to do, let MediaWiki handle this. return true; } // Determine the language $extension = ExtensionRegistry::getInstance(); $models = $extension->getAttribute('SyntaxHighlightModels'); $model = $content->getModel(); if (!isset($models[$model])) { // We don't care about this model, carry on. return true; } $lexer = $models[$model]; // Hope that $wgSyntaxHighlightModels does not contain silly types. $text = ContentHandler::getContentText($content); if (!$text) { // Oops! Non-text content? Let MediaWiki handle this. return true; } // Parse using the standard parser to get links etc. into the database, HTML is replaced below. // We could do this using $content->fillParserOutput(), but alas it is 'protected'. if ($content instanceof TextContent && in_array($model, $wgTextModelsToParse)) { $output = $wgParser->parse($text, $title, $options, true, true, $revId); } $status = self::highlight($text, $lexer); if (!$status->isOK()) { return true; } $out = $status->getValue(); $output->addModuleStyles('ext.pygments'); $output->setText('<div dir="ltr">' . $out . '</div>'); // Inform MediaWiki that we have parsed this page and it shouldn't mess with it. return false; }
/** * Get the temporary prepared edit stash key for a user * * This key can be used for caching prepared edits provided: * - a) The $user was used for PST options * - b) The parser output was made from the PST using cannonical matching options * * @param Title $title * @param Content $content * @param User $user User to get parser options from * @return string */ protected static function getStashKey(Title $title, Content $content, User $user) { $hash = sha1(implode(':', array($content->getModel(), $content->getDefaultFormat(), sha1($content->serialize($content->getDefaultFormat())), $user->getId() ?: md5($user->getName()), $user->getId() ? $user->getDBTouched() : '-'))); return wfMemcKey('prepared-edit', md5($title->getPrefixedDBkey()), $hash); }
/** * Constructor * * @param $row Mixed: either a database row or an array * @throws MWException * @access private */ function __construct($row) { if (is_object($row)) { $this->mId = intval($row->rev_id); $this->mPage = intval($row->rev_page); $this->mTextId = intval($row->rev_text_id); $this->mComment = $row->rev_comment; $this->mUser = intval($row->rev_user); $this->mMinorEdit = intval($row->rev_minor_edit); $this->mTimestamp = $row->rev_timestamp; $this->mDeleted = intval($row->rev_deleted); if (!isset($row->rev_parent_id)) { $this->mParentId = null; } else { $this->mParentId = intval($row->rev_parent_id); } if (!isset($row->rev_len)) { $this->mSize = null; } else { $this->mSize = intval($row->rev_len); } if (!isset($row->rev_sha1)) { $this->mSha1 = null; } else { $this->mSha1 = $row->rev_sha1; } if (isset($row->page_latest)) { $this->mCurrent = $row->rev_id == $row->page_latest; $this->mTitle = Title::newFromRow($row); } else { $this->mCurrent = false; $this->mTitle = null; } if (!isset($row->rev_content_model) || is_null($row->rev_content_model)) { $this->mContentModel = null; # determine on demand if needed } else { $this->mContentModel = strval($row->rev_content_model); } if (!isset($row->rev_content_format) || is_null($row->rev_content_format)) { $this->mContentFormat = null; # determine on demand if needed } else { $this->mContentFormat = strval($row->rev_content_format); } // Lazy extraction... $this->mText = null; if (isset($row->old_text)) { $this->mTextRow = $row; } else { // 'text' table row entry will be lazy-loaded $this->mTextRow = null; } // Use user_name for users and rev_user_text for IPs... $this->mUserText = null; // lazy load if left null if ($this->mUser == 0) { $this->mUserText = $row->rev_user_text; // IP user } elseif (isset($row->user_name)) { $this->mUserText = $row->user_name; // logged-in user } $this->mOrigUserText = $row->rev_user_text; } elseif (is_array($row)) { // Build a new revision to be saved... global $wgUser; // ugh # if we have a content object, use it to set the model and type if (!empty($row['content'])) { // @todo when is that set? test with external store setup! check out insertOn() [dk] if (!empty($row['text_id'])) { throw new MWException("Text already stored in external store (id {$row['text_id']}), " . "can't serialize content object"); } $row['content_model'] = $row['content']->getModel(); # note: mContentFormat is initializes later accordingly # note: content is serialized later in this method! # also set text to null? } $this->mId = isset($row['id']) ? intval($row['id']) : null; $this->mPage = isset($row['page']) ? intval($row['page']) : null; $this->mTextId = isset($row['text_id']) ? intval($row['text_id']) : null; $this->mUserText = isset($row['user_text']) ? strval($row['user_text']) : $wgUser->getName(); $this->mUser = isset($row['user']) ? intval($row['user']) : $wgUser->getId(); $this->mMinorEdit = isset($row['minor_edit']) ? intval($row['minor_edit']) : 0; $this->mTimestamp = isset($row['timestamp']) ? strval($row['timestamp']) : wfTimestampNow(); $this->mDeleted = isset($row['deleted']) ? intval($row['deleted']) : 0; $this->mSize = isset($row['len']) ? intval($row['len']) : null; $this->mParentId = isset($row['parent_id']) ? intval($row['parent_id']) : null; $this->mSha1 = isset($row['sha1']) ? strval($row['sha1']) : null; $this->mContentModel = isset($row['content_model']) ? strval($row['content_model']) : null; $this->mContentFormat = isset($row['content_format']) ? strval($row['content_format']) : null; // Enforce spacing trimming on supplied text $this->mComment = isset($row['comment']) ? trim(strval($row['comment'])) : null; $this->mText = isset($row['text']) ? rtrim(strval($row['text'])) : null; $this->mTextRow = null; $this->mTitle = isset($row['title']) ? $row['title'] : null; // if we have a Content object, override mText and mContentModel if (!empty($row['content'])) { if (!$row['content'] instanceof Content) { throw new MWException('`content` field must contain a Content object.'); } $handler = $this->getContentHandler(); $this->mContent = $row['content']; $this->mContentModel = $this->mContent->getModel(); $this->mContentHandler = null; $this->mText = $handler->serializeContent($row['content'], $this->getContentFormat()); } elseif (!is_null($this->mText)) { $handler = $this->getContentHandler(); $this->mContent = $handler->unserializeContent($this->mText); } // If we have a Title object, make sure it is consistent with mPage. if ($this->mTitle && $this->mTitle->exists()) { if ($this->mPage === null) { // if the page ID wasn't known, set it now $this->mPage = $this->mTitle->getArticleID(); } elseif ($this->mTitle->getArticleID() !== $this->mPage) { // Got different page IDs. This may be legit (e.g. during undeletion), // but it seems worth mentioning it in the log. wfDebug("Page ID " . $this->mPage . " mismatches the ID " . $this->mTitle->getArticleID() . " provided by the Title object."); } } $this->mCurrent = false; // If we still have no length, see it we have the text to figure it out if (!$this->mSize) { if (!is_null($this->mContent)) { $this->mSize = $this->mContent->getSize(); } else { #NOTE: this should never happen if we have either text or content object! $this->mSize = null; } } // Same for sha1 if ($this->mSha1 === null) { $this->mSha1 = is_null($this->mText) ? null : self::base36Sha1($this->mText); } // force lazy init $this->getContentModel(); $this->getContentFormat(); } else { throw new MWException('Revision constructor passed invalid row format.'); } $this->mUnpatrolled = null; }
/** * Change an existing article or create a new article. Updates RC and all necessary caches, * optionally via the deferred update array. * * @param Content $content New content * @param string $summary Edit summary * @param int $flags Bitfield: * EDIT_NEW * Article is known or assumed to be non-existent, create a new one * EDIT_UPDATE * Article is known or assumed to be pre-existing, update it * EDIT_MINOR * Mark this edit minor, if the user is allowed to do so * EDIT_SUPPRESS_RC * Do not log the change in recentchanges * EDIT_FORCE_BOT * Mark the edit a "bot" edit regardless of user rights * EDIT_AUTOSUMMARY * Fill in blank summaries with generated text where possible * * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the * article will be detected. If EDIT_UPDATE is specified and the article * doesn't exist, the function will return an edit-gone-missing error. If * EDIT_NEW is specified and the article does exist, an edit-already-exists * error will be returned. These two conditions are also possible with * auto-detection due to MediaWiki's performance-optimised locking strategy. * * @param bool|int $baseRevId The revision ID this edit was based off, if any. * This is not the parent revision ID, rather the revision ID for older * content used as the source for a rollback, for example. * @param User $user The user doing the edit * @param string $serialFormat Format for storing the content in the * database. * * @throws MWException * @return Status Possible errors: * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't * set the fatal flag of $status. * edit-gone-missing: In update mode, but the article didn't exist. * edit-conflict: In update mode, the article changed unexpectedly. * edit-no-change: Warning that the text was the same as before. * edit-already-exists: In creation mode, but the article already exists. * * Extensions may define additional errors. * * $return->value will contain an associative array with members as follows: * new: Boolean indicating if the function attempted to create a new article. * revision: The revision object for the inserted revision, or null. * * @since 1.21 * @throws MWException */ public function doEditContent(Content $content, $summary, $flags = 0, $baseRevId = false, User $user = null, $serialFormat = null) { global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; // Low-level sanity check if ($this->mTitle->getText() === '') { throw new MWException('Something is trying to edit an article with an empty title'); } if (!$content->getContentHandler()->canBeUsedOn($this->getTitle())) { return Status::newFatal('content-not-allowed-here', ContentHandler::getLocalizedName($content->getModel()), $this->getTitle()->getPrefixedText()); } $user = is_null($user) ? $wgUser : $user; $status = Status::newGood(array()); // Load the data from the master database if needed. // The caller may already loaded it from the master or even loaded it using // SELECT FOR UPDATE, so do not override that using clear(). $this->loadPageData('fromdbmaster'); $flags = $this->checkFlags($flags); // handle hook $hook_args = array(&$this, &$user, &$content, &$summary, $flags & EDIT_MINOR, null, null, &$flags, &$status); if (!Hooks::run('PageContentSave', $hook_args) || !ContentHandler::runLegacyHooks('ArticleSave', $hook_args)) { wfDebug(__METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n"); if ($status->isOK()) { $status->fatal('edit-hook-aborted'); } return $status; } // Silently ignore EDIT_MINOR if not allowed $isminor = $flags & EDIT_MINOR && $user->isAllowed('minoredit'); $bot = $flags & EDIT_FORCE_BOT; $old_revision = $this->getRevision(); // current revision $old_content = $this->getContent(Revision::RAW); // current revision's content $oldsize = $old_content ? $old_content->getSize() : 0; $oldid = $this->getLatest(); $oldIsRedirect = $this->isRedirect(); $oldcountable = $this->isCountable(); $handler = $content->getContentHandler(); // Provide autosummaries if one is not provided and autosummaries are enabled. if ($wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '') { if (!$old_content) { $old_content = null; } $summary = $handler->getAutosummary($old_content, $content, $flags); } $editInfo = $this->prepareContentForEdit($content, null, $user, $serialFormat); $serialized = $editInfo->pst; /** * @var Content $content */ $content = $editInfo->pstContent; $newsize = $content->getSize(); $dbw = wfGetDB(DB_MASTER); $now = wfTimestampNow(); if ($flags & EDIT_UPDATE) { // Update article, but only if changed. $status->value['new'] = false; if (!$oldid) { // Article gone missing wfDebug(__METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n"); $status->fatal('edit-gone-missing'); return $status; } elseif (!$old_content) { // Sanity check for bug 37225 throw new MWException("Could not find text for current revision {$oldid}."); } $revision = new Revision(array('page' => $this->getId(), 'title' => $this->getTitle(), 'comment' => $summary, 'minor_edit' => $isminor, 'text' => $serialized, 'len' => $newsize, 'parent_id' => $oldid, 'user' => $user->getId(), 'user_text' => $user->getName(), 'timestamp' => $now, 'content_model' => $content->getModel(), 'content_format' => $serialFormat)); // XXX: pass content object?! $changed = !$content->equals($old_content); if ($changed) { $prepStatus = $content->prepareSave($this, $flags, $oldid, $user); $status->merge($prepStatus); if (!$status->isOK()) { return $status; } $dbw->begin(__METHOD__); // Get the latest page_latest value while locking it. // Do a CAS style check to see if it's the same as when this method // started. If it changed then bail out before touching the DB. $latestNow = $this->lockAndGetLatest(); if ($latestNow != $oldid) { $dbw->commit(__METHOD__); // Page updated or deleted in the mean time $status->fatal('edit-conflict'); return $status; } // At this point we are now comitted to returning an OK // status unless some DB query error or other exception comes up. // This way callers don't have to call rollback() if $status is bad // unless they actually try to catch exceptions (which is rare). $revisionId = $revision->insertOn($dbw); // Update page_latest and friends to reflect the new revision if (!$this->updateRevisionOn($dbw, $revision, null, $oldIsRedirect)) { $dbw->rollback(__METHOD__); throw new MWException("Failed to update page row to use new revision."); } Hooks::run('NewRevisionFromEditComplete', array($this, $revision, $baseRevId, $user)); // Update recentchanges if (!($flags & EDIT_SUPPRESS_RC)) { // Mark as patrolled if the user can do so $patrolled = $wgUseRCPatrol && !count($this->mTitle->getUserPermissionsErrors('autopatrol', $user)); // Add RC row to the DB RecentChange::notifyEdit($now, $this->mTitle, $isminor, $user, $summary, $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, $revisionId, $patrolled); } $user->incEditCount(); $dbw->commit(__METHOD__); $this->mTimestamp = $now; } else { // Bug 32948: revision ID must be set to page {{REVISIONID}} and // related variables correctly $revision->setId($this->getLatest()); } // Update links tables, site stats, etc. $this->doEditUpdates($revision, $user, array('changed' => $changed, 'oldcountable' => $oldcountable, 'oldrevision' => $old_revision)); if (!$changed) { $status->warning('edit-no-change'); $revision = null; // Update page_touched, this is usually implicit in the page update // Other cache updates are done in onArticleEdit() $this->mTitle->invalidateCache($now); } } else { // Create new article $status->value['new'] = true; $prepStatus = $content->prepareSave($this, $flags, $oldid, $user); $status->merge($prepStatus); if (!$status->isOK()) { return $status; } $dbw->begin(__METHOD__); // Add the page record unless one already exists for the title $newid = $this->insertOn($dbw); if ($newid === false) { $dbw->commit(__METHOD__); // nothing inserted $status->fatal('edit-already-exists'); return $status; // nothing done } // At this point we are now comitted to returning an OK // status unless some DB query error or other exception comes up. // This way callers don't have to call rollback() if $status is bad // unless they actually try to catch exceptions (which is rare). // Save the revision text... $revision = new Revision(array('page' => $newid, 'title' => $this->getTitle(), 'comment' => $summary, 'minor_edit' => $isminor, 'text' => $serialized, 'len' => $newsize, 'user' => $user->getId(), 'user_text' => $user->getName(), 'timestamp' => $now, 'content_model' => $content->getModel(), 'content_format' => $serialFormat)); $revisionId = $revision->insertOn($dbw); // Bug 37225: use accessor to get the text as Revision may trim it $content = $revision->getContent(); // sanity; get normalized version if ($content) { $newsize = $content->getSize(); } // Update the page record with revision data if (!$this->updateRevisionOn($dbw, $revision, 0)) { $dbw->rollback(__METHOD__); throw new MWException("Failed to update page row to use new revision."); } Hooks::run('NewRevisionFromEditComplete', array($this, $revision, false, $user)); // Update recentchanges if (!($flags & EDIT_SUPPRESS_RC)) { // Mark as patrolled if the user can do so $patrolled = ($wgUseRCPatrol || $wgUseNPPatrol) && !count($this->mTitle->getUserPermissionsErrors('autopatrol', $user)); // Add RC row to the DB RecentChange::notifyNew($now, $this->mTitle, $isminor, $user, $summary, $bot, '', $newsize, $revisionId, $patrolled); } $user->incEditCount(); $dbw->commit(__METHOD__); $this->mTimestamp = $now; // Update links, etc. $this->doEditUpdates($revision, $user, array('created' => true, 'oldrevision' => $old_revision)); $hook_args = array(&$this, &$user, $content, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision); ContentHandler::runLegacyHooks('ArticleInsertComplete', $hook_args); Hooks::run('PageContentInsertComplete', $hook_args); } // Return the new revision (or null) to the caller $status->value['revision'] = $revision; $hook_args = array(&$this, &$user, $content, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId); ContentHandler::runLegacyHooks('ArticleSaveComplete', $hook_args); Hooks::run('PageContentSaveComplete', $hook_args); // Promote user to any groups they meet the criteria for DeferredUpdates::addCallableUpdate(function () use($user) { $user->addAutopromoteOnceGroups('onEdit'); $user->addAutopromoteOnceGroups('onView'); // b/c }); return $status; }
/** * Hook into Content::getParserOutput to provide syntax highlighting for * script content. * * @return bool * @since MW 1.21 */ public static function renderHook( Content $content, Title $title, ParserOptions $options, $generateHtml, ParserOutput &$output ) { global $wgSyntaxHighlightModels, $wgUseSiteCss; // Determine the language $model = $content->getModel(); if ( !isset( $wgSyntaxHighlightModels[$model] ) ) { // We don't care about this model, carry on. return true; } if ( !$generateHtml ) { // Nothing to do. return false; } // Hope that $wgSyntaxHighlightModels does not contain silly types. $text = Contenthandler::getContentText( $content ); if ( $text === null || $text === false ) { // Oops! Non-text content? return false; } $lang = $wgSyntaxHighlightModels[$model]; // Attempt to format $geshi = self::prepare( $text, $lang ); if( $geshi instanceof GeSHi ) { $out = $geshi->parse_code(); if( !$geshi->error() ) { // Done $output->addHeadItem( self::buildHeadItem( $geshi ), "source-$lang" ); $output->setText( "<div dir=\"ltr\">{$out}</div>" ); if( $wgUseSiteCss ) { $output->addModuleStyles( 'ext.geshi.local' ); } return false; } } // Bottle out return true; }
/** * Hook into Content::getParserOutput to provide syntax highlighting for * script content. * * @return bool * @since MW 1.21 */ public static function renderHook(Content $content, Title $title, $revId, ParserOptions $options, $generateHtml, ParserOutput &$output) { global $wgSyntaxHighlightModels, $wgUseSiteCss, $wgParser, $wgTextModelsToParse; // Determine the language $model = $content->getModel(); if (!isset($wgSyntaxHighlightModels[$model])) { // We don't care about this model, carry on. return true; } if (!$generateHtml) { // Nothing special for us to do, let MediaWiki handle this. return true; } // Hope that $wgSyntaxHighlightModels does not contain silly types. $text = ContentHandler::getContentText($content); if ($text === null || $text === false) { // Oops! Non-text content? Let MediaWiki handle this. return true; } // Parse using the standard parser to get links etc. into the database, HTML is replaced below. // We could do this using $content->fillParserOutput(), but alas it is 'protected'. if ($content instanceof TextContent && in_array($model, $wgTextModelsToParse)) { $output = $wgParser->parse($text, $title, $options, true, true, $revId); } $lang = $wgSyntaxHighlightModels[$model]; // Attempt to format $geshi = self::prepare($text, $lang); if ($geshi instanceof GeSHi) { $out = $geshi->parse_code(); if (!$geshi->error()) { // Done $output->addModuleStyles("ext.geshi.language.{$lang}"); $output->setText("<div dir=\"ltr\">{$out}</div>"); if ($wgUseSiteCss) { $output->addModuleStyles('ext.geshi.local'); } // Inform MediaWiki that we have parsed this page and it shouldn't mess with it. return false; } } // Bottle out return true; }
/** * Get hash of the content, factoring in model/format * * @param Content $content * @return string */ private static function getContentHash(Content $content) { return sha1(implode("\n", [$content->getModel(), $content->getDefaultFormat(), $content->serialize($content->getDefaultFormat())])); }