host=gerrit.wikimedia.org
port=29418
project=mediawiki/core.git
-defaultbranch=master
+defaultbranch=Wikidata
*/
public $mParserOptions;
- var $mContent; // !<
+ var $mContent; // !< #BC cruft
+
+ /**
+ * @var Content
+ */
+ var $mContentObject;
+
var $mContentLoaded = false; // !<
var $mOldId; // !<
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 );
* 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__ );
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;
}
}
* 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();
# 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()
}
$this->mRevision = $this->mPage->getRevision();
+
if ( !$this->mRevision ) {
wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" );
wfProfileOut( __METHOD__ );
// @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;
}
/**
* @return Revision|null
*/
public function getRevisionFetched() {
- $this->fetchContent();
+ $this->fetchContentObject();
return $this->mRevision;
}
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 ) {
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;
}
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();
$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 );
* 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( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
- 'clearyourcache' );
+ $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
+ 'clearyourcache' );
+ }
// Give hooks a chance to customise the output
- if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) {
- // Wrap the whole lot in a <pre> and don't parse
- $m = array();
- preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
- $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
- $wgOut->addHTML( htmlspecialchars( $this->mContent ) );
- $wgOut->addHTML( "\n</pre>\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() );
}
}
// 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
* @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 ) ****** //
* @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 );
'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',
'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',
--- /dev/null
+<?php
+
+/**
+ * A content object represents page content, e.g. the text to show on a page.
+ * Content objects have no knowledge about how they relate to Wiki pages.
+ * Content objects are imutable.
+ *
+ */
+abstract class Content {
+
+ public function __construct( $modelName = null ) {
+ $this->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] <vvv> Hooks are ugly; make CodeHighlighter interface and a config to set the class which handles syntax highlighting
+ # [12:00] <vvv> 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 .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
+ $html .= htmlspecialchars( $this->getNativeData() );
+ $html .= "\n</pre>\n";
+
+ return $html;
+ }
+
+}
+
+class CssContent extends TextContent {
+ public function __construct( $text ) {
+ parent::__construct($text, CONTENT_MODEL_CSS);
+ }
+
+ protected function getHtml( ) {
+ $html = "";
+ $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
+ $html .= htmlspecialchars( $this->getNativeData() );
+ $html .= "\n</pre>\n";
+
+ return $html;
+ }
+}
+
+#FUTURE: special type for redirects?!
+#FUTURE: MultipartMultipart < WikipageContent (Main + Links + X)
+#FUTURE: LinksContent < LanguageLinksContent, CategoriesContent
+#EXAMPLE: CoordinatesContent
+#EXAMPLE: WikidataContent
--- /dev/null
+<?php
+
+class MWContentSerializationException extends MWException {
+
+}
+
+
+/**
+ * A content handler knows how do deal with a specific type of content on a wiki page.
+ * Content is stored in the database in a serialized form (using a serialization format aka mime type)
+ * and is be unserialized into it's native PHP represenation (the content model).
+ *
+ * Some content types have a flat model, that is, their native represenation is the
+ * same as their serialized form. Examples would be JavaScript and CSS code. As of now,
+ * this also applies to wikitext (mediawiki's default content type), but wikitext
+ * content may be represented by a DOM or AST structure in the future.
+ *
+ */
+abstract class ContentHandler {
+
+ public static function getContentText( Content $content = null ) {
+ global $wgContentHandlerTextFallback;
+
+ if ( is_null( $content ) ) {
+ return '';
+ }
+
+ if ( $content instanceof TextContent ) {
+ return $content->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( '' );
+ }
+
+}
'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 <pre>
+);
+
/**
* Resizing can be done using PHP's internal image libraries or using
* ImageMagick or another third-party converter, e.g. GraphicMagick.
$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
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');
+
*/
const AS_IMAGE_REDIRECT_LOGGED = 234;
+ /**
+ * Status: can't parse content
+ */
+ const AS_PARSE_ERROR = 240;
+
/**
* @var Article
*/
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
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;
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
}
/**
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 );
# 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() ) ) );
// 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" );
}
} 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 = '';
}
}
+ $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',
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' ) ) {
* @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' );
# 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 {
wfMsgNoTrans( 'undo-' . $undoMsg ) . '</div>', true, /* interface */true );
}
- if ( $text === false ) {
- $text = $this->getOriginalContent();
+ if ( $content === false ) {
+ $content = $this->getOriginalContent();
}
}
}
wfProfileOut( __METHOD__ );
- return $text;
+ return $content;
}
/**
* @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;
}
}
* 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;
}
/**
*
* @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 );
$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 );
}
/**
case self::AS_FILTERING:
return false;
+ case self::AS_PARSE_ERROR:
+ $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>');
+ #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'] : '';
# 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 );
$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' ) {
// 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
// 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;
}
}
* @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 );
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 );
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 {
} elseif ( $contextTitle->exists() && $this->section != '' ) {
$msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
} else {
- $msg = $contextTitle->exists() || ( $contextTitle->getNamespace() == NS_MEDIAWIKI && $contextTitle->getDefaultMessageText() !== false ) ?\r
- 'editing' : 'creating';\r
+ $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;\r
- if ( $displayTitle === false ) {\r
- $displayTitle = $contextTitle->getPrefixedText();\r
+ $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
+ if ( $displayTitle === false ) {
+ $displayTitle = $contextTitle->getPrefixedText();
}
$wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
}
}
}
+ #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' ) ) );
$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 ) );
// 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 {
$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 );
* 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() ) {
$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 {
if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
$wgOut->wrapWikiMsg( '<h2>$1</h2>', "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( '<h2>$1</h2>', "yourtext" );
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 = "<div id='mw-{$level}{$format}preview'>" . wfMsg( "{$level}{$format}preview" ) . "</div>";
+ } 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' );
$wgOut->addHTML( '</div>' );
$wgOut->wrapWikiMsg( '<h2>$1</h2>', "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( '<h2>$1</h2>', "yourtext" );
$this->showTextbox2();
// 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 );
$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',
$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
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,
*
* @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
/**@}}*/
* @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();
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
*/
$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
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
protected $mTextRow;
protected $mTitle;
protected $mCurrent;
+ protected $mContentModelName;
+ protected $mContentFormat;
+ protected $mContent;
+ protected $mContentHandler;
const DELETED_TEXT = 1;
const DELETED_COMMENT = 2;
'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
'rev_deleted',
'rev_len',
'rev_parent_id',
- 'rev_sha1'
+ 'rev_sha1',
+ 'rev_content_format',
+ 'rev_content_model'
);
}
$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 ) ) {
// 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;
$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;
$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
}
/**
* @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)
*
*
* @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
*/
$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();
$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',
'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;
--- /dev/null
+<?php
+/**
+ * See docs/deferred.txt
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * Abstract base class for update jobs that put some secondary data extracted
+ * from article content into the database.
+ */
+abstract class SecondaryDBDataUpdate extends SecondaryDataUpdate {
+
+ /**@{{
+ * @private
+ */
+ var $mDb, //!< Database connection reference
+ $mOptions; //!< SELECT options to be used (array)
+ /**@}}*/
+
+ /**
+ * Constructor
+ **/
+ public function __construct( ) {
+ global $wgAntiLockFlags;
+
+ parent::__construct( );
+
+ if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
+ $this->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__
+ );
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * See docs/deferred.txt
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * Abstract base class for update jobs that do something with some secondary
+ * data extracted from article.
+ */
+abstract class SecondaryDataUpdate {
+
+ /**
+ * Constructor
+ */
+ public function __construct( ) {
+ # noop
+ }
+
+ /**
+ * Perform update.
+ */
+ public abstract function doUpdate();
+
+ /**
+ * Conveniance method, calls doUpdate() on every element in the array.
+ *
+ * @static
+ * @param $updates array
+ */
+ public static function runUpdates( $updates ) {
+ if ( empty( $updates ) ) return; # nothing to do
+
+ foreach ( $updates as $update ) {
+ $update->doUpdate();
+ }
+ }
+
+}
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()
}
}
$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;
}
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
*
* @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;
}
/**
* @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() );
}
/**
* @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 ) );
}
/**
* @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 ) );
}
/**
'page_touched',
'page_latest',
'page_len',
+ 'page_content_model',
);
}
* @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
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...
*
* 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 );
*
* @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
*/
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 );
}
/**
*/
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;
}
&& $parserOptions->getStubThreshold() == 0
&& $this->mTitle->exists()
&& ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
- && $this->mTitle->isWikitextPage();
+ && $this->mTitle->isWikitextPage(); #FIXME: ask ContentHandler if cachable!
}
/**
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;
}
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() );
* @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;
}
/**
* @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.
* 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
$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' );
$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();
'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__ );
'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 );
$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 ) {
# 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
// 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' );
/**
* 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;
$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;
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;
$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'] ) );
}
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
}
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'] ) {
* @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,
) );
'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'
}
# 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 {
* @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 );
}
/**
* @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
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();
}
* @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 );
}
$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 );
$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();
}
}
$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();
}
}
$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( '', '' );
}
}
$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
// 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() ) );
}
// 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'];
}
// 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 );
$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 );
// 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 );
$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'] = '';
}
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;
$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();
}
* @private
* @ingroup DifferenceEngine
*/
-class _DiffOp {
+class _DiffOp { #FIXME: no longer private!
var $type;
var $orig;
var $closing;
* @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 ) {
* @private
* @ingroup DifferenceEngine
*/
-class _DiffOp_Delete extends _DiffOp {
+class _DiffOp_Delete extends _DiffOp { #FIXME: no longer private!
var $type = 'delete';
function __construct( $lines ) {
* @private
* @ingroup DifferenceEngine
*/
-class _DiffOp_Add extends _DiffOp {
+class _DiffOp_Add extends _DiffOp { #FIXME: no longer private!
var $type = 'add';
function __construct( $lines ) {
* @private
* @ingroup DifferenceEngine
*/
-class _DiffOp_Change extends _DiffOp {
+class _DiffOp_Change extends _DiffOp { #FIXME: no longer private!
var $type = 'change';
function __construct( $orig, $closing ) {
* @private
* @ingroup DifferenceEngine
*/
-class _DiffEngine {
+class _DiffEngine { #FIXME: no longer private!
const MAX_XREF_LENGTH = 10000;
* @private
* @ingroup DifferenceEngine
*/
-class Diff {
+class Diff extends DiffResult {
var $edits;
/**
* @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;
}
/**
* @private
*/
var $mOldid, $mNewid;
- var $mOldtext, $mNewtext;
+ var $mOldContent, $mNewContent;
protected $mDiffLang;
/**
$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 <pre> and don't parse
- $m = array();
- preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
- $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
- $out->addHTML( htmlspecialchars( $this->mNewtext ) );
- $out->addHTML( "\n</pre>\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 ) ) {
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 ) ) ) {
}
}
+ /**
+ * 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__ );
/**
* 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
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;
}
}
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;
}
}
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' ),
);
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
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' ),
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
$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;
}
$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();
}
*/
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,
$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="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
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;
+ }
}
if ( !$revision ) {
return null;
}
- return $revision->getRawText();
+ return $revision->getRawText(); #FIXME: get raw data from content object after checking the type;
}
/* Methods */
$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
$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() ),
'ar_deleted',
'ar_len',
'ar_sha1',
+ 'ar_content_format',
+ 'ar_content_model',
),
array( 'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey(),
'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(),
* @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(
"<div>" .
$this->diffHeader( $currentRev, 'n' ) .
"</td>\n" .
"</tr>" .
- $diffEngine->generateDiffBody(
- $previousRev->getText(), $currentRev->getText() ) .
+ $diffEngine->generateContentDiffBody(
+ $previousRev->getContent(), $currentRev->getContent() ) .
"</table>" .
"</div>\n"
);
'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.',
--- /dev/null
+ALTER TABLE /*$wgDBprefix*/archive
+ ADD ar_content_format varbinary(64) DEFAULT NULL;
--- /dev/null
+ALTER TABLE /*$wgDBprefix*/archive
+ ADD ar_content_model varbinary(32) DEFAULT NULL;
--- /dev/null
+ALTER TABLE /*$wgDBprefix*/page
+ ADD page_content_model varbinary(32) DEFAULT NULL;
--- /dev/null
+ALTER TABLE /*$wgDBprefix*/revision
+ ADD rev_content_format varbinary(64) DEFAULT NULL;
--- /dev/null
+ALTER TABLE /*$wgDBprefix*/revision
+ ADD rev_content_model varbinary(32) DEFAULT NULL;
# 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" );
$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__ );
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);
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
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);