From: daniel ' . wfMessage( 'newpage' )->text() . ' ' . htmlspecialchars( $revision->getUserText() ) . $msg .
htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
- " and don't parse
- $m = array();
- preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
- $outputPage->addHTML( "
\n" );
- $outputPage->addHTML( htmlspecialchars( $this->mContent ) );
- $outputPage->addHTML( "\n
\n" );
+ if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->fetchContentObject(), $this->getTitle(), $wgOut ) ) ) {
+ $po = $this->mContentObject->getParserOutput( $this->getTitle() );
+ $wgOut->addHTML( $po->getText() );
}
}
@@ -1377,7 +1474,13 @@ class Article extends Page {
// Generate deletion reason
$hasHistory = false;
if ( !$reason ) {
- $reason = $this->generateReason( $hasHistory );
+ try {
+ $reason = $this->generateReason( $hasHistory );
+ } catch (MWException $e) {
+ # if a page is horribly broken, we still want to be able to delete it. so be lenient about errors here.
+ wfDebug("Error while building auto delete summary: $e");
+ $reason = '';
+ }
}
// If the page has a history, insert a warning
@@ -1617,6 +1720,8 @@ class Article extends Page {
* @return ParserOutput or false if the given revsion ID is not found
*/
public function getParserOutput( $oldid = null, User $user = null ) {
+ //XXX: bypasses mParserOptions and thus setParserOptions()
+
if ( $user === null ) {
$parserOptions = $this->getParserOptions();
} else {
@@ -1626,6 +1731,21 @@ class Article extends Page {
return $this->mPage->getParserOutput( $parserOptions, $oldid );
}
+ /**
+ * Override the ParserOptions used to render the primary article wikitext.
+ *
+ * @param ParserOptions $options
+ * @throws MWException if the parser options where already initialized.
+ */
+ public function setParserOptions( ParserOptions $options ) {
+ if ( $this->mParserOptions ) {
+ throw new MWException( "can't change parser options after they have already been set" );
+ }
+
+ // clone, so if $options is modified later, it doesn't confuse the parser cache.
+ $this->mParserOptions = clone $options;
+ }
+
/**
* Get parser options suitable for rendering the primary article wikitext
* @return ParserOptions
@@ -1891,7 +2011,9 @@ class Article extends Page {
* @return mixed
*/
public function generateReason( &$hasHistory ) {
- return $this->mPage->getAutoDeleteReason( $hasHistory );
+ $title = $this->mPage->getTitle();
+ $handler = ContentHandler::getForTitle( $title );
+ return $handler->getAutoDeleteReason( $title, $hasHistory );
}
// ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** //
@@ -1929,6 +2051,7 @@ class Article extends Page {
* @param $newtext
* @param $flags
* @return string
+ * @deprecated since 1.WD, use ContentHandler::getAutosummary() instead
*/
public static function getAutosummary( $oldtext, $newtext, $flags ) {
return WikiPage::getAutosummary( $oldtext, $newtext, $flags );
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index 706e1c83e2..788e3f2fb4 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -253,6 +253,7 @@ $wgAutoloadLocalClasses = array(
'UnlistedSpecialPage' => 'includes/SpecialPage.php',
'UploadSourceAdapter' => 'includes/Import.php',
'UppercaseCollation' => 'includes/Collation.php',
+ 'Uri' => 'includes/Uri.php',
'User' => 'includes/User.php',
'UserArray' => 'includes/UserArray.php',
'UserArrayFromResult' => 'includes/UserArray.php',
@@ -288,6 +289,20 @@ $wgAutoloadLocalClasses = array(
'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
'ZipDirectoryReaderError' => 'includes/ZipDirectoryReader.php',
+ # content handler
+ 'Content' => 'includes/Content.php',
+ 'AbstractContent' => 'includes/Content.php',
+ 'ContentHandler' => 'includes/ContentHandler.php',
+ 'CssContent' => 'includes/Content.php',
+ 'TextContentHandler' => 'includes/ContentHandler.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
'CachedAction' => 'includes/actions/CachedAction.php',
'CreditsAction' => 'includes/actions/CreditsAction.php',
@@ -330,6 +345,7 @@ $wgAutoloadLocalClasses = array(
'ApiFormatDump' => 'includes/api/ApiFormatDump.php',
'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php',
'ApiFormatJson' => 'includes/api/ApiFormatJson.php',
+ 'ApiFormatNone' => 'includes/api/ApiFormatNone.php',
'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php',
'ApiFormatRaw' => 'includes/api/ApiFormatRaw.php',
'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php',
@@ -1057,6 +1073,13 @@ $wgAutoloadLocalClasses = array(
'TestFileIterator' => 'tests/testHelpers.inc',
'TestRecorder' => 'tests/testHelpers.inc',
+ # tests/phpunit
+ 'RevisionStorageTest' => 'tests/phpunit/includes/RevisionStorageTest.php',
+ 'WikiPageTest' => 'tests/phpunit/includes/WikiPageTest.php',
+ 'WikitextContentTest' => 'tests/phpunit/includes/WikitextContentTest.php',
+ 'JavascriptContentTest' => 'tests/phpunit/includes/JavascriptContentTest.php',
+ 'DummyContentHandlerForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
+ 'DummyContentForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
# tests/phpunit/includes
'GenericArrayObjectTest' => 'tests/phpunit/includes/libs/GenericArrayObjectTest.php',
diff --git a/includes/Content.php b/includes/Content.php
new file mode 100644
index 0000000000..da3b8ffc77
--- /dev/null
+++ b/includes/Content.php
@@ -0,0 +1,1471 @@
+getContentHandler()->getDefaultFormat()
+ *
+ * @since WD.1
+ *
+ * @return String
+ */
+ public function getDefaultFormat();
+
+ /**
+ * Convenience method that returns the list of serialization formats
+ * supported for the content model that this Content object uses.
+ *
+ * Shorthand for $this->getContentHandler()->getSupportedFormats()
+ *
+ * @since WD.1
+ *
+ * @return Array of supported serialization formats
+ */
+ public function getSupportedFormats();
+
+ /**
+ * Returns true if $format is a supported serialization format for this
+ * Content object, false if it isn't.
+ *
+ * Note that this should always return true if $format is null, because null
+ * stands for the default serialization.
+ *
+ * Shorthand for $this->getContentHandler()->isSupportedFormat( $format )
+ *
+ * @since WD.1
+ *
+ * @param $format string The format to check
+ * @return bool Whether the format is supported
+ */
+ public function isSupportedFormat( $format );
+
+ /**
+ * Convenience method for serializing this Content object.
+ *
+ * Shorthand for $this->getContentHandler()->serializeContent( $this, $format )
+ *
+ * @since WD.1
+ *
+ * @param $format null|string The desired serialization format (or null for
+ * the default format).
+ * @return string Serialized form of this Content object
+ */
+ public function serialize( $format = null );
+
+ /**
+ * Returns true if this Content object represents empty content.
+ *
+ * @since WD.1
+ *
+ * @return bool Whether this Content object is empty
+ */
+ public function isEmpty();
+
+ /**
+ * Returns whether the content is valid. This is intended for local validity
+ * checks, not considering global consistency.
+ *
+ * Content needs to be valid before it can be saved.
+ *
+ * This default implementation always returns true.
+ *
+ * @since WD.1
+ *
+ * @return boolean
+ */
+ public function isValid();
+
+ /**
+ * Returns true if this Content objects is conceptually equivalent to the
+ * given Content object.
+ *
+ * Contract:
+ *
+ * - Will return false if $that is null.
+ * - Will return true if $that === $this.
+ * - Will return false if $that->getModelName() != $this->getModel().
+ * - Will return false if $that->getNativeData() is not equal to $this->getNativeData(),
+ * where the meaning of "equal" depends on the actual data model.
+ *
+ * Implementations should be careful to make equals() transitive and reflexive:
+ *
+ * - $a->equals( $b ) <=> $b->equals( $a )
+ * - $a->equals( $b ) && $b->equals( $c ) ==> $a->equals( $c )
+ *
+ * @since WD.1
+ *
+ * @param $that Content The Content object to compare to
+ * @return bool True if this Content object is equal to $that, false otherwise.
+ */
+ public function equals( Content $that = null );
+
+ /**
+ * Return a copy of this Content object. The following must be true for the
+ * object returned:
+ *
+ * if $copy = $original->copy()
+ *
+ * - get_class($original) === get_class($copy)
+ * - $original->getModel() === $copy->getModel()
+ * - $original->equals( $copy )
+ *
+ * If and only if the Content object is immutable, the copy() method can and
+ * should return $this. That is, $copy === $original may be true, but only
+ * for immutable content objects.
+ *
+ * @since WD.1
+ *
+ * @return Content. A copy of this object
+ */
+ public function copy( );
+
+ /**
+ * 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).
+ *
+ * @since WD.1
+ *
+ * @param $hasLinks Bool: If it is known whether this content contains
+ * links, provide this information here, to avoid redundant parsing to
+ * find out.
+ * @return boolean
+ */
+ public function isCountable( $hasLinks = null ) ;
+
+
+ /**
+ * Parse the Content object and generate a ParserOutput from the result.
+ * $result->getText() can be used to obtain the generated HTML. If no HTML
+ * is needed, $generateHtml can be set to false; in that case,
+ * $result->getText() may return null.
+ *
+ * @param $title Title The page title to use as a context for rendering
+ * @param $revId null|int The revision being rendered (optional)
+ * @param $options null|ParserOptions Any parser options
+ * @param $generateHtml Boolean Whether to generate HTML (default: true). If false,
+ * the result of calling getText() on the ParserOutput object returned by
+ * this method is undefined.
+ *
+ * @since WD.1
+ *
+ * @return ParserOutput
+ */
+ public function getParserOutput( Title $title,
+ $revId = null,
+ ParserOptions $options = null, $generateHtml = true );
+ # TODO: make RenderOutput and RenderOptions base classes
+
+ /**
+ * Returns a list of DataUpdate objects for recording information about this
+ * Content in some secondary data store. If the optional second argument,
+ * $old, is given, the updates may model only the changes that need to be
+ * made to replace information about the old content with information about
+ * the new content.
+ *
+ * This default implementation calls
+ * $this->getParserOutput( $content, $title, null, null, false ),
+ * and then calls getSecondaryDataUpdates( $title, $recursive ) on the
+ * resulting ParserOutput object.
+ *
+ * Subclasses may implement this to determine the necessary updates more
+ * efficiently, or make use of information about the old content.
+ *
+ * @param $title Title The context for determining the necessary updates
+ * @param $old Content|null An optional Content object representing the
+ * previous content, i.e. the content being replaced by this Content
+ * object.
+ * @param $recursive boolean Whether to include recursive updates (default:
+ * false).
+ * @param $parserOutput ParserOutput|null Optional ParserOutput object.
+ * Provide if you have one handy, to avoid re-parsing of the content.
+ *
+ * @return Array. A list of DataUpdate objects for putting information
+ * about this content object somewhere.
+ *
+ * @since WD.1
+ */
+ public function getSecondaryDataUpdates( Title $title,
+ Content $old = null,
+ $recursive = true, ParserOutput $parserOutput = 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).
+ *
+ * @since WD.1
+ *
+ * @return Array of Titles, with the destination last
+ */
+ public function getRedirectChain();
+
+ /**
+ * Construct the redirect destination from this content and return a Title,
+ * 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.
+ *
+ * @since WD.1
+ *
+ * @return Title: The corresponding Title
+ */
+ public function getRedirectTarget();
+
+ /**
+ * 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.
+ *
+ * There is usually no need to override the default behaviour, subclasses that
+ * want to implement redirects should override getRedirectTarget().
+ *
+ * @since WD.1
+ *
+ * @return Title
+ */
+ public function getUltimateRedirectTarget();
+
+ /**
+ * Returns whether this Content represents a redirect.
+ * Shorthand for getRedirectTarget() !== null.
+ *
+ * @since WD.1
+ *
+ * @return bool
+ */
+ public function isRedirect();
+
+ /**
+ * If this Content object is a redirect, this method updates the redirect target.
+ * Otherwise, it does nothing.
+ *
+ * @since WD.1
+ *
+ * @param Title $target the new redirect target
+ *
+ * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
+ */
+ public function updateRedirect( Title $target );
+
+ /**
+ * Returns the section with the given ID.
+ *
+ * @since WD.1
+ *
+ * @param $sectionId string The section's ID, given as a numeric string.
+ * The ID "0" retrieves the section before the first heading, "1" the
+ * text between the first heading (included) and the second heading
+ * (excluded), etc.
+ * @return Content|Boolean|null The section, or false if no such section
+ * exist, or null if sections are not supported.
+ */
+ public function getSection( $sectionId );
+
+ /**
+ * Replaces a section of the content and returns a Content object with the
+ * section replaced.
+ *
+ * @since WD.1
+ *
+ * @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 = '' );
+
+ /**
+ * Returns a Content object with pre-save transformations applied (or this
+ * object if no transformations apply).
+ *
+ * @since WD.1
+ *
+ * @param $title Title
+ * @param $user User
+ * @param $popts null|ParserOptions
+ * @return Content
+ */
+ public function preSaveTransform( Title $title, User $user, ParserOptions $popts );
+
+ /**
+ * 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.
+ *
+ * @since WD.1
+ *
+ * @param $header string
+ * @return Content
+ */
+ public function addSectionHeader( $header );
+
+ /**
+ * Returns a Content object with preload transformations applied (or this
+ * object if no transformations apply).
+ *
+ * @since WD.1
+ *
+ * @param $title Title
+ * @param $popts null|ParserOptions
+ * @return Content
+ */
+ public function preloadTransform( Title $title, ParserOptions $popts );
+
+ /**
+ * Prepare Content for saving. Called before Content is saved by WikiPage::doEditContent() and in
+ * similar places.
+ *
+ * This may be used to check the content's consistency with global state. This function should
+ * NOT write any information to the database.
+ *
+ * Note that this method will usually be called inside the same transaction bracket that will be used
+ * to save the new revision.
+ *
+ * Note that this method is called before any update to the page table is performed. This means that
+ * $page may not yet know a page ID.
+ *
+ * @param WikiPage $page The page to be saved.
+ * @param int $flags bitfield for use with EDIT_XXX constants, see WikiPage::doEditContent()
+ * @param int $baseRevId the ID of the current revision
+ * @param User $user
+ *
+ * @return Status A status object indicating whether the content was successfully prepared for saving.
+ * If the returned status indicates an error, a rollback will be performed and the
+ * transaction aborted.
+ *
+ * @see see WikiPage::doEditContent()
+ */
+ public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user );
+
+ /**
+ * Returns a list of updates to perform when this content is deleted.
+ * The necessary updates may be taken from the Content object, or depend on
+ * the current state of the database.
+ *
+ * @since WD.1
+ *
+ * @param $page \WikiPage the deleted page
+ * @param $parserOutput null|\ParserOutput optional parser output object
+ * for efficient access to meta-information about the content object.
+ * Provide if you have one handy.
+ *
+ * @return array A list of DataUpdate instances that will clean up the
+ * database after deletion.
+ */
+ public function getDeletionUpdates( WikiPage $page,
+ ParserOutput $parserOutput = null );
+
+ /**
+ * Returns true if this Content object matches the given magic word.
+ *
+ * @param MagicWord $word the magic word to match
+ *
+ * @return bool whether this Content object matches the given magic word.
+ */
+ public function matchMagicWord( MagicWord $word );
+
+ # TODO: ImagePage and CategoryPage interfere with per-content action handlers
+ # TODO: make sure WikiSearch extension still works
+ # TODO: make sure ReplaceTemplates extension still works
+ # TODO: nice&sane integration of GeSHi syntax highlighting
+ # [11:59] \n";
+ $html .= $this->getHighlightHtml( );
+ $html .= "\n
\n";
+
+ return $html;
+ }
+}
+
+/**
+ * @since WD.1
+ */
+class CssContent extends TextContent {
+ public function __construct( $text ) {
+ parent::__construct( $text, CONTENT_MODEL_CSS );
+ }
+
+ /**
+ * Returns a Content object with pre-save transformations applied using
+ * Parser::preSaveTransform().
+ *
+ * @param $title Title
+ * @param $user User
+ * @param $popts ParserOptions
+ * @return Content
+ */
+ public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+ global $wgParser;
+ // @todo: make pre-save transformation optional for script pages
+
+ $text = $this->getNativeData();
+ $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+ return new CssContent( $pst );
+ }
+
+
+ protected function getHtml( ) {
+ $html = "";
+ $html .= "\n";
+ $html .= $this->getHighlightHtml( );
+ $html .= "\n
\n";
+
+ return $html;
+ }
+}
diff --git a/includes/ContentHandler.php b/includes/ContentHandler.php
new file mode 100644
index 0000000000..bf1349adc8
--- /dev/null
+++ b/includes/ContentHandler.php
@@ -0,0 +1,1136 @@
+getNativeData().
+ *
+ * If $content is not a TextContent object, the behavior of this method
+ * depends on the global $wgContentHandlerTextFallback:
+ * - If $wgContentHandlerTextFallback is 'fail' and $content is not a
+ * TextContent object, an MWException is thrown.
+ * - If $wgContentHandlerTextFallback is 'serialize' and $content is not a
+ * TextContent object, $content->serialize() is called to get a string
+ * form of the content.
+ * - If $wgContentHandlerTextFallback is 'ignore' and $content is not a
+ * TextContent object, this method returns null.
+ * - otherwise, the behaviour is undefined.
+ *
+ * @since WD.1
+ * @deprecated since WD.1. Always try to use the content object.
+ *
+ * @static
+ * @param $content Content|null
+ * @return null|string the textual form of $content, if available
+ * @throws MWException if $content is not an instance of TextContent and
+ * $wgContentHandlerTextFallback was set to 'fail'.
+ */
+ 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->getModel()
+ );
+ }
+
+ if ( $wgContentHandlerTextFallback == 'serialize' ) {
+ return $content->serialize();
+ }
+
+ return null;
+ }
+
+ /**
+ * Convenience function for creating a Content object from a given textual
+ * representation.
+ *
+ * $text will be deserialized into a Content object of the model specified
+ * by $modelId (or, if that is not given, $title->getContentModel()) using
+ * the given format.
+ *
+ * @since WD.1
+ *
+ * @static
+ *
+ * @param $text string the textual representation, will be
+ * unserialized to create the Content object
+ * @param $title null|Title the title of the page this text belongs to.
+ * Required if $modelId is not provided.
+ * @param $modelId null|string the model to deserialize to. If not provided,
+ * $title->getContentModel() is used.
+ * @param $format null|string the format to use for deserialization. If not
+ * given, the model's default format is used.
+ *
+ * @return Content a Content object representing $text
+ *
+ * @throw MWException if $model or $format is not supported or if $text can
+ * not be unserialized using $format.
+ */
+ public static function makeContent( $text, Title $title = null,
+ $modelId = null, $format = null )
+ {
+ if ( is_null( $modelId ) ) {
+ if ( is_null( $title ) ) {
+ throw new MWException( "Must provide a Title object or a content model ID." );
+ }
+
+ $modelId = $title->getContentModel();
+ }
+
+ $handler = ContentHandler::getForModelID( $modelId );
+ return $handler->unserializeContent( $text, $format );
+ }
+
+ /**
+ * Returns the name of the default content model to be used for the page
+ * with the given title.
+ *
+ * Note: There should rarely be need to call this method directly.
+ * To determine the actual content model for a given page, use
+ * Title::getContentModel().
+ *
+ * Which model is to be used by default for the page is determined based
+ * on several factors:
+ * - The global setting $wgNamespaceContentModels specifies a content model
+ * per namespace.
+ * - The hook DefaultModelFor may be used to override the page's default
+ * model.
+ * - Pages in NS_MEDIAWIKI and NS_USER default to the CSS or JavaScript
+ * model if they end in .js or .css, respectively.
+ * - Pages in NS_MEDIAWIKI default to the wikitext model otherwise.
+ * - The hook TitleIsCssOrJsPage may be used to force a page to use the CSS
+ * or JavaScript model if they end in .js or .css, respectively.
+ * - The hook TitleIsWikitextPage may be used to force a page to use the
+ * wikitext model.
+ *
+ * If none of the above applies, the wikitext model is used.
+ *
+ * Note: this is used by, and may thus not use, Title::getContentModel()
+ *
+ * @since WD.1
+ *
+ * @static
+ * @param $title Title
+ * @return null|string default model name for the page given by $title
+ */
+ public static function getDefaultModelFor( Title $title ) {
+ global $wgNamespaceContentModels;
+
+ // NOTE: this method must not rely on $title->getContentModel() directly or indirectly,
+ // because it is used to initialize the mContentModel member.
+
+ $ns = $title->getNamespace();
+
+ $ext = false;
+ $m = null;
+ $model = null;
+
+ if ( !empty( $wgNamespaceContentModels[ $ns ] ) ) {
+ $model = $wgNamespaceContentModels[ $ns ];
+ }
+
+ // Hook can determine default model
+ if ( !wfRunHooks( 'ContentHandlerDefaultModelFor', array( $title, &$model ) ) ) {
+ 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 it must be wikitext
+
+ return CONTENT_MODEL_WIKITEXT;
+ }
+
+ /**
+ * Returns the appropriate ContentHandler singleton for the given title.
+ *
+ * @since WD.1
+ *
+ * @static
+ * @param $title Title
+ * @return ContentHandler
+ */
+ public static function getForTitle( Title $title ) {
+ $modelId = $title->getContentModel();
+ return ContentHandler::getForModelID( $modelId );
+ }
+
+ /**
+ * Returns the appropriate ContentHandler singleton for the given Content
+ * object.
+ *
+ * @since WD.1
+ *
+ * @static
+ * @param $content Content
+ * @return ContentHandler
+ */
+ public static function getForContent( Content $content ) {
+ $modelId = $content->getModel();
+ return ContentHandler::getForModelID( $modelId );
+ }
+
+ /**
+ * @var Array A Cache of ContentHandler instances by model id
+ */
+ static $handlers;
+
+ /**
+ * Returns the ContentHandler singleton for the given model ID. Use the
+ * CONTENT_MODEL_XXX constants to identify the desired content model.
+ *
+ * ContentHandler singletons are taken from the global $wgContentHandlers
+ * array. Keys in that array are model names, the values are either
+ * ContentHandler singleton objects, or strings specifying the appropriate
+ * subclass of ContentHandler.
+ *
+ * If a class name is encountered when looking up the singleton for a given
+ * model name, the class is instantiated and the class name is replaced by
+ * the resulting singleton in $wgContentHandlers.
+ *
+ * If no ContentHandler is defined for the desired $modelId, the
+ * ContentHandler may be provided by the ContentHandlerForModelID hook.
+ * If no ContentHandler can be determined, an MWException is raised.
+ *
+ * @since WD.1
+ *
+ * @static
+ * @param $modelId String The ID of the content model for which to get a
+ * handler. Use CONTENT_MODEL_XXX constants.
+ * @return ContentHandler The ContentHandler singleton for handling the
+ * model given by $modelId
+ * @throws MWException if no handler is known for $modelId.
+ */
+ public static function getForModelID( $modelId ) {
+ global $wgContentHandlers;
+
+ if ( isset( ContentHandler::$handlers[$modelId] ) ) {
+ return ContentHandler::$handlers[$modelId];
+ }
+
+ if ( empty( $wgContentHandlers[$modelId] ) ) {
+ $handler = null;
+
+ wfRunHooks( 'ContentHandlerForModelID', array( $modelId, &$handler ) );
+
+ if ( $handler === null ) {
+ throw new MWException( "No handler for model #$modelId registered in \$wgContentHandlers" );
+ }
+
+ if ( !( $handler instanceof ContentHandler ) ) {
+ throw new MWException( "ContentHandlerForModelID must supply a ContentHandler instance" );
+ }
+ } else {
+ $class = $wgContentHandlers[$modelId];
+ $handler = new $class( $modelId );
+
+ if ( !( $handler instanceof ContentHandler ) ) {
+ throw new MWException( "$class from \$wgContentHandlers is not compatible with ContentHandler" );
+ }
+ }
+
+ ContentHandler::$handlers[$modelId] = $handler;
+ return ContentHandler::$handlers[$modelId];
+ }
+
+ /**
+ * Returns the localized name for a given content model.
+ *
+ * Model names are localized using system messages. Message keys
+ * have the form content-model-$name, where $name is getContentModelName( $id ).
+ *
+ * @static
+ * @param $name String The content model ID, as given by a CONTENT_MODEL_XXX
+ * constant or returned by Revision::getContentModel().
+ *
+ * @return string The content format's localized name.
+ * @throws MWException if the model id isn't known.
+ */
+ public static function getLocalizedName( $name ) {
+ $key = "content-model-$name";
+
+ if ( wfEmptyMsg( $key ) ) return $name;
+ else return wfMsg( $key );
+ }
+
+ public static function getContentModels() {
+ global $wgContentHandlers;
+
+ return array_keys( $wgContentHandlers );
+ }
+
+ public static function getAllContentFormats() {
+ global $wgContentHandlers;
+
+ $formats = array();
+
+ foreach ( $wgContentHandlers as $model => $class ) {
+ $handler = ContentHandler::getForModelID( $model );
+ $formats = array_merge( $formats, $handler->getSupportedFormats() );
+ }
+
+ $formats = array_unique( $formats );
+ return $formats;
+ }
+
+ // ------------------------------------------------------------------------
+
+ protected $mModelID;
+ protected $mSupportedFormats;
+
+ /**
+ * Constructor, initializing the ContentHandler instance with its model ID
+ * and a list of supported formats. Values for the parameters are typically
+ * provided as literals by subclass's constructors.
+ *
+ * @param $modelId String (use CONTENT_MODEL_XXX constants).
+ * @param $formats array List for supported serialization formats
+ * (typically as MIME types)
+ */
+ public function __construct( $modelId, $formats ) {
+ $this->mModelID = $modelId;
+ $this->mSupportedFormats = $formats;
+
+ $this->mModelName = preg_replace( '/(Content)?Handler$/', '', get_class( $this ) );
+ $this->mModelName = preg_replace( '/[_\\\\]/', '', $this->mModelName );
+ $this->mModelName = strtolower( $this->mModelName );
+ }
+
+ /**
+ * Serializes a Content object of the type supported by this ContentHandler.
+ *
+ * @since WD.1
+ *
+ * @abstract
+ * @param $content Content The Content object to serialize
+ * @param $format null|String The desired serialization format
+ * @return string Serialized form of the content
+ */
+ public abstract function serializeContent( Content $content, $format = null );
+
+ /**
+ * Unserializes a Content object of the type supported by this ContentHandler.
+ *
+ * @since WD.1
+ *
+ * @abstract
+ * @param $blob string serialized form of the content
+ * @param $format null|String the format used for serialization
+ * @return Content the Content object created by deserializing $blob
+ */
+ public abstract function unserializeContent( $blob, $format = null );
+
+ /**
+ * Creates an empty Content object of the type supported by this
+ * ContentHandler.
+ *
+ * @since WD.1
+ *
+ * @return Content
+ */
+ public abstract function makeEmptyContent();
+
+ /**
+ * Returns the model id that identifies the content model this
+ * ContentHandler can handle. Use with the CONTENT_MODEL_XXX constants.
+ *
+ * @since WD.1
+ *
+ * @return String The model ID
+ */
+ public function getModelID() {
+ return $this->mModelID;
+ }
+
+ /**
+ * Throws an MWException if $model_id is not the ID of the content model
+ * supported by this ContentHandler.
+ *
+ * @since WD.1
+ *
+ * @param String $model_id The model to check
+ *
+ * @throws MWException
+ */
+ protected function checkModelID( $model_id ) {
+ if ( $model_id !== $this->mModelID ) {
+ throw new MWException( "Bad content model: " .
+ "expected {$this->mModelID} " .
+ "but got $model_id." );
+ }
+ }
+
+ /**
+ * Returns a list of serialization formats supported by the
+ * serializeContent() and unserializeContent() methods of this
+ * ContentHandler.
+ *
+ * @since WD.1
+ *
+ * @return array of serialization formats as MIME type like strings
+ */
+ public function getSupportedFormats() {
+ return $this->mSupportedFormats;
+ }
+
+ /**
+ * The format used for serialization/deserialization by default by this
+ * ContentHandler.
+ *
+ * This default implementation will return the first element of the array
+ * of formats that was passed to the constructor.
+ *
+ * @since WD.1
+ *
+ * @return string the name of the default serialization format as a MIME type
+ */
+ public function getDefaultFormat() {
+ return $this->mSupportedFormats[0];
+ }
+
+ /**
+ * Returns true if $format is a serialization format supported by this
+ * ContentHandler, and false otherwise.
+ *
+ * Note that if $format is null, this method always returns true, because
+ * null means "use the default format".
+ *
+ * @since WD.1
+ *
+ * @param $format string the serialization format to check
+ * @return bool
+ */
+ public function isSupportedFormat( $format ) {
+
+ if ( !$format ) {
+ return true; // this means "use the default"
+ }
+
+ return in_array( $format, $this->mSupportedFormats );
+ }
+
+ /**
+ * Throws an MWException if isSupportedFormat( $format ) is not true.
+ * Convenient for checking whether a format provided as a parameter is
+ * actually supported.
+ *
+ * @param $format string the serialization format to check
+ *
+ * @throws MWException
+ */
+ protected function checkFormat( $format ) {
+ if ( !$this->isSupportedFormat( $format ) ) {
+ throw new MWException(
+ "Format $format is not supported for content model "
+ . $this->getModelID()
+ );
+ }
+ }
+
+ /**
+ * Returns overrides for action handlers.
+ * Classes listed here will be used instead of the default one when
+ * (and only when) $wgActions[$action] === true. This allows subclasses
+ * to override the default action handlers.
+ *
+ * @since WD.1
+ *
+ * @return Array
+ */
+ public function getActionOverrides() {
+ return array();
+ }
+
+ /**
+ * Factory for creating an appropriate DifferenceEngine for this content model.
+ *
+ * @since WD.1
+ *
+ * @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 int|string 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 createDifferenceEngine( IContextSource $context,
+ $old = 0, $new = 0,
+ $rcid = 0, # FIXME: use everywhere!
+ $refreshCache = false, $unhide = false
+ ) {
+ $this->checkModelID( $context->getTitle()->getContentModel() );
+
+ $diffEngineClass = $this->getDiffEngineClass();
+
+ return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
+ }
+
+ /**
+ * Get the language in which the content of the given page is written.
+ *
+ * This default implementation just returns $wgContLang (except for pages in the MediaWiki namespace)
+ *
+ * Note that the pages language is not cacheable, since it may in some cases depend on user settings.
+ *
+ * Also note that the page language may or may not depend on the actual content of the page,
+ * that is, this method may load the content in order to determine the language.
+ *
+ * @since 1.WD
+ *
+ * @param Title $title the page to determine the language for.
+ * @param Content|null $content the page's content, if you have it handy, to avoid reloading it.
+ *
+ * @return Language the page's language
+ */
+ public function getPageLanguage( Title $title, Content $content = null ) {
+ global $wgContLang;
+
+ if ( $title->getNamespace() == NS_MEDIAWIKI ) {
+ // Parse mediawiki messages with correct target language
+ list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() );
+ return wfGetLangObj( $lang );
+ }
+
+ return $wgContLang;
+ }
+
+ /**
+ * Get the language in which the content of this page is written when
+ * viewed by user. Defaults to $this->getPageLanguage(), but if the user
+ * specified a preferred variant, the variant will be used.
+ *
+ * This default implementation just returns $this->getPageLanguage( $title, $content ) unless
+ * the user specified a preferred variant.
+ *
+ * Note that the pages view language is not cacheable, since it depends on user settings.
+ *
+ * Also note that the page language may or may not depend on the actual content of the page,
+ * that is, this method may load the content in order to determine the language.
+ *
+ * @since 1.WD
+ *
+ * @param Title $title the page to determine the language for.
+ * @param Content|null $content the page's content, if you have it handy, to avoid reloading it.
+ *
+ * @return Language the page's language for viewing
+ */
+ public function getPageViewLanguage( Title $title, Content $content = null ) {
+ $pageLang = $this->getPageLanguage( $title, $content );
+
+ if ( $title->getNamespace() !== NS_MEDIAWIKI ) {
+ // If the user chooses a variant, the content is actually
+ // in a language whose code is the variant code.
+ $variant = $pageLang->getPreferredVariant();
+ if ( $pageLang->getCode() !== $variant ) {
+ $pageLang = Language::factory( $variant );
+ }
+ }
+
+ return $pageLang;
+ }
+
+ /**
+ * Determines whether the content type handled by this ContentHandler
+ * can be used on the given page.
+ *
+ * This default implementation always returns true.
+ * Subclasses may override this to restrict the use of this content model to specific locations,
+ * typically based on the namespace or some other aspect of the title, such as a special suffix
+ * (e.g. ".svg" for SVG content).
+ *
+ * @param Title $title the page's title.
+ *
+ * @return bool true if content of this kind can be used on the given page, false otherwise.
+ */
+ public function canBeUsedOn( Title $title ) {
+ return true;
+ }
+
+ /**
+ * Returns the name of the diff engine to use.
+ *
+ * @since WD.1
+ *
+ * @return string
+ */
+ protected function getDiffEngineClass() {
+ return 'DifferenceEngine';
+ }
+
+ /**
+ * 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.
+ *
+ * @since WD.1
+ *
+ * @param $oldContent Content|string String
+ * @param $myContent Content|string String
+ * @param $yourContent Content|string String
+ *
+ * @return Content|Bool
+ */
+ public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
+ return false;
+ }
+
+ /**
+ * Return an applicable auto-summary if one exists for the given edit.
+ *
+ * @since WD.1
+ *
+ * @param $oldContent Content|null: the previous text of the page.
+ * @param $newContent Content|null: The submitted text of the page.
+ * @param $flags int Bit mask: a bit mask of flags submitted for the edit.
+ *
+ * @return string An appropriate auto-summary, or an empty string.
+ */
+ public function getAutosummary( Content $oldContent = null, Content $newContent = null, $flags ) {
+ global $wgContLang;
+
+ // Decide what kind of auto-summary is needed.
+
+ // Redirect auto-summaries
+
+ /**
+ * @var $ot Title
+ * @var $rt Title
+ */
+
+ $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null;
+ $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null;
+
+ if ( is_object( $rt ) ) {
+ if ( !is_object( $ot )
+ || !$rt->equals( $ot )
+ || $ot->getFragment() != $rt->getFragment() )
+ {
+ $truncatedtext = $newContent->getTextForSummary(
+ 250
+ - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
+ - strlen( $rt->getFullText() ) );
+
+ return wfMessage( 'autoredircomment', $rt->getFullText() )
+ ->rawParams( $truncatedtext )->inContentLanguage()->text();
+ }
+ }
+
+ // New page auto-summaries
+ 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( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
+
+ return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
+ ->inContentLanguage()->text();
+ }
+
+ // Blanking auto-summaries
+ if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
+ return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
+ } elseif ( !empty( $oldContent )
+ && $oldContent->getSize() > 10 * $newContent->getSize()
+ && $newContent->getSize() < 500 )
+ {
+ // Removing more than 90% of the article
+
+ $truncatedtext = $newContent->getTextForSummary(
+ 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
+
+ return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
+ ->inContentLanguage()->text();
+ }
+
+ // If we reach this point, there's no applicable auto-summary for our
+ // case, so our auto-summary is empty.
+ return '';
+ }
+
+ /**
+ * Auto-generates a deletion reason
+ *
+ * @since WD.1
+ *
+ * @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
+ *
+ * @XXX &$hasHistory is extremely ugly, it's here because
+ * WikiPage::getAutoDeleteReason() and Article::getReason()
+ * have it / want it.
+ */
+ 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;
+
+ $this->checkModelID( $content->getModel() );
+
+ // 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 = $prev->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 = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
+ } else {
+ if ( $onlyAuthor ) {
+ $reason = wfMessage(
+ 'excontentauthor',
+ '$1',
+ $onlyAuthor
+ )->inContentLanguage()->text();
+ } else {
+ $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
+ }
+ }
+
+ 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.
+ *
+ * @since WD.1
+ *
+ * @param $current Revision The current text
+ * @param $undo Revision The revision to undo
+ * @param $undoafter 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 ) {
+ $cur_content = $current->getContent();
+
+ if ( empty( $cur_content ) ) {
+ return false; // no page
+ }
+
+ $undo_content = $undo->getContent();
+ $undoafter_content = $undoafter->getContent();
+
+ $this->checkModelID( $cur_content->getModel() );
+ $this->checkModelID( $undo_content->getModel() );
+ $this->checkModelID( $undoafter_content->getModel() );
+
+ if ( $cur_content->equals( $undo_content ) ) {
+ // No use doing a merge if it's just a straight revert.
+ return $undoafter_content;
+ }
+
+ $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content );
+
+ return $undone_content;
+ }
+
+ /**
+ * Returns true for content models that support caching using the
+ * ParserCache mechanism. See WikiPage::isParserCacheUser().
+ *
+ * @since WD.1
+ *
+ * @return bool
+ */
+ public function isParserCacheSupported() {
+ return true;
+ }
+
+ /**
+ * Returns true if this content model supports sections.
+ *
+ * This default implementation returns false.
+ *
+ * @return boolean whether sections are supported.
+ */
+ public function supportsSections() {
+ return false;
+ }
+
+ /**
+ * Call a legacy hook that uses text instead of Content objects.
+ * Will log a warning when a matching hook function is registered.
+ * If the textual representation of the content is changed by the
+ * hook function, a new Content object is constructed from the new
+ * text.
+ *
+ * @param $event String: event name
+ * @param $args Array: parameters passed to hook functions
+ * @param $warn bool: whether to log a warning (default: true). Should generally be true,
+ * may be set to false for testing.
+ *
+ * @return Boolean True if no handler aborted the hook
+ */
+ public static function runLegacyHooks( $event, $args = array(), $warn = true ) {
+ if ( !Hooks::isRegistered( $event ) ) {
+ return true; // nothing to do here
+ }
+
+ if ( $warn ) {
+ wfWarn( "Using obsolete hook $event" );
+ }
+
+ // convert Content objects to text
+ $contentObjects = array();
+ $contentTexts = array();
+
+ foreach ( $args as $k => $v ) {
+ if ( $v instanceof Content ) {
+ /* @var Content $v */
+
+ $contentObjects[$k] = $v;
+
+ $v = $v->serialize();
+ $contentTexts[ $k ] = $v;
+ $args[ $k ] = $v;
+ }
+ }
+
+ // call the hook functions
+ $ok = wfRunHooks( $event, $args );
+
+ // see if the hook changed the text
+ foreach ( $contentTexts as $k => $orig ) {
+ /* @var Content $content */
+
+ $modified = $args[ $k ];
+ $content = $contentObjects[$k];
+
+ if ( $modified !== $orig ) {
+ // text was changed, create updated Content object
+ $content = $content->getContentHandler()->unserializeContent( $modified );
+ }
+
+ $args[ $k ] = $content;
+ }
+
+ return $ok;
+ }
+}
+
+/**
+ * @since WD.1
+ */
+abstract class TextContentHandler extends ContentHandler {
+
+ public function __construct( $modelId, $formats ) {
+ parent::__construct( $modelId, $formats );
+ }
+
+ /**
+ * Returns the content's text as-is.
+ *
+ * @param $content Content
+ * @param $format string|null
+ * @return mixed
+ */
+ public function serializeContent( 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.
+ *
+ * All three Content objects passed as parameters must have the same
+ * content model.
+ *
+ * This text-based implementation uses wfMerge().
+ *
+ * @param $oldContent \Content|string String
+ * @param $myContent \Content|string String
+ * @param $yourContent \Content|string String
+ *
+ * @return Content|Bool
+ */
+ public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
+ $this->checkModelID( $oldContent->getModel() );
+ $this->checkModelID( $myContent->getModel() );
+ $this->checkModelID( $yourContent->getModel() );
+
+ $format = $this->getDefaultFormat();
+
+ $old = $this->serializeContent( $oldContent, $format );
+ $mine = $this->serializeContent( $myContent, $format );
+ $yours = $this->serializeContent( $yourContent, $format );
+
+ $ok = wfMerge( $old, $mine, $yours, $result );
+
+ if ( !$ok ) {
+ return false;
+ }
+
+ if ( !$result ) {
+ return $this->makeEmptyContent();
+ }
+
+ $mergedContent = $this->unserializeContent( $result, $format );
+ return $mergedContent;
+ }
+
+}
+
+/**
+ * @since WD.1
+ */
+class WikitextContentHandler extends TextContentHandler {
+
+ public function __construct( $modelId = CONTENT_MODEL_WIKITEXT ) {
+ parent::__construct( $modelId, array( CONTENT_FORMAT_WIKITEXT ) );
+ }
+
+ public function unserializeContent( $text, $format = null ) {
+ $this->checkFormat( $format );
+
+ return new WikitextContent( $text );
+ }
+
+ public function makeEmptyContent() {
+ return new WikitextContent( '' );
+ }
+
+ /**
+ * Returns true because wikitext supports sections.
+ *
+ * @return boolean whether sections are supported.
+ */
+ public function supportsSections() {
+ return true;
+ }
+}
+
+# XXX: make ScriptContentHandler base class, do highlighting stuff there?
+
+/**
+ * @since WD.1
+ */
+class JavaScriptContentHandler extends TextContentHandler {
+
+ public function __construct( $modelId = CONTENT_MODEL_JAVASCRIPT ) {
+ parent::__construct( $modelId, array( CONTENT_FORMAT_JAVASCRIPT ) );
+ }
+
+ public function unserializeContent( $text, $format = null ) {
+ $this->checkFormat( $format );
+
+ return new JavaScriptContent( $text );
+ }
+
+ public function makeEmptyContent() {
+ return new JavaScriptContent( '' );
+ }
+
+ /**
+ * Returns the english language, because JS is english, and should be handled as such.
+ *
+ * @return Language wfGetLangObj( 'en' )
+ *
+ * @see ContentHandler::getPageLanguage()
+ */
+ public function getPageLanguage( Title $title, Content $content = null ) {
+ return wfGetLangObj( 'en' );
+ }
+
+ /**
+ * Returns the english language, because CSS is english, and should be handled as such.
+ *
+ * @return Language wfGetLangObj( 'en' )
+ *
+ * @see ContentHandler::getPageViewLanguage()
+ */
+ public function getPageViewLanguage( Title $title, Content $content = null ) {
+ return wfGetLangObj( 'en' );
+ }
+}
+
+/**
+ * @since WD.1
+ */
+class CssContentHandler extends TextContentHandler {
+
+ public function __construct( $modelId = CONTENT_MODEL_CSS ) {
+ parent::__construct( $modelId, array( CONTENT_FORMAT_CSS ) );
+ }
+
+ public function unserializeContent( $text, $format = null ) {
+ $this->checkFormat( $format );
+
+ return new CssContent( $text );
+ }
+
+ public function makeEmptyContent() {
+ return new CssContent( '' );
+ }
+
+ /**
+ * Returns the english language, because CSS is english, and should be handled as such.
+ *
+ * @return Language wfGetLangObj( 'en' )
+ *
+ * @see ContentHandler::getPageLanguage()
+ */
+ public function getPageLanguage( Title $title, Content $content = null ) {
+ return wfGetLangObj( 'en' );
+ }
+
+ /**
+ * Returns the english language, because CSS is english, and should be handled as such.
+ *
+ * @return Language wfGetLangObj( 'en' )
+ *
+ * @see ContentHandler::getPageViewLanguage()
+ */
+ public function getPageViewLanguage( Title $title, Content $content = null ) {
+ return wfGetLangObj( 'en' );
+ }
+}
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 2fa0c57a54..da4d0ed216 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -732,6 +732,16 @@ $wgMediaHandlers = array(
'image/x-djvu' => 'DjVuHandler', // compat
);
+/**
+ * Plugins for page content model handling.
+ * Each entry in the array maps a model id 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
+);
+
/**
* Resizing can be done using PHP's internal image libraries or using
* ImageMagick or another third-party converter, e.g. GraphicMagick.
@@ -6223,6 +6233,31 @@ $wgSeleniumConfigFile = null;
$wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
$wgDBtestpassword = '';
+/**
+ * Associative array mapping namespace IDs to the name of the content model pages in that namespace should have by
+ * default (use the CONTENT_MODEL_XXX constants). If no special content type is defined for a given namespace,
+ * pages in that namespace will use the CONTENT_MODEL_WIKITEXT (except for the special case of JS and CS pages).
+ */
+$wgNamespaceContentModels = array();
+
+/**
+ * How to react if a plain text version of a non-text Content object is requested using ContentHandler::getContentText():
+ *
+ * * 'ignore': return null
+ * * 'fail': throw an MWException
+ * * 'serializeContent': serializeContent to default format
+ */
+$wgContentHandlerTextFallback = 'ignore';
+
+/**
+ * Compatibility switch for running ContentHandler code withoput a schema update.
+ * Set to false to disable use of the database fields introduced by the ContentHandler facility.
+ *
+ * @deprecated this is only here to allow code deployment without a database schema update on large sites.
+ * get rid of it in the next version.
+ */
+$wgContentHandlerUseDB = true;
+
/**
* Whether the user must enter their password to change their e-mail address
*
diff --git a/includes/Defines.php b/includes/Defines.php
index be9f981602..b5894936d1 100644
--- a/includes/Defines.php
+++ b/includes/Defines.php
@@ -261,7 +261,7 @@ define( 'APCOND_BLOCKED', 8 );
define( 'APCOND_ISBOT', 9 );
/**@}*/
-/**
+/** @{
* Protocol constants for wfExpandUrl()
*/
define( 'PROTO_HTTP', 'http://' );
@@ -270,3 +270,34 @@ define( 'PROTO_RELATIVE', '//' );
define( 'PROTO_CURRENT', null );
define( 'PROTO_CANONICAL', 1 );
define( 'PROTO_INTERNAL', 2 );
+/**@}*/
+
+/**@{
+ * Content model ids, used by Content and ContentHandler.
+ * These IDs will be exposed in the API and XML dumps.
+ *
+ * Extensions that define their own content model IDs should take
+ * care to avoid conflicts. Using the extension name as a prefix is recommended.
+ */
+define( 'CONTENT_MODEL_WIKITEXT', 'wikitext' );
+define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' );
+define( 'CONTENT_MODEL_CSS', 'css' );
+define( 'CONTENT_MODEL_TEXT', 'text' );
+/**@}*/
+
+/**@{
+ * Content formats, used by Content and ContentHandler.
+ * These should be MIME types, and will be exposed in the API and XML dumps.
+ *
+ * Extensions are free to use the below formats, or define their own.
+ * It is recommended to stick with the conventions for MIME types.
+ */
+define( 'CONTENT_FORMAT_WIKITEXT', 'text/x-wiki' ); // wikitext
+define( 'CONTENT_FORMAT_JAVASCRIPT', 'text/javascript' ); // for js pages
+define( 'CONTENT_FORMAT_CSS', 'text/css' ); // for css pages
+define( 'CONTENT_FORMAT_TEXT', 'text/plain' ); // for future use, e.g. with some plain-html messages.
+define( 'CONTENT_FORMAT_HTML', 'text/html' ); // for future use, e.g. with some plain-html messages.
+define( 'CONTENT_FORMAT_SERIALIZED', 'application/vnd.php.serialized' ); // for future use with the api and for extensions
+define( 'CONTENT_FORMAT_JSON', 'application/json' ); // for future use with the api, and for use by extensions
+define( 'CONTENT_FORMAT_XML', 'application/xml' ); // for future use with the api, and for use by extensions
+/**@}*/
diff --git a/includes/DeprecatedGlobal.php b/includes/DeprecatedGlobal.php
index 4d7b968987..d48bd0b0ca 100644
--- a/includes/DeprecatedGlobal.php
+++ b/includes/DeprecatedGlobal.php
@@ -27,7 +27,7 @@
*/
class DeprecatedGlobal extends StubObject {
- // The m's are to stay consistent with parent class.
+ // The m's are to stay consistent with parent class.
protected $mRealValue, $mVersion;
function __construct( $name, $realValue, $version = false ) {
diff --git a/includes/EditPage.php b/includes/EditPage.php
index 2115eb0a2d..fe0e95eed7 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -155,6 +155,11 @@ class EditPage {
*/
const AS_IMAGE_REDIRECT_LOGGED = 234;
+ /**
+ * Status: can't parse content
+ */
+ const AS_PARSE_ERROR = 240;
+
/**
* HTML id and name for the beginning of the edit form.
*/
@@ -214,6 +219,7 @@ class EditPage {
var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
var $edittime = '', $section = '', $sectiontitle = '', $starttime = '';
var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
+ var $content_model = null, $content_format = null;
# Placeholders for text injection by hooks (must be HTML)
# extensions should take care to _append_ to the present value
@@ -225,7 +231,7 @@ class EditPage {
public $editFormTextBottom = '';
public $editFormTextAfterContent = '';
public $previewTextAfterContent = '';
- public $mPreloadText = '';
+ public $mPreloadContent = null;
/* $didSave should be set to true whenever an article was succesfully altered. */
public $didSave = false;
@@ -233,12 +239,24 @@ class EditPage {
public $suppressIntro = false;
+ /**
+ * Set to true to allow editing of non-text content types.
+ *
+ * @var bool
+ */
+ public $allowNonTextContent = false;
+
/**
* @param $article Article
*/
public function __construct( Article $article ) {
$this->mArticle = $article;
$this->mTitle = $article->getTitle();
+
+ $this->content_model = $this->mTitle->getContentModel();
+
+ $handler = ContentHandler::getForModelID( $this->content_model );
+ $this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision
}
/**
@@ -450,10 +468,10 @@ class EditPage {
return;
}
- $content = $this->getContent();
+ $content = $this->getContentObject();
# Use the normal message if there's nothing to display
- if ( $this->firsttime && $content === '' ) {
+ if ( $this->firsttime && $content->isEmpty() ) {
$action = $this->mTitle->exists() ? 'edit' :
( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
throw new PermissionsError( $action, $permErrors );
@@ -467,13 +485,14 @@ class EditPage {
# If the user made changes, preserve them when showing the markup
# (This happens when a user is blocked during edit, for instance)
if ( !$this->firsttime ) {
- $content = $this->textbox1;
+ $text = $this->textbox1;
$wgOut->addWikiMsg( 'viewyourtext' );
} else {
+ $text = $this->toEditText( $content );
$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() ) ) );
@@ -711,10 +730,17 @@ class EditPage {
}
}
+ $this->oldid = $request->getInt( 'oldid' );
+
$this->bot = $request->getBool( 'bot', true );
$this->nosummary = $request->getBool( 'nosummary' );
- $this->oldid = $request->getInt( 'oldid' );
+ $content_handler = ContentHandler::getForTitle( $this->mTitle );
+ $this->content_model = $request->getText( 'model', $content_handler->getModelID() ); #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',
@@ -747,7 +773,10 @@ class EditPage {
function initialiseForm() {
global $wgUser;
$this->edittime = $this->mArticle->getTimestamp();
- $this->textbox1 = $this->getContent( false );
+
+ $content = $this->getContentObject( false ); #TODO: track content object?!
+ $this->textbox1 = $this->toEditText( $content );
+
// activate checkboxes if user wants them to be always active
# Sort out the "watch" checkbox
if ( $wgUser->getOption( 'watchdefault' ) ) {
@@ -776,33 +805,56 @@ class EditPage {
* @param $def_text string
* @return mixed string on success, $def_text for invalid sections
* @private
+ * @deprecated since 1.WD
+ * @todo: deprecated, replace usage everywhere
*/
- function getContent( $def_text = '' ) {
- global $wgOut, $wgRequest, $wgParser;
+ function getContent( $def_text = false ) {
+ wfDeprecated( __METHOD__, '1.WD' );
+
+ if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
+ $def_content = $this->toEditContent( $def_text );
+ } else {
+ $def_content = false;
+ }
+
+ $content = $this->getContentObject( $def_content );
+
+ // Note: EditPage should only be used with text based content anyway.
+ return $this->toEditText( $content );
+ }
+
+ private function getContentObject( $def_content = null ) {
+ 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 = $this->toEditContent( $msg );
}
- 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' );
@@ -818,15 +870,16 @@ class EditPage {
# Sanity check, make sure it's the right page,
# the revisions exist and they were not deleted.
- # Otherwise, $text will be left as-is.
+ # Otherwise, $content will be left as-is.
if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
$undorev->getPage() == $oldrev->getPage() &&
$undorev->getPage() == $this->mTitle->getArticleID() &&
!$undorev->isDeleted( Revision::DELETED_TEXT ) &&
!$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
- $text = $this->mArticle->getUndoText( $undorev, $oldrev );
- if ( $text === false ) {
+ $content = $this->mArticle->getUndoContent( $undorev, $oldrev );
+
+ if ( $content === false ) {
# Warn the user that something went wrong
$undoMsg = 'failure';
} else {
@@ -859,14 +912,14 @@ class EditPage {
wfMessage( 'undo-' . $undoMsg )->plain() . '', true, /* interface */true );
}
- if ( $text === false ) {
- $text = $this->getOriginalContent();
+ if ( $content === false ) {
+ $content = $this->getOriginalContent();
}
}
}
wfProfileOut( __METHOD__ );
- return $text;
+ return $content;
}
/**
@@ -885,39 +938,69 @@ class EditPage {
*/
private function getOriginalContent() {
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()->getContentModel();
+ $handler = ContentHandler::getForModelID( $this->content_model );
+
+ return $handler->makeEmptyContent();
}
- return $this->mArticle->getContent();
+ $content = $revision->getContent();
+ 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.WD
* @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()->getContentModel();
+ $handler = ContentHandler::getForModelID( $this->content_model );
+
+ return $handler->makeEmptyContent();
} else {
- return $text;
+ # nasty side-effect, but needed for consistency
+ $this->content_model = $rev->getContentModel();
+ $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.WD
*/
public function setPreloadedText( $text ) {
- $this->mPreloadText = $text;
+ wfDeprecated( __METHOD__, "1.WD" );
+
+ $content = $this->toEditContent( $text );
+
+ $this->setPreloadedContent( $content );
+ }
+
+ /**
+ * Use this method before edit() to preload some content into the edit box
+ *
+ * @param $content Content
+ *
+ * @since 1.WD
+ */
+ public function setPreloadedContent( Content $content ) {
+ $this->mPreloadedContent = $content;
}
/**
@@ -925,23 +1008,47 @@ class EditPage {
* an earlier setPreloadText() or by loading the given page.
*
* @param $preload String: representing the title to preload from.
+ *
* @return String
+ *
+ * @deprecated since 1.WD, use getPreloadedContent() instead
*/
- protected function getPreloadedText( $preload ) {
- global $wgUser, $wgParser;
+ protected function getPreloadedText( $preload ) { #NOTE: B/C only, replace usage!
+ wfDeprecated( __METHOD__, "1.WD" );
+
+ $content = $this->getPreloadedContent( $preload );
+ $text = $this->toEditText( $content );
+
+ return $text;
+ }
- if ( !empty( $this->mPreloadText ) ) {
- return $this->mPreloadText;
+ /**
+ * Get the contents to be preloaded into the box, either set by
+ * an earlier setPreloadText() or by loading the given page.
+ *
+ * @param $preload String: representing the title to preload from.
+ *
+ * @return Content
+ *
+ * @since 1.WD
+ */
+ protected function getPreloadedContent( $preload ) { #@todo: use this!
+ global $wgUser;
+
+ if ( !empty( $this->mPreloadContent ) ) {
+ return $this->mPreloadContent;
}
+ $handler = ContentHandler::getForTitle( $this->getTitle() );
+
if ( $preload === '' ) {
- return '';
+ return $handler->makeEmptyContent();
}
$title = Title::newFromText( $preload );
# Check for existence to avoid getting MediaWiki:Noarticletext
if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
- return '';
+ return $handler->makeEmptyContent();
}
$page = WikiPage::factory( $title );
@@ -949,13 +1056,15 @@ class EditPage {
$title = $page->getRedirectTarget();
# Same as before
if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
- return '';
+ return $handler->makeEmptyContent();
}
$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 );
}
/**
@@ -1003,6 +1112,10 @@ class EditPage {
case self::AS_HOOK_ERROR:
return false;
+ case self::AS_PARSE_ERROR:
+ $wgOut->addWikiText( '$1
', "yourdiff" );
- $de = new DifferenceEngine( $this->mArticle->getContext() );
- $de->setText( $this->textbox2, $this->textbox1 );
- $de->showDiff(
+ $content1 = $this->toEditContent( $this->textbox1 );
+ $content2 = $this->toEditContent( $this->textbox2 );
+
+ $handler = ContentHandler::getForModelID( $this->content_model );
+ $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
+ $de->setContent( $content2, $content1 );
+ $de->showDiff(
wfMessage( 'yourtext' )->parse(),
wfMessage( 'storedversion' )->text()
);
@@ -2600,82 +2843,95 @@ HTML
return $parsedNote;
}
- if ( $this->mTriedSave && !$this->mTokenOk ) {
- if ( $this->mTokenOkExceptSuffix ) {
- $note = wfMessage( 'token_suffix_mismatch' )->plain();
- } else {
- $note = wfMessage( 'session_fail_preview' )->plain();
- }
- } elseif ( $this->incompleteForm ) {
- $note = wfMessage( 'edit_form_incomplete' )->plain();
- } else {
- $note = wfMessage( 'previewnote' )->plain() .
- ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
- }
+ $note = '';
- $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
+ try {
+ $content = $this->toEditContent( $this->textbox1 );
- $parserOptions->setEditSection( false );
- $parserOptions->setIsPreview( true );
- $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
+ if ( $this->mTriedSave && !$this->mTokenOk ) {
+ if ( $this->mTokenOkExceptSuffix ) {
+ $note = wfMessage( 'token_suffix_mismatch' )->plain() ;
- # don't parse non-wikitext pages, show message about preview
- if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
- if ( $this->mTitle->isCssJsSubpage() ) {
- $level = 'user';
- } elseif ( $this->mTitle->isCssOrJsPage() ) {
- $level = 'site';
- } else {
- $level = false;
- }
-
- # Used messages to make sure grep find them:
- # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
- $class = 'mw-code';
- if ( $level ) {
- if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
- $previewtext = "\n" . htmlspecialchars( $this->textbox1 ) . "\n
\n";
- } else {
- $toparse = $this->textbox1;
+ if ( $content->getModel() == CONTENT_MODEL_CSS ) {
+ $format = 'css';
+ } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
+ $format = 'js';
+ } else {
+ $format = false;
+ }
- # If we're adding a comment, we need to show the
- # summary as the headline
- if ( $this->section == "new" && $this->summary != "" ) {
- $toparse = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )->inContentLanguage()->text() . "\n\n" . $toparse;
+ # Used messages to make sure grep find them:
+ # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
+ if( $level && $format ) {
+ $note = "
\n
and don't parse - $m = array(); - preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m ); - $out->addHTML( "\n" ); - $out->addHTML( htmlspecialchars( $this->mNewtext ) ); - $out->addHTML( "\n\n" ); + if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { + // NOTE: deprecated 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 ) ) ) { + } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { + // Handled by extension + } elseif( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { + // NOTE: deprecated hook, B/C only // Handled by extension } else { // Normal page @@ -536,13 +542,7 @@ class DifferenceEngine extends ContextSource { $wikiPage = WikiPage::factory( $this->mNewPage ); } - $parserOptions = $wikiPage->makeParserOptions( $this->getContext() ); - - if ( !$this->mNewRev->isCurrent() ) { - $parserOptions->setEditSection( false ); - } - - $parserOutput = $wikiPage->getParserOutput( $parserOptions, $this->mNewid ); + $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev ); # WikiPage::getParserOutput() should not return false, but just in case if( $parserOutput ) { @@ -556,6 +556,17 @@ class DifferenceEngine extends ContextSource { wfProfileOut( __METHOD__ ); } + protected function getParserOutput( WikiPage $page, Revision $rev ) { + $parserOptions = $page->makeParserOptions( $this->getContext() ); + + if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( "edit" ) ) { + $parserOptions->setEditSection( false ); + } + + $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() ); + return $parserOutput; + } + /** * Get the diff text, send it to the OutputPage object * Returns false if the diff could not be generated, otherwise returns true @@ -652,7 +663,7 @@ class DifferenceEngine extends ContextSource { return false; } - $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); + $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent ); // Save to cache for 7 days if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) { @@ -689,14 +700,56 @@ class DifferenceEngine extends ContextSource { } } + /** + * Generate a diff, no caching. + * + * Subclasses may override this to provide a + * + * @param $old Content: old content + * @param $new Content: new content + * + * @since 1.WD + */ + function generateContentDiffBody( Content $old, Content $new ) { + if ( !( $old instanceof TextContent ) ) { + throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " + . "override generateContentDiffBody to fix this." ); + } + + if ( !( $new instanceof TextContent ) ) { + throw new MWException( "Diff not implemented for " . get_class( $new ) . "; " + . "override generateContentDiffBody to fix this." ); + } + + $otext = $old->serialize(); + $ntext = $new->serialize(); + + 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 - * @return bool|string + * @deprecated since 1.WD, use generateContentDiffBody() instead! */ function generateDiffBody( $otext, $ntext ) { + wfDeprecated( __METHOD__, "1.WD" ); + + 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 generateTextDiffBody( $otext, $ntext ) { global $wgExternalDiffEngine, $wgContLang; wfProfileIn( __METHOD__ ); @@ -859,7 +912,7 @@ class DifferenceEngine extends ContextSource { * the visibility of the revision and a link to edit the page. * @return String HTML fragment */ - private function getRevisionHeader( Revision $rev, $complete = '' ) { + protected function getRevisionHeader( Revision $rev, $complete = '' ) { $lang = $this->getLanguage(); $user = $this->getUser(); $revtimestamp = $rev->getTimestamp(); @@ -951,10 +1004,25 @@ class DifferenceEngine extends ContextSource { /** * Use specified text instead of loading from the database + * @deprecated since 1.WD, use setContent() instead. */ function setText( $oldText, $newText ) { - $this->mOldtext = $oldText; - $this->mNewtext = $newText; + wfDeprecated( __METHOD__, "1.WD" ); + + $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.WD + */ + function setContent( Content $oldContent, Content $newContent ) { + $this->mOldContent = $oldContent; + $this->mNewContent = $newContent; + $this->mTextLoaded = 2; $this->mRevisionsLoaded = true; } @@ -1082,14 +1150,14 @@ class DifferenceEngine extends ContextSource { return false; } if ( $this->mOldRev ) { - $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER ); - if ( $this->mOldtext === false ) { + $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER ); + if ( $this->mOldContent === false ) { return false; } } if ( $this->mNewRev ) { - $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); - if ( $this->mNewtext === false ) { + $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER ); + if ( $this->mNewContent === false ) { return false; } } @@ -1110,7 +1178,7 @@ class DifferenceEngine extends ContextSource { if ( !$this->loadRevisionData() ) { return false; } - $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); + $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER ); return true; } } diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 695c4e9eab..449cc45e68 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -1186,8 +1186,9 @@ class LocalFile extends File { } else { # New file; create the description page. # There's already a log entry, so don't make a second RC entry - # Squid and file cache for the description page are purged by doEdit. - $status = $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user ); + # Squid and file cache for the description page are purged by doEditContent. + $content = ContentHandler::makeContent( $pageText, $descTitle ); + $status = $wikiPage->doEditContent( $content, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user ); if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction $dbw->begin(); @@ -1475,9 +1476,9 @@ class LocalFile extends File { global $wgParser; $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL ); if ( !$revision ) return false; - $text = $revision->getText(); - if ( !$text ) return false; - $pout = $wgParser->parse( $text, $this->title, new ParserOptions() ); + $content = $revision->getContent(); + if ( !$content ) return false; + $pout = $content->getParserOutput( $this->title, null, new ParserOptions() ); return $pout->getText(); } diff --git a/includes/installer/Ibm_db2Updater.php b/includes/installer/Ibm_db2Updater.php index f812ac2774..6b7e88de2c 100644 --- a/includes/installer/Ibm_db2Updater.php +++ b/includes/installer/Ibm_db2Updater.php @@ -87,6 +87,13 @@ class Ibm_db2Updater extends DatabaseUpdater { // 1.20 array( 'addTable', 'config', 'patch-config.sql' ), + + // 1.WD + 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' ), ); } } diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php index ac5dbd747c..8140527839 100644 --- a/includes/installer/Installer.php +++ b/includes/installer/Installer.php @@ -1594,13 +1594,16 @@ abstract class Installer { $status = Status::newGood(); try { $page = WikiPage::factory( Title::newMainPage() ); - $page->doEdit( wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" . - wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text(), - '', - EDIT_NEW, - false, - User::newFromName( 'MediaWiki default' ) + $content = new WikitextContent ( + wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" . + wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text() ); + + $page->doEditContent( $content, + '', + EDIT_NEW, + false, + User::newFromName( 'MediaWiki default' ) ); } catch (MWException $e) { //using raw, because $wgShowExceptionDetails can not be set yet $status->fatal( 'config-install-mainpage-failed', $e->getMessage() ); diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php index bd55cb4084..d92ab4a06c 100644 --- a/includes/installer/MysqlUpdater.php +++ b/includes/installer/MysqlUpdater.php @@ -206,6 +206,7 @@ class MysqlUpdater extends DatabaseUpdater { array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ), array( 'addField', 'uploadstash', 'us_chunk_inx', 'patch-uploadstash_chunk.sql' ), array( 'addfield', 'job', 'job_timestamp', 'patch-jobs-add-timestamp.sql' ), + array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase.sql' ), // 1.20 @@ -214,6 +215,13 @@ class MysqlUpdater extends DatabaseUpdater { array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ), array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ), array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ), + + // 1.WD + 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' ), ); } diff --git a/includes/installer/OracleUpdater.php b/includes/installer/OracleUpdater.php index a741f262d5..d16de061a1 100644 --- a/includes/installer/OracleUpdater.php +++ b/includes/installer/OracleUpdater.php @@ -71,6 +71,13 @@ class OracleUpdater extends DatabaseUpdater { array( 'addIndex', 'ipblocks', 'i05', 'patch-ipblocks_i05_index.sql' ), array( 'addIndex', 'revision', 'i05', 'patch-revision_i05_index.sql' ), + //1.WD + 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' ), + // KEEP THIS AT THE BOTTOM!! array( 'doRebuildDuplicateFunction' ), diff --git a/includes/installer/SqliteUpdater.php b/includes/installer/SqliteUpdater.php index bc5fcacfb5..1486683730 100644 --- a/includes/installer/SqliteUpdater.php +++ b/includes/installer/SqliteUpdater.php @@ -85,6 +85,7 @@ class SqliteUpdater extends DatabaseUpdater { array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ), array( 'addField', 'uploadstash', 'us_chunk_inx', 'patch-uploadstash_chunk.sql' ), array( 'addfield', 'job', 'job_timestamp', 'patch-jobs-add-timestamp.sql' ), + array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ug_group-length-increase.sql' ), // 1.20 @@ -93,6 +94,13 @@ class SqliteUpdater extends DatabaseUpdater { array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ), array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ), array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ), + + // 1.WD + 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' ), ); } diff --git a/includes/job/DoubleRedirectJob.php b/includes/job/DoubleRedirectJob.php index 08af997585..ebbd8343b6 100644 --- a/includes/job/DoubleRedirectJob.php +++ b/includes/job/DoubleRedirectJob.php @@ -94,8 +94,8 @@ class DoubleRedirectJob extends Job { wfDebug( __METHOD__.": target redirect already deleted, ignoring\n" ); return true; } - $text = $targetRev->getText(); - $currentDest = Title::newFromRedirect( $text ); + $content = $targetRev->getContent(); + $currentDest = $content->getRedirectTarget(); if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) { wfDebug( __METHOD__.": Redirect has changed since the job was queued\n" ); return true; @@ -103,7 +103,7 @@ class DoubleRedirectJob extends Job { # Check for a suppression tag (used e.g. in periodically archived discussions) $mw = MagicWord::get( 'staticredirect' ); - if ( $mw->match( $text ) ) { + if ( $content->matchMagicWord( $mw ) ) { wfDebug( __METHOD__.": skipping: suppressed with __STATICREDIRECT__\n" ); return true; } @@ -125,14 +125,10 @@ class DoubleRedirectJob extends Job { $currentDest->getFragment() ); # Fix the text - # Remember that redirect pages can have categories, templates, etc., - # so the regex has to be fairly general - $newText = preg_replace( '/ \[ \[ [^\]]* \] \] /x', - '[[' . $newTitle->getFullText() . ']]', - $text, 1 ); - - if ( $newText === $text ) { - $this->setLastError( 'Text unchanged???' ); + $newContent = $content->updateRedirect( $newTitle ); + + if ( $newContent->equals( $content ) ) { + $this->setLastError( 'Content unchanged???' ); return false; } @@ -144,7 +140,7 @@ class DoubleRedirectJob extends Job { $reason = wfMessage( 'double-redirect-fixed-' . $this->reason, $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText() )->inContentLanguage()->text(); - $article->doEdit( $newText, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC, false, $this->getUser() ); + $article->doEditContent( $newContent, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC, false, $this->getUser() ); $wgUser = $oldUser; return true; diff --git a/includes/job/RefreshLinksJob.php b/includes/job/RefreshLinksJob.php index b23951c6f5..c4370f4511 100644 --- a/includes/job/RefreshLinksJob.php +++ b/includes/job/RefreshLinksJob.php @@ -70,16 +70,16 @@ class RefreshLinksJob extends Job { } public static function runForTitleInternal( Title $title, Revision $revision, $fname ) { - global $wgParser, $wgContLang; + global $wgContLang; wfProfileIn( $fname . '-parse' ); $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); - $parserOutput = $wgParser->parse( - $revision->getText(), $title, $options, true, true, $revision->getId() ); + $content = $revision->getContent(); + $parserOutput = $content->getParserOutput( $title, $revision->getId(), $options, false ); wfProfileOut( $fname . '-parse' ); wfProfileIn( $fname . '-update' ); - $updates = $parserOutput->getSecondaryDataUpdates( $title, false ); + $updates = $content->getSecondaryDataUpdates( $title, null, false, $parserOutput ); DataUpdate::runUpdates( $updates ); wfProfileOut( $fname . '-update' ); } diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index 2a24bee79a..61b4239a35 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -1948,6 +1948,7 @@ class Parser { # Interwikis wfProfileIn( __METHOD__."-interwiki" ); if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) { + // FIXME: the above check prevents links to sites with identifiers that are not language codes $this->mOutput->addLanguageLink( $nt->getFullText() ); $s = rtrim( $s . $prefix ); $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail; @@ -3588,7 +3589,13 @@ class Parser { } if ( $rev ) { - $text = $rev->getText(); + $content = $rev->getContent(); + $text = $content->getWikitextForTransclusion(); + + if ( $text === false || $text === null ) { + $text = false; + break; + } } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) { global $wgContLang; $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage(); @@ -3596,16 +3603,17 @@ class Parser { $text = false; break; } + $content = $message->content(); $text = $message->plain(); } else { break; } - if ( $text === false ) { + if ( !$content ) { break; } # Redirect? $finalTitle = $title; - $title = Title::newFromRedirect( $text ); + $title = $content->getRedirectTarget(); } return array( 'text' => $text, diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php index 41b4a38550..cf8b4a97df 100644 --- a/includes/parser/ParserOutput.php +++ b/includes/parser/ParserOutput.php @@ -50,7 +50,7 @@ class ParserOutput extends CacheTime { $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 $mSecondaryDataUpdates = array(); # List of instances of SecondaryDataObject(), used to cause some information extracted from the page in a custom place. + private $mSecondaryDataUpdates = array(); # List of DataUpdate, used to save info from the page somewhere else. const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)((?:mw:)?editsection>))#'; @@ -375,9 +375,13 @@ class ParserOutput extends CacheTime { * extracted from the page's content, including a LinksUpdate object for all links stored in * this ParserOutput object. * + * @note: Avoid using this method directly, use ContentHandler::getSecondaryDataUpdates() instead! The content + * handler may provide additional update objects. + * * @since 1.20 * - * @param $title Title of the page we're updating. If not given, a title object will be created based on $this->getTitleText() + * @param $title Title The 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 DataUpdate diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index 623a26924e..b3aabecc81 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -81,7 +81,16 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { if ( !$revision ) { return null; } - return $revision->getRawText(); + + $content = $revision->getContent( Revision::RAW ); + $model = $content->getModel(); + + if ( $model !== CONTENT_MODEL_CSS && $model !== CONTENT_MODEL_JAVASCRIPT ) { + wfDebug( __METHOD__ . "bad content model #$model for JS/CSS page!\n" ); + return null; + } + + return $content->getNativeData(); //NOTE: this is safe, we know it's JS or CSS } /* Methods */ diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php index 2ccb6d3164..acff026390 100644 --- a/includes/search/SearchEngine.php +++ b/includes/search/SearchEngine.php @@ -804,11 +804,14 @@ class SearchResult { */ protected function initText() { if ( !isset( $this->mText ) ) { - if ( $this->mRevision != null ) - $this->mText = $this->mRevision->getText(); - else // TODO: can we fetch raw wikitext for commons images? + if ( $this->mRevision != null ) { + //TODO: if we could plug in some code that knows about special content models *and* about + // special features of the search engine, the search could benefit. + $content = $this->mRevision->getContent(); + $this->mText = $content->getTextForSearchIndex(); + } else { // TODO: can we fetch raw wikitext for commons images? $this->mText = ''; - + } } } @@ -819,6 +822,8 @@ class SearchResult { function getTextSnippet( $terms ) { global $wgUser, $wgAdvancedSearchHighlighting; $this->initText(); + + // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter. list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs( $wgUser ); $h = new SearchHighlighter(); if ( $wgAdvancedSearchHighlighting ) diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php index bf7de3f5a5..fb6532665a 100644 --- a/includes/specials/SpecialBooksources.php +++ b/includes/specials/SpecialBooksources.php @@ -144,8 +144,17 @@ class SpecialBookSources extends SpecialPage { $title = Title::makeTitleSafe( NS_PROJECT, $page ); # Show list in content language if( is_object( $title ) && $title->exists() ) { $rev = Revision::newFromTitle( $title, false, Revision::READ_NORMAL ); - $this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) ); - return true; + $content = $rev->getContent(); + + if ( $content instanceof TextContent ) { + //XXX: in the future, this could be stored as structured data, defining a list of book sources + + $text = $content->getNativeData(); + $this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $text ) ); + return true; + } else { + throw new MWException( "Unexpected content type for book sources: " . $content->getModel() ); + } } # Fall back to the defaults given in the language file diff --git a/includes/specials/SpecialComparePages.php b/includes/specials/SpecialComparePages.php index 9e3c52b981..2b7036c4f4 100644 --- a/includes/specials/SpecialComparePages.php +++ b/includes/specials/SpecialComparePages.php @@ -111,14 +111,19 @@ class SpecialComparePages extends SpecialPage { $rev2 = self::revOrTitle( $data['Revision2'], $data['Page2'] ); if( $rev1 && $rev2 ) { - $de = new DifferenceEngine( $form->getContext(), - $rev1, - $rev2, - null, // rcid - ( $data['Action'] == 'purge' ), - ( $data['Unhide'] == '1' ) - ); - $de->showDiffPage( true ); + $revision = Revision::newFromId( $rev1 ); + + if ( $revision ) { // NOTE: $rev1 was already checked, should exist. + $contentHandler = $revision->getContentHandler(); + $de = $contentHandler->createDifferenceEngine( $form->getContext(), + $rev1, + $rev2, + null, // rcid + ( $data['Action'] == 'purge' ), + ( $data['Unhide'] == '1' ) + ); + $de->showDiffPage( true ); + } } } diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index 8e15d554fe..b202e6b312 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -459,11 +459,12 @@ class SpecialNewpages extends IncludableSpecialPage { protected function feedItemDesc( $row ) { $revision = Revision::newFromId( $row->rev_id ); if( $revision ) { + //XXX: include content model/type in feed item? return '' . htmlspecialchars( $revision->getUserText() ) . $this->msg( 'colon-separator' )->inContentLanguage()->escaped() . htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) . "
\n
\n" . - nl2br( htmlspecialchars( $revision->getText() ) ) . ""; + nl2br( htmlspecialchars( $revision->getContent()->serialize() ) ) . ""; } return ''; } diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index d8e0b97c45..1ef112477f 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -32,7 +32,16 @@ class PageArchive { * @var Title */ protected $title; - var $fileStatus; + + /** + * @var Status + */ + protected $fileStatus; + + /** + * @var Status + */ + protected $revisionStatus; function __construct( $title ) { if( is_null( $title ) ) { @@ -112,12 +121,22 @@ class PageArchive { * @return ResultWrapper */ function listRevisions() { + global $wgContentHandlerNoDB; + $dbr = wfGetDB( DB_SLAVE ); + + $fields = array( + 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', + 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1', + ); + + if ( !$wgContentHandlerNoDB ) { + $fields[] = 'ar_content_format'; + $fields[] = 'ar_content_model'; + } + $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' - ), + $fields, array( 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey() ), __METHOD__, @@ -174,28 +193,38 @@ class PageArchive { * @return Revision */ function getRevision( $timestamp ) { + global $wgContentHandlerNoDB; + $dbr = wfGetDB( DB_SLAVE ); + + $fields = array( + 'ar_rev_id', + 'ar_text', + 'ar_comment', + 'ar_user', + 'ar_user_text', + 'ar_timestamp', + 'ar_minor_edit', + 'ar_flags', + 'ar_text_id', + 'ar_deleted', + 'ar_len', + 'ar_sha1', + ); + + if ( !$wgContentHandlerNoDB ) { + $fields[] = 'ar_content_format'; + $fields[] = 'ar_content_model'; + } + $row = $dbr->selectRow( 'archive', - array( - 'ar_rev_id', - 'ar_text', - 'ar_comment', - 'ar_user', - 'ar_user_text', - 'ar_timestamp', - 'ar_minor_edit', - 'ar_flags', - 'ar_text_id', - 'ar_deleted', - 'ar_len', - 'ar_sha1', - ), + $fields, array( 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey(), 'ar_timestamp' => $dbr->timestamp( $timestamp ) ), __METHOD__ ); if( $row ) { - return Revision::newFromArchiveRow( $row, array( 'page' => $this->title->getArticleID() ) ); + return Revision::newFromArchiveRow( $row, array( 'title' => $this->title ) ); } else { return null; } @@ -341,7 +370,7 @@ class PageArchive { if( $restoreFiles && $this->title->getNamespace() == NS_FILE ) { $img = wfLocalFile( $this->title ); $this->fileStatus = $img->restore( $fileVersions, $unsuppress ); - if ( !$this->fileStatus->isOk() ) { + if ( !$this->fileStatus->isOK() ) { return false; } $filesRestored = $this->fileStatus->successCount; @@ -350,10 +379,12 @@ class PageArchive { } if( $restoreText ) { - $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress, $comment ); - if( $textRestored === false ) { // It must be one of UNDELETE_* + $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment ); + if( !$this->revisionStatus->isOK() ) { return false; } + + $textRestored = $this->revisionStatus->getValue(); } else { $textRestored = 0; } @@ -401,11 +432,13 @@ class PageArchive { * @param $comment String * @param $unsuppress Boolean: remove all ar_deleted/fa_deleted restrictions of seletected revs * - * @return Mixed: number of revisions restored or false on failure + * @return Status, containing the number of revisions restored on success */ private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) { + global $wgContentHandlerNoDB; + if ( wfReadOnly() ) { - return false; + throw new ReadOnlyError(); } $restoreAll = empty( $timestamps ); @@ -435,9 +468,14 @@ class PageArchive { $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp', array( 'rev_id' => $previousRevId ), __METHOD__ ); + if( $previousTimestamp === false ) { wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" ); - return 0; + + $status = Status::newGood( 0 ); + $status->warning( 'undeleterevision-missing' ); + + return $status; } } else { # Have to create a new article... @@ -457,24 +495,31 @@ class PageArchive { $oldones = "ar_timestamp IN ( {$oldts} )"; } + $fields = array( + 'ar_rev_id', + 'ar_text', + 'ar_comment', + 'ar_user', + 'ar_user_text', + 'ar_timestamp', + 'ar_minor_edit', + 'ar_flags', + 'ar_text_id', + 'ar_deleted', + 'ar_page_id', + 'ar_len', + 'ar_sha1'); + + if ( !$wgContentHandlerNoDB ) { + $fields[] = 'ar_content_format'; + $fields[] = 'ar_content_model'; + } + /** * Select each archived revision... */ $result = $dbw->select( 'archive', - /* fields */ array( - 'ar_rev_id', - 'ar_text', - 'ar_comment', - 'ar_user', - 'ar_user_text', - 'ar_timestamp', - 'ar_minor_edit', - 'ar_flags', - 'ar_text_id', - 'ar_deleted', - 'ar_page_id', - 'ar_len', - 'ar_sha1' ), + $fields, /* WHERE */ array( 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey(), @@ -486,17 +531,38 @@ class PageArchive { $rev_count = $dbw->numRows( $result ); if( !$rev_count ) { wfDebug( __METHOD__ . ": no revisions to restore\n" ); - return false; // ??? + + $status = Status::newGood( 0 ); + $status->warning( "undelete-no-results" ); + return $status; } $ret->seek( $rev_count - 1 ); // move to last $row = $ret->fetchObject(); // get newest archived rev $ret->seek( 0 ); // move back + // grab the content to check consistency with global state before restoring the page. + $revision = Revision::newFromArchiveRow( $row, + array( + 'title' => $article->getTitle(), // used to derive default content model + ) ); + + $m = $revision->getContentModel(); + + $user = User::newFromName( $revision->getRawUserText(), false ); + $content = $revision->getContent( Revision::RAW ); + + //NOTE: article ID may not be known yet. prepareSave() should not modify the database. + $status = $content->prepareSave( $article, 0, -1, $user ); + + if ( !$status->isOK() ) { + return $status; + } + if( $makepage ) { // Check the state of the newest to-be version... if( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) { - return false; // we can't leave the current revision like this! + return Status::newFatal( "undeleterevdel" ); } // Safe to insert now... $newid = $article->insertOn( $dbw ); @@ -506,7 +572,7 @@ class PageArchive { if( $row->ar_timestamp > $previousTimestamp ) { // Check the state of the newest to-be version... if( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) { - return false; // we can't leave the current revision like this! + return Status::newFatal( "undeleterevdel" ); } } } @@ -527,7 +593,7 @@ class PageArchive { // unless we are specifically removing all restrictions... $revision = Revision::newFromArchiveRow( $row, array( - 'page' => $pageId, + 'title' => $this->title, 'deleted' => $unsuppress ? 0 : $row->ar_deleted ) ); @@ -546,7 +612,7 @@ class PageArchive { // Was anything restored at all? if ( $restored == 0 ) { - return 0; + return Status::newGood( 0 ); } $created = (bool)$newid; @@ -566,13 +632,18 @@ class PageArchive { $update->doUpdate(); } - return $restored; + return Status::newGood( $restored ); } /** * @return Status */ function getFileStatus() { return $this->fileStatus; } + + /** + * @return Status + */ + function getRevisionStatus() { return $this->revisionStatus; } } /** @@ -780,11 +851,13 @@ class SpecialUndelete extends SpecialPage { private function showRevision( $timestamp ) { if( !preg_match( '/[0-9]{14}/', $timestamp ) ) { - return 0; + return; } $archive = new PageArchive( $this->mTargetObj ); - wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) ); + if ( !wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) ) ) { + return; + } $rev = $archive->getRevision( $timestamp ); $out = $this->getOutput(); @@ -834,7 +907,11 @@ class SpecialUndelete extends SpecialPage { $t = $lang->userTime( $timestamp, $user ); $userLink = Linker::revUserTools( $rev ); - if( $this->mPreview ) { + $content = $rev->getContent( Revision::FOR_THIS_USER, $user ); + + $isText = ( $content instanceof TextContent ); + + if( $this->mPreview || $isText ) { $openDiv = ''; } else { $openDiv = ''; @@ -851,23 +928,48 @@ class SpecialUndelete extends SpecialPage { $out->addHTML( $this->msg( 'undelete-revision' )->rawParams( $link )->params( $time )->rawParams( $userLink )->params( $d, $t )->parse() . '' ); - wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ); - if( $this->mPreview ) { + if ( !wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ) ) { + return; + } + + if( $this->mPreview || !$isText ) { + // NOTE: non-text content has no source view, so always use rendered preview + // Hide [edit]s $popts = $out->parserOptions(); $popts->setEditSection( false ); - $out->parserOptions( $popts ); - $out->addWikiTextTitleTidy( $rev->getText( Revision::FOR_THIS_USER, $user ), $this->mTargetObj, true ); + + $pout = $content->getParserOutput( $this->mTargetObj, $rev->getId(), $popts, true ); + $out->addParserOutput( $pout ); } + if ( $isText ) { + // source view for textual content + $sourceView = Xml::element( 'textarea', array( + 'readonly' => 'readonly', + 'cols' => intval( $user->getOption( 'cols' ) ), + 'rows' => intval( $user->getOption( 'rows' ) ) ), + $content->getNativeData() . "\n" ); + + $previewButton = Xml::element( 'input', array( + 'type' => 'submit', + 'name' => 'preview', + 'value' => $this->msg( 'showpreview' )->text() ) ); + } else { + $sourceView = ''; + $previewButton = ''; + } + + $diffButton = Xml::element( 'input', array( + 'name' => 'diff', + 'type' => 'submit', + 'value' => $this->msg( 'showdiff' )->text() ) ); + $out->addHTML( - Xml::element( 'textarea', array( - 'readonly' => 'readonly', - 'cols' => intval( $user->getOption( 'cols' ) ), - 'rows' => intval( $user->getOption( 'rows' ) ) ), - $rev->getText( Revision::FOR_THIS_USER, $user ) . "\n" ) . - Xml::openElement( 'div' ) . + $sourceView . + Xml::openElement( 'div', array( + 'style' => 'clear: both' ) ) . Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) ) ) ) . @@ -883,14 +985,8 @@ class SpecialUndelete extends SpecialPage { 'type' => 'hidden', 'name' => 'wpEditToken', 'value' => $user->getEditToken() ) ) . - Xml::element( 'input', array( - 'type' => 'submit', - 'name' => 'preview', - 'value' => $this->msg( 'showpreview' )->text() ) ) . - Xml::element( 'input', array( - 'name' => 'diff', - 'type' => 'submit', - 'value' => $this->msg( 'showdiff' )->text() ) ) . + $previewButton . + $diffButton . Xml::closeElement( 'form' ) . Xml::closeElement( 'div' ) ); } @@ -904,7 +1000,11 @@ class SpecialUndelete extends SpecialPage { * @return String: HTML */ function showDiff( $previousRev, $currentRev ) { - $diffEngine = new DifferenceEngine( $this->getContext() ); + $diffContext = clone $this->getContext(); + $diffContext->setTitle( $currentRev->getTitle() ); + $diffContext->setWikiPage( WikiPage::factory( $currentRev->getTitle() ) ); + + $diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext ); $diffEngine->showDiffStyle(); $this->getOutput()->addHTML( "" . @@ -921,9 +1021,9 @@ class SpecialUndelete extends SpecialPage { $this->diffHeader( $currentRev, 'n' ) . "\n" . "" . - $diffEngine->generateDiffBody( - $previousRev->getText( Revision::FOR_THIS_USER, $this->getUser() ), - $currentRev->getText( Revision::FOR_THIS_USER, $this->getUser() ) ) . + $diffEngine->generateContentDiffBody( + $previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ), + $currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ) ) . "" . "\n" ); @@ -1180,7 +1280,10 @@ class SpecialUndelete extends SpecialPage { private function formatRevisionRow( $row, $earliestLiveTime, $remaining ) { $rev = Revision::newFromArchiveRow( $row, - array( 'page' => $this->mTargetObj->getArticleID() ) ); + array( + 'title' => $this->mTargetObj + ) ); + $revTextSize = ''; $ts = wfTimestamp( TS_MW, $row->ar_timestamp ); // Build checkboxen... @@ -1417,11 +1520,15 @@ class SpecialUndelete extends SpecialPage { $out->addHTML( $this->msg( 'undeletedpage' )->rawParams( $link )->parse() ); } else { $out->setPageTitle( $this->msg( 'undelete-error' ) ); - $out->addWikiMsg( 'cannotundelete' ); - $out->addWikiMsg( 'undeleterevdel' ); } - // Show file deletion warnings and errors + // Show revision undeletion warnings and errors + $status = $archive->getRevisionStatus(); + if( $status && !$status->isGood() ) { + $out->addWikiText( '' . $status->getWikiText( 'cannotundelete', 'cannotundelete' ) . '' ); + } + + // Show file undeletion warnings and errors $status = $archive->getFileStatus(); if( $status && !$status->isGood() ) { $out->addWikiText( '' . $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) . '' ); diff --git a/includes/upload/UploadFromUrl.php b/includes/upload/UploadFromUrl.php index c11d7192a4..fdd2b1ae0b 100644 --- a/includes/upload/UploadFromUrl.php +++ b/includes/upload/UploadFromUrl.php @@ -70,13 +70,14 @@ class UploadFromUrl extends UploadBase { if ( !count( $wgCopyUploadsDomains ) ) { return true; } - $parsedUrl = wfParseUrl( $url ); - if ( !$parsedUrl ) { + $uri = new Uri( $url ); + $parsedDomain = $uri->getHost(); + if ( $parsedDomain === null ) { return false; } $valid = false; foreach( $wgCopyUploadsDomains as $domain ) { - if ( $parsedUrl['host'] === $domain ) { + if ( $parsedDomain === $domain ) { $valid = true; break; } diff --git a/languages/Language.php b/languages/Language.php index 40d1f36cd9..472871fc89 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -419,6 +419,16 @@ class Language { */ public function setNamespaces( array $namespaces ) { $this->namespaceNames = $namespaces; + $this->mNamespaceIds = null; + } + + /** + * Resets all of the namespace caches. Mainly used for testing + */ + public function resetNamespaces( ) { + $this->namespaceNames = null; + $this->mNamespaceIds = null; + $this->namespaceAliases = null; } /** diff --git a/languages/LanguageConverter.php b/languages/LanguageConverter.php index 8b7d6cbec0..a21156261c 100644 --- a/languages/LanguageConverter.php +++ b/languages/LanguageConverter.php @@ -925,7 +925,11 @@ class LanguageConverter { if ( $title && $title->exists() ) { $revision = Revision::newFromTitle( $title ); if ( $revision ) { - $txt = $revision->getRawText(); + if ( $revision->getContentModel() == CONTENT_MODEL_WIKITEXT ) { + $txt = $revision->getContent( Revision::RAW )->getNativeData(); + } + + //@todo: in the future, use a specialized content model, perhaps based on json! } } } diff --git a/languages/classes/LanguageFi.php b/languages/classes/LanguageFi.php index 1865cc5ce1..6a2820d148 100644 --- a/languages/classes/LanguageFi.php +++ b/languages/classes/LanguageFi.php @@ -102,7 +102,7 @@ class LanguageFi extends Language { 'monday' => 'maanantai', 'tuesday' => 'tiistai', 'wednesday' => 'keskiviikko', - 'thursay' => 'torstai', + 'thursday' => 'torstai', 'friday' => 'perjantai', 'saturday' => 'lauantai', 'sunday' => 'sunnuntai', diff --git a/languages/messages/MessagesDe.php b/languages/messages/MessagesDe.php index 107b280d0c..cbd55344b5 100644 --- a/languages/messages/MessagesDe.php +++ b/languages/messages/MessagesDe.php @@ -2603,7 +2603,8 @@ Der aktuelle Text der gelöschten Seite ist nur Administratoren zugänglich.', 'undeletedrevisions' => '{{PLURAL:$1|1 Version wurde|$1 Versionen wurden}} wiederhergestellt', 'undeletedrevisions-files' => '{{PLURAL:$1|1 Version|$1 Versionen}} und {{PLURAL:$2|1 Datei|$2 Dateien}} wurden wiederhergestellt', 'undeletedfiles' => '{{PLURAL:$1|1 Datei wurde|$1 Dateien wurden}} wiederhergestellt', -'cannotundelete' => 'Wiederherstellung fehlgeschlagen; jemand anderes hat die Seite bereits wiederhergestellt.', +'cannotundelete' => 'Wiederherstellung fehlgeschlagen: +$1', 'undeletedpage' => "'''â$1â''' wurde wiederhergestellt. Im [[Special:Log/delete|Lösch-Logbuch]] findest du eine Ãbersicht der gelöschten und wiederhergestellten Seiten.", diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index a97dfe7620..e96418dc0c 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -895,6 +895,7 @@ $1', 'portal-url' => 'Project:Community portal', 'privacy' => 'Privacy policy', 'privacypage' => 'Project:Privacy policy', +'content-failed-to-parse' => "Failed to parse $2 content for $1 model: $3", 'badaccess' => 'Permission error', 'badaccess-group0' => 'You are not allowed to execute the action you have requested.', @@ -1430,7 +1431,7 @@ If you save it, any changes made since this revision will be lost.", 'yourdiff' => 'Differences', 'copyrightwarning' => "Please note that all contributions to {{SITENAME}} are considered to be released under the $2 (see $1 for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
-You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource. +You are also promising us that you wrote this yourself, or copied editpageit from a public domain or similar free resource. '''Do not submit copyrighted work without permission!'''", 'copyrightwarning2' => "Please note that all contributions to {{SITENAME}} may be edited, altered, or removed by other contributors. If you do not want your writing to be edited mercilessly, then do not submit it here.
@@ -1487,6 +1488,8 @@ It already exists.', 'addsection-preload' => '', # do not translate or duplicate this message to other languages 'addsection-editintro' => '', # do not translate or duplicate this message to other languages 'defaultmessagetext' => 'Default message text', +'invalid-content-data' => 'Invalid content data', +'content-not-allowed-here' => '"$1" content is not allowed on page [[$2]]', # Parser/template warnings 'expensive-parserfunction-warning' => "'''Warning:''' This page contains too many expensive parser function calls. @@ -3070,8 +3073,8 @@ You may have a bad link, or the revision may have been restored or removed from 'undeletedrevisions' => '{{PLURAL:$1|1 revision|$1 revisions}} restored', 'undeletedrevisions-files' => '{{PLURAL:$1|1 revision|$1 revisions}} and {{PLURAL:$2|1 file|$2 files}} restored', 'undeletedfiles' => '{{PLURAL:$1|1 file|$1 files}} restored', -'cannotundelete' => 'Undelete failed; -someone else may have undeleted the page first.', +'cannotundelete' => 'Undelete failed: +$1', 'undeletedpage' => "'''$1 has been restored''' Consult the [[Special:Log/delete|deletion log]] for a record of recent deletions and restorations.", @@ -4944,4 +4947,10 @@ Otherwise, you can use the easy form below. Your comment will be added to the pa 'duration-centuries' => '$1 {{PLURAL:$1|century|centuries}}', 'duration-millennia' => '$1 {{PLURAL:$1|millennium|millennia}}', +# Content model IDs for the ContentHandler facility; used by ContentHandler::getContentModel() +'content-model-wikitext' => 'wikitext', +'content-model-javascript' => 'JavaScript', +'content-model-css' => 'CSS', +'content-model-text' => 'plain text', + ); diff --git a/languages/messages/MessagesGa.php b/languages/messages/MessagesGa.php index 522a976a5f..1cdba1eef2 100644 --- a/languages/messages/MessagesGa.php +++ b/languages/messages/MessagesGa.php @@ -1773,6 +1773,9 @@ iarradh sábháil. Is dócha gur nasc chuig suÃomh seachtrach ba chúis leis.', 'spamprotectionmatch' => 'Truicear ár scagaire dramhála ag an téacs seo a leanas: $1', 'spambot_username' => 'MediaWiki turscar glanadh', +# Info page +'pageinfo-subjectpage' => 'Leathanach', + # Skin names 'skinname-standard' => 'Clasaiceach', 'skinname-nostalgia' => 'Sean-nós', diff --git a/languages/messages/MessagesQqq.php b/languages/messages/MessagesQqq.php index 858ea564f5..a1e4a73ad7 100644 --- a/languages/messages/MessagesQqq.php +++ b/languages/messages/MessagesQqq.php @@ -1053,6 +1053,10 @@ Please report at [[Support]] if you are unable to properly translate this messag 'moveddeleted-notice' => 'Shown on top of a deleted page in normal view modus ([http://translatewiki.net/wiki/Test example]).', 'edit-conflict' => "An 'Edit conflict' happens when more than one edit is being made to a page at the same time. This would usually be caused by separate individuals working on the same page. However, if the system is slow, several edits from one individual could back up and attempt to apply simultaneously - causing the conflict.", 'defaultmessagetext' => 'Caption above the default message text shown on the left-hand side of a diff displayed after clicking âShow changesâ when creating a new page in the MediaWiki: namespace', +'invalid-content-data' => 'Error message indicating that the page\'s content can not be saved because it is invalid. This may occurr for some non-text content types.', +'content-not-allowed-here' => 'Error message indicating that the desired content model is not supported in given localtion. +* $1 is the human readable name of the content model +* $1 is the title of the page in question.', # Parser/template warnings 'expensive-parserfunction-warning' => 'On some (expensive) [[MetaWikipedia:Help:ParserFunctions|parser functions]] (e.g.) there is a limit of how many times it may be used. This is an error message shown when the limit is exceeded. @@ -2882,6 +2886,8 @@ Options for the duration of the page protection. Example: See e.g. [[MediaWiki:P {{Identical|Reset}}', 'undeleteinvert' => '{{Identical|Invert selection}}', 'undeletecomment' => '{{Identical|Reason}}', +'cannotundelete' => 'Message shown when undeletion failed for some reason. +*
{{#ifexist:}} $1
is the combined wikitext of messages for all errors that caused the failure.', 'undelete-search-title' => 'Page title when showing the search form in Special:Undelete', 'undelete-search-submit' => '{{Identical|Search}}', 'undelete-error' => 'Page title when a page could not be undeleted', @@ -4883,4 +4889,10 @@ $4 is the gender of the target user.', 'api-error-uploaddisabled' => 'API error message that can be used for client side localisation of API errors.', 'api-error-verification-error' => 'The word "extension" refers to the part behind the last dot in a file name, that by convention gives a hint about the kind of data format which a files contents are in.', +# Content model IDs for the ContentHandler facility; used by ContentHandler::getContentModel() +'content-model-wikitext' => 'Name for the wikitext content model, used when decribing what type of content a page contains.', +'content-model-javascript' => 'Name for the JavaScript content model, used when decribing what type of content a page contains.', +'content-model-css' => 'Name for the CSS content model, used when decribing what type of content a page contains.', +'content-model-text' => 'Name for the plain text content model, used when decribing what type of content a page contains.', + ); diff --git a/maintenance/Maintenance.php b/maintenance/Maintenance.php index 4151723797..cff77b2d71 100644 --- a/maintenance/Maintenance.php +++ b/maintenance/Maintenance.php @@ -1144,7 +1144,8 @@ abstract class Maintenance { $title = $titleObj->getPrefixedDBkey(); $this->output( "$title..." ); # Update searchindex - $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getText() ); + # TODO: pass the Content object to SearchUpdate, let the search engine decide how to deal with it. + $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent()->getTextForSearchIndex() ); $u->doUpdate(); $this->output( "\n" ); } diff --git a/maintenance/archives/patch-archive-ar_content_format.sql b/maintenance/archives/patch-archive-ar_content_format.sql new file mode 100644 index 0000000000..81f9fca8cb --- /dev/null +++ b/maintenance/archives/patch-archive-ar_content_format.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/archive + ADD ar_content_format varbinary(64) DEFAULT NULL; diff --git a/maintenance/archives/patch-archive-ar_content_model.sql b/maintenance/archives/patch-archive-ar_content_model.sql new file mode 100644 index 0000000000..1a8b630e4c --- /dev/null +++ b/maintenance/archives/patch-archive-ar_content_model.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/archive + ADD ar_content_model varbinary(32) DEFAULT NULL; diff --git a/maintenance/archives/patch-page-page_content_model.sql b/maintenance/archives/patch-page-page_content_model.sql new file mode 100644 index 0000000000..30434d93ce --- /dev/null +++ b/maintenance/archives/patch-page-page_content_model.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/page + ADD page_content_model varbinary(32) DEFAULT NULL; diff --git a/maintenance/archives/patch-revision-rev_content_format.sql b/maintenance/archives/patch-revision-rev_content_format.sql new file mode 100644 index 0000000000..22aeb8a760 --- /dev/null +++ b/maintenance/archives/patch-revision-rev_content_format.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/revision + ADD rev_content_format varbinary(64) DEFAULT NULL; diff --git a/maintenance/archives/patch-revision-rev_content_model.sql b/maintenance/archives/patch-revision-rev_content_model.sql new file mode 100644 index 0000000000..1ba05721cc --- /dev/null +++ b/maintenance/archives/patch-revision-rev_content_model.sql @@ -0,0 +1,2 @@ +ALTER TABLE /*$wgDBprefix*/revision + ADD rev_content_model varbinary(32) DEFAULT NULL; diff --git a/maintenance/checkBadRedirects.php b/maintenance/checkBadRedirects.php index 670b93de2b..4ba7e66b6c 100644 --- a/maintenance/checkBadRedirects.php +++ b/maintenance/checkBadRedirects.php @@ -50,7 +50,7 @@ class CheckBadRedirects extends Maintenance { $title = Title::makeTitle( $row->page_namespace, $row->page_title ); $rev = Revision::newFromId( $row->page_latest ); if ( $rev ) { - $target = Title::newFromRedirect( $rev->getText() ); + $target = $rev->getContent()->getRedirectTarget(); if ( !$target ) { $this->output( $title->getPrefixedText() . "\n" ); } diff --git a/maintenance/cleanupSpam.php b/maintenance/cleanupSpam.php index e20bcd87d6..98385694b3 100644 --- a/maintenance/cleanupSpam.php +++ b/maintenance/cleanupSpam.php @@ -103,7 +103,8 @@ class CleanupSpam extends Maintenance { $rev = Revision::newFromTitle( $title ); $currentRevId = $rev->getId(); - while ( $rev && ( $rev->isDeleted( Revision::DELETED_TEXT ) || LinkFilter::matchEntry( $rev->getText() , $domain ) ) ) { + while ( $rev && ( $rev->isDeleted( Revision::DELETED_TEXT ) + || LinkFilter::matchEntry( $rev->getContent( Revision::RAW ), $domain ) ) ) { $rev = $rev->getPrevious(); } @@ -117,8 +118,10 @@ class CleanupSpam extends Maintenance { $page = WikiPage::factory( $title ); if ( $rev ) { // Revert to this revision + $content = $rev->getContent( Revision::RAW ); + $this->output( "reverting\n" ); - $page->doEdit( $rev->getText(), wfMessage( 'spam_reverting', $domain )->inContentLanguage()->text(), + $page->doEditContent( $content, wfMessage( 'spam_reverting', $domain )->inContentLanguage()->text(), EDIT_UPDATE, $rev->getId() ); } elseif ( $this->hasOption( 'delete' ) ) { // Didn't find a non-spammy revision, blank the page @@ -126,8 +129,11 @@ class CleanupSpam extends Maintenance { $page->doDeleteArticle( wfMessage( 'spam_deleting', $domain )->inContentLanguage()->text() ); } else { // Didn't find a non-spammy revision, blank the page + $handler = ContentHandler::getForTitle( $title ); + $content = $handler->makeEmptyContent(); + $this->output( "blanking\n" ); - $page->doEdit( '', wfMessage( 'spam_blanking', $domain )->inContentLanguage()->text() ); + $page->doEditContent( $content, wfMessage( 'spam_blanking', $domain )->inContentLanguage()->text() ); } $dbw->commit( __METHOD__ ); } diff --git a/maintenance/compareParsers.php b/maintenance/compareParsers.php index a3337173c2..1f3ac1c3b3 100644 --- a/maintenance/compareParsers.php +++ b/maintenance/compareParsers.php @@ -114,15 +114,24 @@ class CompareParsers extends DumpIterator { $parser1 = new $parser1Name(); $parser2 = new $parser2Name(); - $output1 = $parser1->parse( $rev->getText(), $title, $this->options ); - $output2 = $parser2->parse( $rev->getText(), $title, $this->options ); + $content = $rev->getContent(); + + if ( $content->getModel() !== CONTENT_MODEL_WIKITEXT ) { + $this->error( "Page {$title->getPrefixedText()} does not contain wikitext but {$content->getModel()}\n" ); + return; + } + + $text = strval( $content->getNativeData() ); + + $output1 = $parser1->parse( $text, $title, $this->options ); + $output2 = $parser2->parse( $text, $title, $this->options ); if ( $output1->getText() != $output2->getText() ) { $this->failed++; $this->error( "Parsing for {$title->getPrefixedText()} differs\n" ); if ( $this->saveFailed ) { - file_put_contents( $this->saveFailed . '/' . rawurlencode( $title->getPrefixedText() ) . ".txt", $rev->getText()); + file_put_contents( $this->saveFailed . '/' . rawurlencode( $title->getPrefixedText() ) . ".txt", $text ); } if ( $this->showDiff ) { $this->output( wfDiff( $this->stripParameters( $output1->getText() ), $this->stripParameters( $output2->getText() ), '' ) ); diff --git a/maintenance/cssjanus/cssjanus.py b/maintenance/cssjanus/cssjanus.py old mode 100755 new mode 100644 diff --git a/maintenance/cssjanus/csslex.py b/maintenance/cssjanus/csslex.py old mode 100755 new mode 100644 diff --git a/maintenance/dev/install.sh b/maintenance/dev/install.sh old mode 100755 new mode 100644 diff --git a/maintenance/dev/installmw.sh b/maintenance/dev/installmw.sh old mode 100755 new mode 100644 diff --git a/maintenance/dev/installphp.sh b/maintenance/dev/installphp.sh old mode 100755 new mode 100644 diff --git a/maintenance/dev/start.sh b/maintenance/dev/start.sh old mode 100755 new mode 100644 diff --git a/maintenance/dumpIterator.php b/maintenance/dumpIterator.php index 3657f960f7..870d632126 100644 --- a/maintenance/dumpIterator.php +++ b/maintenance/dumpIterator.php @@ -168,7 +168,7 @@ class SearchDump extends DumpIterator { * @param $rev Revision */ public function processRevision( $rev ) { - if ( preg_match( $this->getOption( 'regex' ), $rev->getText() ) ) { + if ( preg_match( $this->getOption( 'regex' ), $rev->getContent()->getTextForSearchIndex() ) ) { $this->output( $rev->getTitle() . " matches at edit from " . $rev->getTimestamp() . "\n" ); } } diff --git a/maintenance/edit.php b/maintenance/edit.php index 59df5e8863..ad4c12fd19 100644 --- a/maintenance/edit.php +++ b/maintenance/edit.php @@ -68,10 +68,11 @@ class EditCLI extends Maintenance { # Read the text $text = $this->getStdin( Maintenance::STDIN_ALL ); + $content = ContentHandler::makeContent( $text, $wgTitle ); # Do the edit $this->output( "Saving... " ); - $status = $page->doEdit( $text, $summary, + $status = $page->doEditContent( $content, $summary, ( $minor ? EDIT_MINOR : 0 ) | ( $bot ? EDIT_FORCE_BOT : 0 ) | ( $autoSummary ? EDIT_AUTOSUMMARY : 0 ) | diff --git a/maintenance/getText.php b/maintenance/getText.php index 3e2f854010..f6adfe2be1 100644 --- a/maintenance/getText.php +++ b/maintenance/getText.php @@ -52,12 +52,12 @@ class GetTextMaint extends Maintenance { $titleText = $title->getPrefixedText(); $this->error( "Page $titleText does not exist.\n", true ); } - $text = $rev->getText( $this->hasOption( 'show-private' ) ? Revision::RAW : Revision::FOR_PUBLIC ); - if ( $text === false ) { + $content = $rev->getContent( $this->hasOption( 'show-private' ) ? Revision::RAW : Revision::FOR_PUBLIC ); + if ( $content === false ) { $titleText = $title->getPrefixedText(); $this->error( "Couldn't extract the text from $titleText.\n", true ); } - $this->output( $text ); + $this->output( $content->serialize() ); } } diff --git a/maintenance/hiphop/make b/maintenance/hiphop/make old mode 100755 new mode 100644 diff --git a/maintenance/hiphop/run-server b/maintenance/hiphop/run-server old mode 100755 new mode 100644 diff --git a/maintenance/importSiteScripts.php b/maintenance/importSiteScripts.php index e369cb1592..e25ee2be64 100644 --- a/maintenance/importSiteScripts.php +++ b/maintenance/importSiteScripts.php @@ -56,13 +56,17 @@ class ImportSiteScripts extends Maintenance { } $this->output( "Importing $page\n" ); - $url = wfAppendQuery( $baseUrl, array( + $uri = new Uri( $baseUrl ); + $uri->extendQuery( array( 'action' => 'raw', 'title' => "MediaWiki:{$page}" ) ); + $url = $uri->toString(); + $text = Http::get( $url ); $wikiPage = WikiPage::factory( $title ); - $wikiPage->doEdit( $text, "Importing from $url", 0, false, $user ); + $content = ContentHandler::makeContent( $text, $wikiPage->getTitle() ); + $wikiPage->doEditContent( $content, "Importing from $url", 0, false, $user ); } } @@ -79,7 +83,9 @@ class ImportSiteScripts extends Maintenance { $pages = array(); do { - $url = wfAppendQuery( $baseUrl, $data ); + $uri = new Uri( $baseUrl ); + $uri->extendQuery( $data ); + $url = $uri->toString(); $strResult = Http::get( $url ); //$result = FormatJson::decode( $strResult ); // Still broken $result = unserialize( $strResult ); diff --git a/maintenance/importTextFile.php b/maintenance/importTextFile.php index adb5063589..c04989c0a7 100644 --- a/maintenance/importTextFile.php +++ b/maintenance/importTextFile.php @@ -56,7 +56,8 @@ if ( count( $args ) < 1 || isset( $options['help'] ) ) { echo( "\nPerforming edit..." ); $page = WikiPage::factory( $title ); - $page->doEdit( $text, $comment, $flags, false, $user ); + $content = ContentHandler::makeContent( $text, $title ); + $page->doEditContent( $content, $comment, $flags, false, $user ); echo( "done.\n" ); } else { diff --git a/maintenance/populateRevisionLength.php b/maintenance/populateRevisionLength.php index 6c835f4ed2..07c395fbae 100644 --- a/maintenance/populateRevisionLength.php +++ b/maintenance/populateRevisionLength.php @@ -74,16 +74,16 @@ class PopulateRevisionLength extends LoggedUpdateMaintenance { # Go through and update rev_len from these rows. foreach ( $res as $row ) { $rev = new Revision( $row ); - $text = $rev->getRawText(); - if ( !is_string( $text ) ) { + $content = $rev->getContent(); + if ( !$content ) { # This should not happen, but sometimes does (bug 20757) - $this->output( "Text of revision {$row->rev_id} unavailable!\n" ); + $this->output( "Content of revision {$row->rev_id} unavailable!\n" ); $missing++; } else { # Update the row... $db->update( 'revision', - array( 'rev_len' => strlen( $text ) ), + array( 'rev_len' => $content->getSize() ), array( 'rev_id' => $row->rev_id ), __METHOD__ ); $count++; diff --git a/maintenance/populateRevisionSha1.php b/maintenance/populateRevisionSha1.php index 2e14d31e8f..382b7be4d8 100644 --- a/maintenance/populateRevisionSha1.php +++ b/maintenance/populateRevisionSha1.php @@ -143,14 +143,14 @@ class PopulateRevisionSha1 extends LoggedUpdateMaintenance { $rev = ( $table === 'archive' ) ? Revision::newFromArchiveRow( $row ) : new Revision( $row ); - $text = $rev->getRawText(); + $text = $rev->getSerializedData(); } catch ( MWException $e ) { - $this->output( "Text of revision with {$idCol}={$row->$idCol} unavailable!\n" ); + $this->output( "Data of revision with {$idCol}={$row->$idCol} unavailable!\n" ); return false; // bug 22624? } if ( !is_string( $text ) ) { # This should not happen, but sometimes does (bug 20757) - $this->output( "Text of revision with {$idCol}={$row->$idCol} unavailable!\n" ); + $this->output( "Data of revision with {$idCol}={$row->$idCol} unavailable!\n" ); return false; } else { $db->update( $table, @@ -174,10 +174,10 @@ class PopulateRevisionSha1 extends LoggedUpdateMaintenance { $this->output( "Text of revision with timestamp {$row->ar_timestamp} unavailable!\n" ); return false; // bug 22624? } - $text = $rev->getRawText(); + $text = $rev->getSerializedData(); if ( !is_string( $text ) ) { # This should not happen, but sometimes does (bug 20757) - $this->output( "Text of revision with timestamp {$row->ar_timestamp} unavailable!\n" ); + $this->output( "Data of revision with timestamp {$row->ar_timestamp} unavailable!\n" ); return false; } else { # Archive table as no PK, but (NS,title,time) should be near unique. diff --git a/maintenance/preprocessDump.php b/maintenance/preprocessDump.php index 5952fd960c..87fc997454 100644 --- a/maintenance/preprocessDump.php +++ b/maintenance/preprocessDump.php @@ -78,8 +78,14 @@ class PreprocessDump extends DumpIterator { * @param $rev Revision */ public function processRevision( $rev ) { + $content = $rev->getContent( Revision::RAW ); + + if ( $content->getModel() !== CONTENT_MODEL_WIKITEXT ) { + return; + } + try { - $this->mPreprocessor->preprocessToObj( $rev->getText(), 0 ); + $this->mPreprocessor->preprocessToObj( strval( $content->getNativeData() ), 0 ); } catch(Exception $e) { $this->error("Caught exception " . $e->getMessage() . " in " . $rev->getTitle()->getPrefixedText() ); diff --git a/maintenance/refreshLinks.php b/maintenance/refreshLinks.php index 3d9270b8d3..535808d606 100644 --- a/maintenance/refreshLinks.php +++ b/maintenance/refreshLinks.php @@ -216,18 +216,16 @@ class RefreshLinks extends Maintenance { return; } - $text = $page->getRawText(); - if ( $text === false ) { + $content = $page->getContent( REVISION::RAW ); + if ( null === false ) { return; } $dbw = wfGetDB( DB_MASTER ); $dbw->begin( __METHOD__ ); - $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); - $parserOutput = $wgParser->parse( $text, $page->getTitle(), $options, true, true, $page->getLatest() ); - $update = new LinksUpdate( $page->getTitle(), $parserOutput, false ); - $update->doUpdate(); + $updates = $content->getSecondaryDataUpdates( $page->getTitle() ); + DataUpdate::runUpdates( $updates ); $dbw->commit( __METHOD__ ); } diff --git a/maintenance/renderDump.php b/maintenance/renderDump.php index 24bedfa4b4..ad9a380f7b 100644 --- a/maintenance/renderDump.php +++ b/maintenance/renderDump.php @@ -100,10 +100,10 @@ class DumpRenderer extends Maintenance { $this->output( sprintf( "%s\n", $filename, $display ) ); $user = new User(); - $parser = new $wgParserConf['class'](); $options = ParserOptions::newFromUser( $user ); - $output = $parser->parse( $rev->getText(), $title, $options ); + $content = $rev->getContent(); + $output = $content->getParserOutput( $title, null, $options ); file_put_contents( $filename, "getText(); + $text = $revision->getSerializedData(); $uncompressedSize += strlen( $text ); $hashes[$row->rev_id] = md5( $text ); $keys[$row->rev_id] = $blob->addItem( $text ); diff --git a/maintenance/tables.sql b/maintenance/tables.sql index 062052f77c..89e6ea5674 100644 --- a/maintenance/tables.sql +++ b/maintenance/tables.sql @@ -260,7 +260,10 @@ CREATE TABLE /*_*/page ( page_latest int unsigned NOT NULL, -- Uncompressed length in bytes of the page's current source text. - page_len int unsigned NOT NULL + page_len int unsigned NOT NULL, + + -- content model, see CONTENT_MODEL_XXX constants + page_content_model int unsigned default NULL ) /*$wgDBTableOptions*/; CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title); @@ -316,7 +319,13 @@ CREATE TABLE /*_*/revision ( rev_parent_id int unsigned default NULL, -- SHA-1 text content hash in base-36 - rev_sha1 varbinary(32) NOT NULL default '' + rev_sha1 varbinary(32) NOT NULL default '', + + -- content model, see CONTENT_MODEL_XXX constants + rev_content_model int unsigned default NULL, + + -- content format, see CONTENT_FORMAT_XXX constants + rev_content_format int unsigned default NULL ) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024; -- In case tables are created as MyISAM, use row hints for MySQL <5.0 to avoid 4GB limit @@ -427,7 +436,14 @@ CREATE TABLE /*_*/archive ( ar_parent_id int unsigned default NULL, -- SHA-1 text content hash in base-36 - ar_sha1 varbinary(32) NOT NULL default '' + ar_sha1 varbinary(32) NOT NULL default '', + + -- content model, see CONTENT_MODEL_XXX constants + ar_content_model int unsigned default NULL, + + -- content format, see CONTENT_FORMAT_XXX constants + ar_content_format int unsigned default NULL + ) /*$wgDBTableOptions*/; CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp); diff --git a/tests/parser/parserTest.inc b/tests/parser/parserTest.inc index cd5983379c..47d3e71100 100644 --- a/tests/parser/parserTest.inc +++ b/tests/parser/parserTest.inc @@ -1204,7 +1204,7 @@ class ParserTest { } } - $page->doEdit( $text, '', EDIT_NEW ); + $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW ); $wgCapitalLinks = $oldCapitalLinks; } diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index 1cc45e087f..d3dd38936f 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -19,6 +19,16 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { protected $reuseDB = false; protected $tablesUsed = array(); // tables with data + protected $restoreGlobals = array( // global variables to restore for each test + 'wgLang', + 'wgContLang', + 'wgLanguageCode', + 'wgUser', + 'wgTitle', + ); + + private $savedGlobals = array(); + private static $dbSetup = false; /** @@ -119,7 +129,21 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { return $fname; } - protected function tearDown() { + protected function setup() { + parent::setup(); + + foreach ( $this->restoreGlobals as $var ) { + $v = $GLOBALS[ $var ]; + + if ( is_object( $v ) || is_array( $v ) ) { + $v = clone $v; + } + + $this->savedGlobals[ $var ] = $v; + } + } + + protected function teardown() { // Cleaning up temporary files foreach ( $this->tmpfiles as $fname ) { if ( is_file( $fname ) || ( is_link( $fname ) ) ) { @@ -136,7 +160,12 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } } - parent::tearDown(); + // restore saved globals + foreach ( $this->savedGlobals as $k => $v ) { + $GLOBALS[ $k ] = $v; + } + + parent::teardown(); } function dbPrefix() { @@ -212,11 +241,12 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { //Make 1 page with 1 revision $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); if ( !$page->getId() == 0 ) { - $page->doEdit( 'UTContent', - 'UTPageSummary', - EDIT_NEW, - false, - User::newFromName( 'UTSysop' ) ); + $page->doEditContent( + new WikitextContent( 'UTContent' ), + 'UTPageSummary', + EDIT_NEW, + false, + User::newFromName( 'UTSysop' ) ); } } diff --git a/tests/phpunit/includes/ArticleTablesTest.php b/tests/phpunit/includes/ArticleTablesTest.php index 17cee6e808..967ffa17e0 100644 --- a/tests/phpunit/includes/ArticleTablesTest.php +++ b/tests/phpunit/includes/ArticleTablesTest.php @@ -16,14 +16,14 @@ class ArticleTablesTest extends MediaWikiLangTestCase { $wgContLang = Language::factory( 'es' ); $wgLang = Language::factory( 'fr' ); - $status = $page->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', 0, false, $user ); + $status = $page->doEditContent( new WikitextContent( '{{:{{int:history}}}}' ), 'Test code for bug 14404', 0, false, $user ); $templates1 = $title->getTemplateLinksFrom(); $wgLang = Language::factory( 'de' ); $page->mPreparedEdit = false; // In order to force the rerendering of the same wikitext // We need an edit, a purge is not enough to regenerate the tables - $status = $page->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', EDIT_UPDATE, false, $user ); + $status = $page->doEditContent( new WikitextContent( '{{:{{int:history}}}}' ), 'Test code for bug 14404', EDIT_UPDATE, false, $user ); $templates2 = $title->getTemplateLinksFrom(); $this->assertEquals( $templates1, $templates2 ); diff --git a/tests/phpunit/includes/ArticleTest.php b/tests/phpunit/includes/ArticleTest.php index 846d2b86c2..744417b1fc 100644 --- a/tests/phpunit/includes/ArticleTest.php +++ b/tests/phpunit/includes/ArticleTest.php @@ -54,6 +54,9 @@ class ArticleTest extends MediaWikiTestCase { * Checks for the existence of the backwards compatibility static functions (forwarders to WikiPage class) */ function testStaticFunctions() { + $this->hideDeprecated( 'Article::getAutosummary' ); + $this->hideDeprecated( 'WikiPage::getAutosummary' ); + $this->assertEquals( WikiPage::selectFields(), Article::selectFields(), "Article static functions" ); $this->assertEquals( true, is_callable( "Article::onArticleCreate" ), diff --git a/tests/phpunit/includes/ContentHandlerTest.php b/tests/phpunit/includes/ContentHandlerTest.php new file mode 100644 index 0000000000..633f72d340 --- /dev/null +++ b/tests/phpunit/includes/ContentHandlerTest.php @@ -0,0 +1,415 @@ +resetNamespaces(); # reset namespace cache + } + + public function teardown() { + global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang; + + unset( $wgExtraNamespaces[ 12312 ] ); + unset( $wgExtraNamespaces[ 12313 ] ); + + unset( $wgNamespaceContentModels[ 12312 ] ); + unset( $wgContentHandlers[ "testing" ] ); + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + + parent::teardown(); + } + + public function dataGetDefaultModelFor() { + return array( + array( 'Foo', CONTENT_MODEL_WIKITEXT ), + array( 'Foo.js', CONTENT_MODEL_WIKITEXT ), + array( 'Foo/bar.js', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo.js', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'User:Foo/bar.css', CONTENT_MODEL_CSS ), + array( 'User talk:Foo/bar.css', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.js.xxx', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.xxx', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'MediaWiki:Foo.css', CONTENT_MODEL_CSS ), + array( 'MediaWiki:Foo.JS', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ), + ); + } + + /** + * @dataProvider dataGetDefaultModelFor + */ + public function testGetDefaultModelFor( $title, $expectedModelId ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedModelId, ContentHandler::getDefaultModelFor( $title ) ); + } + /** + * @dataProvider dataGetDefaultModelFor + */ + public function testGetForTitle( $title, $expectedContentModel ) { + $title = Title::newFromText( $title ); + $handler = ContentHandler::getForTitle( $title ); + $this->assertEquals( $expectedContentModel, $handler->getModelID() ); + } + + public function dataGetLocalizedName() { + return array( + array( null, null ), + array( "xyzzy", null ), + + array( CONTENT_MODEL_JAVASCRIPT, '/javascript/i' ), //XXX: depends on content language + ); + } + + /** + * @dataProvider dataGetLocalizedName + */ + public function testGetLocalizedName( $id, $expected ) { + $name = ContentHandler::getLocalizedName( $id ); + + if ( $expected ) { + $this->assertNotNull( $name, "no name found for content model $id" ); + $this->assertTrue( preg_match( $expected, $name ) > 0 , + "content model name for #$id did not match pattern $expected" ); + } else { + $this->assertEquals( $id, $name, "localization of unknown model $id should have " + . "fallen back to use the model id directly." ); + } + } + + public function dataGetPageLanguage() { + global $wgLanguageCode; + + return array( + array( "Main", $wgLanguageCode ), + array( "Dummy:Foo", $wgLanguageCode ), + array( "MediaWiki:common.js", 'en' ), + array( "User:Foo/common.js", 'en' ), + array( "MediaWiki:common.css", 'en' ), + array( "User:Foo/common.css", 'en' ), + array( "User:Foo", $wgLanguageCode ), + + array( CONTENT_MODEL_JAVASCRIPT, 'javascript' ), + ); + } + + /** + * @dataProvider dataGetPageLanguage + */ + public function testGetPageLanguage( $title, $expected ) { + if ( is_string( $title ) ) { + $title = Title::newFromText( $title ); + } + + $expected = wfGetLangObj( $expected ); + + $handler = ContentHandler::getForTitle( $title ); + $lang = $handler->getPageLanguage( $title ); + + $this->assertEquals( $expected->getCode(), $lang->getCode() ); + } + + public function testGetContentText_Null( ) { + global $wgContentHandlerTextFallback; + + $content = null; + + $wgContentHandlerTextFallback = 'fail'; + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( '', $text ); + + $wgContentHandlerTextFallback = 'serialize'; + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( '', $text ); + + $wgContentHandlerTextFallback = 'ignore'; + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( '', $text ); + } + + public function testGetContentText_TextContent( ) { + global $wgContentHandlerTextFallback; + + $content = new WikitextContent( "hello world" ); + + $wgContentHandlerTextFallback = 'fail'; + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( $content->getNativeData(), $text ); + + $wgContentHandlerTextFallback = 'serialize'; + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( $content->serialize(), $text ); + + $wgContentHandlerTextFallback = 'ignore'; + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( $content->getNativeData(), $text ); + } + + public function testGetContentText_NonTextContent( ) { + global $wgContentHandlerTextFallback; + + $content = new DummyContentForTesting( "hello world" ); + + $wgContentHandlerTextFallback = 'fail'; + + try { + $text = ContentHandler::getContentText( $content ); + + $this->fail( "ContentHandler::getContentText should have thrown an exception for non-text Content object" ); + } catch (MWException $ex) { + // as expected + } + + $wgContentHandlerTextFallback = 'serialize'; + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( $content->serialize(), $text ); + + $wgContentHandlerTextFallback = 'ignore'; + $text = ContentHandler::getContentText( $content ); + $this->assertNull( $text ); + } + + #public static function makeContent( $text, Title $title, $modelId = null, $format = null ) + + public function dataMakeContent() { + return array( + array( 'hallo', 'Test', null, null, CONTENT_MODEL_WIKITEXT, 'hallo', false ), + array( 'hallo', 'MediaWiki:Test.js', null, null, CONTENT_MODEL_JAVASCRIPT, 'hallo', false ), + array( serialize('hallo'), 'Dummy:Test', null, null, "testing", 'hallo', false ), + + array( 'hallo', 'Test', null, CONTENT_FORMAT_WIKITEXT, CONTENT_MODEL_WIKITEXT, 'hallo', false ), + array( 'hallo', 'MediaWiki:Test.js', null, CONTENT_FORMAT_JAVASCRIPT, CONTENT_MODEL_JAVASCRIPT, 'hallo', false ), + array( serialize('hallo'), 'Dummy:Test', null, "testing", "testing", 'hallo', false ), + + array( 'hallo', 'Test', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, 'hallo', false ), + array( 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, 'hallo', false ), + array( serialize('hallo'), 'Dummy:Test', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, serialize('hallo'), false ), + + array( 'hallo', 'Test', CONTENT_MODEL_WIKITEXT, "testing", null, null, true ), + array( 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, "testing", null, null, true ), + array( 'hallo', 'Dummy:Test', CONTENT_MODEL_JAVASCRIPT, "testing", null, null, true ), + ); + } + + /** + * @dataProvider dataMakeContent + */ + public function testMakeContent( $data, $title, $modelId, $format, $expectedModelId, $expectedNativeData, $shouldFail ) { + global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers; + + $title = Title::newFromText( $title ); + + try { + $content = ContentHandler::makeContent( $data, $title, $modelId, $format ); + + if ( $shouldFail ) $this->fail( "ContentHandler::makeContent should have failed!" ); + + $this->assertEquals( $expectedModelId, $content->getModel(), 'bad model id' ); + $this->assertEquals( $expectedNativeData, $content->getNativeData(), 'bads native data' ); + } catch ( MWException $ex ) { + if ( !$shouldFail ) $this->fail( "ContentHandler::makeContent failed unexpectedly: " . $ex->getMessage() ); + else $this->assertTrue( true ); // dummy, so we don't get the "test did not perform any assertions" message. + } + + } + + public function testSupportsSections() { + $this->markTestIncomplete( "not yet implemented" ); + } + + public function testRunLegacyHooks() { + Hooks::register( 'testRunLegacyHooks', __CLASS__ . '::dummyHookHandler' ); + + $content = new WikitextContent( 'test text' ); + $ok = ContentHandler::runLegacyHooks( 'testRunLegacyHooks', array( 'foo', &$content, 'bar' ), false ); + + $this->assertTrue( $ok, "runLegacyHooks should have returned true" ); + $this->assertEquals( "TEST TEXT", $content->getNativeData() ); + } + + public static function dummyHookHandler( $foo, &$text, $bar ) { + if ( $text === null || $text === false ) { + return false; + } + + $text = strtoupper( $text ); + + return true; + } +} + +class DummyContentHandlerForTesting extends ContentHandler { + + public function __construct( $dataModel ) { + parent::__construct( $dataModel, array( "testing" ) ); + } + + /** + * Serializes Content object of the type supported by this ContentHandler. + * + * @param Content $content the Content object to serialize + * @param null $format the desired serialization format + * @return String serialized form of the content + */ + public function serializeContent( Content $content, $format = null ) + { + return $content->serialize(); + } + + /** + * Unserializes a Content object of the type supported by this ContentHandler. + * + * @param $blob String serialized form of the content + * @param null $format the format used for serialization + * @return Content the Content object created by deserializing $blob + */ + public function unserializeContent( $blob, $format = null ) + { + $d = unserialize( $blob ); + return new DummyContentForTesting( $d ); + } + + /** + * Creates an empty Content object of the type supported by this ContentHandler. + * + */ + public function makeEmptyContent() + { + return new DummyContentForTesting( '' ); + } +} + +class DummyContentForTesting extends AbstractContent { + + public function __construct( $data ) { + parent::__construct( "testing" ); + + $this->data = $data; + } + + public function serialize( $format = null ) { + return serialize( $this->data ); + } + + /** + * @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 function getTextForSearchIndex() + { + return ''; + } + + /** + * @return String the wikitext to include when another page includes this content, or false if the content is not + * includable in a wikitext page. + */ + public function getWikitextForTransclusion() + { + return false; + } + + /** + * 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 function getTextForSummary( $maxlength = 250 ) + { + return ''; + } + + /** + * 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 function getNativeData() + { + return $this->data; + } + + /** + * returns the content's nominal size in bogo-bytes. + * + * @return int + */ + public function getSize() + { + return strlen( $this->data ); + } + + /** + * Return a copy of this Content object. The following must be true for the object returned + * if $copy = $original->copy() + * + * * get_class($original) === get_class($copy) + * * $original->getModel() === $copy->getModel() + * * $original->equals( $copy ) + * + * If and only if the Content object is imutable, the copy() method can and should + * return $this. That is, $copy === $original may be true, but only for imutable content + * objects. + * + * @return Content. A copy of this object + */ + public function copy() + { + return $this; + } + + /** + * 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. + * @return boolean + */ + public function isCountable( $hasLinks = null ) + { + return false; + } + + /** + * @param Title $title + * @param null $revId + * @param null|ParserOptions $options + * @param Boolean $generateHtml whether to generate Html (default: true). If false, + * the result of calling getText() on the ParserOutput object returned by + * this method is undefined. + * + * @return ParserOutput + */ + public function getParserOutput( Title $title, $revId = null, ParserOptions $options = NULL, $generateHtml = true ) + { + return new ParserOutput( $this->getNativeData() ); + } +} diff --git a/tests/phpunit/includes/CssContentTest.php b/tests/phpunit/includes/CssContentTest.php new file mode 100644 index 0000000000..163ce48eab --- /dev/null +++ b/tests/phpunit/includes/CssContentTest.php @@ -0,0 +1,47 @@ +\n", "\nhello <world>\n\n\n"), + // @todo: more...? + ); + } + + + # ================================================================================================================= + + public function testGetModel() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_CSS, $content->getModel() ); + } + + public function testGetContentHandler() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_CSS, $content->getContentHandler()->getModelID() ); + } + + public function dataEquals( ) { + return array( + array( new CssContent( "hallo" ), null, false ), + array( new CssContent( "hallo" ), new CssContent( "hallo" ), true ), + array( new CssContent( "hallo" ), new WikitextContent( "hallo" ), false ), + array( new CssContent( "hallo" ), new CssContent( "HALLO" ), false ), + ); + } + +} diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php index 9097d301f5..129f271bf0 100644 --- a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php +++ b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php @@ -108,7 +108,7 @@ class GlobalTest extends MediaWikiTestCase { array( array( 'foo' => 1 ), 'foo=1' ), // number test array( array( 'foo' => true ), 'foo=1' ), // true test array( array( 'foo' => false ), '' ), // false test - array( array( 'foo' => null ), '' ), // null test + array( array( 'foo' => null ), 'foo' ), // null test array( array( 'foo' => 'A&B=5+6@!"\'' ), 'foo=A%26B%3D5%2B6%40%21%22%27' ), // urlencoding test array( array( 'foo' => 'bar', 'baz' => 'is', 'asdf' => 'qwerty' ), 'foo=bar&baz=is&asdf=qwerty' ), // multi-item test array( array( 'foo' => array( 'bar' => 'baz' ) ), 'foo%5Bbar%5D=baz' ), diff --git a/tests/phpunit/includes/JavascriptContentTest.php b/tests/phpunit/includes/JavascriptContentTest.php new file mode 100644 index 0000000000..f8a9bdfbd8 --- /dev/null +++ b/tests/phpunit/includes/JavascriptContentTest.php @@ -0,0 +1,259 @@ +\n", + "\nhello <world>\n\n\n"), + // @todo: more...? + ); + } + + public function dataGetSection() { + return array( + array( WikitextContentTest::$sections, + "0", + null + ), + array( WikitextContentTest::$sections, + "2", + null + ), + array( WikitextContentTest::$sections, + "8", + null + ), + ); + } + + public function dataReplaceSection() { + return array( + array( WikitextContentTest::$sections, + "0", + "No more", + null, + null + ), + array( WikitextContentTest::$sections, + "", + "No more", + null, + null + ), + array( WikitextContentTest::$sections, + "2", + "== TEST ==\nmore fun", + null, + null + ), + array( WikitextContentTest::$sections, + "8", + "No more", + null, + null + ), + array( WikitextContentTest::$sections, + "new", + "No more", + "New", + null + ), + ); + } + + public function testAddSectionHeader( ) { + $content = $this->newContent( 'hello world' ); + $c = $content->addSectionHeader( 'test' ); + + $this->assertTrue( $content->equals( $c ) ); + } + + // XXX: currently, preSaveTransform is applied to scripts. this may change or become optional. + /* + public function dataPreSaveTransform() { + return array( + array( 'hello this is ~~~', + "hello this is ~~~", + ), + array( 'hello \'\'this\'\' is~~~ ', + 'hello \'\'this\'\' is~~~ ', + ), + ); + } + */ + + public function dataPreloadTransform() { + return array( + array( 'hello this is ~~~', + "hello this is ~~~", + ), + array( 'hello \'\'this\'\' isfoo bar ', + 'hello \'\'this\'\' isfoo bar ', + ), + ); + } + + public function dataGetRedirectTarget() { + return array( + array( '#REDIRECT [[Test]]', + null, + ), + array( '#REDIRECT Test', + null, + ), + array( '* #REDIRECT [[Test]]', + null, + ), + ); + } + + /** + * @todo: test needs database! + */ + /* + public function getRedirectChain() { + $text = $this->getNativeData(); + return Title::newFromRedirectArray( $text ); + } + */ + + /** + * @todo: test needs database! + */ + /* + public function getUltimateRedirectTarget() { + $text = $this->getNativeData(); + return Title::newFromRedirectRecurse( $text ); + } + */ + + + public function dataIsCountable() { + return array( + array( '', + null, + 'any', + true + ), + array( 'Foo', + null, + 'any', + true + ), + array( 'Foo', + null, + 'comma', + false + ), + array( 'Foo, bar', + null, + 'comma', + false + ), + array( 'Foo', + null, + 'link', + false + ), + array( 'Foo [[bar]]', + null, + 'link', + false + ), + array( 'Foo', + true, + 'link', + false + ), + array( 'Foo [[bar]]', + false, + 'link', + false + ), + array( '#REDIRECT [[bar]]', + true, + 'any', + true + ), + array( '#REDIRECT [[bar]]', + true, + 'comma', + false + ), + array( '#REDIRECT [[bar]]', + true, + 'link', + false + ), + ); + } + + public function dataGetTextForSummary() { + return array( + array( "hello\nworld.", + 16, + 'hello world.', + ), + array( 'hello world.', + 8, + 'hello...', + ), + array( '[[hello world]].', + 8, + '[[hel...', + ), + ); + } + + public function testMatchMagicWord( ) { + $mw = MagicWord::get( "staticredirect" ); + + $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" ); + $this->assertFalse( $content->matchMagicWord( $mw ), "should not have matched magic word, since it's not wikitext" ); + } + + public function testUpdateRedirect( ) { + $target = Title::newFromText( "testUpdateRedirect_target" ); + + $content = $this->newContent( "#REDIRECT [[Someplace]]" ); + $newContent = $content->updateRedirect( $target ); + + $this->assertTrue( $content->equals( $newContent ), "content should be unchanged since it's not wikitext" ); + } + + # ================================================================================================================= + + public function testGetModel() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getModel() ); + } + + public function testGetContentHandler() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getContentHandler()->getModelID() ); + } + + public function dataEquals( ) { + return array( + array( new JavascriptContent( "hallo" ), null, false ), + array( new JavascriptContent( "hallo" ), new JavascriptContent( "hallo" ), true ), + array( new JavascriptContent( "hallo" ), new CssContent( "hallo" ), false ), + array( new JavascriptContent( "hallo" ), new JavascriptContent( "HALLO" ), false ), + ); + } + +} diff --git a/tests/phpunit/includes/LinksUpdateTest.php b/tests/phpunit/includes/LinksUpdateTest.php index 494620015a..a50a2235e2 100644 --- a/tests/phpunit/includes/LinksUpdateTest.php +++ b/tests/phpunit/includes/LinksUpdateTest.php @@ -146,7 +146,9 @@ class LinksUpdateTest extends MediaWikiTestCase { protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, Array $expectedRows ) { $update = new LinksUpdate( $title, $parserOutput ); + $update->beginTransaction(); $update->doUpdate(); + $update->commitTransaction(); $this->assertSelect( $table, $fields, $condition, $expectedRows ); } diff --git a/tests/phpunit/includes/RevisionStorageTest.php b/tests/phpunit/includes/RevisionStorageTest.php index 8a7facece4..4786e3d44f 100644 --- a/tests/phpunit/includes/RevisionStorageTest.php +++ b/tests/phpunit/includes/RevisionStorageTest.php @@ -3,6 +3,7 @@ /** * Test class for Revision storage. * + * @group ContentHandler * @group Database * ^--- important, causes temporary tables to be used instead of the real database * @@ -11,6 +12,9 @@ */ class RevisionStorageTest extends MediaWikiTestCase { + /** + * @var WikiPage $the_page + */ var $the_page; function __construct( $name = null, array $data = array(), $dataName = '' ) { @@ -35,11 +39,35 @@ class RevisionStorageTest extends MediaWikiTestCase { } public function setUp() { + global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang; + + $wgExtraNamespaces[ 12312 ] = 'Dummy'; + $wgExtraNamespaces[ 12313 ] = 'Dummy_talk'; + + $wgNamespaceContentModels[ 12312 ] = 'DUMMY'; + $wgContentHandlers[ 'DUMMY' ] = 'DummyContentHandlerForTesting'; + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + if ( !$this->the_page ) { $this->the_page = $this->createPage( 'RevisionStorageTest_the_page', "just a dummy page" ); } } + public function tearDown() { + global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang; + + unset( $wgExtraNamespaces[ 12312 ] ); + unset( $wgExtraNamespaces[ 12313 ] ); + + unset( $wgNamespaceContentModels[ 12312 ] ); + unset( $wgContentHandlers[ 'DUMMY' ] ); + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + } + protected function makeRevision( $props = null ) { if ( $props === null ) $props = array(); @@ -63,7 +91,8 @@ class RevisionStorageTest extends MediaWikiTestCase { $page->doDeleteArticle( "done" ); } - $page->doEdit( $text, "testing", EDIT_NEW ); + $content = ContentHandler::makeContent( $text, $page->getTitle(), $model ); + $page->doEditContent( $content, "testing", EDIT_NEW ); return $page; } @@ -75,6 +104,8 @@ class RevisionStorageTest extends MediaWikiTestCase { $this->assertEquals( $orig->getPage(), $rev->getPage() ); $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() ); $this->assertEquals( $orig->getUser(), $rev->getUser() ); + $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() ); + $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() ); $this->assertEquals( $orig->getSha1(), $rev->getSha1() ); } @@ -158,7 +189,7 @@ class RevisionStorageTest extends MediaWikiTestCase { $page = $this->createPage( 'RevisionStorageTest_testFetchRevision', 'one' ); $id1 = $page->getRevision()->getId(); - $page->doEdit( 'two', 'second rev' ); + $page->doEditContent( new WikitextContent( 'two' ), 'second rev' ); $id2 = $page->getRevision()->getId(); $res = Revision::fetchRevision( $page->getTitle() ); @@ -179,12 +210,23 @@ class RevisionStorageTest extends MediaWikiTestCase { */ public function testSelectFields() { + global $wgContentHandlerUseDB; + $fields = Revision::selectFields(); $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields'); $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields'); $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields'); $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields'); + + if ( $wgContentHandlerUseDB ) { + $this->assertTrue( in_array( 'rev_content_model', $fields ), + 'missing rev_content_model in list of fields'); + $this->assertTrue( in_array( 'rev_content_format', $fields ), + 'missing rev_content_format in list of fields'); + } else { + $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' ); + } } /** @@ -205,12 +247,25 @@ class RevisionStorageTest extends MediaWikiTestCase { */ public function testGetText() { + $this->hideDeprecated( 'Revision::getText' ); + $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) ); $rev = Revision::newFromId( $orig->getId() ); $this->assertEquals( 'hello hello.', $rev->getText() ); } + /** + * @covers Revision::getContent + */ + public function testGetContent() + { + $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( 'hello hello.', $rev->getContent()->getNativeData() ); + } + /** * @covers Revision::revText */ @@ -228,11 +283,51 @@ class RevisionStorageTest extends MediaWikiTestCase { */ public function testGetRawText() { + $this->hideDeprecated( 'Revision::getRawText' ); + $orig = $this->makeRevision( array( 'text' => 'hello hello raw.' ) ); $rev = Revision::newFromId( $orig->getId() ); $this->assertEquals( 'hello hello raw.', $rev->getRawText() ); } + + /** + * @covers Revision::getContentModel + */ + public function testGetContentModel() + { + global $wgContentHandlerUseDB; + + if ( !$wgContentHandlerUseDB ) { + $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' ); + } + + $orig = $this->makeRevision( array( 'text' => 'hello hello.', + 'content_model' => CONTENT_MODEL_JAVASCRIPT ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() ); + } + + /** + * @covers Revision::getContentFormat + */ + public function testGetContentFormat() + { + global $wgContentHandlerUseDB; + + if ( !$wgContentHandlerUseDB ) { + $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' ); + } + + $orig = $this->makeRevision( array( 'text' => 'hello hello.', + 'content_model' => CONTENT_MODEL_JAVASCRIPT, + 'content_format' => CONTENT_FORMAT_JAVASCRIPT ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( CONTENT_FORMAT_JAVASCRIPT, $rev->getContentFormat() ); + } + /** * @covers Revision::isCurrent */ @@ -247,7 +342,7 @@ class RevisionStorageTest extends MediaWikiTestCase { $rev1x = Revision::newFromId( $rev1->getId() ); $this->assertTrue( $rev1x->isCurrent() ); - $page->doEdit( 'Bla bla', 'second rev' ); + $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ), 'second rev' ); $rev2 = $page->getRevision(); # @todo: find out if this should be true @@ -270,7 +365,8 @@ class RevisionStorageTest extends MediaWikiTestCase { $this->assertNull( $rev1->getPrevious() ); - $page->doEdit( 'Bla bla', 'second rev testGetPrevious' ); + $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ), + 'second rev testGetPrevious' ); $rev2 = $page->getRevision(); $this->assertNotNull( $rev2->getPrevious() ); @@ -287,7 +383,8 @@ class RevisionStorageTest extends MediaWikiTestCase { $this->assertNull( $rev1->getNext() ); - $page->doEdit( 'Bla bla', 'second rev testGetNext' ); + $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ), + 'second rev testGetNext' ); $rev2 = $page->getRevision(); $this->assertNotNull( $rev1->getNext() ); @@ -305,9 +402,11 @@ class RevisionStorageTest extends MediaWikiTestCase { $dbw = wfGetDB( DB_MASTER ); $rev = Revision::newNullRevision( $dbw, $page->getId(), 'a null revision', false ); - $this->assertNotEquals( $orig->getId(), $rev->getId(), 'new null revision shold have a different id from the original revision' ); - $this->assertEquals( $orig->getTextId(), $rev->getTextId(), 'new null revision shold have the same text id as the original revision' ); - $this->assertEquals( 'some testing text', $rev->getText() ); + $this->assertNotEquals( $orig->getId(), $rev->getId(), + 'new null revision shold have a different id from the original revision' ); + $this->assertEquals( $orig->getTextId(), $rev->getTextId(), + 'new null revision shold have the same text id as the original revision' ); + $this->assertEquals( 'some testing text', $rev->getContent()->getNativeData() ); } public function dataUserWasLastToEdit() { @@ -351,6 +450,7 @@ class RevisionStorageTest extends MediaWikiTestCase { # zero $revisions[0] = new Revision( array( 'page' => $page->getId(), + 'title' => $page->getTitle(), // we need the title to determine the page's default content model 'timestamp' => '20120101000000', 'user' => $userA->getId(), 'text' => 'zero', @@ -361,6 +461,7 @@ class RevisionStorageTest extends MediaWikiTestCase { # one $revisions[1] = new Revision( array( 'page' => $page->getId(), + 'title' => $page->getTitle(), // still need the title, because $page->getId() is 0 (there's no entry in the page table) 'timestamp' => '20120101000100', 'user' => $userA->getId(), 'text' => 'one', @@ -371,6 +472,7 @@ class RevisionStorageTest extends MediaWikiTestCase { # two $revisions[2] = new Revision( array( 'page' => $page->getId(), + 'title' => $page->getTitle(), 'timestamp' => '20120101000200', 'user' => $userB->getId(), 'text' => 'two', @@ -381,6 +483,7 @@ class RevisionStorageTest extends MediaWikiTestCase { # three $revisions[3] = new Revision( array( 'page' => $page->getId(), + 'title' => $page->getTitle(), 'timestamp' => '20120101000300', 'user' => $userA->getId(), 'text' => 'three', @@ -391,6 +494,7 @@ class RevisionStorageTest extends MediaWikiTestCase { # four $revisions[4] = new Revision( array( 'page' => $page->getId(), + 'title' => $page->getTitle(), 'timestamp' => '20120101000200', 'user' => $userA->getId(), 'text' => 'zero', diff --git a/tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php b/tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php new file mode 100644 index 0000000000..3dfaa8de3f --- /dev/null +++ b/tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php @@ -0,0 +1,86 @@ +saveContentHandlerNoDB = $wgContentHandlerUseDB; + + $wgContentHandlerUseDB = false; + + $dbw = wfGetDB( DB_MASTER ); + + $page_table = $dbw->tableName( 'page' ); + $revision_table = $dbw->tableName( 'revision' ); + $archive_table = $dbw->tableName( 'archive' ); + + if ( $dbw->fieldExists( $page_table, 'page_content_model' ) ) { + $dbw->query( "alter table $page_table drop column page_content_model" ); + $dbw->query( "alter table $revision_table drop column rev_content_model" ); + $dbw->query( "alter table $revision_table drop column rev_content_format" ); + $dbw->query( "alter table $archive_table drop column ar_content_model" ); + $dbw->query( "alter table $archive_table drop column ar_content_format" ); + } + + parent::setUp(); + } + + function tearDown() { + global $wgContentHandlerUseDB; + + parent::tearDown(); + + $wgContentHandlerUseDB = $this->saveContentHandlerNoDB; + } + + /** + * @covers Revision::selectFields + */ + public function testSelectFields() + { + $fields = Revision::selectFields(); + + $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields'); + $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields'); + $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields'); + $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields'); + + $this->assertFalse( in_array( 'rev_content_model', $fields ), 'missing rev_content_model in list of fields'); + $this->assertFalse( in_array( 'rev_content_format', $fields ), 'missing rev_content_format in list of fields'); + } + + /** + * @covers Revision::getContentModel + */ + public function testGetContentModel() + { + $orig = $this->makeRevision( array( 'text' => 'hello hello.', 'content_model' => CONTENT_MODEL_JAVASCRIPT ) ); + $rev = Revision::newFromId( $orig->getId() ); + + //NOTE: database fields for the content_model are disabled, so the model name is not retained. + // We expect to get the default here instead of what was suppleid when creating the revision. + $this->assertEquals( CONTENT_MODEL_WIKITEXT, $rev->getContentModel() ); + } + + + /** + * @covers Revision::getContentFormat + */ + public function testGetContentFormat() + { + $orig = $this->makeRevision( array( 'text' => 'hello hello.', 'content_model' => CONTENT_MODEL_JAVASCRIPT, 'content_format' => 'text/javascript' ) ); + $rev = Revision::newFromId( $orig->getId() ); + + $this->assertEquals( CONTENT_FORMAT_WIKITEXT, $rev->getContentFormat() ); + } + +} + + diff --git a/tests/phpunit/includes/RevisionTest.php b/tests/phpunit/includes/RevisionTest.php index d7654db9f2..377891490b 100644 --- a/tests/phpunit/includes/RevisionTest.php +++ b/tests/phpunit/includes/RevisionTest.php @@ -1,25 +1,54 @@ false, 'wgCompressRevisions' => false, + + 'wgContentHandlerTextFallback' => $GLOBALS['wgContentHandlerTextFallback'], + 'wgExtraNamespaces' => $GLOBALS['wgExtraNamespaces'], + 'wgNamespaceContentModels' => $GLOBALS['wgNamespaceContentModels'], + 'wgContentHandlers' => $GLOBALS['wgContentHandlers'], ); + foreach ( $globalSet as $var => $data ) { $this->saveGlobals[$var] = $GLOBALS[$var]; $GLOBALS[$var] = $data; } + + global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang; + $wgExtraNamespaces[ 12312 ] = 'Dummy'; + $wgExtraNamespaces[ 12313 ] = 'Dummy_talk'; + + $wgNamespaceContentModels[ 12312 ] = "testing"; + $wgContentHandlers[ "testing" ] = 'DummyContentHandlerForTesting'; + $wgContentHandlers[ "RevisionTestModifyableContent" ] = 'RevisionTestModifyableContentHandler'; + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + + global $wgContentHandlerTextFallback; + $wgContentHandlerTextFallback = 'ignore'; } function tearDown() { + global $wgContLang; + foreach ( $this->saveGlobals as $var => $data ) { $GLOBALS[$var] = $data; } + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache } function testGetRevisionText() { @@ -120,6 +149,293 @@ class RevisionTest extends MediaWikiTestCase { $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", Revision::getRevisionText( $row ), "getRevisionText" ); } + + # ================================================================================================================= + + /** + * @param string $text + * @param string $title + * @param string $model + * @return Revision + */ + function newTestRevision( $text, $title = "Test", $model = CONTENT_MODEL_WIKITEXT, $format = null ) { + if ( is_string( $title ) ) { + $title = Title::newFromText( $title ); + } + + $content = ContentHandler::makeContent( $text, $title, $model, $format ); + + $rev = new Revision( + array( + 'id' => 42, + 'page' => 23, + 'title' => $title, + + 'content' => $content, + 'length' => $content->getSize(), + 'comment' => "testing", + 'minor_edit' => false, + + 'content_format' => $format, + ) + ); + + return $rev; + } + + function dataGetContentModel() { + return array( + array( 'hello world', 'Hello', null, null, CONTENT_MODEL_WIKITEXT ), + array( 'hello world', 'User:hello/there.css', null, null, CONTENT_MODEL_CSS ), + array( serialize('hello world'), 'Dummy:Hello', null, null, "testing" ), + ); + } + + /** + * @group Database + * @dataProvider dataGetContentModel + */ + function testGetContentModel( $text, $title, $model, $format, $expectedModel ) { + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedModel, $rev->getContentModel() ); + } + + function dataGetContentFormat() { + return array( + array( 'hello world', 'Hello', null, null, CONTENT_FORMAT_WIKITEXT ), + array( 'hello world', 'Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ), + array( 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ), + array( serialize('hello world'), 'Dummy:Hello', null, null, "testing" ), + ); + } + + /** + * @group Database + * @dataProvider dataGetContentFormat + */ + function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) { + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedFormat, $rev->getContentFormat() ); + } + + function dataGetContentHandler() { + return array( + array( 'hello world', 'Hello', null, null, 'WikitextContentHandler' ), + array( 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ), + array( serialize('hello world'), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ), + ); + } + + /** + * @group Database + * @dataProvider dataGetContentHandler + */ + function testGetContentHandler( $text, $title, $model, $format, $expectedClass ) { + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedClass, get_class( $rev->getContentHandler() ) ); + } + + function dataGetContent() { + return array( + array( 'hello world', 'Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ), + array( serialize('hello world'), 'Hello', "testing", null, Revision::FOR_PUBLIC, serialize('hello world') ), + array( serialize('hello world'), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, serialize('hello world') ), + ); + } + + /** + * @group Database + * @dataProvider dataGetContent + */ + function testGetContent( $text, $title, $model, $format, $audience, $expectedSerialization ) { + $rev = $this->newTestRevision( $text, $title, $model, $format ); + $content = $rev->getContent( $audience ); + + $this->assertEquals( $expectedSerialization, is_null( $content ) ? null : $content->serialize( $format ) ); + } + + function dataGetText() { + return array( + array( 'hello world', 'Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ), + array( serialize('hello world'), 'Hello', "testing", null, Revision::FOR_PUBLIC, null ), + array( serialize('hello world'), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, null ), + ); + } + + /** + * @group Database + * @dataProvider dataGetText + */ + function testGetText( $text, $title, $model, $format, $audience, $expectedText ) { + $this->hideDeprecated( 'Revision::getText' ); + + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedText, $rev->getText( $audience ) ); + } + + /** + * @group Database + * @dataProvider dataGetText + */ + function testGetRawText( $text, $title, $model, $format, $audience, $expectedText ) { + $this->hideDeprecated( 'Revision::getRawText' ); + + $rev = $this->newTestRevision( $text, $title, $model, $format ); + + $this->assertEquals( $expectedText, $rev->getRawText( $audience ) ); + } + + + public function dataGetSize( ) { + return array( + array( "hello world.", null, 12 ), + array( serialize( "hello world." ), "testing", 12 ), + ); + } + + /** + * @covers Revision::getSize + * @group Database + * @dataProvider dataGetSize + */ + public function testGetSize( $text, $model, $expected_size ) + { + $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model ); + $this->assertEquals( $expected_size, $rev->getSize() ); + } + + public function dataGetSha1( ) { + return array( + array( "hello world.", null, Revision::base36Sha1( "hello world." ) ), + array( serialize( "hello world." ), "testing", Revision::base36Sha1( serialize( "hello world." ) ) ), + ); + } + + /** + * @covers Revision::getSha1 + * @group Database + * @dataProvider dataGetSha1 + */ + public function testGetSha1( $text, $model, $expected_hash ) + { + $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model ); + $this->assertEquals( $expected_hash, $rev->getSha1() ); + } + + public function testConstructWithText() { + $this->hideDeprecated( "Revision::getText" ); + + $rev = new Revision( array( + 'text' => 'hello world.', + 'content_model' => CONTENT_MODEL_JAVASCRIPT + )); + + $this->assertNotNull( $rev->getText(), 'no content text' ); + $this->assertNotNull( $rev->getContent(), 'no content object available' ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() ); + } + + public function testConstructWithContent() { + $this->hideDeprecated( "Revision::getText" ); + + $title = Title::newFromText( 'RevisionTest_testConstructWithContent' ); + + $rev = new Revision( array( + 'content' => ContentHandler::makeContent( 'hello world.', $title, CONTENT_MODEL_JAVASCRIPT ), + )); + + $this->assertNotNull( $rev->getText(), 'no content text' ); + $this->assertNotNull( $rev->getContent(), 'no content object available' ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() ); + } + + /** + * Tests whether $rev->getContent() returns a clone when needed. + * + * @group Database + */ + function testGetContentClone( ) { + $content = new RevisionTestModifyableContent( "foo" ); + + $rev = new Revision( + array( + 'id' => 42, + 'page' => 23, + 'title' => Title::newFromText( "testGetContentClone_dummy" ), + + 'content' => $content, + 'length' => $content->getSize(), + 'comment' => "testing", + 'minor_edit' => false, + ) + ); + + $content = $rev->getContent( Revision::RAW ); + $content->setText( "bar" ); + + $content2 = $rev->getContent( Revision::RAW ); + $this->assertNotSame( $content, $content2, "expected a clone" ); // content is mutable, expect clone + $this->assertEquals( "foo", $content2->getText() ); // clone should contain the original text + + $content2->setText( "bla bla" ); + $this->assertEquals( "bar", $content->getText() ); // clones should be independent + } + + + /** + * Tests whether $rev->getContent() returns the same object repeatedly if appropriate. + * + * @group Database + */ + function testGetContentUncloned() { + $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT ); + $content = $rev->getContent( Revision::RAW ); + $content2 = $rev->getContent( Revision::RAW ); + + // for immutable content like wikitext, this should be the same object + $this->assertSame( $content, $content2 ); + } + +} + +class RevisionTestModifyableContent extends TextContent { + public function __construct( $text ) { + parent::__construct( $text, "RevisionTestModifyableContent" ); + } + + public function copy( ) { + return new RevisionTestModifyableContent( $this->mText ); + } + + public function getText() { + return $this->mText; + } + + public function setText( $text ) { + $this->mText = $text; + } + } +class RevisionTestModifyableContentHandler extends TextContentHandler { + public function __construct( ) { + parent::__construct( "RevisionTestModifyableContent", array( CONTENT_FORMAT_TEXT ) ); + } + + public function unserializeContent( $text, $format = null ) { + $this->checkFormat( $format ); + + return new RevisionTestModifyableContent( $text ); + } + + public function makeEmptyContent() { + return new RevisionTestModifyableContent( '' ); + } +} diff --git a/tests/phpunit/includes/TemplateCategoriesTest.php b/tests/phpunit/includes/TemplateCategoriesTest.php index 39ce6e314e..03b94aea7f 100644 --- a/tests/phpunit/includes/TemplateCategoriesTest.php +++ b/tests/phpunit/includes/TemplateCategoriesTest.php @@ -13,14 +13,14 @@ class TemplateCategoriesTest extends MediaWikiLangTestCase { $user = new User(); $user->mRights = array( 'createpage', 'edit', 'purge' ); - $status = $page->doEdit( '{{Categorising template}}', 'Create a page with a template', 0, false, $user ); + $status = $page->doEditContent( new WikitextContent( '{{Categorising template}}' ), 'Create a page with a template', 0, false, $user ); $this->assertEquals( array() , $title->getParentCategories() ); $template = WikiPage::factory( Title::newFromText( 'Template:Categorising template' ) ); - $status = $template->doEdit( '[[Category:Solved bugs]]', 'Add a category through a template', 0, false, $user ); + $status = $template->doEditContent( new WikitextContent( '[[Category:Solved bugs]]' ), 'Add a category through a template', 0, false, $user ); // Run the job queue $jobs = new RunJobs; diff --git a/tests/phpunit/includes/TimestampTest.php b/tests/phpunit/includes/TimestampTest.php index 231228f55f..fd4ceb7257 100644 --- a/tests/phpunit/includes/TimestampTest.php +++ b/tests/phpunit/includes/TimestampTest.php @@ -43,6 +43,13 @@ class TimestampTest extends MediaWikiTestCase { * Test human readable timestamp format. */ function testHumanOutput() { + global $wgLang; + + $wgLang = Language::factory( 'es' ); + $timestamp = new MWTimestamp( time() - 3600 ); + $this->assertEquals( "hace una hora", $timestamp->getHumanTimestamp()->toString() ); + + $wgLang = Language::factory( 'en' ); $timestamp = new MWTimestamp( time() - 3600 ); $this->assertEquals( "1 hour ago", $timestamp->getHumanTimestamp()->toString() ); } diff --git a/tests/phpunit/includes/TitleMethodsTest.php b/tests/phpunit/includes/TitleMethodsTest.php index aed658baa3..de8f18fb5b 100644 --- a/tests/phpunit/includes/TitleMethodsTest.php +++ b/tests/phpunit/includes/TitleMethodsTest.php @@ -1,7 +1,34 @@ resetNamespaces(); # reset namespace cache + } + + public function teardown() { + global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContLang; + + unset( $wgExtraNamespaces[ 12302 ] ); + unset( $wgExtraNamespaces[ 12303 ] ); + + unset( $wgNamespaceContentModels[ 12302 ] ); + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + } + public function dataEquals() { return array( array( 'Main Page', 'Main Page', true ), @@ -75,6 +102,47 @@ class TitleMethodsTest extends MediaWikiTestCase { $this->assertEquals( $expectedBool, $title->hasSubjectNamespace( $ns ) ); } + public function dataGetContentModel() { + return array( + array( 'Foo', CONTENT_MODEL_WIKITEXT ), + array( 'Foo.js', CONTENT_MODEL_WIKITEXT ), + array( 'Foo/bar.js', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo.js', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'User:Foo/bar.css', CONTENT_MODEL_CSS ), + array( 'User talk:Foo/bar.css', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.js.xxx', CONTENT_MODEL_WIKITEXT ), + array( 'User:Foo/bar.xxx', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'MediaWiki:Foo.css', CONTENT_MODEL_CSS ), + array( 'MediaWiki:Foo/bar.css', CONTENT_MODEL_CSS ), + array( 'MediaWiki:Foo.JS', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ), + array( 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ), + array( 'TEST-JS:Foo', CONTENT_MODEL_JAVASCRIPT ), + array( 'TEST-JS:Foo.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'TEST-JS:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ), + array( 'TEST-JS_TALK:Foo.js', CONTENT_MODEL_WIKITEXT ), + ); + } + + /** + * @dataProvider dataGetContentModel + */ + public function testGetContentModel( $title, $expectedModelId ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedModelId, $title->getContentModel() ); + } + + /** + * @dataProvider dataGetContentModel + */ + public function testHasContentModel( $title, $expectedModelId ) { + $title = Title::newFromText( $title ); + $this->assertTrue( $title->hasContentModel( $expectedModelId ) ); + } + public function dataIsCssOrJsPage() { return array( array( 'Foo', false ), @@ -92,6 +160,8 @@ class TitleMethodsTest extends MediaWikiTestCase { array( 'MediaWiki:Foo.JS', false ), array( 'MediaWiki:Foo.CSS', false ), array( 'MediaWiki:Foo.css.xxx', false ), + array( 'TEST-JS:Foo', false ), + array( 'TEST-JS:Foo.js', false ), ); } @@ -119,6 +189,8 @@ class TitleMethodsTest extends MediaWikiTestCase { array( 'MediaWiki:Foo.js', false ), array( 'User:Foo/bar.JS', false ), array( 'User:Foo/bar.CSS', false ), + array( 'TEST-JS:Foo', false ), + array( 'TEST-JS:Foo.js', false ), ); } @@ -187,6 +259,9 @@ class TitleMethodsTest extends MediaWikiTestCase { array( 'MediaWiki:Foo/bar.css', false ), array( 'User:Foo/bar.JS', true ), array( 'User:Foo/bar.CSS', true ), + array( 'TEST-JS:Foo', false ), + array( 'TEST-JS:Foo.js', false ), + array( 'TEST-JS_TALK:Foo.js', true ), ); } diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php index f61652df19..2a8ec44fc8 100644 --- a/tests/phpunit/includes/TitleTest.php +++ b/tests/phpunit/includes/TitleTest.php @@ -1,5 +1,10 @@ 'http', + 'delimiter' => '://', + 'user' => null, + 'pass' => null, + 'host' => 'example.com', + 'port' => null, + 'path' => '/', + 'query' => null, + 'fragment' => null, + ), + ), + array( + '//mediawiki.org/wiki/Main_Page', + array( + 'scheme' => null, + 'delimiter' => '//', + 'user' => null, + 'pass' => null, + 'host' => 'mediawiki.org', + 'port' => null, + 'path' => '/wiki/Main_Page', + 'query' => null, + 'fragment' => null, + ), + ), + array( + 'http://user:pass@example.com/', + array( + 'scheme' => 'http', + 'delimiter' => '://', + 'user' => 'user', + 'pass' => 'pass', + 'host' => 'example.com', + 'port' => null, + 'path' => '/', + 'query' => null, + 'fragment' => null, + ), + ), + array( + '/?asdf=asdf', + array( + 'scheme' => null, + 'delimiter' => null, + 'user' => null, + 'pass' => null, + 'host' => null, + 'port' => null, + 'path' => '/', + 'query' => 'asdf=asdf', + 'fragment' => null, + ), + ), + array( + '?asdf=asdf#asdf', + array( + 'scheme' => null, + 'delimiter' => null, + 'user' => null, + 'pass' => null, + 'host' => null, + 'port' => null, + 'path' => null, + 'query' => 'asdf=asdf', + 'fragment' => 'asdf', + ), + ) + ); + } + + /** + * Ensure that get* methods properly match the appropriate getComponent( key ) value + * @dataProvider dataUris + */ + function testGetters( $uri ) { + $uri = new Uri( $uri ); + $getterMap = array( + 'getProtocol' => 'scheme', + 'getUser' => 'user', + 'getPassword' => 'pass', + 'getHost' => 'host', + 'getPort' => 'port', + 'getPath' => 'path', + 'getQueryString' => 'query', + 'getFragment' => 'fragment', + ); + foreach ( $getterMap as $fn => $c ) { + $this->assertSame( $uri->{$fn}(), $uri->getComponent( $c ), "\$uri->{$fn}(); matches \$uri->getComponent( '$c' );" ); + } + } + + /** + * Ensure that Uri has the proper components for our example uris + * @dataProvider dataUris + */ + function testComponents( $uri, $components ) { + $uri = new Uri( $uri ); + + $this->assertSame( $components['scheme'], $uri->getProtocol(), 'Correct scheme' ); + $this->assertSame( $components['delimiter'], $uri->getDelimiter(), 'Correct delimiter' ); + $this->assertSame( $components['user'], $uri->getUser(), 'Correct user' ); + $this->assertSame( $components['pass'], $uri->getPassword(), 'Correct pass' ); + $this->assertSame( $components['host'], $uri->getHost(), 'Correct host' ); + $this->assertSame( $components['port'], $uri->getPort(), 'Correct port' ); + $this->assertSame( $components['path'], $uri->getPath(), 'Correct path' ); + $this->assertSame( $components['query'], $uri->getQueryString(), 'Correct query' ); + $this->assertSame( $components['fragment'], $uri->getFragment(), 'Correct fragment' ); + } + + /** + * Ensure that the aliases work for various components. + */ + function testAliases() { + $url = "//myuser@test.com"; + $uri = new Uri( $url ); + + // Set the aliases. + $uri->setComponent( 'protocol', 'https' ); + $uri->setComponent( 'password', 'mypass' ); + + // Now try getting them. + $this->assertSame( 'https', $uri->getComponent( 'protocol' ), 'Correct protocol (alias for scheme)' ); + $this->assertSame( 'mypass', $uri->getComponent( 'password' ), 'Correct password (alias for pass)' ); + + // Finally check their actual names. + $this->assertSame( 'https', $uri->getProtocol(), 'Alias for scheme works' ); + $this->assertSame( 'mypass', $uri->getPassword(), 'Alias for pass works' ); + } + + /** + * Ensure that Uri's helper methods return the correct data + */ + function testHelpers() { + $uri = new Uri( 'http://a:b@example.com:8080/path?query=value' ); + + $this->assertSame( 'a:b', $uri->getUserInfo(), 'Correct getUserInfo' ); + $this->assertSame( 'example.com:8080', $uri->getHostPort(), 'Correct getHostPort' ); + $this->assertSame( 'a:b@example.com:8080', $uri->getAuthority(), 'Correct getAuthority' ); + $this->assertSame( '/path?query=value', $uri->getRelativePath(), 'Correct getRelativePath' ); + $this->assertSame( 'http://a:b@example.com:8080/path?query=value', $uri->toString(), 'Correct toString' ); + } + + /** + * Ensure that Uri's extend method properly overrides keys + */ + function testExtend() { + $uri = new Uri( 'http://example.org/?a=b&hello=world' ); + $uri->extendQuery( 'a=c&foo=bar' ); + $this->assertSame( 'a=c&hello=world&foo=bar', $uri->getQueryString() ); + } +} diff --git a/tests/phpunit/includes/WikiPageTest.php b/tests/phpunit/includes/WikiPageTest.php index 2949a3af8f..49b27af671 100644 --- a/tests/phpunit/includes/WikiPageTest.php +++ b/tests/phpunit/includes/WikiPageTest.php @@ -1,5 +1,6 @@ pages_to_delete = array(); + + LinkCache::singleton()->clear(); # avoid cached redirect status, etc } public function tearDown() { @@ -49,6 +52,10 @@ class WikiPageTest extends MediaWikiLangTestCase { parent::tearDown(); } + /** + * @param Title $title + * @return WikiPage + */ protected function newPage( $title ) { if ( is_string( $title ) ) $title = Title::newFromText( $title ); @@ -59,16 +66,89 @@ class WikiPageTest extends MediaWikiLangTestCase { return $p; } + + /** + * @param String|Title|WikiPage $page + * @param String $text + * @param int $model + * + * @return WikiPage + */ protected function createPage( $page, $text, $model = null ) { if ( is_string( $page ) ) $page = Title::newFromText( $page ); - if ( $page instanceof Title ) $page = $this->newPage( $page ); - $page->doEdit( $text, "testing", EDIT_NEW ); + if ( $page instanceof Title ) { + $title = $page; + $page = $this->newPage( $page ); + } else { + $title = null; + } + + $content = ContentHandler::makeContent( $text, $page->getTitle(), $model ); + $page->doEditContent( $content, "testing", EDIT_NEW ); return $page; } + public function testDoEditContent() { + $title = Title::newFromText( "WikiPageTest_testDoEditContent" ); + + $page = $this->newPage( $title ); + + $content = ContentHandler::makeContent( "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam " + . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.", + $title ); + + $page->doEditContent( $content, "[[testing]] 1" ); + + $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" ); + $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" ); + $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" ); + $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" ); + + $id = $page->getId(); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' ); + + # ------------------------ + $page = new WikiPage( $title ); + + $retrieved = $page->getContent(); + $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' ); + + # ------------------------ + $content = ContentHandler::makeContent( "At vero eos et accusam et justo duo [[dolores]] et ea rebum. " + . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.", + $title ); + + $page->doEditContent( $content, "testing 2" ); + + # ------------------------ + $page = new WikiPage( $title ); + + $retrieved = $page->getContent(); + $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' ); + + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' ); + } + public function testDoEdit() { + $this->hideDeprecated( "WikiPage::doEdit" ); + $this->hideDeprecated( "WikiPage::getText" ); + $this->hideDeprecated( "Revision::getText" ); + $title = Title::newFromText( "WikiPageTest_testDoEdit" ); $page = $this->newPage( $title ); @@ -76,13 +156,23 @@ class WikiPageTest extends MediaWikiLangTestCase { $text = "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam " . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat."; - $page->doEdit( $text, "testing 1" ); + $page->doEdit( $text, "[[testing]] 1" ); + $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" ); + $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" ); $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" ); $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" ); $id = $page->getId(); + # ------------------------ + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) ); + $n = $res->numRows(); + $res->free(); + + $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' ); + # ------------------------ $page = new WikiPage( $title ); @@ -113,6 +203,8 @@ class WikiPageTest extends MediaWikiLangTestCase { public function testDoQuickEdit() { global $wgUser; + $this->hideDeprecated( "WikiPage::doQuickEdit" ); + $page = $this->createPage( "WikiPageTest_testDoQuickEdit", "original text" ); $text = "quick text"; @@ -123,13 +215,29 @@ class WikiPageTest extends MediaWikiLangTestCase { $this->assertEquals( $text, $page->getText() ); } + public function testDoQuickEditContent() { + global $wgUser; + + $page = $this->createPage( "WikiPageTest_testDoQuickEditContent", "original text" ); + + $content = ContentHandler::makeContent( "quick text", $page->getTitle() ); + $page->doQuickEditContent( $content, $wgUser, "testing q" ); + + # --------------------- + $page = new WikiPage( $page->getTitle() ); + $this->assertTrue( $content->equals( $page->getContent() ) ); + } + public function testDoDeleteArticle() { $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo" ); $id = $page->getId(); $page->doDeleteArticle( "testing deletion" ); + $this->assertFalse( $page->getTitle()->getArticleID() > 0, "Title object should now have page id 0" ); + $this->assertFalse( $page->getId() > 0, "WikiPage should now have page id 0" ); $this->assertFalse( $page->exists(), "WikiPage::exists should return false after page was deleted" ); + $this->assertNull( $page->getContent(), "WikiPage::getContent should return null after page was deleted" ); $this->assertFalse( $page->getText(), "WikiPage::getText should return false after page was deleted" ); $t = Title::newFromText( $page->getTitle()->getPrefixedText() ); @@ -171,10 +279,25 @@ class WikiPageTest extends MediaWikiLangTestCase { $rev = $page->getRevision(); $this->assertEquals( $page->getLatest(), $rev->getId() ); - $this->assertEquals( "some text", $rev->getText() ); + $this->assertEquals( "some text", $rev->getContent()->getNativeData() ); + } + + public function testGetContent() { + $page = $this->newPage( "WikiPageTest_testGetContent" ); + + $content = $page->getContent(); + $this->assertNull( $content ); + + # ----------------- + $this->createPage( $page, "some text" ); + + $content = $page->getContent(); + $this->assertEquals( "some text", $content->getNativeData() ); } public function testGetText() { + $this->hideDeprecated( "WikiPage::getText" ); + $page = $this->newPage( "WikiPageTest_testGetText" ); $text = $page->getText(); @@ -188,6 +311,8 @@ class WikiPageTest extends MediaWikiLangTestCase { } public function testGetRawText() { + $this->hideDeprecated( "WikiPage::getRawText" ); + $page = $this->newPage( "WikiPageTest_testGetRawText" ); $text = $page->getRawText(); @@ -200,7 +325,32 @@ class WikiPageTest extends MediaWikiLangTestCase { $this->assertEquals( "some text", $text ); } - + public function testGetContentModel() { + global $wgContentHandlerUseDB; + + if ( !$wgContentHandlerUseDB ) { + $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' ); + } + + $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() ); + } + + public function testGetContentHandler() { + global $wgContentHandlerUseDB; + + if ( !$wgContentHandlerUseDB ) { + $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' ); + } + + $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( 'JavaScriptContentHandler', get_class( $page->getContentHandler() ) ); + } + public function testExists() { $page = $this->newPage( "WikiPageTest_testExists" ); $this->assertFalse( $page->exists() ); @@ -259,6 +409,10 @@ class WikiPageTest extends MediaWikiLangTestCase { public function testGetRedirectTarget( $title, $text, $target ) { $page = $this->createPage( $title, $text ); + # sanity check, because this test seems to fail for no reason for some people. + $c = $page->getContent(); + $this->assertEquals( 'WikitextContent', get_class( $c ) ); + # now, test the actual redirect $t = $page->getRedirectTarget(); $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() ); @@ -371,15 +525,19 @@ class WikiPageTest extends MediaWikiLangTestCase { public function testIsCountable( $title, $text, $mode, $expected ) { global $wgArticleCountMethod; - $old = $wgArticleCountMethod; + $oldArticleCountMethod = $wgArticleCountMethod; $wgArticleCountMethod = $mode; $page = $this->createPage( $title, $text ); - $editInfo = $page->prepareTextForEdit( $page->getText() ); + $hasLinks = wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, + array( 'pl_from' => $page->getId() ), __METHOD__ ); + + $editInfo = $page->prepareContentForEdit( $page->getContent() ); $v = $page->isCountable(); $w = $page->isCountable( $editInfo ); - $wgArticleCountMethod = $old; + + $wgArticleCountMethod = $oldArticleCountMethod; $this->assertEquals( $expected, $v, "isCountable( null ) returned unexpected value " . var_export( $v, true ) . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" ); @@ -448,7 +606,9 @@ more stuff "2", "== TEST ==\nmore fun", null, - trim( preg_replace( '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==", WikiPageTest::$sections ) ) + trim( preg_replace( '/^== test ==.*== foo ==/sm', + "== TEST ==\nmore fun\n\n== foo ==", + WikiPageTest::$sections ) ) ), array( 'WikiPageTest_testReplaceSection', WikiPageTest::$sections, @@ -471,6 +631,8 @@ more stuff * @dataProvider dataReplaceSection */ public function testReplaceSection( $title, $text, $section, $with, $sectionTitle, $expected ) { + $this->hideDeprecated( "WikiPage::replaceSection" ); + $page = $this->createPage( $title, $text ); $text = $page->replaceSection( $section, $with, $sectionTitle ); $text = trim( $text ); @@ -478,6 +640,18 @@ more stuff $this->assertEquals( $expected, $text ); } + /** + * @dataProvider dataReplaceSection + */ + public function testReplaceSectionContent( $title, $text, $section, $with, $sectionTitle, $expected ) { + $page = $this->createPage( $title, $text ); + + $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() ); + $c = $page->replaceSectionContent( $section, $content, $sectionTitle ); + + $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) ); + } + /* @todo FIXME: fix this! public function testGetUndoText() { global $wgDiff3; @@ -538,19 +712,22 @@ more stuff $text = "one"; $page = $this->newPage( "WikiPageTest_testDoRollback" ); - $page->doEdit( $text, "section one", EDIT_NEW, false, $admin ); + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), + "section one", EDIT_NEW, false, $admin ); $user1 = new User(); $user1->setName( "127.0.1.11" ); $text .= "\n\ntwo"; $page = new WikiPage( $page->getTitle() ); - $page->doEdit( $text, "adding section two", 0, false, $user1 ); + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), + "adding section two", 0, false, $user1 ); $user2 = new User(); $user2->setName( "127.0.2.13" ); $text .= "\n\nthree"; $page = new WikiPage( $page->getTitle() ); - $page->doEdit( $text, "adding section three", 0, false, $user2 ); + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), + "adding section three", 0, false, $user2 ); # we are having issues with doRollback spuriously failing. apparently the last revision somehow goes missing # or not committed under some circumstances. so, make sure the last revision has the right user name. @@ -577,8 +754,9 @@ more stuff } $page = new WikiPage( $page->getTitle() ); - $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" ); - $this->assertEquals( "one\n\ntwo", $page->getText() ); + $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(), + "rollback did not revert to the correct revision" ); + $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() ); } /** @@ -590,14 +768,16 @@ more stuff $text = "one"; $page = $this->newPage( "WikiPageTest_testDoRollback" ); - $page->doEdit( $text, "section one", EDIT_NEW, false, $admin ); + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), + "section one", EDIT_NEW, false, $admin ); $rev1 = $page->getRevision(); $user1 = new User(); $user1->setName( "127.0.1.11" ); $text .= "\n\ntwo"; $page = new WikiPage( $page->getTitle() ); - $page->doEdit( $text, "adding section two", 0, false, $user1 ); + $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), + "adding section two", 0, false, $user1 ); # now, try the rollback $admin->addGroup( "sysop" ); #XXX: make the test user a sysop... @@ -609,8 +789,9 @@ more stuff } $page = new WikiPage( $page->getTitle() ); - $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" ); - $this->assertEquals( "one", $page->getText() ); + $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(), + "rollback did not revert to the correct revision" ); + $this->assertEquals( "one", $page->getContent()->getNativeData() ); } public function dataGetAutosummary( ) { @@ -658,11 +839,14 @@ more stuff * @dataProvider dataGetAutoSummary */ public function testGetAutosummary( $old, $new, $flags, $expected ) { + $this->hideDeprecated( "WikiPage::getAutosummary" ); + $page = $this->newPage( "WikiPageTest_testGetAutosummary" ); $summary = $page->getAutosummary( $old, $new, $flags ); - $this->assertTrue( (bool)preg_match( $expected, $summary ), "Autosummary didn't match expected pattern $expected: $summary" ); + $this->assertTrue( (bool)preg_match( $expected, $summary ), + "Autosummary didn't match expected pattern $expected: $summary" ); } public function dataGetAutoDeleteReason( ) { @@ -739,7 +923,9 @@ more stuff if ( !empty( $edit[1] ) ) $user->setName( $edit[1] ); else $user = $wgUser; - $page->doEdit( $edit[0], "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user ); + $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() ); + + $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user ); $c += 1; } @@ -747,9 +933,11 @@ more stuff $reason = $page->getAutoDeleteReason( $hasHistory ); if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) $this->assertEquals( $expectedResult, $reason ); - else $this->assertTrue( (bool)preg_match( $expectedResult, $reason ), "Autosummary didn't match expected pattern $expectedResult: $reason" ); + else $this->assertTrue( (bool)preg_match( $expectedResult, $reason ), + "Autosummary didn't match expected pattern $expectedResult: $reason" ); - $this->assertEquals( $expectedHistory, $hasHistory, "expected \$hasHistory to be " . var_export( $expectedHistory, true ) ); + $this->assertEquals( $expectedHistory, $hasHistory, + "expected \$hasHistory to be " . var_export( $expectedHistory, true ) ); $page->doDeleteArticle( "done" ); } diff --git a/tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php b/tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php new file mode 100644 index 0000000000..1af6806b79 --- /dev/null +++ b/tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php @@ -0,0 +1,64 @@ +saveContentHandlerNoDB = $wgContentHandlerUseDB; + + $wgContentHandlerUseDB = false; + + $dbw = wfGetDB( DB_MASTER ); + + $page_table = $dbw->tableName( 'page' ); + $revision_table = $dbw->tableName( 'revision' ); + $archive_table = $dbw->tableName( 'archive' ); + + if ( $dbw->fieldExists( $page_table, 'page_content_model' ) ) { + $dbw->query( "alter table $page_table drop column page_content_model" ); + $dbw->query( "alter table $revision_table drop column rev_content_model" ); + $dbw->query( "alter table $revision_table drop column rev_content_format" ); + $dbw->query( "alter table $archive_table drop column ar_content_model" ); + $dbw->query( "alter table $archive_table drop column ar_content_format" ); + } + } + + function tearDown() { + global $wgContentHandlerUseDB; + + $wgContentHandlerUseDB = $this->saveContentHandlerNoDB; + + parent::tearDown(); + } + + public function testGetContentModel() { + $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT ); + + $page = new WikiPage( $page->getTitle() ); + + // NOTE: since the content model is not recorded in the database, + // we expect to get the default, namely CONTENT_MODEL_WIKITEXT + $this->assertEquals( CONTENT_MODEL_WIKITEXT, $page->getContentModel() ); + } + + public function testGetContentHandler() { + $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT ); + + // NOTE: since the content model is not recorded in the database, + // we expect to get the default, namely CONTENT_MODEL_WIKITEXT + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( 'WikitextContentHandler', get_class( $page->getContentHandler() ) ); + } + +} + + diff --git a/tests/phpunit/includes/WikitextContentHandlerTest.php b/tests/phpunit/includes/WikitextContentHandlerTest.php new file mode 100644 index 0000000000..a7615cfb48 --- /dev/null +++ b/tests/phpunit/includes/WikitextContentHandlerTest.php @@ -0,0 +1,199 @@ +handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT ); + } + + public function teardown() { + } + + public function testSerializeContent( ) { + $content = new WikitextContent( 'hello world' ); + + $this->assertEquals( 'hello world', $this->handler->serializeContent( $content ) ); + $this->assertEquals( 'hello world', $this->handler->serializeContent( $content, CONTENT_FORMAT_WIKITEXT ) ); + + try { + $this->handler->serializeContent( $content, 'dummy/foo' ); + $this->fail( "serializeContent() should have failed on unknown format" ); + } catch ( MWException $e ) { + // ok, as expected + } + } + + public function testUnserializeContent( ) { + $content = $this->handler->unserializeContent( 'hello world' ); + $this->assertEquals( 'hello world', $content->getNativeData() ); + + $content = $this->handler->unserializeContent( 'hello world', CONTENT_FORMAT_WIKITEXT ); + $this->assertEquals( 'hello world', $content->getNativeData() ); + + try { + $this->handler->unserializeContent( 'hello world', 'dummy/foo' ); + $this->fail( "unserializeContent() should have failed on unknown format" ); + } catch ( MWException $e ) { + // ok, as expected + } + } + + public function testMakeEmptyContent() { + $content = $this->handler->makeEmptyContent(); + + $this->assertTrue( $content->isEmpty() ); + $this->assertEquals( '', $content->getNativeData() ); + } + + public function dataIsSupportedFormat( ) { + return array( + array( null, true ), + array( CONTENT_FORMAT_WIKITEXT, true ), + array( 99887766, false ), + ); + } + + /** + * @dataProvider dataIsSupportedFormat + */ + public function testIsSupportedFormat( $format, $supported ) { + $this->assertEquals( $supported, $this->handler->isSupportedFormat( $format ) ); + } + + public function dataMerge3( ) { + return array( + array( "first paragraph + + second paragraph\n", + + "FIRST paragraph + + second paragraph\n", + + "first paragraph + + SECOND paragraph\n", + + "FIRST paragraph + + SECOND paragraph\n", + ), + + array( "first paragraph + second paragraph\n", + + "Bla bla\n", + + "Blubberdibla\n", + + false, + ), + + ); + } + + /** + * @dataProvider dataMerge3 + */ + public function testMerge3( $old, $mine, $yours, $expected ) { + global $wgDiff3; + + if ( !$wgDiff3 ) { + $this->markTestSkipped( "Can't test merge3(), since \$wgDiff3 is not configured" ); + } + + if ( !file_exists( $wgDiff3 ) ) { + #XXX: this sucks, since it uses arcane internal knowledge about TextContentHandler::merge3 and wfMerge. + $this->markTestSkipped( "Can't test merge3(), since \$wgDiff3 is misconfigured: can't find $wgDiff3" ); + } + + // test merge + $oldContent = new WikitextContent( $old ); + $myContent = new WikitextContent( $mine ); + $yourContent = new WikitextContent( $yours ); + + $merged = $this->handler->merge3( $oldContent, $myContent, $yourContent ); + + $this->assertEquals( $expected, $merged ? $merged->getNativeData() : $merged ); + } + + public function dataGetAutosummary( ) { + return array( + array( + 'Hello there, world!', + '#REDIRECT [[Foo]]', + 0, + '/^Redirected page .*Foo/' + ), + + array( + null, + 'Hello world!', + EDIT_NEW, + '/^Created page .*Hello/' + ), + + array( + 'Hello there, world!', + '', + 0, + '/^Blanked/' + ), + + array( + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut + labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et + ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', + 'Hello world!', + 0, + '/^Replaced .*Hello/' + ), + + array( + 'foo', + 'bar', + 0, + '/^$/' + ), + ); + } + + /** + * @dataProvider dataGetAutoSummary + */ + public function testGetAutosummary( $old, $new, $flags, $expected ) { + global $wgLanguageCode, $wgContLang; + + $oldContent = is_null( $old ) ? null : new WikitextContent( $old ); + $newContent = is_null( $new ) ? null : new WikitextContent( $new ); + + $summary = $this->handler->getAutosummary( $oldContent, $newContent, $flags ); + + $this->assertTrue( (bool)preg_match( $expected, $summary ), "Autosummary didn't match expected pattern $expected: $summary" ); + } + + /** + * @todo Text case required database! + */ + /* + public function testGetAutoDeleteReason( Title $title, &$hasHistory ) { + } + */ + + /** + * @todo Text case required database! + */ + /* + public function testGetUndoContent( Revision $current, Revision $undo, Revision $undoafter = null ) { + } + */ + +} diff --git a/tests/phpunit/includes/WikitextContentTest.php b/tests/phpunit/includes/WikitextContentTest.php new file mode 100644 index 0000000000..7a7ba914b1 --- /dev/null +++ b/tests/phpunit/includes/WikitextContentTest.php @@ -0,0 +1,552 @@ +setName( '127.0.0.1' ); + + $this->context = new RequestContext( new FauxRequest() ); + $this->context->setTitle( Title::newFromText( "Test" ) ); + $this->context->setUser( $wgUser ); + } + + public function newContent( $text ) { + return new WikitextContent( $text ); + } + + + public function dataGetParserOutput() { + return array( + array("WikitextContentTest_testGetParserOutput", "hello ''world''\n", "hello world\n
"), + // @todo: more...? + ); + } + + /** + * @dataProvider dataGetParserOutput + */ + public function testGetParserOutput( $title, $text, $expectedHtml ) { + $title = Title::newFromText( $title ); + $content = ContentHandler::makeContent( $text, $title ); + + $po = $content->getParserOutput( $title ); + + $this->assertEquals( $expectedHtml, $po->getText() ); + // @todo: assert more properties + } + + public function dataGetSecondaryDataUpdates() { + return array( + array("WikitextContentTest_testGetSecondaryDataUpdates_1", "hello ''world''\n", + array( 'LinksUpdate' => array( 'mRecursive' => true, + 'mLinks' => array() ) ) + ), + array("WikitextContentTest_testGetSecondaryDataUpdates_2", "hello [[world test 21344]]\n", + array( 'LinksUpdate' => array( 'mRecursive' => true, + 'mLinks' => array( array( 'World_test_21344' => 0 ) ) ) ) + ), + // @todo: more...? + ); + } + + /** + * @dataProvider dataGetSecondaryDataUpdates + * @group Database + */ + public function testGetSecondaryDataUpdates( $title, $text, $expectedStuff ) { + $title = Title::newFromText( $title ); + $title->resetArticleID( 2342 ); //dummy id. fine as long as we don't try to execute the updates! + + $handler = ContentHandler::getForModelID( $title->getContentModel() ); + $content = ContentHandler::makeContent( $text, $title ); + + $updates = $content->getSecondaryDataUpdates( $title ); + + // make updates accessible by class name + foreach ( $updates as $update ) { + $class = get_class( $update ); + $updates[ $class ] = $update; + } + + foreach ( $expectedStuff as $class => $fieldValues ) { + $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" ); + + $update = $updates[ $class ]; + + foreach ( $fieldValues as $field => $value ) { + $v = $update->$field; #if the field doesn't exist, just crash and burn + $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" ); + } + } + } + + + static $sections = + +"Intro + +== stuff == +hello world + +== test == +just a test + +== foo == +more stuff +"; + + public function dataGetSection() { + return array( + array( WikitextContentTest::$sections, + "0", + "Intro" + ), + array( WikitextContentTest::$sections, + "2", +"== test == +just a test" + ), + array( WikitextContentTest::$sections, + "8", + false + ), + ); + } + + /** + * @dataProvider dataGetSection + */ + public function testGetSection( $text, $sectionId, $expectedText ) { + $content = $this->newContent( $text ); + + $sectionContent = $content->getSection( $sectionId ); + + $this->assertEquals( $expectedText, is_null( $sectionContent ) ? null : $sectionContent->getNativeData() ); + } + + public function dataReplaceSection() { + return array( + array( WikitextContentTest::$sections, + "0", + "No more", + null, + trim( preg_replace( '/^Intro/sm', 'No more', WikitextContentTest::$sections ) ) + ), + array( WikitextContentTest::$sections, + "", + "No more", + null, + "No more" + ), + array( WikitextContentTest::$sections, + "2", + "== TEST ==\nmore fun", + null, + trim( preg_replace( '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==", WikitextContentTest::$sections ) ) + ), + array( WikitextContentTest::$sections, + "8", + "No more", + null, + WikitextContentTest::$sections + ), + array( WikitextContentTest::$sections, + "new", + "No more", + "New", + trim( WikitextContentTest::$sections ) . "\n\n\n== New ==\n\nNo more" + ), + ); + } + + /** + * @dataProvider dataReplaceSection + */ + public function testReplaceSection( $text, $section, $with, $sectionTitle, $expected ) { + $content = $this->newContent( $text ); + $c = $content->replaceSection( $section, $this->newContent( $with ), $sectionTitle ); + + $this->assertEquals( $expected, is_null( $c ) ? null : $c->getNativeData() ); + } + + public function testAddSectionHeader( ) { + $content = $this->newContent( 'hello world' ); + $content = $content->addSectionHeader( 'test' ); + + $this->assertEquals( "== test ==\n\nhello world", $content->getNativeData() ); + } + + public function dataPreSaveTransform() { + return array( + array( 'hello this is ~~~', + "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]", + ), + array( 'hello \'\'this\'\' is~~~ ', + 'hello \'\'this\'\' is~~~ ', + ), + ); + } + + /** + * @dataProvider dataPreSaveTransform + */ + public function testPreSaveTransform( $text, $expected ) { + global $wgContLang; + + $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang ); + + $content = $this->newContent( $text ); + $content = $content->preSaveTransform( $this->context->getTitle(), $this->context->getUser(), $options ); + + $this->assertEquals( $expected, $content->getNativeData() ); + } + + public function dataPreloadTransform() { + return array( + array( 'hello this is ~~~', + "hello this is ~~~", + ), + array( 'hello \'\'this\'\' isfoo bar ', + 'hello \'\'this\'\' is bar', + ), + ); + } + + /** + * @dataProvider dataPreloadTransform + */ + public function testPreloadTransform( $text, $expected ) { + global $wgContLang; + $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang ); + + $content = $this->newContent( $text ); + $content = $content->preloadTransform( $this->context->getTitle(), $options ); + + $this->assertEquals( $expected, $content->getNativeData() ); + } + + public function dataGetRedirectTarget() { + return array( + array( '#REDIRECT [[Test]]', + 'Test', + ), + array( '#REDIRECT Test', + null, + ), + array( '* #REDIRECT [[Test]]', + null, + ), + ); + } + + /** + * @dataProvider dataGetRedirectTarget + */ + public function testGetRedirectTarget( $text, $expected ) { + $content = $this->newContent( $text ); + $t = $content->getRedirectTarget( ); + + if ( is_null( $expected ) ) $this->assertNull( $t, "text should not have generated a redirect target: $text" ); + else $this->assertEquals( $expected, $t->getPrefixedText() ); + } + + /** + * @dataProvider dataGetRedirectTarget + */ + public function isRedirect( $text, $expected ) { + $content = $this->newContent( $text ); + + $this->assertEquals( !is_null($expected), $content->isRedirect() ); + } + + + /** + * @todo: test needs database! + */ + /* + public function getRedirectChain() { + $text = $this->getNativeData(); + return Title::newFromRedirectArray( $text ); + } + */ + + /** + * @todo: test needs database! + */ + /* + public function getUltimateRedirectTarget() { + $text = $this->getNativeData(); + return Title::newFromRedirectRecurse( $text ); + } + */ + + + public function dataIsCountable() { + return array( + array( '', + null, + 'any', + true + ), + array( 'Foo', + null, + 'any', + true + ), + array( 'Foo', + null, + 'comma', + false + ), + array( 'Foo, bar', + null, + 'comma', + true + ), + array( 'Foo', + null, + 'link', + false + ), + array( 'Foo [[bar]]', + null, + 'link', + true + ), + array( 'Foo', + true, + 'link', + true + ), + array( 'Foo [[bar]]', + false, + 'link', + false + ), + array( '#REDIRECT [[bar]]', + true, + 'any', + false + ), + array( '#REDIRECT [[bar]]', + true, + 'comma', + false + ), + array( '#REDIRECT [[bar]]', + true, + 'link', + false + ), + ); + } + + + /** + * @dataProvider dataIsCountable + * @group Database + */ + public function testIsCountable( $text, $hasLinks, $mode, $expected ) { + global $wgArticleCountMethod; + + $old = $wgArticleCountMethod; + $wgArticleCountMethod = $mode; + + $content = $this->newContent( $text ); + + $v = $content->isCountable( $hasLinks, $this->context->getTitle() ); + $wgArticleCountMethod = $old; + + $this->assertEquals( $expected, $v, "isCountable() returned unexpected value " . var_export( $v, true ) + . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" ); + } + + public function dataGetTextForSummary() { + return array( + array( "hello\nworld.", + 16, + 'hello world.', + ), + array( 'hello world.', + 8, + 'hello...', + ), + array( '[[hello world]].', + 8, + 'hel...', + ), + ); + } + + /** + * @dataProvider dataGetTextForSummary + */ + public function testGetTextForSummary( $text, $maxlength, $expected ) { + $content = $this->newContent( $text ); + + $this->assertEquals( $expected, $content->getTextForSummary( $maxlength ) ); + } + + + public function testGetTextForSearchIndex( ) { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( "hello world.", $content->getTextForSearchIndex() ); + } + + public function testCopy() { + $content = $this->newContent( "hello world." ); + $copy = $content->copy(); + + $this->assertTrue( $content->equals( $copy ), "copy must be equal to original" ); + $this->assertEquals( "hello world.", $copy->getNativeData() ); + } + + public function testGetSize( ) { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( 12, $content->getSize() ); + } + + public function testGetNativeData( ) { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( "hello world.", $content->getNativeData() ); + } + + public function testGetWikitextForTransclusion( ) { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( "hello world.", $content->getWikitextForTransclusion() ); + } + + public function testMatchMagicWord( ) { + $mw = MagicWord::get( "staticredirect" ); + + $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" ); + $this->assertTrue( $content->matchMagicWord( $mw ), "should have matched magic word" ); + + $content = $this->newContent( "#REDIRECT [[FOO]]" ); + $this->assertFalse( $content->matchMagicWord( $mw ), "should not have matched magic word" ); + } + + public function testUpdateRedirect( ) { + $target = Title::newFromText( "testUpdateRedirect_target" ); + + // test with non-redirect page + $content = $this->newContent( "hello world." ); + $newContent = $content->updateRedirect( $target ); + + $this->assertTrue( $content->equals( $newContent ), "content should be unchanged" ); + + // test with actual redirect + $content = $this->newContent( "#REDIRECT [[Someplace]]" ); + $newContent = $content->updateRedirect( $target ); + + $this->assertFalse( $content->equals( $newContent ), "content should have changed" ); + $this->assertTrue( $newContent->isRedirect(), "new content should be a redirect" ); + + $this->assertEquals( $target->getFullText(), $newContent->getRedirectTarget()->getFullText() ); + } + + # ================================================================================================================= + + public function testGetModel() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getModel() ); + } + + public function testGetContentHandler() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getContentHandler()->getModelID() ); + } + + public function dataIsEmpty( ) { + return array( + array( '', true ), + array( ' ', false ), + array( '0', false ), + array( 'hallo welt.', false ), + ); + } + + /** + * @dataProvider dataIsEmpty + */ + public function testIsEmpty( $text, $empty ) { + $content = $this->newContent( $text ); + + $this->assertEquals( $empty, $content->isEmpty() ); + } + + public function dataEquals( ) { + return array( + array( new WikitextContent( "hallo" ), null, false ), + array( new WikitextContent( "hallo" ), new WikitextContent( "hallo" ), true ), + array( new WikitextContent( "hallo" ), new JavascriptContent( "hallo" ), false ), + array( new WikitextContent( "hallo" ), new WikitextContent( "HALLO" ), false ), + ); + } + + /** + * @dataProvider dataEquals + */ + public function testEquals( Content $a, Content $b = null, $equal = false ) { + $this->assertEquals( $equal, $a->equals( $b ) ); + } + + public function dataGetDeletionUpdates() { + return array( + array("WikitextContentTest_testGetSecondaryDataUpdates_1", "hello ''world''\n", + array( 'LinksDeletionUpdate' => array( ) ) + ), + array("WikitextContentTest_testGetSecondaryDataUpdates_2", "hello [[world test 21344]]\n", + array( 'LinksDeletionUpdate' => array( ) ) + ), + // @todo: more...? + ); + } + + /** + * @dataProvider dataGetDeletionUpdates + */ + public function testDeletionUpdates( $title, $text, $expectedStuff ) { + $title = Title::newFromText( $title ); + $title->resetArticleID( 2342 ); //dummy id. fine as long as we don't try to execute the updates! + + $handler = ContentHandler::getForModelID( $title->getContentModel() ); + $content = ContentHandler::makeContent( $text, $title ); + + $updates = $content->getDeletionUpdates( WikiPage::factory( $title ) ); + + // make updates accessible by class name + foreach ( $updates as $update ) { + $class = get_class( $update ); + $updates[ $class ] = $update; + } + + foreach ( $expectedStuff as $class => $fieldValues ) { + $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" ); + + $update = $updates[ $class ]; + + foreach ( $fieldValues as $field => $value ) { + $v = $update->$field; #if the field doesn't exist, just crash and burn + $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" ); + } + } + } + +} diff --git a/tests/phpunit/includes/api/ApiEditPageTest.php b/tests/phpunit/includes/api/ApiEditPageTest.php index 5297d6daa7..f02c6d7e60 100644 --- a/tests/phpunit/includes/api/ApiEditPageTest.php +++ b/tests/phpunit/includes/api/ApiEditPageTest.php @@ -10,11 +10,38 @@ */ class ApiEditPageTest extends ApiTestCase { - function setUp() { - parent::setUp(); + public function setup() { + global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang; + + parent::setup(); + + $wgExtraNamespaces[ 12312 ] = 'Dummy'; + $wgExtraNamespaces[ 12313 ] = 'Dummy_talk'; + + $wgNamespaceContentModels[ 12312 ] = "testing"; + $wgContentHandlers[ "testing" ] = 'DummyContentHandlerForTesting'; + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + $this->doLogin(); } + public function teardown() { + global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang; + + unset( $wgExtraNamespaces[ 12312 ] ); + unset( $wgExtraNamespaces[ 12313 ] ); + + unset( $wgNamespaceContentModels[ 12312 ] ); + unset( $wgContentHandlers[ "testing" ] ); + + MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache + $wgContLang->resetNamespaces(); # reset namespace cache + + parent::teardown(); + } + function testEdit( ) { $name = 'ApiEditPageTest_testEdit'; @@ -25,7 +52,7 @@ class ApiEditPageTest extends ApiTestCase { 'text' => 'some text', ) ); $apiResult = $apiResult[0]; - # Validate API result data + // Validate API result data $this->assertArrayHasKey( 'edit', $apiResult ); $this->assertArrayHasKey( 'result', $apiResult['edit'] ); $this->assertEquals( 'Success', $apiResult['edit']['result'] ); @@ -66,6 +93,33 @@ class ApiEditPageTest extends ApiTestCase { ); } + function testNonTextEdit( ) { + $name = 'Dummy:ApiEditPageTest_testNonTextEdit'; + $data = serialize( 'some bla bla text' ); + + // -- test new page -------------------------------------------- + $apiResult = $this->doApiRequestWithToken( array( + 'action' => 'edit', + 'title' => $name, + 'text' => $data, ) ); + $apiResult = $apiResult[0]; + + // Validate API result data + $this->assertArrayHasKey( 'edit', $apiResult ); + $this->assertArrayHasKey( 'result', $apiResult['edit'] ); + $this->assertEquals( 'Success', $apiResult['edit']['result'] ); + + $this->assertArrayHasKey( 'new', $apiResult['edit'] ); + $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] ); + + $this->assertArrayHasKey( 'pageid', $apiResult['edit'] ); + + // validate resulting revision + $page = WikiPage::factory( Title::newFromText( $name ) ); + $this->assertEquals( "testing", $page->getContentModel() ); + $this->assertEquals( $data, $page->getContent()->serialize() ); + } + function testEditAppend() { $this->markTestIncomplete( "not yet implemented" ); } diff --git a/tests/phpunit/includes/filerepo/FileBackendTest.php b/tests/phpunit/includes/filerepo/FileBackendTest.php index a2dc5c6ca3..1364d8be1a 100644 --- a/tests/phpunit/includes/filerepo/FileBackendTest.php +++ b/tests/phpunit/includes/filerepo/FileBackendTest.php @@ -1,6 +1,9 @@ clear(); $page = WikiPage::factory( $title ); - $page->doEdit( $text, $comment, 0, false, $user ); + $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user ); $this->pageList[] = array( $title, $page->getId() ); diff --git a/tests/phpunit/install-phpunit.sh b/tests/phpunit/install-phpunit.sh old mode 100755 new mode 100644 diff --git a/tests/phpunit/maintenance/DumpTestCase.php b/tests/phpunit/maintenance/DumpTestCase.php index d13443899c..aceedf9879 100644 --- a/tests/phpunit/maintenance/DumpTestCase.php +++ b/tests/phpunit/maintenance/DumpTestCase.php @@ -35,7 +35,7 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { * @throws MWExcepion */ protected function addRevision( Page $page, $text, $summary ) { - $status = $page->doEdit( $text, $summary ); + $status = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), $summary ); if ( $status->isGood() ) { $value = $status->getValue(); $revision = $value['revision']; @@ -295,9 +295,12 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { * @param $text_sha1 string: the base36 SHA-1 of the revision's text * @param $text string|false: (optional) The revision's string, or false to check for a * revision stub + * @param $model String: the expected content model id (default: CONTENT_MODEL_WIKITEXT) + * @param $format String: the expected format model id (default: CONTENT_FORMAT_WIKITEXT) * @param $parentid int|false: (optional) id of the parent revision */ - protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false ) { + protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false, + $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT ) { $this->assertNodeStart( "revision" ); $this->skipWhitespace(); @@ -315,9 +318,33 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { $this->skipWhitespace(); $this->assertTextNode( "comment", $summary ); + $this->skipWhitespace(); + + if ( $this->xml->name == "text" ) { + // note:tag may occur here or at the very end. + $text_found = true; + $this->assertText( $id, $text_id, $text_bytes, $text ); + } else { + $text_found = false; + } $this->assertTextNode( "sha1", $text_sha1 ); + $this->assertTextNode( "model", $model ); + $this->skipWhitespace(); + + $this->assertTextNode( "format", $format ); + $this->skipWhitespace(); + + if ( !$text_found ) { + $this->assertText( $id, $text_id, $text_bytes, $text ); + } + + $this->assertNodeEnd( "revision" ); + $this->skipWhitespace(); + } + + protected function assertText( $id, $text_id, $text_bytes, $text ) { $this->assertNodeStart( "text", false ); if ( $text_bytes !== false ) { $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes, @@ -344,9 +371,5 @@ abstract class DumpTestCase extends MediaWikiLangTestCase { $this->assertNodeEnd( "text" ); $this->skipWhitespace(); } - - $this->assertNodeEnd( "revision" ); - $this->skipWhitespace(); } - } diff --git a/tests/phpunit/maintenance/backupPrefetchTest.php b/tests/phpunit/maintenance/backupPrefetchTest.php index 8ff855744b..5178154dbe 100644 --- a/tests/phpunit/maintenance/backupPrefetchTest.php +++ b/tests/phpunit/maintenance/backupPrefetchTest.php @@ -199,6 +199,8 @@ class BaseDumpTest extends MediaWikiTestCase { BackupDumperTestP1Summary1 0bolhl6ol7i6x0e7yq91gxgaan39j87 BackupDumperTestP1Text1 +1 +1 '; @@ -216,6 +218,8 @@ class BaseDumpTest extends MediaWikiTestCase {BackupDumperTestP2Summary1 jprywrymfhysqllua29tj3sc7z39dl2 BackupDumperTestP2Text1 +1 +1 '; @@ -243,6 +249,8 @@ class BaseDumpTest extends MediaWikiTestCase { 5 @@ -227,6 +231,8 @@ class BaseDumpTest extends MediaWikiTestCase {BackupDumperTestP2Summary4 extra 6o1ciaxa6pybnqprmungwofc4lv00wv BackupDumperTestP2Text4 some additional Text +1 +1 Talk BackupDumperTestP1 Summary1 nktofwzd0tl192k3zfepmlzxoax1lpe +1 +1 Talk about BackupDumperTestP1 Text1 diff --git a/tests/phpunit/maintenance/backupTextPassTest.php b/tests/phpunit/maintenance/backupTextPassTest.php index a0bbadf991..b17284edf3 100644 --- a/tests/phpunit/maintenance/backupTextPassTest.php +++ b/tests/phpunit/maintenance/backupTextPassTest.php @@ -383,7 +383,7 @@ class TextPassDumperTest extends DumpTestCase { $this->assertEmpty( $files, "Remaining unchecked files" ); // ... and have dealt with more than one checkpoint file - $this->assertGreaterThan( 1, $checkpointFiles, "# of checkpoint files" ); + $this->assertGreaterThan( 1, $checkpointFiles, "expected more than 1 checkpoint to have been created. Checkpoint interval is $checkpointAfter seconds, maybe your computer is too fast?" ); $this->expectETAOutput(); } @@ -481,6 +481,8 @@ class TextPassDumperTest extends DumpTestCase {BackupDumperTestP1Summary1 0bolhl6ol7i6x0e7yq91gxgaan39j87 +wikitext +text/x-wiki @@ -497,6 +499,8 @@ class TextPassDumperTest extends DumpTestCase { BackupDumperTestP2Summary1 jprywrymfhysqllua29tj3sc7z39dl2 +wikitext +text/x-wiki @@ -508,6 +512,8 @@ class TextPassDumperTest extends DumpTestCase { BackupDumperTestP2Summary2 b7vj5ks32po5m1z1t1br4o7scdwwy95 +wikitext +text/x-wiki @@ -519,6 +525,8 @@ class TextPassDumperTest extends DumpTestCase { BackupDumperTestP2Summary3 jfunqmh1ssfb8rs43r19w98k28gg56r +wikitext +text/x-wiki @@ -530,6 +538,8 @@ class TextPassDumperTest extends DumpTestCase { @@ -548,6 +558,8 @@ class TextPassDumperTest extends DumpTestCase {BackupDumperTestP2Summary4 extra 6o1ciaxa6pybnqprmungwofc4lv00wv +wikitext +text/x-wiki Talk BackupDumperTestP1 Summary1 nktofwzd0tl192k3zfepmlzxoax1lpe +wikitext +text/x-wiki diff --git a/tests/phpunit/maintenance/fetchTextTest.php b/tests/phpunit/maintenance/fetchTextTest.php index e7ffa01c34..fd6db0abd4 100644 --- a/tests/phpunit/maintenance/fetchTextTest.php +++ b/tests/phpunit/maintenance/fetchTextTest.php @@ -111,7 +111,7 @@ class FetchTextTest extends MediaWikiTestCase { * @throws MWExcepion */ private function addRevision( $page, $text, $summary ) { - $status = $page->doEdit( $text, $summary ); + $status = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), $summary ); if ( $status->isGood() ) { $value = $status->getValue(); $revision = $value['revision']; diff --git a/tests/phpunit/suite.xml b/tests/phpunit/suite.xml index f286fa1187..f4ee50093b 100644 --- a/tests/phpunit/suite.xml +++ b/tests/phpunit/suite.xml @@ -11,8 +11,8 @@ timeoutForSmallTests="2" timeoutForMediumTests="10" timeoutForLargeTests="60" - strict="true" - verbose="true"> + strict="false" + verbose="false"> includes diff --git a/thumb.php b/thumb.php index 2edc28027a..bdb24f927c 100644 --- a/thumb.php +++ b/thumb.php @@ -71,10 +71,9 @@ function wfThumbHandle404() { } # Just get the URI path (REDIRECT_URL/REQUEST_URI is either a full URL or a path) if ( substr( $uriPath, 0, 1 ) !== '/' ) { - $bits = wfParseUrl( $uriPath ); - if ( $bits && isset( $bits['path'] ) ) { - $uriPath = $bits['path']; - } else { + $uri = new Uri( $uriPath ); + $uriPath = $uri->getPath(); + if ( $uriPath === null ) { wfThumbError( 404, 'The source file for the specified thumbnail does not exist.' ); return; }