From: Jens Ohlig Date: Wed, 11 Apr 2012 12:24:29 +0000 (+0200) Subject: Merge branch 'master' into Wikidata X-Git-Tag: 1.31.0-rc.0~22097^2^2~247^2~7 X-Git-Url: http://git.cyclocoop.org/?a=commitdiff_plain;h=10e91851b2568267e5ccb7bbd0ab24ec4f2a195a;hp=697c683bd6fc6fecf79b123225d3d7ea370ec093;p=lhc%2Fweb%2Fwiklou.git Merge branch 'master' into Wikidata Conflicts: .gitreview includes/Article.php includes/AutoLoader.php includes/EditPage.php includes/LinksUpdate.php includes/WikiPage.php includes/installer/Ibm_db2Updater.php includes/installer/MysqlUpdater.php includes/installer/OracleUpdater.php includes/installer/SqliteUpdater.php maintenance/refreshLinks.php --- diff --git a/.gitreview b/.gitreview index f6438d58e7..7e1473a6a9 100644 --- a/.gitreview +++ b/.gitreview @@ -2,4 +2,4 @@ host=gerrit.wikimedia.org port=29418 project=mediawiki/core.git -defaultbranch=master +defaultbranch=Wikidata diff --git a/includes/Article.php b/includes/Article.php index 393f770bf7..d516ce91a7 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -37,7 +37,13 @@ class Article extends Page { */ public $mParserOptions; - var $mContent; // !< + var $mContent; // !< #BC cruft + + /** + * @var Content + */ + var $mContentObject; + var $mContentLoaded = false; // !< var $mOldId; // !< @@ -112,13 +118,14 @@ class Article extends Page { if ( !$page ) { switch( $title->getNamespace() ) { case NS_FILE: - $page = new ImagePage( $title ); + $page = new ImagePage( $title ); #FIXME: teach ImagePage to use ContentHandler break; case NS_CATEGORY: - $page = new CategoryPage( $title ); + $page = new CategoryPage( $title ); #FIXME: teach ImagePage to use ContentHandler break; default: - $page = new Article( $title ); + $handler = ContentHandler::getForTitle( $title ); + $page = $handler->createArticle( $title ); } } $page->setContext( $context ); @@ -188,9 +195,27 @@ class Article extends Page { * This function has side effects! Do not use this function if you * only want the real revision text if any. * - * @return string Return the text of this revision + * @deprecated in 1.20; use getContentObject() instead + * + * @return string The text of this revision */ public function getContent() { + wfDeprecated( __METHOD__, '1.20' ); + $content = $this->getContentObject(); + return ContentHandler::getContentText( $content ); + } + + /** + * Note that getContent/loadContent do not follow redirects anymore. + * If you need to fetch redirectable content easily, try + * the shortcut in WikiPage::getRedirectTarget() + * + * This function has side effects! Do not use this function if you + * only want the real revision text if any. + * + * @return Content + */ + public function getContentObject() { global $wgUser; wfProfileIn( __METHOD__ ); @@ -203,17 +228,19 @@ class Article extends Page { if ( $text === false ) { $text = ''; } + + $content = ContentHandler::makeContent( $text, $this->getTitle() ); } else { - $text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' ); + $content = new MessageContent( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', null, 'parsemag' ); } wfProfileOut( __METHOD__ ); - return $text; + return $content; } else { - $this->fetchContent(); + $this->fetchContentObject(); wfProfileOut( __METHOD__ ); - return $this->mContent; + return $this->mContentObject; } } @@ -296,15 +323,44 @@ class Article extends Page { * Does *NOT* follow redirects. * * @return mixed string containing article contents, or false if null + * @deprecated in 1.20, use getContentObject() instead */ - function fetchContent() { - if ( $this->mContentLoaded ) { + protected function fetchContent() { #BC cruft! + wfDeprecated( __METHOD__, '1.20' ); + + if ( $this->mContentLoaded && $this->mContent ) { return $this->mContent; } wfProfileIn( __METHOD__ ); + $content = $this->fetchContentObject(); + + $this->mContent = ContentHandler::getContentText( $content ); #FIXME: get rid of mContent everywhere! + wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); #BC cruft! + + wfProfileOut( __METHOD__ ); + + return $this->mContent; + } + + + /** + * Get text content object + * Does *NOT* follow redirects. + * TODO: when is this null? + * + * @return Content|null + */ + protected function fetchContentObject() { + if ( $this->mContentLoaded ) { + return $this->mContentObject; + } + + wfProfileIn( __METHOD__ ); + $this->mContentLoaded = true; + $this->mContent = null; $oldid = $this->getOldID(); @@ -312,7 +368,7 @@ class Article extends Page { # fails we'll have something telling us what we intended. $t = $this->getTitle()->getPrefixedText(); $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : ''; - $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ; + $this->mContentObject = new MessageContent( 'missing-article', array($t, $d), array() ) ; if ( $oldid ) { # $this->mRevision might already be fetched by getOldIDFromRequest() @@ -332,6 +388,7 @@ class Article extends Page { } $this->mRevision = $this->mPage->getRevision(); + if ( !$this->mRevision ) { wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" ); wfProfileOut( __METHOD__ ); @@ -341,14 +398,14 @@ class Article extends Page { // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks. // We should instead work with the Revision object when we need it... - $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed + $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER ); // Loads if user is allowed $this->mRevIdFetched = $this->mRevision->getId(); - wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); + wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) ); #FIXME: register new hook wfProfileOut( __METHOD__ ); - return $this->mContent; + return $this->mContentObject; } /** @@ -381,7 +438,7 @@ class Article extends Page { * @return Revision|null */ public function getRevisionFetched() { - $this->fetchContent(); + $this->fetchContentObject(); return $this->mRevision; } @@ -540,7 +597,7 @@ class Article extends Page { break; case 3: # This will set $this->mRevision if needed - $this->fetchContent(); + $this->fetchContentObject(); # Are we looking at an old revision if ( $oldid && $this->mRevision ) { @@ -564,18 +621,21 @@ class Article extends Page { wfDebug( __METHOD__ . ": showing CSS/JS source\n" ); $this->showCssOrJsPage(); $outputDone = true; - } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) { + } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->fetchContentObject(), $this->getTitle(), $wgOut ) ) ) { #FIXME: document new hook! + # Allow extensions do their own custom view for certain pages + $outputDone = true; + } elseif( Hooks::isRegistered( 'ArticleViewCustom' ) && !wfRunHooks( 'ArticleViewCustom', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated! #FIXME: deprecate hook! # Allow extensions do their own custom view for certain pages $outputDone = true; } else { - $text = $this->getContent(); - $rt = Title::newFromRedirectArray( $text ); + $content = $this->getContentObject(); + $rt = $content->getRedirectChain(); if ( $rt ) { wfDebug( __METHOD__ . ": showing redirect=no page\n" ); # Viewing a redirect page (e.g. with parameter redirect=no) $wgOut->addHTML( $this->viewRedirect( $rt ) ); # Parse just to get categories, displaytitle, etc. - $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions ); + $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid, $parserOptions ); $wgOut->addParserOutputNoText( $this->mParserOutput ); $outputDone = true; } @@ -586,7 +646,7 @@ class Article extends Page { wfDebug( __METHOD__ . ": doing uncached parse\n" ); $poolArticleView = new PoolWorkArticleView( $this, $parserOptions, - $this->getRevIdFetched(), $useParserCache, $this->getContent() ); + $this->getRevIdFetched(), $useParserCache, $this->getContentObject() ); if ( !$poolArticleView->execute() ) { $error = $poolArticleView->getError(); @@ -680,7 +740,9 @@ class Article extends Page { $unhide = $wgRequest->getInt( 'unhide' ) == 1; $oldid = $this->getOldID(); - $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide ); + $contentHandler = ContentHandler::getForTitle( $this->getTitle() ); + $de = $contentHandler->getDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide ); + // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; $de->showDiffPage( $diffOnly ); @@ -698,23 +760,21 @@ class Article extends Page { * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these * page views. */ - protected function showCssOrJsPage() { + protected function showCssOrJsPage( $showCacheHint = true ) { global $wgOut; - $dir = $this->getContext()->getLanguage()->getDir(); - $lang = $this->getContext()->getLanguage()->getCode(); + if ( $showCacheHint ) { + $dir = $this->getContext()->getLanguage()->getDir(); + $lang = $this->getContext()->getLanguage()->getCode(); - $wgOut->wrapWikiMsg( "
\n$1\n
", - 'clearyourcache' ); + $wgOut->wrapWikiMsg( "
\n$1\n
", + 'clearyourcache' ); + } // Give hooks a chance to customise the output - if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) { - // Wrap the whole lot in a
 and don't parse
-			$m = array();
-			preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
-			$wgOut->addHTML( "
\n" );
-			$wgOut->addHTML( htmlspecialchars( $this->mContent ) );
-			$wgOut->addHTML( "\n
\n" ); + if ( !Hooks::isRegistered('ShowRawCssJs') || wfRunHooks( 'ShowRawCssJs', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated #FIXME: hook is deprecated + $po = $this->mContentObject->getParserOutput(); + $wgOut->addHTML( $po->getText() ); } } @@ -1349,7 +1409,13 @@ class Article extends Page { // Generate deletion reason $hasHistory = false; if ( !$reason ) { - $reason = $this->generateReason( $hasHistory ); + try { + $reason = $this->generateReason( $hasHistory ); + } catch (MWException $e) { + # if a page is horribly broken, we still want to be able to delete it. so be lenient about errors here. + wfDebug("Error while building auto delete summary: $e"); + $reason = ''; + } } // If the page has a history, insert a warning @@ -1866,7 +1932,9 @@ class Article extends Page { * @return mixed */ public function generateReason( &$hasHistory ) { - return $this->mPage->getAutoDeleteReason( $hasHistory ); + $title = $this->mPage->getTitle(); + $handler = ContentHandler::getForTitle( $title ); + return $handler->getAutoDeleteReason( $title, $hasHistory ); } // ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** // @@ -1904,6 +1972,7 @@ class Article extends Page { * @param $newtext * @param $flags * @return string + * @deprecated since 1.20, use ContentHandler::getAutosummary() instead */ public static function getAutosummary( $oldtext, $newtext, $flags ) { return WikiPage::getAutosummary( $oldtext, $newtext, $flags ); diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index a1bbc3c0aa..02ad3db760 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -195,6 +195,8 @@ $wgAutoloadLocalClasses = array( 'RevisionList' => 'includes/RevisionList.php', 'RSSFeed' => 'includes/Feed.php', 'Sanitizer' => 'includes/Sanitizer.php', + 'SecondaryDataUpdate' => 'includes/SecondaryDataUpdate.php', + 'SecondaryDBDataUpdate' => 'includes/SecondaryDBDataUpdate.php', 'ScopedPHPTimeout' => 'includes/ScopedPHPTimeout.php', 'SiteConfiguration' => 'includes/SiteConfiguration.php', 'SiteStats' => 'includes/SiteStats.php', @@ -259,6 +261,18 @@ $wgAutoloadLocalClasses = array( 'ZhClient' => 'includes/ZhClient.php', 'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php', + # content handler + 'Content' => 'includes/Content.php', + 'ContentHandler' => 'includes/ContentHandler.php', + 'CssContent' => 'includes/Content.php', + 'CssContentHandler' => 'includes/ContentHandler.php', + 'JavaScriptContent' => 'includes/Content.php', + 'JavaScriptContentHandler' => 'includes/ContentHandler.php', + 'MessageContent' => 'includes/Content.php', + 'TextContent' => 'includes/Content.php', + 'WikitextContent' => 'includes/Content.php', + 'WikitextContentHandler' => 'includes/ContentHandler.php', + # includes/actions 'CreditsAction' => 'includes/actions/CreditsAction.php', 'DeleteAction' => 'includes/actions/DeleteAction.php', diff --git a/includes/Content.php b/includes/Content.php new file mode 100644 index 0000000000..913eb067c8 --- /dev/null +++ b/includes/Content.php @@ -0,0 +1,612 @@ +mModelName = $modelName; + } + + public function getModelName() { + return $this->mModelName; + } + + protected function checkModelName( $modelName ) { + if ( $modelName !== $this->mModelName ) { + throw new MWException( "Bad content model: expected " . $this->mModelName . " but got found " . $modelName ); + } + } + + public function getContentHandler() { + return ContentHandler::getForContent( $this ); + } + + public function getDefaultFormat() { + return $this->getContentHandler()->getDefaultFormat(); + } + + public function getSupportedFormats() { + return $this->getContentHandler()->getSupportedFormats(); + } + + public function isSupportedFormat( $format ) { + if ( !$format ) return true; # this means "use the default" + + return $this->getContentHandler()->isSupportedFormat( $format ); + } + + protected function checkFormat( $format ) { + if ( !$this->isSupportedFormat( $format ) ) { + throw new MWException( "Format $format is not supported for content model " . $this->getModelName() ); + } + } + + public function serialize( $format = null ) { + return $this->getContentHandler()->serialize( $this, $format ); + } + + /** + * @return String a string representing the content in a way useful for building a full text search index. + * If no useful representation exists, this method returns an empty string. + */ + public abstract function getTextForSearchIndex( ); + + /** + * @return String the wikitext to include when another page includes this content, or false if the content is not + * includable in a wikitext page. + */ + #TODO: allow native handling, bypassing wikitext representation, like for includable special pages. + public abstract function getWikitextForTransclusion( ); #FIXME: use in parser, etc! + + /** + * Returns a textual representation of the content suitable for use in edit summaries and log messages. + * + * @param int $maxlength maximum length of the summary text + * @return String the summary text + */ + public abstract function getTextForSummary( $maxlength = 250 ); + + /** + * Returns native represenation of the data. Interpretation depends on the data model used, + * as given by getDataModel(). + * + * @return mixed the native representation of the content. Could be a string, a nested array + * structure, an object, a binary blob... anything, really. + */ + public abstract function getNativeData( ); #FIXME: review all calls carefully, caller must be aware of content model! + + /** + * returns the content's nominal size in bogo-bytes. + * + * @return int + */ + public abstract function getSize( ); + + public function isEmpty() { + return $this->getSize() == 0; + } + + public function equals( Content $that ) { + if ( empty( $that ) ) return false; + if ( $that === $this ) return true; + if ( $that->getModelName() !== $this->getModelName() ) return false; + + return $this->getNativeData() == $that->getNativeData(); + } + + /** + * Returns true if this content is countable as a "real" wiki page, provided + * that it's also in a countable location (e.g. a current revision in the main namespace). + * + * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here, + * to avoid redundant parsing to find out. + */ + public abstract function isCountable( $hasLinks = null ) ; + + /** + * @param null|Title $title + * @param null $revId + * @param null|ParserOptions $options + * @return ParserOutput + */ + public abstract function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = NULL ); + + /** + * Construct the redirect destination from this content and return an + * array of Titles, or null if this content doesn't represent a redirect. + * The last element in the array is the final destination after all redirects + * have been resolved (up to $wgMaxRedirects times). + * + * @return Array of Titles, with the destination last + */ + public function getRedirectChain() { + return null; + } + + /** + * Construct the redirect destination from this content and return an + * array of Titles, or null if this content doesn't represent a redirect. + * This will only return the immediate redirect target, useful for + * the redirect table and other checks that don't need full recursion. + * + * @return Title: The corresponding Title + */ + public function getRedirectTarget() { + return null; + } + + /** + * Construct the redirect destination from this content and return the + * Title, or null if this content doesn't represent a redirect. + * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit + * in order to provide (hopefully) the Title of the final destination instead of another redirect. + * + * @return Title + */ + public function getUltimateRedirectTarget() { + return null; + } + + public function isRedirect() { + return $this->getRedirectTarget() != null; + } + + /** + * Returns the section with the given id. + * + * The default implementation returns null. + * + * @param String $sectionId the section's id + * @return Content|Boolean|null the section, or false if no such section exist, or null if sections are not supported + */ + public function getSection( $sectionId ) { + return null; + } + + /** + * Replaces a section of the content and returns a Content object with the section replaced. + * + * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new" + * @param $with Content: new content of the section + * @param $sectionTitle String: new section's subject, only if $section is 'new' + * @return string Complete article text, or null if error + */ + public function replaceSection( $section, Content $with, $sectionTitle = '' ) { + return $this; + } + + /** + * Returns a Content object with pre-save transformations applied (or this object if no transformations apply). + * + * @param Title $title + * @param User $user + * @param null|ParserOptions $popts + * @return Content + */ + public function preSaveTransform( Title $title, User $user, ParserOptions $popts = null ) { + return $this; + } + + /** + * Returns a new WikitextContent object with the given section heading prepended, if supported. + * The default implementation just returns this Content object unmodified, ignoring the section header. + * + * @param $header String + * @return Content + */ + public function addSectionHeader( $header ) { + return $this; + } + + /** + * Returns a Content object with preload transformations applied (or this object if no transformations apply). + * + * @param Title $title + * @param null|ParserOptions $popts + * @return Content + */ + public function preloadTransform( Title $title, ParserOptions $popts = null ) { + return $this; + } + + # TODO: minimize special cases for CSS/JS; how to handle extra message for JS/CSS previews?? + # TODO: handle ImagePage and CategoryPage + # TODO: hook into dump generation to serialize and record model and format! + + # TODO: make sure we cover lucene search / wikisearch. + # TODO: make sure ReplaceTemplates still works + # TODO: nice&sane integration of GeSHi syntax highlighting + # [11:59] Hooks are ugly; make CodeHighlighter interface and a config to set the class which handles syntax highlighting + # [12:00] And default it to a DummyHighlighter + + # TODO: make sure we cover the external editor interface (does anyone actually use that?!) + + # TODO: tie into API to provide contentModel for Revisions + # TODO: tie into API to provide serialized version and contentFormat for Revisions + # TODO: tie into API edit interface + # TODO: make EditForm plugin for EditPage + + # XXX: isCacheable( ) # can/should we do this here? +} + +/** + * Content object implementation for representing flat text. The + */ +abstract class TextContent extends Content { + public function __construct( $text, $modelName = null ) { + parent::__construct($modelName); + + $this->mText = $text; + } + + public function getTextForSummary( $maxlength = 250 ) { + global $wgContLang; + + $text = $this->getNativeData(); + + $truncatedtext = $wgContLang->truncate( + preg_replace( "/[\n\r]/", ' ', $text ), + max( 0, $maxlength ) ); + + return $truncatedtext; + } + + /** + * returns the content's nominal size in bogo-bytes. + */ + public function getSize( ) { #FIXME: use! replace strlen in WikiPage. + $text = $this->getNativeData( ); + return strlen( $text ); + } + + /** + * Returns true if this content is not a redirect, and $wgArticleCountMethod is "any". + * + * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here, + * to avoid redundant parsing to find out. + */ + public function isCountable( $hasLinks = null ) { + global $wgArticleCountMethod; + + if ( $this->isRedirect( ) ) { + return false; + } + + if ( $wgArticleCountMethod === 'any' ) { + return true; + } + + return false; + } + + /** + * Returns the text represented by this Content object, as a string. + * + * @return String the raw text + */ + public function getNativeData( ) { + $text = $this->mText; + return $text; + } + + /** + * Returns the text represented by this Content object, as a string. + * + * @return String the raw text + */ + public function getTextForSearchIndex( ) { #FIXME: use! + return $this->getNativeData(); + } + + /** + * Returns the text represented by this Content object, as a string. + * + * @return String the raw text + */ + public function getWikitextForTransclusion( ) { #FIXME: use! + return $this->getNativeData(); + } + + /** + * Returns a generic ParserOutput object, wrapping the HTML returned by getHtml(). + * + * @return ParserOutput representing the HTML form of the text + */ + public function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = null ) { + # generic implementation, relying on $this->getHtml() + + $html = $this->getHtml( $options ); + $po = new ParserOutput( $html ); + + return $po; + } + + protected abstract function getHtml( ); + +} + +class WikitextContent extends TextContent { + public function __construct( $text ) { + parent::__construct($text, CONTENT_MODEL_WIKITEXT); + + $this->mDefaultParserOptions = null; #TODO: use per-class static member?! + } + + protected function getHtml( ) { + throw new MWException( "getHtml() not implemented for wikitext. Use getParserOutput()->getText()." ); + } + + public function getDefaultParserOptions() { + global $wgUser, $wgContLang; + + if ( !$this->mDefaultParserOptions ) { #TODO: use per-class static member?! + $this->mDefaultParserOptions = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang ); + } + + return $this->mDefaultParserOptions; + } + + /** + * Returns a ParserOutput object reesulting from parsing the content's text using $wgParser + * + * @return ParserOutput representing the HTML form of the text + */ + public function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = null ) { + global $wgParser; + + if ( !$options ) { + $options = $this->getDefaultParserOptions(); + } + + $po = $wgParser->parse( $this->mText, $title, $options, true, true, $revId ); + + return $po; + } + + /** + * Returns the section with the given id. + * + * @param String $sectionId the section's id + * @return Content|false|null the section, or false if no such section exist, or null if sections are not supported + */ + public function getSection( $section ) { + global $wgParser; + + $text = $this->getNativeData(); + $sect = $wgParser->getSection( $text, $section, false ); + + return new WikitextContent( $sect ); + } + + /** + * Replaces a section in the wikitext + * + * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new" + * @param $with Content: new content of the section + * @param $sectionTitle String: new section's subject, only if $section is 'new' + * @return string Complete article text, or null if error + */ + public function replaceSection( $section, Content $with, $sectionTitle = '' ) { + global $wgParser; + + wfProfileIn( __METHOD__ ); + + $myModelName = $this->getModelName(); + $sectionModelName = $with->getModelName(); + + if ( $sectionModelName != $myModelName ) { + throw new MWException( "Incompatible content model for section: document uses $myModelName, section uses $sectionModelName." ); + } + + $oldtext = $this->getNativeData(); + $text = $with->getNativeData(); + + if ( $section == 'new' ) { + # Inserting a new section + $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\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; + } + + /** + * Returns a new WikitextContent object with the given section heading prepended. + * + * @param $header String + * @return Content + */ + public function addSectionHeader( $header ) { + $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $this->getNativeData(); + + return new WikitextContent( $text ); + } + + /** + * Returns a Content object with pre-save transformations applied (or this object if no transformations apply). + * + * @param Title $title + * @param User $user + * @param null|ParserOptions $popts + * @return Content + */ + public function preSaveTransform( Title $title, User $user, ParserOptions $popts = null ) { + global $wgParser; + + if ( $popts == null ) $popts = $this->getDefaultParserOptions(); + + $text = $this->getNativeData(); + $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts ); + + return new WikitextContent( $pst ); + } + + /** + * Returns a Content object with preload transformations applied (or this object if no transformations apply). + * + * @param Title $title + * @param null|ParserOptions $popts + * @return Content + */ + public function preloadTransform( Title $title, ParserOptions $popts = null ) { + global $wgParser; + + if ( $popts == null ) $popts = $this->getDefaultParserOptions(); + + $text = $this->getNativeData(); + $plt = $wgParser->getPreloadText( $text, $title, $popts ); + + return new WikitextContent( $plt ); + } + + public function getRedirectChain() { + $text = $this->getNativeData(); + return Title::newFromRedirectArray( $text ); + } + + public function getRedirectTarget() { + $text = $this->getNativeData(); + return Title::newFromRedirect( $text ); + } + + public function getUltimateRedirectTarget() { + $text = $this->getNativeData(); + return Title::newFromRedirectRecurse( $text ); + } + + /** + * Returns true if this content is not a redirect, and this content's text is countable according to + * the criteria defiend by $wgArticleCountMethod. + * + * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here, + * to avoid redundant parsing to find out. + */ + public function isCountable( $hasLinks = null ) { + global $wgArticleCountMethod; + + if ( $this->isRedirect( ) ) { + return false; + } + + $text = $this->getNativeData(); + + switch ( $wgArticleCountMethod ) { + case 'any': + return true; + case 'comma': + if ( $text === false ) { + $text = $this->getRawText(); + } + return strpos( $text, ',' ) !== false; + case 'link': + if ( $hasLinks === null ) { # not know, find out + $po = $this->getParserOutput(); + $links = $po->getLinks(); + $hasLinks = !empty( $links ); + } + + return $hasLinks; + } + } + + public function getTextForSummary( $maxlength = 250 ) { + $truncatedtext = parent::getTextForSummary( $maxlength ); + + #clean up unfinished links + #XXX: make this optional? wasn't there in autosummary, but required for deletion summary. + $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext ); + + return $truncatedtext; + } + +} + +class MessageContent extends TextContent { + public function __construct( $msg_key, $params = null, $options = null ) { + parent::__construct(null, CONTENT_MODEL_WIKITEXT); #XXX: messages may be wikitext, html or plain text! and maybe even something else entirely. + + $this->mMessageKey = $msg_key; + + $this->mParameters = $params; + + if ( !$options ) $options = array(); + $this->mOptions = $options; + + $this->mHtmlOptions = null; + } + + /** + * Returns the message as rendered HTML, using the options supplied to the constructor plus "parse". + */ + protected function getHtml( ) { + $opt = array_merge( $this->mOptions, array('parse') ); + + return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt ); + } + + + /** + * Returns the message as raw text, using the options supplied to the constructor minus "parse" and "parseinline". + */ + public function getNativeData( ) { + $opt = array_diff( $this->mOptions, array('parse', 'parseinline') ); + + return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt ); + } + +} + + +class JavaScriptContent extends TextContent { + public function __construct( $text ) { + parent::__construct($text, CONTENT_MODEL_JAVASCRIPT); + } + + protected function getHtml( ) { + $html = ""; + $html .= "
\n";
+        $html .= htmlspecialchars( $this->getNativeData() );
+        $html .= "\n
\n"; + + return $html; + } + +} + +class CssContent extends TextContent { + public function __construct( $text ) { + parent::__construct($text, CONTENT_MODEL_CSS); + } + + protected function getHtml( ) { + $html = ""; + $html .= "
\n";
+        $html .= htmlspecialchars( $this->getNativeData() );
+        $html .= "\n
\n"; + + return $html; + } +} + +#FUTURE: special type for redirects?! +#FUTURE: MultipartMultipart < WikipageContent (Main + Links + X) +#FUTURE: LinksContent < LanguageLinksContent, CategoriesContent +#EXAMPLE: CoordinatesContent +#EXAMPLE: WikidataContent diff --git a/includes/ContentHandler.php b/includes/ContentHandler.php new file mode 100644 index 0000000000..699e2fd3ac --- /dev/null +++ b/includes/ContentHandler.php @@ -0,0 +1,566 @@ +getNativeData(); + } + + if ( $wgContentHandlerTextFallback == 'fail' ) { + throw new MWException( "Attempt to get text from Content with model " . $content->getModelName() ); + } + + if ( $wgContentHandlerTextFallback == 'serialize' ) { + return $content->serialize(); + } + + return null; + } + + public static function makeContent( $text, Title $title, $modelName = null, $format = null ) { + + if ( is_null( $modelName ) ) { + $modelName = $title->getContentModelName(); + } + + $handler = ContentHandler::getForModelName( $modelName ); + return $handler->unserialize( $text, $format ); + } + + public static function getDefaultModelFor( Title $title ) { + global $wgNamespaceContentModels; + + // NOTE: this method must not rely on $title->getContentModelName() directly or indirectly, + // because it is used to initialized the mContentModelName memebr. + + $ns = $title->getNamespace(); + + $ext = false; + $m = null; + $model = null; + + if ( !empty( $wgNamespaceContentModels[ $ns ] ) ) { + $model = $wgNamespaceContentModels[ $ns ]; + } + + // hook can determin default model + if ( !wfRunHooks( 'DefaultModelFor', array( $title, &$model ) ) ) { #FIXME: document new hook! + if ( !is_null( $model ) ) { + return $model; + } + } + + // Could this page contain custom CSS or JavaScript, based on the title? + $isCssOrJsPage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js)$!u', $title->getText(), $m ); + if ( $isCssOrJsPage ) { + $ext = $m[1]; + } + + // hook can force js/css + wfRunHooks( 'TitleIsCssOrJsPage', array( $title, &$isCssOrJsPage ) ); + + // Is this a .css subpage of a user page? + $isJsCssSubpage = NS_USER == $ns && !$isCssOrJsPage && preg_match( "/\\/.*\\.(js|css)$/", $title->getText(), $m ); + if ( $isJsCssSubpage ) { + $ext = $m[1]; + } + + // is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook? + $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT; + $isWikitext = $isWikitext && !$isCssOrJsPage && !$isJsCssSubpage; + + // hook can override $isWikitext + wfRunHooks( 'TitleIsWikitextPage', array( $title, &$isWikitext ) ); + + if ( !$isWikitext ) { + switch ( $ext ) { + case 'js': + return CONTENT_MODEL_JAVASCRIPT; + case 'css': + return CONTENT_MODEL_CSS; + default: + return is_null( $model ) ? CONTENT_MODEL_TEXT : $model; + } + } + + // we established that is must be wikitext + + return CONTENT_MODEL_WIKITEXT; + } + + public static function getForTitle( Title $title ) { + $modelName = $title->getContentModelName(); + return ContentHandler::getForModelName( $modelName ); + } + + public static function getForContent( Content $content ) { + $modelName = $content->getModelName(); + return ContentHandler::getForModelName( $modelName ); + } + + /** + * @static + * @param $modelName String the name of the content model for which to get a handler. Use CONTENT_MODEL_XXX constants. + * @return ContentHandler + * @throws MWException + */ + public static function getForModelName( $modelName ) { + global $wgContentHandlers; + + if ( empty( $wgContentHandlers[$modelName] ) ) { + $handler = null; + + // TODO: document new hook + wfRunHooks( 'ContentHandlerForModelName', array( $modelName, &$handler ) ); + + if ( $handler ) { // NOTE: may be a string or an object, either is fine! + $wgContentHandlers[$modelName] = $handler; + } else { + throw new MWException( "No handler for model $modelName registered in \$wgContentHandlers" ); + } + } + + if ( is_string( $wgContentHandlers[$modelName] ) ) { + $class = $wgContentHandlers[$modelName]; + $wgContentHandlers[$modelName] = new $class( $modelName ); + } + + return $wgContentHandlers[$modelName]; + } + + // ---------------------------------------------------------------------------------------------------------- + public function __construct( $modelName, $formats ) { + $this->mModelName = $modelName; + $this->mSupportedFormats = $formats; + } + + public function getModelName() { + // for wikitext: wikitext; in the future: wikiast, wikidom? + // for wikidata: wikidata + return $this->mModelName; + } + + protected function checkModelName( $modelName ) { + if ( $modelName !== $this->mModelName ) { + throw new MWException( "Bad content model: expected " . $this->mModelName . " but got found " . $modelName ); + } + } + + public function getSupportedFormats() { + // for wikitext: "text/x-mediawiki-1", "text/x-mediawiki-2", etc + // for wikidata: "application/json", "application/x-php", etc + return $this->mSupportedFormats; + } + + public function getDefaultFormat() { + return $this->mSupportedFormats[0]; + } + + public function isSupportedFormat( $format ) { + + if ( !$format ) { + return true; // this means "use the default" + } + + return in_array( $format, $this->mSupportedFormats ); + } + + protected function checkFormat( $format ) { + if ( !$this->isSupportedFormat( $format ) ) { + throw new MWException( "Format $format is not supported for content model " . $this->getModelName() ); + } + } + + /** + * @abstract + * @param Content $content + * @param null $format + * @return String + */ + public abstract function serialize( Content $content, $format = null ); + + /** + * @abstract + * @param $blob String + * @param null $format + * @return Content + */ + public abstract function unserialize( $blob, $format = null ); + + public abstract function emptyContent(); + + /** + * Return an Article object suitable for viewing the given object + * + * NOTE: does *not* do special handling for Image and Category pages! + * Use Article::newFromTitle() for that! + * + * @param Title $title + * @return Article + * @todo Article is being refactored into an action class, keep track of that + */ + public function createArticle( Title $title ) { + $this->checkModelName( $title->getContentModelName() ); + + $article = new Article($title); + return $article; + } + + /** + * Return an EditPage object suitable for editing the given object + * + * @param Article $article + * @return EditPage + */ + public function createEditPage( Article $article ) { + $this->checkModelName( $article->getContentModelName() ); + + $editPage = new EditPage( $article ); + return $editPage; + } + + /** + * Return an ExternalEdit object suitable for editing the given object + * + * @param IContextSource $context + * @return ExternalEdit + */ + public function createExternalEdit( IContextSource $context ) { + $this->checkModelName( $context->getTitle()->getModelName() ); + + $externalEdit = new ExternalEdit( $context ); + return $externalEdit; + } + + /** + * Factory + * @param $context IContextSource context to use, anything else will be ignored + * @param $old Integer old ID we want to show and diff with. + * @param $new String either 'prev' or 'next'. + * @param $rcid Integer ??? FIXME (default 0) + * @param $refreshCache boolean If set, refreshes the diff cache + * @param $unhide boolean If set, allow viewing deleted revs + * + * @return DifferenceEngine + */ + public function getDifferenceEngine( IContextSource $context, $old = 0, $new = 0, $rcid = 0, #FIMXE: use everywhere! + $refreshCache = false, $unhide = false ) { + + $this->checkModelName( $context->getTitle()->getModelName() ); + + return new DifferenceEngine( $context, $old, $new, $rcid, $refreshCache, $unhide ); + } + + /** + * attempts to merge differences between three versions. + * Returns a new Content object for a clean merge and false for failure or a conflict. + * + * This default implementation always returns false. + * + * @param $oldContent String + * @param $myContent String + * @param $yourContent String + * @return Content|Bool + */ + public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) { + return false; + } + + /** + * Return an applicable autosummary if one exists for the given edit. + * + * @param $oldContent Content|null: the previous text of the page. + * @param $newContent Content|null: The submitted text of the page. + * @param $flags Int bitmask: a bitmask of flags submitted for the edit. + * + * @return string An appropriate autosummary, or an empty string. + */ + public function getAutosummary( Content $oldContent = null, Content $newContent = null, $flags ) { + global $wgContLang; + + // Decide what kind of autosummary is needed. + + // Redirect autosummaries + + $ot = !empty( $ot ) ? $oldContent->getRedirectTarget() : false; + $rt = !empty( $rt ) ? $newContent->getRedirectTarget() : false; + + if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) { + + $truncatedtext = $newContent->getTextForSummary( + 250 + - strlen( wfMsgForContent( 'autoredircomment' ) ) + - strlen( $rt->getFullText() ) ); + + return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext ); + } + + // New page autosummaries + if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) { + // If they're making a new article, give its text, truncated, in the summary. + + $truncatedtext = $newContent->getTextForSummary( + 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ); + + return wfMsgForContent( 'autosumm-new', $truncatedtext ); + } + + // Blanking autosummaries + if ( $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) { + return wfMsgForContent( 'autosumm-blank' ); + } elseif ( $oldContent->getSize() > 10 * $newContent->getSize() && $newContent->getSize() < 500 ) { + // Removing more than 90% of the article + + $truncatedtext = $newContent->getTextForSummary( + 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ); + + return wfMsgForContent( 'autosumm-replace', $truncatedtext ); + } + + // If we reach this point, there's no applicable autosummary for our case, so our + // autosummary is empty. + + return ''; + } + + /** + * Auto-generates a deletion reason + * + * @param $title Title: the page's title + * @param &$hasHistory Boolean: whether the page has a history + * @return mixed String containing deletion reason or empty string, or boolean false + * if no revision occurred + */ + public function getAutoDeleteReason( Title $title, &$hasHistory ) { + $dbw = wfGetDB( DB_MASTER ); + + // Get the last revision + $rev = Revision::newFromTitle( $title ); + + if ( is_null( $rev ) ) { + return false; + } + + // Get the article's contents + $content = $rev->getContent(); + $blank = false; + + // If the page is blank, use the text from the previous revision, + // which can only be blank if there's a move/import/protect dummy revision involved + if ( $content->getSize() == 0 ) { + $prev = $rev->getPrevious(); + + if ( $prev ) { + $content = $rev->getContent(); + $blank = true; + } + } + + // Find out if there was only one contributor + // Only scan the last 20 revisions + $res = $dbw->select( 'revision', 'rev_user_text', + array( 'rev_page' => $title->getArticleID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ), + __METHOD__, + array( 'LIMIT' => 20 ) + ); + + if ( $res === false ) { + // This page has no revisions, which is very weird + return false; + } + + $hasHistory = ( $res->numRows() > 1 ); + $row = $dbw->fetchObject( $res ); + + if ( $row ) { // $row is false if the only contributor is hidden + $onlyAuthor = $row->rev_user_text; + // Try to find a second contributor + foreach ( $res as $row ) { + if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999 + $onlyAuthor = false; + break; + } + } + } else { + $onlyAuthor = false; + } + + // Generate the summary with a '$1' placeholder + if ( $blank ) { + // The current revision is blank and the one before is also + // blank. It's just not our lucky day + $reason = wfMsgForContent( 'exbeforeblank', '$1' ); + } else { + if ( $onlyAuthor ) { + $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor ); + } else { + $reason = wfMsgForContent( 'excontent', '$1' ); + } + } + + if ( $reason == '-' ) { + // Allow these UI messages to be blanked out cleanly + return ''; + } + + // Max content length = max comment length - length of the comment (excl. $1) + $text = $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ); + + // Now replace the '$1' placeholder + $reason = str_replace( '$1', $text, $reason ); + + return $reason; + } + + /** + * 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 + * @param $undo Revision + * @param $undoafter null|Revision Must be an earlier revision than $undo + * @return mixed string on success, false on failure + */ + public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter = null ) { + $cur_content = $current->getContent(); + + if ( empty( $cur_content ) ) { + return false; // no page + } + + $undo_content = $undo->getContent(); + $undoafter_content = $undoafter->getContent(); + + 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; + } +} + + +abstract class TextContentHandler extends ContentHandler { + + public function __construct( $modelName, $formats ) { + parent::__construct( $modelName, $formats ); + } + + public function serialize( Content $content, $format = null ) { + $this->checkFormat( $format ); + return $content->getNativeData(); + } + + /** + * attempts to merge differences between three versions. + * Returns a new Content object for a clean merge and false for failure or a conflict. + * + * This text-based implementation uses wfMerge(). + * + * @param $oldContent String + * @param $myContent String + * @param $yourContent String + * @return Content|Bool + */ + public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) { + $this->checkModelName( $oldContent->getModelName() ); + #TODO: check that all Content objects have the same content model! #XXX: what to do if they don't? + + $format = $this->getDefaultFormat(); + + $old = $this->serialize( $oldContent, $format ); + $mine = $this->serialize( $myContent, $format ); + $yours = $this->serialize( $yourContent, $format ); + + $ok = wfMerge( $old, $mine, $yours, $result ); + + if ( !$ok ) { + return false; + } + + if ( !$result ) { + return $this->emptyContent(); + } + + $mergedContent = $this->unserialize( $result, $format ); + return $mergedContent; + } + + +} +class WikitextContentHandler extends TextContentHandler { + + public function __construct( $modelName = CONTENT_MODEL_WIKITEXT ) { + parent::__construct( $modelName, array( 'application/x-wikitext' ) ); #FIXME: mime + } + + public function unserialize( $text, $format = null ) { + $this->checkFormat( $format ); + + return new WikitextContent( $text ); + } + + public function emptyContent() { + return new WikitextContent( '' ); + } + + +} + +#TODO: make ScriptContentHandler base class with plugin interface for syntax highlighting! + +class JavaScriptContentHandler extends TextContentHandler { + + public function __construct( $modelName = CONTENT_MODEL_WIKITEXT ) { + parent::__construct( $modelName, array( 'text/javascript' ) ); #XXX: or use $wgJsMimeType? this is for internal storage, not HTTP... + } + + public function unserialize( $text, $format = null ) { + return new JavaScriptContent( $text ); + } + + public function emptyContent() { + return new JavaScriptContent( '' ); + } +} + +class CssContentHandler extends TextContentHandler { + + public function __construct( $modelName = CONTENT_MODEL_WIKITEXT ) { + parent::__construct( $modelName, array( 'text/css' ) ); + } + + public function unserialize( $text, $format = null ) { + return new CssContent( $text ); + } + + public function emptyContent() { + return new CssContent( '' ); + } + +} diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 67889844b6..277507f0ec 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -640,6 +640,17 @@ $wgMediaHandlers = array( 'image/x-djvu' => 'DjVuHandler', // compat ); +/** + * Plugins for page content model handling. + * Each entry in the array maps a model name type to a class name + */ +$wgContentHandlers = array( + CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // the usual case + CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', // dumb version, no syntax highlighting + CONTENT_MODEL_CSS => 'CssContentHandler', // dumb version, no syntax highlighting + CONTENT_MODEL_TEXT => 'TextContentHandler', // dumb plain text in
+);
+
 /**
  * Resizing can be done using PHP's internal image libraries or using
  * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@ -5807,6 +5818,22 @@ $wgSeleniumConfigFile = null;
 $wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
 $wgDBtestpassword = '';
 
+/**
+ * Associative array mapping namespace IDs to the name of the content model pages in that namespace should have by
+ * default (use the CONTENT_MODEL_XXX constants). If no special content type is defined for a given namespace,
+ * pages in that namespace will  use the CONTENT_MODEL_WIKITEXT (except for the special case of JS and CS pages).
+ */
+$wgNamespaceContentModels = array();
+
+/**
+ * How to react if a plain text version of a non-text Content object is requested using ContentHandler::getContentText():
+ *
+ * * 'ignore': return null
+ * * 'fail': throw an MWException
+ * * 'serialize': serialize to default format
+ */
+$wgContentHandlerTextFallback = 'ignore';
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
diff --git a/includes/Defines.php b/includes/Defines.php
index e40c9b25ec..610d0a5952 100644
--- a/includes/Defines.php
+++ b/includes/Defines.php
@@ -253,3 +253,12 @@ define( 'PROTO_RELATIVE', '//' );
 define( 'PROTO_CURRENT', null );
 define( 'PROTO_CANONICAL', 1 );
 define( 'PROTO_INTERNAL', 2 );
+
+/**
+ * Content model names, used by Content and ContentHandler
+ */
+define('CONTENT_MODEL_WIKITEXT', 'wikitext');
+define('CONTENT_MODEL_JAVASCRIPT', 'javascript');
+define('CONTENT_MODEL_CSS', 'css');
+define('CONTENT_MODEL_TEXT', 'text');
+
diff --git a/includes/EditPage.php b/includes/EditPage.php
index 69187e488a..afe282117e 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -144,6 +144,11 @@ class EditPage {
 	 */
 	const AS_IMAGE_REDIRECT_LOGGED     = 234;
 
+	/**
+	 * Status: can't parse content
+	 */
+	const AS_PARSE_ERROR                = 240;
+
 	/**
 	 * @var Article
 	 */
@@ -198,6 +203,7 @@ class EditPage {
 	var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
 	var $edittime = '', $section = '', $sectiontitle = '', $starttime = '';
 	var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
+	var $content_model = null, $content_format = null;
 
 	# Placeholders for text injection by hooks (must be HTML)
 	# extensions should take care to _append_ to the present value
@@ -209,7 +215,7 @@ class EditPage {
 	public $editFormTextBottom = '';
 	public $editFormTextAfterContent = '';
 	public $previewTextAfterContent = '';
-	public $mPreloadText = '';
+	public $mPreloadContent = null;
 
 	/* $didSave should be set to true whenever an article was succesfully altered. */
 	public $didSave = false;
@@ -223,6 +229,11 @@ class EditPage {
 	public function __construct( Article $article ) {
 		$this->mArticle = $article;
 		$this->mTitle = $article->getTitle();
+
+		$this->content_model = $this->mTitle->getContentModelName();
+
+		$handler = ContentHandler::getForModelName( $this->content_model );
+		$this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision
 	}
 
 	/**
@@ -434,10 +445,10 @@ class EditPage {
 			return;
 		}
 
-		$content = $this->getContent();
+		$content = $this->getContentObject();
 
 		# Use the normal message if there's nothing to display
-		if ( $this->firsttime && $content === '' ) {
+		if ( $this->firsttime && $content->isEmpty() ) {
 			$action = $this->mTitle->exists() ? 'edit' :
 				( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
 			throw new PermissionsError( $action, $permErrors );
@@ -451,13 +462,14 @@ class EditPage {
 		# If the user made changes, preserve them when showing the markup
 		# (This happens when a user is blocked during edit, for instance)
 		if ( !$this->firsttime ) {
-			$content = $this->textbox1;
+			$text = $this->textbox1;
 			$wgOut->addWikiMsg( 'viewyourtext' );
 		} else {
+			$text = $content->serialize( $this->content_format );
 			$wgOut->addWikiMsg( 'viewsourcetext' );
 		}
 
-		$this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) );
+		$this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
 
 		$wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
 			Linker::formatTemplates( $this->getTemplates() ) ) );
@@ -568,9 +580,9 @@ class EditPage {
 				// Skip this if wpTextbox2 has input, it indicates that we came
 				// from a conflict page with raw page text, not a custom form
 				// modified by subclasses
-				wfProfileIn( get_class( $this ) . "::importContentFormData" );
-				$textbox1 = $this->importContentFormData( $request );
-				if ( isset( $textbox1 ) )
+				wfProfileIn( get_class($this)."::importContentFormData" );
+				$textbox1 = $this->importContentFormData( $request ); #FIXME: what should this return??
+				if ( isset($textbox1) )
 					$this->textbox1 = $textbox1;
 				wfProfileOut( get_class( $this ) . "::importContentFormData" );
 			}
@@ -663,7 +675,7 @@ class EditPage {
 		} else {
 			# Not a posted form? Start with nothing.
 			wfDebug( __METHOD__ . ": Not a posted form.\n" );
-			$this->textbox1     = '';
+			$this->textbox1     = ''; #FIXME: track content object
 			$this->summary      = '';
 			$this->sectiontitle = '';
 			$this->edittime     = '';
@@ -695,10 +707,17 @@ class EditPage {
 			}
 		}
 
+		$this->oldid = $request->getInt( 'oldid' );
+
 		$this->bot = $request->getBool( 'bot', true );
 		$this->nosummary = $request->getBool( 'nosummary' );
 
-		$this->oldid = $request->getInt( 'oldid' );
+		$content_handler = ContentHandler::getForTitle( $this->mTitle );
+		$this->content_model = $request->getText( 'model', $content_handler->getModelName() ); #may be overridden by revision
+		$this->content_format = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision
+
+		#TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed
+		#TODO: check if the desired content model supports the given content format!
 
 		$this->live = $request->getCheck( 'live' );
 		$this->editintro = $request->getText( 'editintro',
@@ -731,7 +750,10 @@ class EditPage {
 	function initialiseForm() {
 		global $wgUser;
 		$this->edittime = $this->mArticle->getTimestamp();
-		$this->textbox1 = $this->getContent( false );
+
+		$content = $this->getContentObject( false ); #TODO: track content object?!
+		$this->textbox1 = $content->serialize( $this->content_format );
+
 		// activate checkboxes if user wants them to be always active
 		# Sort out the "watch" checkbox
 		if ( $wgUser->getOption( 'watchdefault' ) ) {
@@ -760,33 +782,52 @@ class EditPage {
 	 * @param $def_text string
 	 * @return mixed string on success, $def_text for invalid sections
 	 * @private
+	 * @deprecated since 1.20
 	 */
-	function getContent( $def_text = '' ) {
-		global $wgOut, $wgRequest, $wgParser;
+	function getContent( $def_text = false ) { #FIXME: deprecated, replace usage!
+		if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
+			$def_content = ContentHandler::makeContent( $def_text, $this->getTitle() );
+		} else {
+			$def_content = false;
+		}
+
+		$content = $this->getContentObject( $def_content );
+
+		return $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?
+	}
+
+	private function getContentObject( $def_content = null ) { #FIXME: use this!
+		global $wgOut, $wgRequest;
 
 		wfProfileIn( __METHOD__ );
 
-		$text = false;
+		$content = false;
 
 		// For message page not locally set, use the i18n message.
 		// For other non-existent articles, use preload text if any.
 		if ( !$this->mTitle->exists() || $this->section == 'new' ) {
 			if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
 				# If this is a system message, get the default text.
-				$text = $this->mTitle->getDefaultMessageText();
+				$msg = $this->mTitle->getDefaultMessageText();
+
+				$content = new WikitextContent($msg); //XXX: really hardcode wikitext here?
 			}
-			if ( $text === false ) {
+			if ( $content === false ) {
 				# If requested, preload some text.
 				$preload = $wgRequest->getVal( 'preload',
 					// Custom preload text for new sections
 					$this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
-				$text = $this->getPreloadedText( $preload );
+
+				$content = $this->getPreloadedContent( $preload );
 			}
 		// For existing pages, get text based on "undo" or section parameters.
 		} else {
 			if ( $this->section != '' ) {
 				// Get section edit text (returns $def_text for invalid sections)
-				$text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text );
+				$orig = $this->getOriginalContent();
+				$content = $orig ? $orig->getSection( $this->section ) : null;
+
+				if ( !$content ) $content = $def_content;
 			} else {
 				$undoafter = $wgRequest->getInt( 'undoafter' );
 				$undo = $wgRequest->getInt( 'undo' );
@@ -802,15 +843,16 @@ class EditPage {
 
 					# Sanity check, make sure it's the right page,
 					# the revisions exist and they were not deleted.
-					# Otherwise, $text will be left as-is.
+					# Otherwise, $content will be left as-is.
 					if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
 						$undorev->getPage() == $oldrev->getPage() &&
 						$undorev->getPage() == $this->mTitle->getArticleID() &&
 						!$undorev->isDeleted( Revision::DELETED_TEXT ) &&
 						!$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
 
-						$text = $this->mArticle->getUndoText( $undorev, $oldrev );
-						if ( $text === false ) {
+						$content = $this->mArticle->getUndoContent( $undorev, $oldrev );
+
+						if ( $content === false ) {
 							# Warn the user that something went wrong
 							$undoMsg = 'failure';
 						} else {
@@ -842,14 +884,14 @@ class EditPage {
 						wfMsgNoTrans( 'undo-' . $undoMsg ) . '', true, /* interface */true );
 				}
 
-				if ( $text === false ) {
-					$text = $this->getOriginalContent();
+				if ( $content === false ) {
+					$content = $this->getOriginalContent();
 				}
 			}
 		}
 
 		wfProfileOut( __METHOD__ );
-		return $text;
+		return $content;
 	}
 
 	/**
@@ -866,31 +908,45 @@ class EditPage {
 	 * @since 1.19
 	 * @return string
 	 */
-	private function getOriginalContent() {
+	private function getOriginalContent() { #FIXME: use Content! set content_model and content_format!
 		if ( $this->section == 'new' ) {
-			return $this->getCurrentText();
+			return $this->getCurrentContent();
 		}
 		$revision = $this->mArticle->getRevisionFetched();
 		if ( $revision === null ) {
-			return '';
+			if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModelName();
+			$handler = ContentHandler::getForModelName( $this->content_model );
+
+			return $handler->emptyContent();
 		}
-		return $this->mArticle->getContent();
+
+		$content = $this->mArticle->getContentObject();
+		return $content;
 	}
 
 	/**
-	 * Get the actual text of the page. This is basically similar to
-	 * WikiPage::getRawText() except that when the page doesn't exist an empty
-	 * string is returned instead of false.
+	 * Get the current content of the page. This is basically similar to
+	 * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
+	 * content object is returned instead of null.
 	 *
-	 * @since 1.19
+	 * @since 1.20
 	 * @return string
 	 */
-	private function getCurrentText() {
-		$text = $this->mArticle->getRawText();
-		if ( $text === false ) {
-			return '';
+	private function getCurrentContent() {
+		$rev = $this->mArticle->getRevision();
+		$content = $rev ? $rev->getContent( Revision::RAW ) : null;
+
+		if ( $content  === false || $content === null ) {
+			if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModelName();
+			$handler = ContentHandler::getForModelName( $this->content_model );
+
+			return $handler->emptyContent();
 		} else {
-			return $text;
+			#FIXME: nasty side-effect!
+			$this->content_model = $rev->getContentModelName();
+			$this->content_format = $rev->getContentFormat();
+
+			return $content;
 		}
 	}
 
@@ -898,9 +954,23 @@ class EditPage {
 	 * Use this method before edit() to preload some text into the edit box
 	 *
 	 * @param $text string
+	 * @deprecated since 1.20
+	 */
+	public function setPreloadedText( $text ) { #FIXME: deprecated, use setPreloadedContent()
+		wfDeprecated( __METHOD__, "1.20" );
+
+		$content = ContentHandler::makeContent( $text, $this->getTitle() );
+
+		$this->setPreloadedContent( $content );
+	}
+
+	/**
+	 * Use this method before edit() to preload some content into the edit box
+	 *
+	 * @param $content Content
 	 */
-	public function setPreloadedText( $text ) {
-		$this->mPreloadText = $text;
+	public function setPreloadedContent( Content $content ) { #FIXME: use this!
+		$this->mPreloadedContent = $content;
 	}
 
 	/**
@@ -909,22 +979,34 @@ class EditPage {
 	 *
 	 * @param $preload String: representing the title to preload from.
 	 * @return String
+	 * @deprecated since 1.20
 	 */
-	protected function getPreloadedText( $preload ) {
-		global $wgUser, $wgParser;
+	protected function getPreloadedText( $preload ) { #FIXME: B/C only, replace usage!
+		wfDeprecated( __METHOD__, "1.20" );
 
-		if ( !empty( $this->mPreloadText ) ) {
-			return $this->mPreloadText;
+		$content = $this->getPreloadedContent( $preload );
+		$text = $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?!
+
+		return $text;
+	}
+
+	protected function getPreloadedContent( $preload ) { #FIXME: use this!
+		global $wgUser;
+
+		if ( !empty( $this->mPreloadContent ) ) {
+			return $this->mPreloadContent;
 		}
 
+		$handler = ContentHandler::getForTitle( $this->getTitle() );
+
 		if ( $preload === '' ) {
-			return '';
+			return $handler->emptyContent();
 		}
 
 		$title = Title::newFromText( $preload );
 		# Check for existence to avoid getting MediaWiki:Noarticletext
 		if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
-			return '';
+			return $handler->emptyContent();
 		}
 
 		$page = WikiPage::factory( $title );
@@ -932,13 +1014,15 @@ class EditPage {
 			$title = $page->getRedirectTarget();
 			# Same as before
 			if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
-				return '';
+				return $handler->emptyContent();
 			}
 			$page = WikiPage::factory( $title );
 		}
 
 		$parserOptions = ParserOptions::newFromUser( $wgUser );
-		return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions );
+		$content = $page->getContent( Revision::RAW );
+
+		return $content->preloadTransform( $title, $parserOptions );
 	}
 
 	/**
@@ -988,6 +1072,11 @@ class EditPage {
 			case self::AS_FILTERING:
 				return false;
 
+			case self::AS_PARSE_ERROR:
+				$wgOut->addWikiText( '
' . $status->getWikiText() . '
'); + #FIXME: cause editform to be shown again, not just an error! + return false; + case self::AS_SUCCESS_NEW_ARTICLE: $query = $resultDetails['redirect'] ? 'redirect=no' : ''; $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : ''; @@ -1076,7 +1165,7 @@ class EditPage { # Check image redirect if ( $this->mTitle->getNamespace() == NS_FILE && - Title::newFromRedirect( $this->textbox1 ) instanceof Title && + Title::newFromRedirect( $this->textbox1 ) instanceof Title && #FIXME: use content handler to check for redirect !$wgUser->isAllowed( 'upload' ) ) { $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; $status->setResult( false, $code ); @@ -1192,38 +1281,52 @@ class EditPage { $aid = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE ); $new = ( $aid == 0 ); - if ( $new ) { - // Late check for create permission, just in case *PARANOIA* - if ( !$this->mTitle->userCan( 'create' ) ) { - $status->fatal( 'nocreatetext' ); - $status->value = self::AS_NO_CREATE_PERMISSION; - wfDebug( __METHOD__ . ": no create permission\n" ); - wfProfileOut( __METHOD__ ); - return $status; - } + try { + if ( $new ) { + // Late check for create permission, just in case *PARANOIA* + if ( !$this->mTitle->userCan( 'create' ) ) { + $status->fatal( 'nocreatetext' ); + $status->value = self::AS_NO_CREATE_PERMISSION; + wfDebug( __METHOD__ . ": no create permission\n" ); + wfProfileOut( __METHOD__ ); + return $status; + } - # Don't save a new article if it's blank. - if ( $this->textbox1 == '' ) { - $status->setResult( false, self::AS_BLANK_ARTICLE ); - wfProfileOut( __METHOD__ ); - return $status; - } + # Don't save a new article if it's blank. + if ( $this->textbox1 == '' ) { + $status->setResult( false, self::AS_BLANK_ARTICLE ); + wfProfileOut( __METHOD__ ); + return $status; + } - // Run post-section-merge edit filter - if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) { - # Error messages etc. could be handled within the hook... - $status->fatal( 'hookaborted' ); - $status->value = self::AS_HOOK_ERROR; - wfProfileOut( __METHOD__ ); - return $status; - } elseif ( $this->hookError != '' ) { - # ...or the hook could be expecting us to produce an error - $status->fatal( 'hookaborted' ); - $status->value = self::AS_HOOK_ERROR_EXPECTED; - wfProfileOut( __METHOD__ ); - return $status; - } +<<<<<<< HEAD + // Run post-section-merge edit filter + if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) { + # Error messages etc. could be handled within the hook... + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; + wfProfileOut( __METHOD__ ); + return $status; + } elseif ( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; + wfProfileOut( __METHOD__ ); + return $status; + } + + $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); + # Handle the user preference to force summaries here. Check if it's not a redirect. + if ( !$this->allowBlankSummary && !$content->isRedirect() ) { + if ( md5( $this->summary ) == $this->autoSumm ) { + $this->missingSummary = true; + $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh + $status->value = self::AS_SUMMARY_NEEDED; + wfProfileOut( __METHOD__ ); + return $status; + } +======= $text = $this->textbox1; $result['sectionanchor'] = ''; if ( $this->section == 'new' ) { @@ -1251,39 +1354,80 @@ class EditPage { // Create a link to the new section from the edit summary. $cleanSummary = $wgParser->stripSectionName( $this->summary ); $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); +>>>>>>> master } - } - $status->value = self::AS_SUCCESS_NEW_ARTICLE; + $result['sectionanchor'] = ''; + if ( $this->section == 'new' ) { + if ( $this->sectiontitle !== '' ) { + // Insert the section title above the content. + $content = $content->addSectionHeader( $this->sectiontitle ); + + // Jump to the new section + $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); + + // If no edit summary was specified, create one automatically from the section + // title and have it link to the new section. Otherwise, respect the summary as + // passed. + if ( $this->summary === '' ) { + $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle ); + } + } elseif ( $this->summary !== '' ) { + // Insert the section title above the content. + $content = $content->addSectionHeader( $this->sectiontitle ); + + // Jump to the new section + $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); - } else { + // Create a link to the new section from the edit summary. + $cleanSummary = $wgParser->stripSectionName( $this->summary ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); + } + } - # Article exists. Check for edit conflict. + $status->value = self::AS_SUCCESS_NEW_ARTICLE; - $this->mArticle->clear(); # Force reload of dates, etc. - $timestamp = $this->mArticle->getTimestamp(); + } else { - wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" ); + # Article exists. Check for edit conflict. - if ( $timestamp != $this->edittime ) { - $this->isConflict = true; - if ( $this->section == 'new' ) { - if ( $this->mArticle->getUserText() == $wgUser->getName() && - $this->mArticle->getComment() == $this->summary ) { - // Probably a duplicate submission of a new comment. - // This can happen when squid resends a request after - // a timeout but the first one actually went through. - wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" ); - } else { - // New comment; suppress conflict. + $this->mArticle->clear(); # Force reload of dates, etc. + $timestamp = $this->mArticle->getTimestamp(); + + wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" ); + + if ( $timestamp != $this->edittime ) { + $this->isConflict = true; + if ( $this->section == 'new' ) { + if ( $this->mArticle->getUserText() == $wgUser->getName() && + $this->mArticle->getComment() == $this->summary ) { + // Probably a duplicate submission of a new comment. + // This can happen when squid resends a request after + // a timeout but the first one actually went through. + wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" ); + } else { + // New comment; suppress conflict. + $this->isConflict = false; + wfDebug( __METHOD__ .": conflict suppressed; new section\n" ); + } + } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) { + # Suppress edit conflict with self, except for section edits where merging is required. + wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); $this->isConflict = false; +<<<<<<< HEAD +======= wfDebug( __METHOD__ . ": conflict suppressed; new section\n" ); +>>>>>>> master } - } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) { - # Suppress edit conflict with self, except for section edits where merging is required. - wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); - $this->isConflict = false; } +<<<<<<< HEAD + + // If sectiontitle is set, use it, otherwise use the summary as the section title (for + // backwards compatibility with old forms/bots). + if ( $this->sectiontitle !== '' ) { + $sectionTitle = $this->sectiontitle; +======= } // If sectiontitle is set, use it, otherwise use the summary as the section title (for @@ -1311,137 +1455,173 @@ class EditPage { // Successful merge! Maybe we should tell the user the good news? $this->isConflict = false; wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" ); +>>>>>>> master } else { - $this->section = ''; - $this->textbox1 = $text; - wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" ); + $sectionTitle = $this->summary; } - } - if ( $this->isConflict ) { - $status->setResult( false, self::AS_CONFLICT_DETECTED ); - wfProfileOut( __METHOD__ ); - return $status; - } + $textbox_content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); + $content = false; - // Run post-section-merge edit filter - if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) { - # Error messages etc. could be handled within the hook... - $status->fatal( 'hookaborted' ); - $status->value = self::AS_HOOK_ERROR; - wfProfileOut( __METHOD__ ); - return $status; - } elseif ( $this->hookError != '' ) { - # ...or the hook could be expecting us to produce an error - $status->fatal( 'hookaborted' ); - $status->value = self::AS_HOOK_ERROR_EXPECTED; - wfProfileOut( __METHOD__ ); - return $status; - } + if ( $this->isConflict ) { + wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" ); - # Handle the user preference to force summaries here, but not for null edits - if ( $this->section != 'new' && !$this->allowBlankSummary - && $this->getOriginalContent() != $text - && !Title::newFromRedirect( $text ) ) # check if it's not a redirect - { - if ( md5( $this->summary ) == $this->autoSumm ) { - $this->missingSummary = true; - $status->fatal( 'missingsummary' ); - $status->value = self::AS_SUMMARY_NEEDED; - wfProfileOut( __METHOD__ ); - return $status; + $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime ); + } else { + wfDebug( __METHOD__ . ": getting section '$this->section'\n" ); + + $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle ); } - } - # And a similar thing for new sections - if ( $this->section == 'new' && !$this->allowBlankSummary ) { - if ( trim( $this->summary ) == '' ) { - $this->missingSummary = true; - $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh - $status->value = self::AS_SUMMARY_NEEDED; + if ( is_null( $content ) ) { + wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); + $this->isConflict = true; + $content = $textbox_content; // do not try to merge here! + } elseif ( $this->isConflict ) { + # Attempt merge + if ( $this->mergeChangesIntoContent( $textbox_content ) ) { + // Successful merge! Maybe we should tell the user the good news? + $content = $textbox_content; + $this->isConflict = false; + wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" ); + } else { + $this->section = ''; + #$this->textbox1 = $text; #redundant, nothing to do here? + wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" ); + } + } + + if ( $this->isConflict ) { + $status->setResult( false, self::AS_CONFLICT_DETECTED ); wfProfileOut( __METHOD__ ); return $status; } - } - # All's well - wfProfileIn( __METHOD__ . '-sectionanchor' ); - $sectionanchor = ''; - if ( $this->section == 'new' ) { - if ( $this->textbox1 == '' ) { - $this->missingComment = true; - $status->fatal( 'missingcommenttext' ); - $status->value = self::AS_TEXTBOX_EMPTY; - wfProfileOut( __METHOD__ . '-sectionanchor' ); + // Run post-section-merge edit filter + if ( !wfRunHooks( 'EditFilterMerged', array( $this, $content->serialize( $this->content_format ), &$this->hookError, $this->summary ) ) + || !wfRunHooks( 'EditFilterMergedContent', array( $this, $content, &$this->hookError, $this->summary ) ) ) { #FIXME: document new hook + # Error messages etc. could be handled within the hook... + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; + wfProfileOut( __METHOD__ ); + return $status; + } elseif ( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; wfProfileOut( __METHOD__ ); return $status; } - if ( $this->sectiontitle !== '' ) { - $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); - // If no edit summary was specified, create one automatically from the section - // title and have it link to the new section. Otherwise, respect the summary as - // passed. - if ( $this->summary === '' ) { - $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); - $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle ); + + # Handle the user preference to force summaries here, but not for null edits + if ( $this->section != 'new' && !$this->allowBlankSummary + && !$content->equals( $this->getOriginalContent() ) + && !$content->isRedirect() ) # check if it's not a redirect + { + if ( md5( $this->summary ) == $this->autoSumm ) { + $this->missingSummary = true; + $status->fatal( 'missingsummary' ); + $status->value = self::AS_SUMMARY_NEEDED; + wfProfileOut( __METHOD__ ); + return $status; } - } elseif ( $this->summary !== '' ) { - $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); - # This is a new section, so create a link to the new section - # in the revision summary. - $cleanSummary = $wgParser->stripSectionName( $this->summary ); - $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); } - } elseif ( $this->section != '' ) { - # Try to get a section anchor from the section source, redirect to edited section if header found - # XXX: might be better to integrate this into Article::replaceSection - # for duplicate heading checking and maybe parsing - $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); - # we can't deal with anchors, includes, html etc in the header for now, - # headline would need to be parsed to improve this - if ( $hasmatch && strlen( $matches[2] ) > 0 ) { - $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] ); + + # And a similar thing for new sections + if ( $this->section == 'new' && !$this->allowBlankSummary ) { + if ( trim( $this->summary ) == '' ) { + $this->missingSummary = true; + $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh + $status->value = self::AS_SUMMARY_NEEDED; + wfProfileOut( __METHOD__ ); + return $status; + } } - } - $result['sectionanchor'] = $sectionanchor; - wfProfileOut( __METHOD__ . '-sectionanchor' ); - // Save errors may fall down to the edit form, but we've now - // merged the section into full text. Clear the section field - // so that later submission of conflict forms won't try to - // replace that into a duplicated mess. - $this->textbox1 = $text; - $this->section = ''; + # All's well + wfProfileIn( __METHOD__ . '-sectionanchor' ); + $sectionanchor = ''; + if ( $this->section == 'new' ) { + if ( $this->textbox1 == '' ) { + $this->missingComment = true; + $status->fatal( 'missingcommenttext' ); + $status->value = self::AS_TEXTBOX_EMPTY; + wfProfileOut( __METHOD__ . '-sectionanchor' ); + wfProfileOut( __METHOD__ ); + return $status; + } + if ( $this->sectiontitle !== '' ) { + $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle ); + // If no edit summary was specified, create one automatically from the section + // title and have it link to the new section. Otherwise, respect the summary as + // passed. + if ( $this->summary === '' ) { + $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle ); + } + } elseif ( $this->summary !== '' ) { + $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); + # This is a new section, so create a link to the new section + # in the revision summary. + $cleanSummary = $wgParser->stripSectionName( $this->summary ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); + } + } elseif ( $this->section != '' ) { + # Try to get a section anchor from the section source, redirect to edited section if header found + # XXX: might be better to integrate this into Article::replaceSection + # for duplicate heading checking and maybe parsing + $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); + # we can't deal with anchors, includes, html etc in the header for now, + # headline would need to be parsed to improve this + if ( $hasmatch && strlen( $matches[2] ) > 0 ) { + $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] ); + } + } + $result['sectionanchor'] = $sectionanchor; + wfProfileOut( __METHOD__ . '-sectionanchor' ); - $status->value = self::AS_SUCCESS_UPDATE; - } + // Save errors may fall down to the edit form, but we've now + // merged the section into full text. Clear the section field + // so that later submission of conflict forms won't try to + // replace that into a duplicated mess. + $this->textbox1 = $content->serialize( $this->content_format ); + $this->section = ''; - // Check for length errors again now that the section is merged in - $this->kblength = (int)( strlen( $text ) / 1024 ); - if ( $this->kblength > $wgMaxArticleSize ) { - $this->tooBig = true; - $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED ); - wfProfileOut( __METHOD__ ); - return $status; - } + $status->value = self::AS_SUCCESS_UPDATE; + } + + // Check for length errors again now that the section is merged in + $this->kblength = (int)( strlen( $content->serialize( $this->content_format ) ) / 1024 ); + if ( $this->kblength > $wgMaxArticleSize ) { + $this->tooBig = true; + $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED ); + wfProfileOut( __METHOD__ ); + return $status; + } - $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | - ( $new ? EDIT_NEW : EDIT_UPDATE ) | - ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) | - ( $bot ? EDIT_FORCE_BOT : 0 ); + $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | + ( $new ? EDIT_NEW : EDIT_UPDATE ) | + ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) | + ( $bot ? EDIT_FORCE_BOT : 0 ); - $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags ); + $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, false, null, $this->content_format ); - if ( $doEditStatus->isOK() ) { - $result['redirect'] = Title::newFromRedirect( $text ) !== null; - $this->commitWatch(); + if ( $doEditStatus->isOK() ) { + $result['redirect'] = $content->isRedirect(); + $this->commitWatch(); + wfProfileOut( __METHOD__ ); + return $status; + } else { + $this->isConflict = true; + $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares + wfProfileOut( __METHOD__ ); + return $doEditStatus; + } + } catch (MWContentSerializationException $ex) { + $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() ); + $status->value = self::AS_PARSE_ERROR; wfProfileOut( __METHOD__ ); return $status; - } else { - $this->isConflict = true; - $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares - wfProfileOut( __METHOD__ ); - return $doEditStatus; } } @@ -1498,8 +1678,33 @@ class EditPage { * @parma $editText string * * @return bool + * @deprecated since 1.20 + */ + function mergeChangesInto( &$editText ){ + wfDebug( __METHOD__, "1.20" ); + + $editContent = ContentHandler::makeContent( $editText, $this->getTitle(), $this->content_model, $this->content_format ); + + $ok = $this->mergeChangesIntoContent( $editContent ); + + if ( $ok ) { + $editText = $editContent->serialize( $this->content_format ); #XXX: really serialize?! + return true; + } else { + return false; + } + } + + /** + * @private + * @todo document + * + * @parma $editText string + * + * @return bool + * @since since 1.20 */ - function mergeChangesInto( &$editText ) { + private function mergeChangesIntoContent( &$editContent ){ wfProfileIn( __METHOD__ ); $db = wfGetDB( DB_MASTER ); @@ -1510,7 +1715,7 @@ class EditPage { wfProfileOut( __METHOD__ ); return false; } - $baseText = $baseRevision->getText(); + $baseContent = $baseRevision->getContent(); // The current state, we want to merge updates into it $currentRevision = Revision::loadFromTitle( $db, $this->mTitle ); @@ -1518,11 +1723,14 @@ class EditPage { wfProfileOut( __METHOD__ ); return false; } - $currentText = $currentRevision->getText(); + $currentContent = $currentRevision->getContent(); - $result = ''; - if ( wfMerge( $baseText, $editText, $currentText, $result ) ) { - $editText = $result; + $handler = ContentHandler::getForModelName( $baseContent->getModelName() ); + + $result = $handler->merge3( $baseContent, $editContent, $currentContent ); + + if ( $result ) { + $editContent = $result; wfProfileOut( __METHOD__ ); return true; } else { @@ -1609,13 +1817,13 @@ class EditPage { } elseif ( $contextTitle->exists() && $this->section != '' ) { $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection'; } else { - $msg = $contextTitle->exists() || ( $contextTitle->getNamespace() == NS_MEDIAWIKI && $contextTitle->getDefaultMessageText() !== false ) ? - 'editing' : 'creating'; + $msg = $contextTitle->exists() || ( $contextTitle->getNamespace() == NS_MEDIAWIKI && $contextTitle->getDefaultMessageText() !== false ) ? + 'editing' : 'creating'; } # Use the title defined by DISPLAYTITLE magic word when present - $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false; - if ( $displayTitle === false ) { - $displayTitle = $contextTitle->getPrefixedText(); + $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false; + if ( $displayTitle === false ) { + $displayTitle = $contextTitle->getPrefixedText(); } $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) ); } @@ -1769,6 +1977,7 @@ class EditPage { } } + #FIXME: add EditForm plugin interface and use it here! #FIXME: search for textarea1 and textares2, and allow EditForm to override all uses. $wgOut->addHTML( Html::openElement( 'form', array( 'id' => 'editform', 'name' => 'editform', 'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ), 'enctype' => 'multipart/form-data' ) ) ); @@ -1829,6 +2038,9 @@ class EditPage { $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) ); + $wgOut->addHTML( Html::hidden( 'format', $this->content_format ) ); + $wgOut->addHTML( Html::hidden( 'model', $this->content_model ) ); + if ( $this->section == 'new' ) { $this->showSummaryInput( true, $this->summary ); $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) ); @@ -1846,7 +2058,9 @@ class EditPage { // resolved between page source edits and custom ui edits using the // custom edit ui. $this->textbox2 = $this->textbox1; - $this->textbox1 = $this->getCurrentText(); + + $content = $this->getCurrentContent(); + $this->textbox1 = $content->serialize( $this->content_format ); $this->showTextbox1(); } else { @@ -2254,7 +2468,7 @@ HTML $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) ); } - protected function showTextbox( $content, $name, $customAttribs = array() ) { + protected function showTextbox( $text, $name, $customAttribs = array() ) { global $wgOut, $wgUser; $wikitext = $this->safeUnicodeOutput( $content ); @@ -2333,8 +2547,16 @@ HTML * save and then make a comparison. */ function showDiff() { - global $wgUser, $wgContLang, $wgParser, $wgOut; + global $wgUser, $wgContLang, $wgOut; + + $oldContent = $this->getOriginalContent(); + + $textboxContent = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), + $this->content_model, $this->content_format ); #XXX: handle parse errors ? + $newContent = $this->mArticle->replaceSectionContent( + $this->section, $textboxContent, + $this->summary, $this->edittime ); $oldtitlemsg = 'currentrev'; # if message does not exist, show diff against the preloaded default if( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) { @@ -2348,17 +2570,30 @@ HTML $newtext = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); + # hanlde legacy text-based hook + $newtext_orig = $newContent->serialize( $this->content_format ); + $newtext = $newtext_orig; #clone wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) ); + if ( $newtext != $newtext_orig ) { + #if the hook changed the text, create a new Content object accordingly. + $newContent = ContentHandler::makeContent( $newtext, $this->getTitle(), $newContent->getModelName() ); #XXX: handle parse errors ? + } + + wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) ); #FIXME: document new hook + $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang ); - $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts ); + $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts ); + if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) { + $oldtitle = wfMsgExt( 'currentrev', array( 'parseinline' ) ); if ( $oldtext !== false || $newtext != '' ) { $oldtitle = wfMsgExt( $oldtitlemsg, array( 'parseinline' ) ); $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) ); - $de = new DifferenceEngine( $this->mArticle->getContext() ); - $de->setText( $oldtext, $newtext ); + $de = $oldContent->getContentHandler()->getDifferenceEngine( $this->mArticle->getContext() ); + $de->setContent( $oldContent, $newContent ); + $difftext = $de->getDiff( $oldtitle, $newtitle ); $de->showDiffStyle(); } else { @@ -2448,8 +2683,12 @@ HTML if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) { $wgOut->wrapWikiMsg( '

$1

', "yourdiff" ); - $de = new DifferenceEngine( $this->mArticle->getContext() ); - $de->setText( $this->textbox2, $this->textbox1 ); + $content1 = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors? + $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors? + + $handler = ContentHandler::getForModelName( $this->content_model ); + $de = $handler->getDifferenceEngine( $this->mArticle->getContext() ); + $de->setContent( $content2, $content1 ); $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) ); $wgOut->wrapWikiMsg( '

$1

', "yourtext" ); @@ -2569,6 +2808,95 @@ HTML return $parsedNote; } + try { + $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); + + if ( $this->mTriedSave && !$this->mTokenOk ) { + if ( $this->mTokenOkExceptSuffix ) { + $note = wfMsg( 'token_suffix_mismatch' ); + } else { + $note = wfMsg( 'session_fail_preview' ); + } + } elseif ( $this->incompleteForm ) { + $note = wfMsg( 'edit_form_incomplete' ); + } elseif ( $this->isCssJsSubpage || $this->mTitle->isCssOrJsPage() ) { + # if this is a CSS or JS page used in the UI, show a special notice + # XXX: stupid php bug won't let us use $this->getContextTitle()->isCssJsSubpage() here -- This note has been there since r3530. Sure the bug was fixed time ago? + + if( $this->mTitle->isCssJsSubpage() ) { + $level = 'user'; + } elseif( $this->mTitle->isCssOrJsPage() ) { + $level = 'site'; + } else { + $level = false; + } + + if ( $content->getModelName() == CONTENT_MODEL_CSS ) { + $format = 'css'; + } elseif ( $content->getModelName() == CONTENT_MODEL_JAVASCRIPT ) { + $format = 'js'; + } else { + $format = false; + } + + # Used messages to make sure grep find them: + # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview + if( $level && $format ) { + $note = "
" . wfMsg( "{$level}{$format}preview" ) . "
"; + } else { + $note = wfMsg( 'previewnote' ); + } + } else { + $note = wfMsg( 'previewnote' ); + } + + $parserOptions = ParserOptions::newFromUser( $wgUser ); + $parserOptions->setEditSection( false ); + $parserOptions->setTidy( true ); + $parserOptions->setIsPreview( true ); + $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' ); + + $rt = $content->getRedirectChain(); + + if ( $rt ) { + $previewHTML = $this->mArticle->viewRedirect( $rt, false ); + } else { + + # If we're adding a comment, we need to show the + # summary as the headline + if ( $this->section == "new" && $this->summary != "" ) { + $content = $content->addSectionHeader( $this->summary ); + } + + $toparse_orig = $content->serialize( $this->content_format ); + $toparse = $toparse_orig; + wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) ); + + if ( $toparse !== $toparse_orig ) { + #hook changed the text, create new Content object + $content = ContentHandler::makeContent( $toparse, $this->getTitle(), $this->content_model, $this->content_format ); + } + + wfRunHooks( 'EditPageGetPreviewContent', array( $this, &$content ) ); # FIXME: document new hook + + $parserOptions->enableLimitReport(); + + #XXX: For CSS/JS pages, we should have called the ShowRawCssJs hook here. But it's now deprecated, so never mind + $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions ); + $parserOutput = $content->getParserOutput( $this->mTitle, null, $parserOptions ); + + $previewHTML = $parserOutput->getText(); + $this->mParserOutput = $parserOutput; + $wgOut->addParserOutputNoText( $parserOutput ); + + if ( count( $parserOutput->getWarnings() ) ) { + $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); + } + } + } catch (MWContentSerializationException $ex) { + $note .= "\n\n" . wfMsg('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() ); + $previewHTML = ''; + } if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { $note = wfMsg( 'token_suffix_mismatch' ); @@ -3078,7 +3406,14 @@ HTML $wgOut->addHTML( '' ); $wgOut->wrapWikiMsg( '

$1

', "yourdiff" ); - $this->showDiff(); + + $handler = ContentHandler::getForTitle( $this->getTitle() ); + $de = $handler->getDifferenceEngine( $this->mArticle->getContext() ); + + $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors? + $de->setContent( $this->getCurrentContent(), $content2 ); + + $de->showDiff( wfMsg( "storedversion" ), wfMsgExt( 'yourtext', 'parseinline' ) ); $wgOut->wrapWikiMsg( '

$1

', "yourtext" ); $this->showTextbox2(); @@ -3234,6 +3569,8 @@ HTML // breaks one of the entities whilst editing. if ( ( substr( $invalue, $i, 1 ) == ";" ) and ( strlen( $hexstring ) <= 6 ) ) { $codepoint = hexdec( $hexstring ); + if ( (substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6) ) { + $codepoint = hexdec($hexstring); $result .= codepointToUtf8( $codepoint ); } else { $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 ); diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php index d280db5b39..bb474399a7 100644 --- a/includes/FeedUtils.php +++ b/includes/FeedUtils.php @@ -117,7 +117,8 @@ class FeedUtils { $diffText = ''; // Don't bother generating the diff if we won't be able to show it if ( $wgFeedDiffCutoff > 0 ) { - $de = new DifferenceEngine( $title, $oldid, $newid ); + $contentHandler = ContentHandler::getForTitle( $title ); + $de = $contentHandler->getDifferenceEngine( $title, $oldid, $newid ); $diffText = $de->getDiff( wfMsg( 'previousrevision' ), // hack wfMsg( 'revisionasof', diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 7453baac5a..5755bdc18f 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -133,7 +133,9 @@ class ImagePage extends Article { $wgOut->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content', 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(), 'class' => 'mw-content-'.$pageLang->getDir() ) ) ); - parent::view(); + + parent::view(); #FIXME: use ContentHandler::makeArticle() !! + $wgOut->addHTML( Xml::closeElement( 'div' ) ); } else { # Just need to set the right headers @@ -244,20 +246,20 @@ class ImagePage extends Article { return $r; } - /** - * Overloading Article's getContent method. - * - * Omit noarticletext if sharedupload; text will be fetched from the - * shared upload server if possible. - * @return string - */ - public function getContent() { - $this->loadFile(); - if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) { - return ''; - } - return parent::getContent(); - } + /** + * Overloading Article's getContentObject method. + * + * Omit noarticletext if sharedupload; text will be fetched from the + * shared upload server if possible. + * @return string + */ + public function getContentObject() { + $this->loadFile(); + if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) { + return null; + } + return parent::getContentObject(); + } protected function openShowImage() { global $wgOut, $wgUser, $wgImageLimits, $wgRequest, diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php index 716e7d8072..1973d95943 100644 --- a/includes/LinksUpdate.php +++ b/includes/LinksUpdate.php @@ -19,23 +19,21 @@ * * @todo document (e.g. one-sentence top-level class description). */ -class LinksUpdate { +class LinksUpdate extends SecondaryDBDataUpdate { /**@{{ * @private */ var $mId, //!< Page ID of the article linked from - $mTitle, //!< Title object of the article linked from - $mParserOutput, //!< Parser output - $mLinks, //!< Map of title strings to IDs for the links in the document + $mTitle, //!< Title object of the article linked from + $mParserOutput, //!< Whether to queue jobs for recursive update + $mLinks, //!< Map of title strings to IDs for the links in the document $mImages, //!< DB keys of the images used, in the array key only $mTemplates, //!< Map of title strings to IDs for the template references, including broken ones $mExternals, //!< URLs of external links, array key only $mCategories, //!< Map of category names to sort keys $mInterlangs, //!< Map of language codes to titles $mProperties, //!< Map of arbitrary name to value - $mDb, //!< Database connection reference - $mOptions, //!< SELECT options to be used (array) $mRecursive; //!< Whether to queue jobs for recursive updates /**@}}*/ @@ -47,23 +45,22 @@ class LinksUpdate { * @param $recursive Boolean: queue jobs for recursive updates? */ function __construct( $title, $parserOutput, $recursive = true ) { - global $wgAntiLockFlags; + parent::__construct( ); - if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) { - $this->mOptions = array(); - } else { - $this->mOptions = array( 'FOR UPDATE' ); - } - $this->mDb = wfGetDB( DB_MASTER ); + if ( !is_object( $title ) ) { + throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " . + "Please see Article::editUpdates() for an invocation example.\n" ); + } - if ( !is_object( $title ) ) { - throw new MWException( "The calling convention to LinksUpdate::__construct() has changed. " . + if ( !is_object( $title ) ) { + throw new MWException( "The calling convention to LinksUpdate::__construct() has changed. " . "Please see WikiPage::doEditUpdates() for an invocation example.\n" ); - } - $this->mTitle = $title; - $this->mId = $title->getArticleID(); + } + $this->mTitle = $title; + $this->mId = $title->getArticleID(); + + $this->mParserOutput = $parserOutput; - $this->mParserOutput = $parserOutput; $this->mLinks = $parserOutput->getLinks(); $this->mImages = $parserOutput->getImages(); $this->mTemplates = $parserOutput->getTemplates(); @@ -253,51 +250,6 @@ class LinksUpdate { wfProfileOut( __METHOD__ ); } - /** - * Invalidate the cache of a list of pages from a single namespace - * - * @param $namespace Integer - * @param $dbkeys Array - */ - function invalidatePages( $namespace, $dbkeys ) { - if ( !count( $dbkeys ) ) { - return; - } - - /** - * Determine which pages need to be updated - * This is necessary to prevent the job queue from smashing the DB with - * large numbers of concurrent invalidations of the same page - */ - $now = $this->mDb->timestamp(); - $ids = array(); - $res = $this->mDb->select( 'page', array( 'page_id' ), - array( - 'page_namespace' => $namespace, - 'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')', - 'page_touched < ' . $this->mDb->addQuotes( $now ) - ), __METHOD__ - ); - foreach ( $res as $row ) { - $ids[] = $row->page_id; - } - if ( !count( $ids ) ) { - return; - } - - /** - * Do the update - * We still need the page_touched condition, in case the row has changed since - * the non-locking select above. - */ - $this->mDb->update( 'page', array( 'page_touched' => $now ), - array( - 'page_id IN (' . $this->mDb->makeList( $ids ) . ')', - 'page_touched < ' . $this->mDb->addQuotes( $now ) - ), __METHOD__ - ); - } - /** * @param $cats */ @@ -324,20 +276,20 @@ class LinksUpdate { $this->invalidatePages( NS_FILE, array_keys( $images ) ); } - /** - * @param $table - * @param $insertions - * @param $fromField - */ - private function dumbTableUpdate( $table, $insertions, $fromField ) { - $this->mDb->delete( $table, array( $fromField => $this->mId ), __METHOD__ ); - if ( count( $insertions ) ) { - # The link array was constructed without FOR UPDATE, so there may - # be collisions. This may cause minor link table inconsistencies, - # which is better than crippling the site with lock contention. - $this->mDb->insert( $table, $insertions, __METHOD__, array( 'IGNORE' ) ); - } - } + /** + * @param $table + * @param $insertions + * @param $fromField + */ + private function dumbTableUpdate( $table, $insertions, $fromField ) { + $this->mDb->delete( $table, array( $fromField => $this->mId ), __METHOD__ ); + if ( count( $insertions ) ) { + # The link array was constructed without FOR UPDATE, so there may + # be collisions. This may cause minor link table inconsistencies, + # which is better than crippling the site with lock contention. + $this->mDb->insert( $table, $insertions, __METHOD__, array( 'IGNORE' ) ); + } + } /** * Update a table by doing a delete query then an insert query @@ -803,22 +755,22 @@ class LinksUpdate { return $arr; } - /** - * Return the title object of the page being updated - * @return Title - */ - public function getTitle() { - return $this->mTitle; - } - - /** - * Returns parser output - * @since 1.19 - * @return ParserOutput - */ - public function getParserOutput() { - return $this->mParserOutput; - } + /** + * Return the title object of the page being updated + * @return Title + */ + public function getTitle() { + return $this->mTitle; + } + + /** + * Returns parser output + * @since 1.19 + * @return ParserOutput + */ + public function getParserOutput() { + return $this->mParserOutput; + } /** * Return the list of images used as generated by the parser diff --git a/includes/Revision.php b/includes/Revision.php index 1147e6aead..07ae586f35 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -20,6 +20,10 @@ class Revision { protected $mTextRow; protected $mTitle; protected $mCurrent; + protected $mContentModelName; + protected $mContentFormat; + protected $mContent; + protected $mContentHandler; const DELETED_TEXT = 1; const DELETED_COMMENT = 2; @@ -125,6 +129,8 @@ class Revision { 'deleted' => $row->ar_deleted, 'len' => $row->ar_len, 'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null, + 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null, + 'content_format' => isset( $row->ar_content_format ) ? $row->ar_content_format : null, ); if ( isset( $row->ar_text ) && !$row->ar_text_id ) { // Pre-1.5 ar_text row @@ -336,7 +342,9 @@ class Revision { 'rev_deleted', 'rev_len', 'rev_parent_id', - 'rev_sha1' + 'rev_sha1', + 'rev_content_format', + 'rev_content_model' ); } @@ -416,6 +424,18 @@ class Revision { $this->mTitle = null; } + if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) { + $this->mContentModelName = null; # determine on demand if needed + } else { + $this->mContentModelName = 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 ) ) { @@ -437,6 +457,19 @@ class Revision { // 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'] ) ) { + if ( !empty( $row['text_id'] ) ) { #FIXME: when is that set? test with external store setup! check out insertOn() + throw new MWException( "Text already stored in external store (id {$row['text_id']}), can't serialize content object" ); + } + + $row['content_model'] = $row['content']->getModelName(); + # 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; @@ -449,6 +482,9 @@ class Revision { $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null; $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null; + $this->mContentModelName = 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; @@ -458,16 +494,29 @@ class Revision { $this->mCurrent = false; # If we still have no length, see it we have the text to figure it out if ( !$this->mSize ) { + #XXX: my be inconsistent with the notion of "size" use for the present content model $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText ); } # Same for sha1 if ( $this->mSha1 === null ) { $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText ); } + + $this->getContentModelName(); # force lazy init + $this->getContentFormat(); # force lazy init + + # if we have a content object, serialize it, overriding mText + if ( !empty( $row['content'] ) ) { + $handler = $this->getContentHandler(); + $this->mText = $handler->serialize( $row['content'], $this->getContentFormat() ); + } } else { throw new MWException( 'Revision constructor passed invalid row format.' ); } $this->mUnpatrolled = null; + + #FIXME: add patch for ar_content_format, ar_content_model, rev_content_format, rev_content_model to installer + #FIXME: add support for ar_content_format, ar_content_model, rev_content_format, rev_content_model to API } /** @@ -727,17 +776,38 @@ class Revision { * @param $user User object to check for, only if FOR_THIS_USER is passed * to the $audience parameter * @return String + * @deprectaed in 1.20, use getContent() instead */ - public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { - if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) { - return ''; - } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) { - return ''; - } else { - return $this->getRawText(); - } + public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { #FIXME: deprecated, replace usage! #FIXME: used a LOT! + wfDeprecated( __METHOD__, '1.20' ); + + $content = $this->getContent(); + return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable } + /** + * Fetch revision content if it's available to the specified audience. + * If the specified audience does not have the ability to view this + * revision, null will be returned. + * + * @param $audience Integer: one of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * @param $user User object to check for, only if FOR_THIS_USER is passed + * to the $audience parameter + * @return Content + */ + public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) { + if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) { + return null; + } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) { + return null; + } else { + return $this->getContentInternal(); + } + } + /** * Alias for getText(Revision::FOR_THIS_USER) * @@ -754,14 +824,63 @@ class Revision { * * @return String */ - public function getRawText() { - if( is_null( $this->mText ) ) { - // Revision text is immutable. Load on demand: - $this->mText = $this->loadText(); - } - return $this->mText; + public function getRawText() { #FIXME: deprecated, replace usage! + return $this->getText( self::RAW ); } + protected function getContentInternal() { + if( is_null( $this->mContent ) ) { + // Revision is immutable. Load on demand: + + $handler = $this->getContentHandler(); + $format = $this->getContentFormat(); + $title = $this->getTitle(); + + if( is_null( $this->mText ) ) { + // Load text on demand: + $this->mText = $this->loadText(); + } + + $this->mContent = is_null( $this->mText ) ? null : $handler->unserialize( $this->mText, $format ); + } + + return $this->mContent; + } + + public function getContentModelName() { + if ( !$this->mContentModelName ) { + $title = $this->getTitle(); + $this->mContentModelName = ( $title ? $title->getContentModelName() : CONTENT_MODEL_WIKITEXT ); + } + + return $this->mContentModelName; + } + + public function getContentFormat() { + if ( !$this->mContentFormat ) { + $handler = $this->getContentHandler(); + $this->mContentFormat = $handler->getDefaultFormat(); + } + + return $this->mContentFormat; + } + + public function getContentHandler() { + if ( !$this->mContentHandler ) { + $title = $this->getTitle(); + + if ( $title ) $model = $title->getContentModelName(); + else $model = CONTENT_MODEL_WIKITEXT; + + $this->mContentHandler = ContentHandler::getForModelName( $model ); + + #XXX: do we need to verify that mContentHandler supports mContentFormat? + # otherwise, a fixed content format may cause problems on insert. + } + + return $this->mContentHandler; + } + /** * @return String */ @@ -983,26 +1102,29 @@ class Revision { $rev_id = isset( $this->mId ) ? $this->mId : $dbw->nextSequenceValue( 'revision_rev_id_seq' ); - $dbw->insert( 'revision', - array( - 'rev_id' => $rev_id, - 'rev_page' => $this->mPage, - 'rev_text_id' => $this->mTextId, - 'rev_comment' => $this->mComment, - 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0, - 'rev_user' => $this->mUser, - 'rev_user_text' => $this->mUserText, - 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ), - 'rev_deleted' => $this->mDeleted, - 'rev_len' => $this->mSize, - 'rev_parent_id' => is_null( $this->mParentId ) - ? $this->getPreviousRevisionId( $dbw ) - : $this->mParentId, - 'rev_sha1' => is_null( $this->mSha1 ) - ? Revision::base36Sha1( $this->mText ) - : $this->mSha1 - ), __METHOD__ - ); + + $row = array( + 'rev_id' => $rev_id, + 'rev_page' => $this->mPage, + 'rev_text_id' => $this->mTextId, + 'rev_comment' => $this->mComment, + 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0, + 'rev_user' => $this->mUser, + 'rev_user_text' => $this->mUserText, + 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ), + 'rev_deleted' => $this->mDeleted, + 'rev_len' => $this->mSize, + 'rev_parent_id' => is_null( $this->mParentId ) + ? $this->getPreviousRevisionId( $dbw ) + : $this->mParentId, + 'rev_sha1' => is_null( $this->mSha1 ) + ? Revision::base36Sha1( $this->mText ) + : $this->mSha1, + 'rev_content_model' => $this->getContentModelName(), + 'rev_content_format' => $this->getContentFormat(), + ); + + $dbw->insert( 'revision', $row, __METHOD__ ); $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId(); @@ -1100,7 +1222,8 @@ class Revision { $current = $dbw->selectRow( array( 'page', 'revision' ), - array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1' ), + array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1', + 'rev_content_model', 'rev_content_format' ), array( 'page_id' => $pageId, 'page_latest=rev_id', @@ -1115,7 +1238,9 @@ class Revision { 'text_id' => $current->rev_text_id, 'parent_id' => $current->page_latest, 'len' => $current->rev_len, - 'sha1' => $current->rev_sha1 + 'sha1' => $current->rev_sha1, + 'content_model' => $current->rev_content_model, + 'content_format' => $current->rev_content_format ) ); } else { $revision = null; diff --git a/includes/SecondaryDBDataUpdate.php b/includes/SecondaryDBDataUpdate.php new file mode 100644 index 0000000000..1adb9a3524 --- /dev/null +++ b/includes/SecondaryDBDataUpdate.php @@ -0,0 +1,93 @@ +mOptions = array(); + } else { + $this->mOptions = array( 'FOR UPDATE' ); + } + $this->mDb = wfGetDB( DB_MASTER ); + } + + /** + * Invalidate the cache of a list of pages from a single namespace + * + * @param $namespace Integer + * @param $dbkeys Array + */ + public function invalidatePages( $namespace, $dbkeys ) { + if ( !count( $dbkeys ) ) { + return; + } + + /** + * Determine which pages need to be updated + * This is necessary to prevent the job queue from smashing the DB with + * large numbers of concurrent invalidations of the same page + */ + $now = $this->mDb->timestamp(); + $ids = array(); + $res = $this->mDb->select( 'page', array( 'page_id' ), + array( + 'page_namespace' => $namespace, + 'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')', + 'page_touched < ' . $this->mDb->addQuotes( $now ) + ), __METHOD__ + ); + foreach ( $res as $row ) { + $ids[] = $row->page_id; + } + if ( !count( $ids ) ) { + return; + } + + /** + * Do the update + * We still need the page_touched condition, in case the row has changed since + * the non-locking select above. + */ + $this->mDb->update( 'page', array( 'page_touched' => $now ), + array( + 'page_id IN (' . $this->mDb->makeList( $ids ) . ')', + 'page_touched < ' . $this->mDb->addQuotes( $now ) + ), __METHOD__ + ); + } + +} diff --git a/includes/SecondaryDataUpdate.php b/includes/SecondaryDataUpdate.php new file mode 100644 index 0000000000..eeee42f884 --- /dev/null +++ b/includes/SecondaryDataUpdate.php @@ -0,0 +1,51 @@ +doUpdate(); + } + } + +} diff --git a/includes/Title.php b/includes/Title.php index 769adb91f6..6152f1cf1e 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -271,12 +271,17 @@ class Title { if ( isset( $row->page_is_redirect ) ) $this->mRedirect = (bool)$row->page_is_redirect; if ( isset( $row->page_latest ) ) - $this->mLatestID = (int)$row->page_latest; + $this->mLatestID = (int)$row->page_latest; # FIXME: whene3ver page_latest is updated, also update page_content_model + if ( isset( $row->page_content_model ) ) + $this->mContentModelName = $row->page_content_model; + else + $this->mContentModelName = null; # initialized lazily in getContentModelName() } else { // page not found $this->mArticleID = 0; $this->mLength = 0; $this->mRedirect = false; $this->mLatestID = 0; + $this->mContentModelName = null; # initialized lazily in getContentModelName() } } @@ -302,6 +307,7 @@ class Title { $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; $t->mUrlform = wfUrlencode( $t->mDbkeyform ); $t->mTextform = str_replace( '_', ' ', $title ); + $t->mContentModelName = null; # initialized lazily in getContentModelName() return $t; } @@ -687,6 +693,29 @@ class Title { return $this->mNamespace; } + /** + * Get the page's content model name + * + * @return Integer: Namespace index + */ + public function getContentModelName() { + if ( empty( $this->mContentModelName ) ) { + $this->mContentModelName = ContentHandler::getDefaultModelFor( $this ); + } + + return $this->mContentModelName; + } + + /** + * Conveniance method for checking a title's content model name + * + * @param $name + * @return true if $this->getContentModelName() == $name + */ + public function hasContentModel( $name ) { + return $this->getContentModelName() == $name; + } + /** * Get the namespace text * @@ -937,22 +966,29 @@ class Title { * @return Bool */ public function isWikitextPage() { - $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage(); - wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) ); - return $retval; + return $this->hasContentModel( CONTENT_MODEL_WIKITEXT ); } /** - * Could this page contain custom CSS or JavaScript, based - * on the title? + * Could this page contain custom CSS or JavaScript for the global UI. + * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS + * or CONTENT_MODEL_JAVASCRIPT. + * + * Note that this method should not return true for pages that contain and show "inactive" CSS or JS. * * @return Bool */ public function isCssOrJsPage() { - $retval = $this->mNamespace == NS_MEDIAWIKI - && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0; - wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) ); - return $retval; + $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace + && ( $this->hasContentModel( CONTENT_MODEL_CSS ) + || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); + + #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure + # hook funktions can force this method to return true even outside the mediawiki namespace. + + wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) ); + + return $isCssOrJsPage; } /** @@ -960,7 +996,8 @@ class Title { * @return Bool */ public function isCssJsSubpage() { - return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) ); + return ( NS_USER == $this->mNamespace && $this->isSubpage() + && $this->isCssOrJsPage() ); } /** @@ -983,7 +1020,8 @@ class Title { * @return Bool */ public function isCssSubpage() { - return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) ); + return ( NS_USER == $this->mNamespace && $this->isSubpage() + && $this->hasContentModel( CONTENT_MODEL_CSS ) ); } /** @@ -992,7 +1030,8 @@ class Title { * @return Bool */ public function isJsSubpage() { - return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) ); + return ( NS_USER == $this->mNamespace && $this->isSubpage() + && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); } /** diff --git a/includes/WikiPage.php b/includes/WikiPage.php index 6cad466414..5ea5d75353 100644 --- a/includes/WikiPage.php +++ b/includes/WikiPage.php @@ -185,6 +185,7 @@ class WikiPage extends Page { 'page_touched', 'page_latest', 'page_len', + 'page_content_model', ); } @@ -323,17 +324,31 @@ class WikiPage extends Page { * @return bool */ public function isRedirect( $text = false ) { - if ( $text === false ) { - if ( !$this->mDataLoaded ) { - $this->loadPageData(); - } + if ( $text === false ) $content = $this->getContent(); + else $content = ContentHandler::makeContent( $text, $this->mTitle ); # TODO: allow model and format to be provided; or better, expect a Content object - return (bool)$this->mIsRedirect; - } else { - return Title::newFromRedirect( $text ) !== null; - } + + if ( empty( $content ) ) return false; + else return $content->isRedirect(); } + /** + * Returns the page's content model name. Will use the revisions actual content model if the page exists, + * and the page's default if the page doesn't exist yet. + * + * @return int + */ + public function getContentModelName() { + if ( $this->exists() ) { + # look at the revision's actual content model + $content = $this->getContent(); + return $content->getModelName(); + } else { + # use the default model for this page + return $this->mTitle->getContentModelName(); + } + } + /** * Loads page_touched and returns a value indicating if it should be used * @return boolean true if not a redirect @@ -407,6 +422,23 @@ class WikiPage extends Page { return null; } + /** + * Get the content of the current revision. No side-effects... + * + * @param $audience Integer: one of: + * Revision::FOR_PUBLIC to be displayed to all users + * Revision::FOR_THIS_USER to be displayed to $wgUser + * Revision::RAW get the text regardless of permissions + * @return Content|null The content of the current revision + */ + public function getContent( $audience = Revision::FOR_PUBLIC ) { + $this->loadLastEdit(); + if ( $this->mLastRevision ) { + return $this->mLastRevision->getContent( $audience ); + } + return false; + } + /** * Get the text of the current revision. No side-effects... * @@ -414,9 +446,11 @@ class WikiPage extends Page { * Revision::FOR_PUBLIC to be displayed to all users * Revision::FOR_THIS_USER to be displayed to $wgUser * Revision::RAW get the text regardless of permissions - * @return String|bool The text of the current revision. False on failure + * @return String|false The text of the current revision + * @deprecated as of 1.20, getContent() should be used instead. */ - public function getText( $audience = Revision::FOR_PUBLIC ) { + public function getText( $audience = Revision::FOR_PUBLIC ) { #FIXME: deprecated, replace usage! + wfDeprecated( __METHOD__, '1.20' ); $this->loadLastEdit(); if ( $this->mLastRevision ) { return $this->mLastRevision->getText( $audience ); @@ -429,14 +463,22 @@ class WikiPage extends Page { * * @return String|bool The text of the current revision. False on failure */ - public function getRawText() { - $this->loadLastEdit(); - if ( $this->mLastRevision ) { - return $this->mLastRevision->getRawText(); - } - return false; + public function getRawText() { #FIXME: deprecated, replace usage! + return $this->getText( Revision::RAW ); } + /** + * Get the content of the current revision. No side-effects... + * + * @return Contet|false The text of the current revision + */ + protected function getNativeData() { #FIXME: examine all uses carefully! caller must be aware of content model! + $content = $this->getContent( Revision::RAW ); + if ( !$content ) return null; + + return $content->getNativeData(); + } + /** * @return string MW timestamp of last article revision */ @@ -558,32 +600,35 @@ class WikiPage extends Page { return false; } - $text = $editInfo ? $editInfo->pst : false; + if ( $editInfo ) { + $content = ContentHandler::makeContent( $editInfo->pst, $this->mTitle ); + # TODO: take model and format from edit info! + } else { + $content = $this->getContent(); + } - if ( $this->isRedirect( $text ) ) { + if ( !$content || $content->isRedirect( ) ) { return false; } - switch ( $wgArticleCountMethod ) { - case 'any': - return true; - case 'comma': - if ( $text === false ) { - $text = $this->getRawText(); - } - return strpos( $text, ',' ) !== false; - case 'link': - if ( $editInfo ) { - // ParserOutput::getLinks() is a 2D array of page links, so - // to be really correct we would need to recurse in the array - // but the main array should only have items in it if there are - // links. - return (bool)count( $editInfo->output->getLinks() ); - } else { - return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, - array( 'pl_from' => $this->getId() ), __METHOD__ ); - } - } + $hasLinks = null; + + if ( $wgArticleCountMethod === 'link' ) { + # nasty special case to avoid re-parsing to detect links + + if ( $editInfo ) { + // ParserOutput::getLinks() is a 2D array of page links, so + // to be really correct we would need to recurse in the array + // but the main array should only have items in it if there are + // links. + $hasLinks = (bool)count( $editInfo->output->getLinks() ); + } else { + $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, + array( 'pl_from' => $this->getId() ), __METHOD__ ); + } + } + + return $content->isCountable( $hasLinks ); } /** @@ -629,7 +674,8 @@ class WikiPage extends Page { */ public function insertRedirect() { // recurse through to only get the final target - $retval = Title::newFromRedirectRecurse( $this->getRawText() ); + $content = $this->getContent(); + $retval = $content ? $content->getUltimateRedirectTarget() : null; if ( !$retval ) { return null; } @@ -825,7 +871,7 @@ class WikiPage extends Page { && $parserOptions->getStubThreshold() == 0 && $this->mTitle->exists() && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() ) - && $this->mTitle->isWikitextPage(); + && $this->mTitle->isWikitextPage(); #FIXME: ask ContentHandler if cachable! } /** @@ -914,7 +960,7 @@ class WikiPage extends Page { if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { if ( $this->mTitle->exists() ) { - $text = $this->getRawText(); + $text = $this->getNativeData(); #FIXME: may not be a string. check Content model! } else { $text = false; } @@ -981,9 +1027,9 @@ class WikiPage extends Page { public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { wfProfileIn( __METHOD__ ); - $text = $revision->getText(); - $len = strlen( $text ); - $rt = Title::newFromRedirectRecurse( $text ); + $content = $revision->getContent(); + $len = $content->getSize(); + $rt = $content->getUltimateRedirectTarget(); $conditions = array( 'page_id' => $this->getId() ); @@ -1102,27 +1148,23 @@ class WikiPage extends Page { * @param $undo Revision * @param $undoafter Revision Must be an earlier revision than $undo * @return mixed string on success, false on failure + * @deprecated since 1.20: use ContentHandler::getUndoContent() instead. */ - public function getUndoText( Revision $undo, Revision $undoafter = null ) { - $cur_text = $this->getRawText(); - if ( $cur_text === false ) { - return false; // no page - } - $undo_text = $undo->getText(); - $undoafter_text = $undoafter->getText(); + public function getUndoText( Revision $undo, Revision $undoafter = null ) { #FIXME: replace usages. + $this->loadLastEdit(); - if ( $cur_text == $undo_text ) { - # No use doing a merge if it's just a straight revert. - return $undoafter_text; - } + if ( $this->mLastRevision ) { + $handler = ContentHandler::getForTitle( $this->getTitle() ); + $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter ); - $undone_text = ''; + if ( !$undone ) { + return false; + } else { + return ContentHandler::getContentText( $undone ); + } + } - if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) { - return false; - } - - return $undone_text; + return false; } /** @@ -1130,55 +1172,54 @@ class WikiPage extends Page { * @param $text String: new text of the section * @param $sectionTitle String: new section's subject, only if $section is 'new' * @param $edittime String: revision timestamp or null to use the current revision - * @return string Complete article text, or null if error + * @return Content new complete article content, or null if error + * @deprected since 1.20, use replaceSectionContent() instead */ - public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) { - wfProfileIn( __METHOD__ ); + public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) { #FIXME: use replaceSectionContent() instead! + wfDeprecated( __METHOD__, '1.20' ); - if ( strval( $section ) == '' ) { - // Whole-page edit; let the whole text through - } else { - // Bug 30711: always use current version when adding a new section - if ( is_null( $edittime ) || $section == 'new' ) { - $oldtext = $this->getRawText(); - if ( $oldtext === false ) { - wfDebug( __METHOD__ . ": no page text\n" ); - wfProfileOut( __METHOD__ ); - return null; - } - } else { - $dbw = wfGetDB( DB_MASTER ); - $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); + $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); #XXX: could make section title, but that's not required. - if ( !$rev ) { - wfDebug( "WikiPage::replaceSection asked for bogus section (page: " . - $this->getId() . "; section: $section; edittime: $edittime)\n" ); - wfProfileOut( __METHOD__ ); - return null; - } + $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime ); - $oldtext = $rev->getText(); - } + return ContentHandler::getContentText( $newContent ); #XXX: unclear what will happen for non-wikitext! + } - if ( $section == 'new' ) { - # Inserting a new section - $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\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; + public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) { + wfProfileIn( __METHOD__ ); - $text = $wgParser->replaceSection( $oldtext, $section, $text ); - } - } + if ( strval( $section ) == '' ) { + // Whole-page edit; let the whole text through + $newContent = $sectionContent; + } else { + // Bug 30711: always use current version when adding a new section + if ( is_null( $edittime ) || $section == 'new' ) { + $oldContent = $this->getContent(); + if ( ! $oldContent ) { + wfDebug( __METHOD__ . ": no page text\n" ); + wfProfileOut( __METHOD__ ); + return null; + } + } else { + $dbw = wfGetDB( DB_MASTER ); + $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); - wfProfileOut( __METHOD__ ); - return $text; - } + if ( !$rev ) { + wfDebug( "WikiPage::replaceSection asked for bogus section (page: " . + $this->getId() . "; section: $section; edittime: $edittime)\n" ); + wfProfileOut( __METHOD__ ); + return null; + } + + $oldContent = $rev->getContent(); + } + + $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle ); + } + + wfProfileOut( __METHOD__ ); + return $newContent; + } /** * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed. @@ -1242,8 +1283,64 @@ class WikiPage extends Page { * revision: The revision object for the inserted revision, or null * * Compatibility note: this function previously returned a boolean value indicating success/failure - */ - public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { + * @deprecated since 1.20: use doEditContent() instead. + */ + public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { #FIXME: use doEditContent() instead + #TODO: log use of deprecated function + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + + return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user ); + } + + /** + * 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 $summary String: 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 $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 + * + * @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 + * + * Compatibility note: this function previously returned a boolean value indicating success/failure + */ + public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, + User $user = null, $serialisation_format = null ) { #FIXME: use this global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries; # Low-level sanity check @@ -1261,10 +1358,25 @@ class WikiPage extends Page { $flags = $this->checkFlags( $flags ); - if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary, - $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) ) - { - wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" ); + # call legacy hook + $hook_ok = wfRunHooks( 'ArticleContentSave', array( &$this, &$user, &$content, &$summary, #FIXME: document new hook! + $flags & EDIT_MINOR, null, null, &$flags, &$status ) ); + + if ( $hook_ok && !empty( $wgHooks['ArticleSave'] ) ) { # avoid serialization overhead if the hook isn't present + $content_text = $content->serialize(); + $txt = $content_text; # clone + + $hook_ok = wfRunHooks( 'ArticleSave', array( &$this, &$user, &$txt, &$summary, #FIXME: deprecate legacy hook! + $flags & EDIT_MINOR, null, null, &$flags, &$status ) ); + + if ( $txt !== $content_text ) { + # if the text changed, unserialize the new version to create an updated Content object. + $content = $content->getContentHandler()->unserialize( $txt ); + } + } + + if ( !$hook_ok ) { + wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" ); if ( $status->isOK() ) { $status->fatal( 'edit-hook-aborted' ); @@ -1278,20 +1390,25 @@ class WikiPage extends Page { $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); $bot = $flags & EDIT_FORCE_BOT; - $oldtext = $this->getRawText(); // current revision - $oldsize = strlen( $oldtext ); + $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 == '' ) { - $summary = self::getAutosummary( $oldtext, $text, $flags ); + if ( !$old_content ) $old_content = null; + $summary = $handler->getAutosummary( $old_content, $content, $flags ); } - $editInfo = $this->prepareTextForEdit( $text, null, $user ); - $text = $editInfo->pst; - $newsize = strlen( $text ); + $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format ); + $serialized = $editInfo->pst; + $content = $editInfo->pstContent; + $newsize = $content->getSize(); $dbw = wfGetDB( DB_MASTER ); $now = wfTimestampNow(); @@ -1319,14 +1436,17 @@ class WikiPage extends Page { 'page' => $this->getId(), 'comment' => $summary, 'minor_edit' => $isminor, - 'text' => $text, + 'text' => $serialized, + 'len' => $newsize, 'parent_id' => $oldid, 'user' => $user->getId(), 'user_text' => $user->getName(), - 'timestamp' => $now + 'timestamp' => $now, + 'content_model' => $content->getModelName(), + 'content_format' => $serialisation_format, ) ); - $changed = ( strcmp( $text, $oldtext ) != 0 ); + $changed = !$content->equals( $old_content ); if ( $changed ) { $dbw->begin( __METHOD__ ); @@ -1424,10 +1544,13 @@ class WikiPage extends Page { 'page' => $newid, 'comment' => $summary, 'minor_edit' => $isminor, - 'text' => $text, + 'text' => $serialized, + 'len' => $newsize, 'user' => $user->getId(), 'user_text' => $user->getName(), - 'timestamp' => $now + 'timestamp' => $now, + 'content_model' => $content->getModelName(), + 'content_format' => $serialisation_format, ) ); $revisionId = $revision->insertOn( $dbw ); @@ -1445,7 +1568,7 @@ class WikiPage extends Page { $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); # Add RC row to the DB $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, - '', strlen( $text ), $revisionId, $patrolled ); + '', $content->getSize(), $revisionId, $patrolled ); # Log auto-patrolled edits if ( $patrolled ) { @@ -1458,8 +1581,11 @@ class WikiPage extends Page { # Update links, etc. $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); - wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary, + wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $serialized, $summary, #FIXME: deprecate legacy hook $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); + + wfRunHooks( 'ArticleContentInsertComplete', array( &$this, &$user, $content, $summary, #FIXME: document new hook + $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); } # Do updates right now unless deferral was requested @@ -1470,9 +1596,12 @@ class WikiPage extends Page { // Return the new revision (or null) to the caller $status->value['revision'] = $revision; - wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary, + wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $serialized, $summary, #FIXME: deprecate legacy hook $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) ); + wfRunHooks( 'ArticleContentSaveComplete', array( &$this, &$user, $content, $summary, #FIXME: document new hook + $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) ); + # Promote user to any groups they meet the criteria for $user->addAutopromoteOnceGroups( 'onEdit' ); @@ -1500,15 +1629,35 @@ class WikiPage extends Page { /** * Prepare text which is about to be saved. * Returns a stdclass with source, pst and output members - * @return bool|object - */ - public function prepareTextForEdit( $text, $revid = null, User $user = null ) { + * @deprecated in 1.20: use prepareContentForEdit instead. + */ + public function prepareTextForEdit( $text, $revid = null, User $user = null ) { #FIXME: use prepareContentForEdit() instead #XXX: who uses this?! + #TODO: log use of deprecated function + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + return $this->prepareContentForEdit( $content, $revid , $user ); + } + + /** + * Prepare content which is about to be saved. + * Returns a stdclass with source, pst and output members + * + * @param \Content $content + * @param null $revid + * @param null|\User $user + * @param null $serialization_format + * @return bool|object + */ + public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) { #FIXME: use this #XXX: really public?! global $wgParser, $wgContLang, $wgUser; $user = is_null( $user ) ? $wgUser : $user; // @TODO fixme: check $user->getId() here??? + if ( $this->mPreparedEdit - && $this->mPreparedEdit->newText == $text + && $this->mPreparedEdit->newContent + && $this->mPreparedEdit->newContent->equals( $content ) && $this->mPreparedEdit->revid == $revid + && $this->mPreparedEdit->format == $serialization_format + #XXX: also check $user here? ) { // Already prepared return $this->mPreparedEdit; @@ -1519,11 +1668,19 @@ class WikiPage extends Page { $edit = (object)array(); $edit->revid = $revid; - $edit->newText = $text; - $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); + + $edit->pstContent = $content->preSaveTransform( $this->mTitle, $user, $popts ); + $edit->pst = $edit->pstContent->serialize( $serialization_format ); + $edit->format = $serialization_format; + $edit->popts = $this->makeParserOptions( 'canonical' ); - $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid ); - $edit->oldText = $this->getRawText(); + $edit->output = $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts ); + + $edit->newContent = $content; + $edit->oldContent = $this->getContent( Revision::RAW ); + + $edit->newText = ContentHandler::getContentText( $edit->newContent ); #FIXME: B/C only! don't use this field! + $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : ''; #FIXME: B/C only! don't use this field! $this->mPreparedEdit = $edit; @@ -1553,13 +1710,13 @@ class WikiPage extends Page { wfProfileIn( __METHOD__ ); $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null ); - $text = $revision->getText(); + $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->prepareTextForEdit( $text, $revision->getId(), $user ); + $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user ); } else { wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); $editInfo = $this->mPreparedEdit; @@ -1571,9 +1728,9 @@ class WikiPage extends Page { $parserCache->save( $editInfo->output, $this, $editInfo->popts ); } - # Update the links tables - $u = new LinksUpdate( $this->mTitle, $editInfo->output ); - $u->doUpdate(); + # Update the links tables and other secondary data + $updates = $editInfo->output->getLinksUpdateAndOtherUpdates( $this->mTitle ); + SecondaryDataUpdate::runUpdates( $updates ); wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); @@ -1617,7 +1774,7 @@ class WikiPage extends Page { } DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) ); - DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) ); + DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) ); # If this is another user's talk page, update newtalk. # Don't do this if $options['changed'] = false (null-edits) nor if @@ -1643,7 +1800,10 @@ class WikiPage extends Page { } if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - MessageCache::singleton()->replace( $shortTitle, $text ); + $msgtext = ContentHandler::getContentText( $content ); #XXX: could skip pseudo-messages like js/css here, based on content model. + if ( $msgtext === false || $msgtext === null ) $msgtext = ''; + + MessageCache::singleton()->replace( $shortTitle, $msgtext ); } if( $options['created'] ) { @@ -1665,13 +1825,33 @@ class WikiPage extends Page { * @param $comment String: comment submitted * @param $minor Boolean: whereas it's a minor modification */ - public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) { + public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) { + #TODO: log use of deprecated function + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + return $this->doQuickEdit( $content, $user, $comment , $minor ); + } + + /** + * Edit an article without doing all that other stuff + * The article must already exist; link tables etc + * are not updated, caches are not flushed. + * + * @param $content Content: content submitted + * @param $user User The relevant user + * @param $comment String: comment submitted + * @param $serialisation_format String: format for storing the content in the database + * @param $minor Boolean: whereas it's a minor modification + */ + public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) { wfProfileIn( __METHOD__ ); + $serialized = $content->serialize( $serialisation_format ); + $dbw = wfGetDB( DB_MASTER ); $revision = new Revision( array( 'page' => $this->getId(), - 'text' => $text, + 'text' => $serialized, + 'length' => $content->getSize(), 'comment' => $comment, 'minor_edit' => $minor ? 1 : 0, ) ); @@ -2013,7 +2193,9 @@ class WikiPage extends Page { 'ar_len' => 'rev_len', 'ar_page_id' => 'page_id', 'ar_deleted' => $bitfield, - 'ar_sha1' => 'rev_sha1' + 'ar_sha1' => 'rev_content_model', + 'ar_content_format' => 'rev_content_format', + 'ar_content_format' => 'rev_sha1' ), array( 'page_id' => $id, 'page_id = rev_page' @@ -2276,7 +2458,7 @@ class WikiPage extends Page { } # Actually store the edit - $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser ); + $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser ); if ( !empty( $status->value['revision'] ) ) { $revId = $status->value['revision']->getId(); } else { @@ -2426,53 +2608,16 @@ class WikiPage extends Page { * @param $newtext String: The submitted text of the page. * @param $flags Int bitmask: a bitmask of flags submitted for the edit. * @return string An appropriate autosummary, or an empty string. + * @deprecated since 1.20, use ContentHandler::getAutosummary() instead */ public static function getAutosummary( $oldtext, $newtext, $flags ) { - global $wgContLang; - - # Decide what kind of autosummary is needed. - - # Redirect autosummaries - $ot = Title::newFromRedirect( $oldtext ); - $rt = Title::newFromRedirect( $newtext ); - - if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) { - $truncatedtext = $wgContLang->truncate( - str_replace( "\n", ' ', $newtext ), - max( 0, 250 - - strlen( wfMsgForContent( 'autoredircomment' ) ) - - strlen( $rt->getFullText() ) - ) ); - return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext ); - } + # NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't. - # New page autosummaries - if ( $flags & EDIT_NEW && strlen( $newtext ) ) { - # If they're making a new article, give its text, truncated, in the summary. + $handler = ContentHandler::getForModelName( CONTENT_MODEL_WIKITEXT ); + $oldContent = $oldtext ? $handler->unserialize( $oldtext ) : null; + $newContent = $newtext ? $handler->unserialize( $newtext ) : null; - $truncatedtext = $wgContLang->truncate( - str_replace( "\n", ' ', $newtext ), - max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) ); - - return wfMsgForContent( 'autosumm-new', $truncatedtext ); - } - - # Blanking autosummaries - if ( $oldtext != '' && $newtext == '' ) { - return wfMsgForContent( 'autosumm-blank' ); - } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) { - # Removing more than 90% of the article - - $truncatedtext = $wgContLang->truncate( - $newtext, - max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) ); - - return wfMsgForContent( 'autosumm-replace', $truncatedtext ); - } - - # If we reach this point, there's no applicable autosummary for our case, so our - # autosummary is empty. - return ''; + return $handler->getAutosummary( $oldContent, $newContent, $flags ); } /** @@ -2481,8 +2626,13 @@ class WikiPage extends Page { * @param &$hasHistory Boolean: whether the page has a history * @return mixed String containing deletion reason or empty string, or boolean false * if no revision occurred + * @deprecated since 1.20, use ContentHandler::getAutoDeleteReason() instead */ public function getAutoDeleteReason( &$hasHistory ) { + #NOTE: stub for backwards-compatibility. + + $handler = ContentHandler::getForTitle( $this->getTitle() ); + $handler->getAutoDeleteReason( $this->getTitle(), $hasHistory ); global $wgContLang; // Get the last revision @@ -2679,6 +2829,7 @@ class WikiPage extends Page { if ( count( $templates_diff ) > 0 ) { # Whee, link updates time. + # Note: we are only interested in links here. We don't need to get other SecondaryDataUpdate items from the parser output. $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); $u->doUpdate(); } @@ -2858,14 +3009,20 @@ class PoolWorkArticleView extends PoolCounterWork { * @param $revid Integer: ID of the revision being parsed * @param $useParserCache Boolean: whether to use the parser cache * @param $parserOptions parserOptions to use for the parse operation - * @param $text String: text to parse or null to load it + * @param $content Content|String: content to parse or null to load it; may also be given as a wikitext string, for BC */ - function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) { + function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) { + if ( is_string($content) ) { #BC: old style call + $modelName = $page->getRevision()->getContentModelName(); + $format = $page->getRevision()->getContentFormat(); + $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelName, $format ); + } + $this->page = $page; $this->revid = $revid; $this->cacheable = $useParserCache; $this->parserOptions = $parserOptions; - $this->text = $text; + $this->content = $content; $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions ); parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid ); } @@ -2905,18 +3062,21 @@ class PoolWorkArticleView extends PoolCounterWork { $isCurrent = $this->revid === $this->page->getLatest(); - if ( $this->text !== null ) { - $text = $this->text; + if ( $this->content !== null ) { + $content = $this->content; } elseif ( $isCurrent ) { - $text = $this->page->getRawText(); + $content = $this->page->getContent( Revision::RAW ); #XXX: why use RAW audience here, and PUBLIC (default) below? } else { $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid ); if ( $rev === null ) { return false; } - $text = $rev->getText(); + $content = $rev->getContent(); #XXX: why use PUBLIC audience here (default), and RAW above? } + $time = - wfTime(); + $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions ); + $time += wfTime(); $time = - microtime( true ); $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(), $this->parserOptions, true, true, $this->revid ); diff --git a/includes/actions/EditAction.php b/includes/actions/EditAction.php index 08a33f4c0d..2888d247d8 100644 --- a/includes/actions/EditAction.php +++ b/includes/actions/EditAction.php @@ -40,14 +40,16 @@ class EditAction extends FormlessAction { $context = $this->getContext(); if ( wfRunHooks( 'CustomEditor', array( $page, $user ) ) ) { + $handler = ContentHandler::getForTitle( $page->getTitle() ); + if ( ExternalEdit::useExternalEngine( $context, 'edit' ) && $this->getName() == 'edit' && !$request->getVal( 'section' ) && !$request->getVal( 'oldid' ) ) { - $extedit = new ExternalEdit( $context ); + $extedit = $handler->createExternalEdit( $context ); $extedit->execute(); } else { - $editor = new EditPage( $page ); + $editor = $handler->createEditPage( $page ); $editor->edit(); } } diff --git a/includes/actions/RawAction.php b/includes/actions/RawAction.php index 5615ad5221..f07b5b6ca2 100644 --- a/includes/actions/RawAction.php +++ b/includes/actions/RawAction.php @@ -135,11 +135,20 @@ class RawAction extends FormlessAction { $request->response()->header( "Last-modified: $lastmod" ); // Public-only due to cache headers - $text = $rev->getText(); + $content = $rev->getContent(); + + if ( !$content instanceof TextContent ) { + wfHttpError( 406, "Not Acceptable", "The requeste page uses the content model `" + . $content->getModelName() . "` which is not supported via this interface." ); + die(); + } + $section = $request->getIntOrNull( 'section' ); if ( $section !== null ) { - $text = $wgParser->getSection( $text, $section ); + $content = $content->getSection( $section ); } + + $text = $content->getNativeData(); } } diff --git a/includes/actions/RollbackAction.php b/includes/actions/RollbackAction.php index 0d9a902727..5aba0b52f1 100644 --- a/includes/actions/RollbackAction.php +++ b/includes/actions/RollbackAction.php @@ -109,7 +109,8 @@ class RollbackAction extends FormlessAction { $this->getOutput()->returnToMain( false, $this->getTitle() ); if ( !$request->getBool( 'hidediff', false ) && !$this->getUser()->getBoolOption( 'norollbackdiff', false ) ) { - $de = new DifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true ); + $contentHandler = ContentHandler::getForTitle( $this->getTitle() ); + $de = $contentHandler->getDifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true ); $de->showDiff( '', '' ); } } diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index 87f096785f..1fac9962ef 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -35,7 +35,8 @@ class ApiComparePages extends ApiBase { $rev1 = $this->revisionOrTitleOrId( $params['fromrev'], $params['fromtitle'], $params['fromid'] ); $rev2 = $this->revisionOrTitleOrId( $params['torev'], $params['totitle'], $params['toid'] ); - $de = new DifferenceEngine( $this->getContext(), + $contentHandler = ContentHandler::getForModelName( $rev1->getContentModelName() ); + $de = $contentHandler->getDifferenceEngine( $this->getContext(), $rev1, $rev2, null, // rcid diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index 8a4a17fd93..42f9737d44 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -123,7 +123,7 @@ class ApiDelete extends ApiBase { // Need to pass a throwaway variable because generateReason expects // a reference $hasHistory = false; - $reason = $page->getAutoDeleteReason( $hasHistory ); + $reason = $page->getAutoDeleteReason( $hasHistory ); #FIXME: use ContentHandler::getAutoDeleteReason() if ( $reason === false ) { return array( array( 'cannotdelete', $title->getPrefixedText() ) ); } diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index 796b049746..133ca9c029 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -117,21 +117,23 @@ class ApiEditPage extends ApiBase { // We do want getContent()'s behavior for non-existent // MediaWiki: pages, though if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI ) { - $content = ''; + $content = null; + $text = ''; } else { - $content = $articleObj->getContent(); + $content = $articleObj->getContentObject(); + $text = ContentHandler::getContentText( $content ); #FIXME: serialize?! get format from params?... } if ( !is_null( $params['section'] ) ) { // Process the content for section edits - global $wgParser; $section = intval( $params['section'] ); - $content = $wgParser->getSection( $content, $section, false ); - if ( $content === false ) { + $sectionContent = $content->getSection( $section ); + $text = ContentHandler::getContentText( $sectionContent ); #FIXME: serialize?! get format from params?... + if ( $text === false || $text === null ) { $this->dieUsage( "There is no section {$section}.", 'nosuchsection' ); } } - $params['text'] = $params['prependtext'] . $content . $params['appendtext']; + $params['text'] = $params['prependtext'] . $text . $params['appendtext']; $toMD5 = $params['prependtext'] . $params['appendtext']; } @@ -248,7 +250,9 @@ class ApiEditPage extends ApiBase { // TODO: Make them not or check if they still do $wgTitle = $titleObj; - $ep = new EditPage( $articleObj ); + $handler = ContentHandler::getForTitle( $titleObj ); + $ep = $handler->createEditPage( $articleObj ); + $ep->setContextTitle( $titleObj ); $ep->importFormData( $req ); diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 141f779372..afff5fde2e 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -318,9 +318,9 @@ class ApiParse extends ApiBase { $page = WikiPage::factory( $titleObj ); - if ( $this->section !== false ) { + if ( $this->section !== false ) { #FIXME: get section Content, get parser output, ... $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId ) - ? 'page id ' . $pageId : $titleObj->getText() ); + ? 'page id ' . $pageId : $titleObj->getText() ); #FIXME: get section... // Not cached (save or load) return $wgParser->parse( $this->text, $titleObj, $popts ); @@ -329,13 +329,14 @@ class ApiParse extends ApiBase { // getParserOutput will save to Parser cache if able $pout = $page->getParserOutput( $popts ); if ( $getWikitext ) { - $this->text = $page->getRawText(); + $this->content = $page->getContent( Revision::RAW ); #FIXME: use $this->content everywhere + $this->text = ContentHandler::getContentText( $this->content ); #FIXME: serialize, get format from params; or use object structure in result? } return $pout; } } - private function getSectionText( $text, $what ) { + private function getSectionText( $text, $what ) { #FIXME: replace with Content::getSection global $wgParser; // Not cached (save or load) $text = $wgParser->getSection( $text, $this->section, false ); diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php index 9e9320fb6c..77898cf72d 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -90,11 +90,11 @@ class ApiPurge extends ApiBase { $popts = ParserOptions::newFromContext( $this->getContext() ); $p_result = $wgParser->parse( $page->getRawText(), $title, $popts, - true, true, $page->getLatest() ); + true, true, $page->getLatest() ); #FIXME: content! # Update the links tables - $u = new LinksUpdate( $title, $p_result ); - $u->doUpdate(); + $updates = $p_result->getLinksUpdateAndOtherUpdates( $title ); + SecondaryDataUpdate::runUpdates( $updates ); $r['linkupdate'] = ''; diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index fa58bdf047..11ea37100d 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -114,7 +114,7 @@ class ApiQueryRevisions extends ApiQueryBase { } if ( !is_null( $params['difftotext'] ) ) { - $this->difftotext = $params['difftotext']; + $this->difftotext = $params['difftotext']; #FIXME: handle non-text content! } elseif ( !is_null( $params['diffto'] ) ) { if ( $params['diffto'] == 'cur' ) { $params['diffto'] = 0; @@ -503,11 +503,13 @@ class ApiQueryRevisions extends ApiQueryBase { $vals['diff'] = array(); $context = new DerivativeContext( $this->getContext() ); $context->setTitle( $title ); + $handler = ContentHandler::getForTitle( $title ); + if ( !is_null( $this->difftotext ) ) { - $engine = new DifferenceEngine( $context ); - $engine->setText( $text, $this->difftotext ); + $engine = $handler->getDifferenceEngine( $context ); + $engine->setText( $text, $this->difftotext ); #FIXME: use content objects!... } else { - $engine = new DifferenceEngine( $context, $revision->getID(), $this->diffto ); + $engine = $handler->getDifferenceEngine( $context, $revision->getID(), $this->diffto ); $vals['diff']['from'] = $engine->getOldid(); $vals['diff']['to'] = $engine->getNewid(); } diff --git a/includes/diff/DairikiDiff.php b/includes/diff/DairikiDiff.php index 72eb5d3c2c..56cee2e2f6 100644 --- a/includes/diff/DairikiDiff.php +++ b/includes/diff/DairikiDiff.php @@ -15,7 +15,7 @@ * @private * @ingroup DifferenceEngine */ -class _DiffOp { +class _DiffOp { #FIXME: no longer private! var $type; var $orig; var $closing; @@ -44,7 +44,7 @@ class _DiffOp { * @private * @ingroup DifferenceEngine */ -class _DiffOp_Copy extends _DiffOp { +class _DiffOp_Copy extends _DiffOp { #FIXME: no longer private! var $type = 'copy'; function __construct( $orig, $closing = false ) { @@ -68,7 +68,7 @@ class _DiffOp_Copy extends _DiffOp { * @private * @ingroup DifferenceEngine */ -class _DiffOp_Delete extends _DiffOp { +class _DiffOp_Delete extends _DiffOp { #FIXME: no longer private! var $type = 'delete'; function __construct( $lines ) { @@ -89,7 +89,7 @@ class _DiffOp_Delete extends _DiffOp { * @private * @ingroup DifferenceEngine */ -class _DiffOp_Add extends _DiffOp { +class _DiffOp_Add extends _DiffOp { #FIXME: no longer private! var $type = 'add'; function __construct( $lines ) { @@ -110,7 +110,7 @@ class _DiffOp_Add extends _DiffOp { * @private * @ingroup DifferenceEngine */ -class _DiffOp_Change extends _DiffOp { +class _DiffOp_Change extends _DiffOp { #FIXME: no longer private! var $type = 'change'; function __construct( $orig, $closing ) { @@ -150,7 +150,7 @@ class _DiffOp_Change extends _DiffOp { * @private * @ingroup DifferenceEngine */ -class _DiffEngine { +class _DiffEngine { #FIXME: no longer private! const MAX_XREF_LENGTH = 10000; @@ -637,7 +637,7 @@ class _DiffEngine { * @private * @ingroup DifferenceEngine */ -class Diff { +class Diff extends DiffResult { var $edits; /** @@ -647,11 +647,36 @@ class Diff { * @param $from_lines array An array of strings. * (Typically these are lines from a file.) * @param $to_lines array An array of strings. + * @param $eng _DiffEngine|null The diff engine to use. */ - function __construct( $from_lines, $to_lines ) { - $eng = new _DiffEngine; - $this->edits = $eng->diff( $from_lines, $to_lines ); - // $this->_check($from_lines, $to_lines); + function __construct( $from_lines, $to_lines, $eng = null ) { + if ( !$eng ) { + $eng = new _DiffEngine(); + } + + $edits = $eng->diff( $from_lines, $to_lines ); + + parent::__construct( $edits ); + + //$this->_check( $from_lines, $to_lines ); + } +} + +/** + * Class representing the result of 'diffin' two sequences of strings. + * @todo document + * @private + * @ingroup DifferenceEngine + */ +class DiffResult { + + /** + * Constructor. + * + * @param $edits array An array of Edit. + */ + function __construct( $edits ) { + $this->edits = $edits; } /** diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index e8f35f0d0a..348f2dc0f3 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -23,7 +23,7 @@ class DifferenceEngine extends ContextSource { * @private */ var $mOldid, $mNewid; - var $mOldtext, $mNewtext; + var $mOldContent, $mNewContent; protected $mDiffLang; /** @@ -486,20 +486,21 @@ class DifferenceEngine extends ContextSource { $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() ); $out->setArticleFlag( true ); - if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { + if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { #NOTE: only needed for B/C: custom rendering of JS/CSS via hook // Stolen from Article::view --AG 2007-10-11 // Give hooks a chance to customise the output // @TODO: standardize this crap into one function - if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) { - // Wrap the whole lot in a
 and don't parse
-					$m = array();
-					preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
-					$out->addHTML( "
\n" );
-					$out->addHTML( htmlspecialchars( $this->mNewtext ) );
-					$out->addHTML( "\n
\n" ); + if ( !Hook::isRegistered( 'ShowRawCssJs' ) + || wfRunHooks( 'ShowRawCssJs', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) { #NOTE: deperecated hook, B/C only + // use the content object's own rendering + $po = $this->mContentObject->getParserOutput(); + $out->addHTML( $po->getText() ); } - } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) { - // Handled by extension + } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { + // Handled by extension + } elseif( Hooks::isRegistered( 'ArticleViewCustom' ) + && !wfRunHooks( 'ArticleViewCustom', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) { #NOTE: deperecated hook, B/C only + // Handled by extension } else { // Normal page if ( $this->getTitle()->equals( $this->mNewPage ) ) { @@ -630,7 +631,9 @@ class DifferenceEngine extends ContextSource { return false; } - $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); + #TODO: make sure both Content objects have the same content model. What do we do if they don't? + + $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent ); // Save to cache for 7 days if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) { @@ -667,14 +670,48 @@ class DifferenceEngine extends ContextSource { } } + /** + * Generate a diff, no caching. + * + * Subclasses may override this to provide a + * + * @param $old Content: old content + * @param $new Content: new content + */ + function generateContentDiffBody( Content $old, Content $new ) { + #XXX: generate a warning if $old or $new are not instances of TextContent? + #XXX: fail if $old and $new don't have the same content model? or what? + + $otext = $old->serialize(); + $ntext = $new->serialize(); + + #XXX: text should be "already segmented". what does that mean? + return $this->generateTextDiffBody( $otext, $ntext ); + } + + /** + * Generate a diff, no caching + * + * @param $otext String: old text, must be already segmented + * @param $ntext String: new text, must be already segmented + * @deprecated since 1.20, use generateContentDiffBody() instead! + */ + function generateDiffBody( $otext, $ntext ) { + wfDeprecated( __METHOD__, "1.20" ); + + return $this->generateTextDiffBody( $otext, $ntext ); + } + /** * Generate a diff, no caching * + * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point. + * * @param $otext String: old text, must be already segmented * @param $ntext String: new text, must be already segmented * @return bool|string */ - function generateDiffBody( $otext, $ntext ) { + function generateTextDiffBody( $otext, $ntext ) { global $wgExternalDiffEngine, $wgContLang; wfProfileIn( __METHOD__ ); @@ -928,13 +965,28 @@ class DifferenceEngine extends ContextSource { /** * Use specified text instead of loading from the database + * @deprecated since 1.20 */ - function setText( $oldText, $newText ) { - $this->mOldtext = $oldText; - $this->mNewtext = $newText; - $this->mTextLoaded = 2; - $this->mRevisionsLoaded = true; - } + function setText( $oldText, $newText ) { #FIXME: no longer use this, use setContent()! + wfDeprecated( __METHOD__, "1.20" ); + + $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() ); + $newContent = ContentHandler::makeContent( $newText, $this->getTitle() ); + + $this->setContent( $oldContent, $newContent ); + } + + /** + * Use specified text instead of loading from the database + * @since 1.20 + */ + function setContent( Content $oldContent, Content $newContent ) { + $this->mOldContent = $oldContent; + $this->mNewContent = $newContent; + + $this->mTextLoaded = 2; + $this->mRevisionsLoaded = true; + } /** * Set the language in which the diff text is written @@ -1059,14 +1111,14 @@ class DifferenceEngine extends ContextSource { return false; } if ( $this->mOldRev ) { - $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER ); - if ( $this->mOldtext === false ) { + $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER ); + if ( $this->mOldContent === false ) { return false; } } if ( $this->mNewRev ) { - $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); - if ( $this->mNewtext === false ) { + $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER ); + if ( $this->mNewContent === false ) { return false; } } @@ -1087,7 +1139,7 @@ class DifferenceEngine extends ContextSource { if ( !$this->loadRevisionData() ) { return false; } - $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); + $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER ); return true; } } diff --git a/includes/installer/Ibm_db2Updater.php b/includes/installer/Ibm_db2Updater.php index 02d7cb1eb7..68573e0356 100644 --- a/includes/installer/Ibm_db2Updater.php +++ b/includes/installer/Ibm_db2Updater.php @@ -70,6 +70,13 @@ class Ibm_db2Updater extends DatabaseUpdater { array( 'addField', 'revision', 'rev_sha1', 'patch-rev_sha1.sql' ), array( 'addField', 'archive', 'ar_sha1', 'patch-ar_sha1.sql' ), + // 1.20 + // content model stuff for WikiData + array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ), + array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ), + array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ), + array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ), + array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ), // 1.20 array( 'addTable', 'config', 'patch-config.sql' ), ); diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php index 5e6ae7eafb..8182733df5 100644 --- a/includes/installer/MysqlUpdater.php +++ b/includes/installer/MysqlUpdater.php @@ -191,6 +191,14 @@ class MysqlUpdater extends DatabaseUpdater { array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ), array( 'addField', 'uploadstash', 'us_chunk_inx', 'patch-uploadstash_chunk.sql' ), array( 'addfield', 'job', 'job_timestamp', 'patch-jobs-add-timestamp.sql' ), + + // 1.20 + // content model stuff for WikiData + array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ), + array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ), + array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ), + array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ), + array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ), array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase.sql' ), // 1.20 diff --git a/includes/installer/OracleUpdater.php b/includes/installer/OracleUpdater.php index 73bbc57751..a79d0b936c 100644 --- a/includes/installer/OracleUpdater.php +++ b/includes/installer/OracleUpdater.php @@ -52,6 +52,13 @@ class OracleUpdater extends DatabaseUpdater { array( 'addField', 'job', 'job_timestamp', 'patch-job_timestamp_field.sql' ), array( 'addIndex', 'job', 'i02', 'patch-job_timestamp_index.sql' ), + // 1.20 + // content model stuff for WikiData + array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ), + array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ), + array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ), + array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ), + array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ), //1.20 array( 'addTable', 'config', 'patch-config.sql' ), diff --git a/includes/installer/SqliteUpdater.php b/includes/installer/SqliteUpdater.php index a98c4db83a..89620433f0 100644 --- a/includes/installer/SqliteUpdater.php +++ b/includes/installer/SqliteUpdater.php @@ -70,6 +70,14 @@ class SqliteUpdater extends DatabaseUpdater { array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ), array( 'addField', 'uploadstash', 'us_chunk_inx', 'patch-uploadstash_chunk.sql' ), array( 'addfield', 'job', 'job_timestamp', 'patch-jobs-add-timestamp.sql' ), + + // 1.20 + // content model stuff for WikiData + array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ), + array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ), + array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ), + array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ), + array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ), array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ug_group-length-increase.sql' ), // 1.20 diff --git a/includes/job/RefreshLinksJob.php b/includes/job/RefreshLinksJob.php index 1aa206f04a..dcc5ab107c 100644 --- a/includes/job/RefreshLinksJob.php +++ b/includes/job/RefreshLinksJob.php @@ -46,9 +46,11 @@ class RefreshLinksJob extends Job { $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() ); wfProfileOut( __METHOD__.'-parse' ); wfProfileIn( __METHOD__.'-update' ); - $update = new LinksUpdate( $this->title, $parserOutput, false ); - $update->doUpdate(); - wfProfileOut( __METHOD__.'-update' ); + + $updates = $parserOutput->getLinksUpdateAndOtherUpdates( $this->title, false ); + SecondaryDataUpdate::runUpdates( $updates ); + + wfProfileOut( __METHOD__.'-update' ); wfProfileOut( __METHOD__ ); return true; } @@ -118,8 +120,10 @@ class RefreshLinksJob2 extends Job { $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() ); wfProfileOut( __METHOD__.'-parse' ); wfProfileIn( __METHOD__.'-update' ); - $update = new LinksUpdate( $title, $parserOutput, false ); - $update->doUpdate(); + + $updates = $parserOutput->getLinksUpdateAndOtherUpdates( $title, false ); + SecondaryDataUpdate::runUpdates( $updates ); + wfProfileOut( __METHOD__.'-update' ); wfWaitForSlaves(); } diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php index 0d597e8592..9cb247f69b 100644 --- a/includes/parser/ParserOutput.php +++ b/includes/parser/ParserOutput.php @@ -30,7 +30,7 @@ class CacheTime { */ function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); } - /** + /**abstract * Sets the number of seconds after which this object should expire. * This value is used with the ParserCache. * If called with a value greater than the value provided at any previous call, @@ -141,8 +141,9 @@ class ParserOutput extends CacheTime { $mProperties = array(), # Name/value pairs to be cached in the DB $mTOCHTML = '', # HTML of the TOC $mTimestamp; # Timestamp of the revision - private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change. - private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys) + private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change. + private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys) + private $mSecondaryDataUpdates = array(); # List of instances of SecondaryDataObject(), used to cause some information extracted from the page in a custom place. const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)())#'; @@ -449,4 +450,53 @@ class ParserOutput extends CacheTime { function recordOption( $option ) { $this->mAccessedOptions[$option] = true; } + + /** + * Adds an update job to the output. Any update jobs added to the output will eventually bexecuted in order to + * store any secondary information extracted from the page's content. + * + * @param SecondaryDataUpdate $update + */ + public function addSecondaryDataUpdate( SecondaryDataUpdate $update ) { + $this->mSecondaryDataUpdates[] = $update; + } + + /** + * Returns any SecondaryDataUpdate jobs to be executed in order to store secondary information + * extracted from the page's content. + * + * This does not automatically include an LinksUpdate object for the links in this ParserOutput instance. + * Use getLinksUpdateAndOtherUpdates() if you want that. + * + * @return array an array of instances of SecondaryDataUpdate + */ + public function getSecondaryDataUpdates() { + return $this->mSecondaryDataUpdates; + } + + /** + * Conveniance method that returns any SecondaryDataUpdate jobs to be executed in order + * to store secondary information extracted from the page's content, including the LinksUpdate object + * for all links stopred in this ParserOutput object. + * + * @param $title Title of the page we're updating. If not given, a title object will be created based on $this->getTitleText() + * @param $recursive Boolean: queue jobs for recursive updates? + * + * @return array an array of instances of SecondaryDataUpdate + */ + public function getLinksUpdateAndOtherUpdates( Title $title = null, $recursive = true ) { + if ( empty( $title ) ) { + $title = Title::newFromText( $this->getTitleText() ); + } + + $linksUpdate = new LinksUpdate( $title, $this, $recursive ); + + if ( empty( $this->mSecondaryDataUpdates ) ) { + return array( $linksUpdate ); + } else { + $updates = array_merge( $this->mSecondaryDataUpdates, array( $linksUpdate ) ); + } + + return $updates; + } } diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index 91a51f896c..631ca64d15 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -80,7 +80,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { if ( !$revision ) { return null; } - return $revision->getRawText(); + return $revision->getRawText(); #FIXME: get raw data from content object after checking the type; } /* Methods */ diff --git a/includes/specials/SpecialComparePages.php b/includes/specials/SpecialComparePages.php index 9e3c52b981..878bda0416 100644 --- a/includes/specials/SpecialComparePages.php +++ b/includes/specials/SpecialComparePages.php @@ -111,7 +111,8 @@ class SpecialComparePages extends SpecialPage { $rev2 = self::revOrTitle( $data['Revision2'], $data['Page2'] ); if( $rev1 && $rev2 ) { - $de = new DifferenceEngine( $form->getContext(), + $contentHandler = ContentHandler::getForModelName( $rev1->getContentModelName() ); + $de = $contentHandler->getDifferenceEngine( $form->getContext(), $rev1, $rev2, null, // rcid diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index 06b578d701..47e88d0e85 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -116,7 +116,8 @@ class PageArchive { $res = $dbr->select( 'archive', array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', - 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1' + 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1', + 'ar_content_format', 'ar_content_model' ), array( 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey() ), @@ -189,6 +190,8 @@ class PageArchive { 'ar_deleted', 'ar_len', 'ar_sha1', + 'ar_content_format', + 'ar_content_model', ), array( 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey(), @@ -462,7 +465,9 @@ class PageArchive { 'ar_deleted', 'ar_page_id', 'ar_len', - 'ar_sha1' ), + 'ar_sha1', + 'ar_content_format', + 'ar_content_model' ), /* WHERE */ array( 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey(), @@ -892,7 +897,8 @@ class SpecialUndelete extends SpecialPage { * @return String: HTML */ function showDiff( $previousRev, $currentRev ) { - $diffEngine = new DifferenceEngine( $this->getContext() ); + $contentHandler = ContentHandler::getForTitle( $this->getTitle() ); + $diffEngine = $contentHandler->getDifferenceEngine( $this->getContext() ); $diffEngine->showDiffStyle(); $this->getOutput()->addHTML( "
" . @@ -909,8 +915,8 @@ class SpecialUndelete extends SpecialPage { $this->diffHeader( $currentRev, 'n' ) . "\n" . "" . - $diffEngine->generateDiffBody( - $previousRev->getText(), $currentRev->getText() ) . + $diffEngine->generateContentDiffBody( + $previousRev->getContent(), $currentRev->getContent() ) . "" . "
\n" ); diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 6b07bc692f..8fd966b8e0 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -887,6 +887,7 @@ $1', 'portal-url' => 'Project:Community portal', 'privacy' => 'Privacy policy', 'privacypage' => 'Project:Privacy policy', +'content-failed-to-parse' => "Failed to parse $2 content for $1 model: $3", 'badaccess' => 'Permission error', 'badaccess-group0' => 'You are not allowed to execute the action you have requested.', diff --git a/maintenance/archives/patch-archive-ar_content_format.sql b/maintenance/archives/patch-archive-ar_content_format.sql new file mode 100644 index 0000000000..81f9fca8cb --- /dev/null +++ b/maintenance/archives/patch-archive-ar_content_format.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/archive + ADD ar_content_format varbinary(64) DEFAULT NULL; diff --git a/maintenance/archives/patch-archive-ar_content_model.sql b/maintenance/archives/patch-archive-ar_content_model.sql new file mode 100644 index 0000000000..1a8b630e4c --- /dev/null +++ b/maintenance/archives/patch-archive-ar_content_model.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/archive + ADD ar_content_model varbinary(32) DEFAULT NULL; diff --git a/maintenance/archives/patch-page-page_content_model.sql b/maintenance/archives/patch-page-page_content_model.sql new file mode 100644 index 0000000000..30434d93ce --- /dev/null +++ b/maintenance/archives/patch-page-page_content_model.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/page + ADD page_content_model varbinary(32) DEFAULT NULL; diff --git a/maintenance/archives/patch-revision-rev_content_format.sql b/maintenance/archives/patch-revision-rev_content_format.sql new file mode 100644 index 0000000000..22aeb8a760 --- /dev/null +++ b/maintenance/archives/patch-revision-rev_content_format.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/revision + ADD rev_content_format varbinary(64) DEFAULT NULL; diff --git a/maintenance/archives/patch-revision-rev_content_model.sql b/maintenance/archives/patch-revision-rev_content_model.sql new file mode 100644 index 0000000000..1ba05721cc --- /dev/null +++ b/maintenance/archives/patch-revision-rev_content_model.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/revision + ADD rev_content_model varbinary(32) DEFAULT NULL; diff --git a/maintenance/populateRevisionLength.php b/maintenance/populateRevisionLength.php index 6626cbc1c5..cec91fb029 100644 --- a/maintenance/populateRevisionLength.php +++ b/maintenance/populateRevisionLength.php @@ -67,7 +67,7 @@ class PopulateRevisionLength extends LoggedUpdateMaintenance { # Go through and update rev_len from these rows. foreach ( $res as $row ) { $rev = new Revision( $row ); - $text = $rev->getRawText(); + $text = $rev->getRawText(); #FIXME: go via Content object; #FIXME: get size via Content object if ( !is_string( $text ) ) { # This should not happen, but sometimes does (bug 20757) $this->output( "Text of revision {$row->rev_id} unavailable!\n" ); diff --git a/maintenance/refreshLinks.php b/maintenance/refreshLinks.php index 7abbc907a9..dd8c8a71f5 100644 --- a/maintenance/refreshLinks.php +++ b/maintenance/refreshLinks.php @@ -221,6 +221,12 @@ class RefreshLinks extends Maintenance { $options = new ParserOptions; $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() ); + + $updates = $parserOutput->getLinksUpdateAndOtherUpdates( $title, false ); + SecondaryDataUpdate::runUpdates( $updates ); + + $dbw->commit(); + // TODO: We don't know what happens here. $update = new LinksUpdate( $title, $parserOutput, false ); $update->doUpdate(); $dbw->commit( __METHOD__ ); diff --git a/maintenance/tables.sql b/maintenance/tables.sql index a848bf5eb4..a0601f1e4a 100644 --- a/maintenance/tables.sql +++ b/maintenance/tables.sql @@ -260,7 +260,10 @@ CREATE TABLE /*_*/page ( page_latest int unsigned NOT NULL, -- Uncompressed length in bytes of the page's current source text. - page_len int unsigned NOT NULL + page_len int unsigned NOT NULL, + + -- content model + page_content_model varbinary(32) default NULL ) /*$wgDBTableOptions*/; CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title); @@ -316,7 +319,13 @@ CREATE TABLE /*_*/revision ( rev_parent_id int unsigned default NULL, -- SHA-1 text content hash in base-36 - rev_sha1 varbinary(32) NOT NULL default '' + rev_sha1 varbinary(32) NOT NULL default '', + + -- content model + rev_content_model varbinary(32) default NULL, + + -- content format (mime type) + rev_content_format varbinary(64) default NULL ) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024; -- In case tables are created as MyISAM, use row hints for MySQL <5.0 to avoid 4GB limit @@ -427,7 +436,14 @@ CREATE TABLE /*_*/archive ( ar_parent_id int unsigned default NULL, -- SHA-1 text content hash in base-36 - ar_sha1 varbinary(32) NOT NULL default '' + ar_sha1 varbinary(32) NOT NULL default '', + + -- content model + ar_content_model varbinary(32) default NULL, + + -- content format (mime type) + ar_content_format varbinary(64) default NULL + ) /*$wgDBTableOptions*/; CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);