From: daniel Date: Tue, 11 Sep 2012 09:43:02 +0000 (+0200) Subject: merged master (2012-09-11) X-Git-Tag: 1.31.0-rc.0~22097^2^2~24 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22sites_tous%22%29%20.%20%22?a=commitdiff_plain;h=12166f46b4d63d3fd3cab68cf4c7090a1646dd09;p=lhc%2Fweb%2Fwiklou.git merged master (2012-09-11) Change-Id: I8e953eaa22f9d331b0af5e780fbeff6d702b23e3 --- 12166f46b4d63d3fd3cab68cf4c7090a1646dd09 diff --cc includes/Content.php index 1b879c1795,0000000000..3bce48e710 mode 100644,000000..100644 --- a/includes/Content.php +++ b/includes/Content.php @@@ -1,1482 -1,0 +1,1482 @@@ +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 $title \Title the title of the deleted page ++ * @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( Title $title, ++ 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: handle ImagePage and CategoryPage + # TODO: make sure we cover lucene search / wikisearch. + # TODO: make sure ReplaceTemplates still works + # FUTURE: nice&sane integration of GeSHi syntax highlighting + # [11:59] Hooks are ugly; make CodeHighlighter interface and a + # config to set the class which handles syntax highlighting + # [12:00] And default it to a DummyHighlighter + + # TODO: make sure we cover the external editor interface (does anyone actually use that?!) + + # TODO: tie into API to provide contentModel for Revisions + # TODO: tie into API to provide serialized version and contentFormat for Revisions + # TODO: tie into API edit interface + # FUTURE: make EditForm plugin for EditPage + + # FUTURE: special type for redirects?! + # FUTURE: MultipartMultipart < WikipageContent (Main + Links + X) + # FUTURE: LinksContent < LanguageLinksContent, CategoriesContent +} + + +/** + * A content object represents page content, e.g. the text to show on a page. + * Content objects have no knowledge about how they relate to Wiki pages. + * + * @since 1.WD + */ +abstract class AbstractContent implements Content { + + /** + * Name of the content model this Content object represents. + * Use with CONTENT_MODEL_XXX constants + * + * @var string $model_id + */ + protected $model_id; + + /** + * @param String $model_id + */ + public function __construct( $model_id = null ) { + $this->model_id = $model_id; + } + + /** + * @see Content::getModel() + */ + public function getModel() { + return $this->model_id; + } + + /** + * Throws an MWException if $model_id is not the id of the content model + * supported by this Content object. + * + * @param $model_id int the model to check + * + * @throws MWException + */ + protected function checkModelID( $model_id ) { + if ( $model_id !== $this->model_id ) { + throw new MWException( "Bad content model: " . + "expected {$this->model_id} " . + "but got $model_id." ); + } + } + + /** + * @see Content::getContentHandler() + */ + public function getContentHandler() { + return ContentHandler::getForContent( $this ); + } + + /** + * @see Content::getDefaultFormat() + */ + public function getDefaultFormat() { + return $this->getContentHandler()->getDefaultFormat(); + } + + /** + * @see Content::getSupportedFormats() + */ + public function getSupportedFormats() { + return $this->getContentHandler()->getSupportedFormats(); + } + + /** + * @see Content::isSupportedFormat() + */ + public function isSupportedFormat( $format ) { + if ( !$format ) { + return true; // this means "use the default" + } + + return $this->getContentHandler()->isSupportedFormat( $format ); + } + + /** + * Throws an MWException if $this->isSupportedFormat( $format ) doesn't + * return true. + * + * @param $format + * @throws MWException + */ + protected function checkFormat( $format ) { + if ( !$this->isSupportedFormat( $format ) ) { + throw new MWException( "Format $format is not supported for content model " . + $this->getModel() ); + } + } + + /** + * @see Content::serialize + */ + public function serialize( $format = null ) { + return $this->getContentHandler()->serializeContent( $this, $format ); + } + + /** + * @see Content::isEmpty() + */ + public function isEmpty() { + return $this->getSize() == 0; + } + + /** + * @see Content::isValid() + */ + public function isValid() { + return true; + } + + /** + * @see Content::equals() + */ + public function equals( Content $that = null ) { + if ( is_null( $that ) ) { + return false; + } + + if ( $that === $this ) { + return true; + } + + if ( $that->getModel() !== $this->getModel() ) { + return false; + } + + return $this->getNativeData() === $that->getNativeData(); + } + + + /** + * Returns a list of DataUpdate objects for recording information about this + * Content in some secondary data store. + * + * This default implementation calls + * $this->getParserOutput( $content, $title, null, null, false ), + * and then calls getSecondaryDataUpdates( $title, $recursive ) on the + * resulting ParserOutput object. + * + * Subclasses may override this to determine the secondary data updates more + * efficiently, preferrably without the need to generate a parser output object. + * + * @see Content::getSecondaryDataUpdates() + * + * @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 + ) { + if ( !$parserOutput ) { + $parserOutput = $this->getParserOutput( $title, null, null, false ); + } + + return $parserOutput->getSecondaryDataUpdates( $title, $recursive ); + } + + + /** + * @see Content::getRedirectChain() + */ + public function getRedirectChain() { + global $wgMaxRedirects; + $title = $this->getRedirectTarget(); + if ( is_null( $title ) ) { + return null; + } + // recursive check to follow double redirects + $recurse = $wgMaxRedirects; + $titles = array( $title ); + while ( --$recurse > 0 ) { + if ( $title->isRedirect() ) { + $page = WikiPage::factory( $title ); + $newtitle = $page->getRedirectTarget(); + } else { + break; + } + // Redirects to some special pages are not permitted + if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) { + // The new title passes the checks, so make that our current + // title so that further recursion can be checked + $title = $newtitle; + $titles[] = $newtitle; + } else { + break; + } + } + return $titles; + } + + /** + * @see Content::getRedirectTarget() + */ + public function getRedirectTarget() { + return null; + } + + /** + * @see Content::getUltimateRedirectTarget() + * @note: migrated here from Title::newFromRedirectRecurse + */ + public function getUltimateRedirectTarget() { + $titles = $this->getRedirectChain(); + return $titles ? array_pop( $titles ) : null; + } + + /** + * @see Content::isRedirect() + * + * @since WD.1 + * + * @return bool + */ + public function isRedirect() { + return $this->getRedirectTarget() !== null; + } + + /** + * @see Content::updateRedirect() + * + * This default implementation always returns $this. + * + * @since WD.1 + * + * @return Content $this + */ + public function updateRedirect( Title $target ) { + return $this; + } + + /** + * @see Content::getSection() + */ + public function getSection( $sectionId ) { + return null; + } + + /** + * @see Content::replaceSection() + */ + public function replaceSection( $section, Content $with, $sectionTitle = '' ) { + return null; + } + + /** + * @see Content::preSaveTransform() + */ + public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) { + return $this; + } + + /** + * @see Content::addSectionHeader() + */ + public function addSectionHeader( $header ) { + return $this; + } + + /** + * @see Content::preloadTransform() + */ + public function preloadTransform( Title $title, ParserOptions $popts ) { + return $this; + } + + /** + * @see Content::prepareSave() + */ + public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user ) { + if ( $this->isValid() ) { + return Status::newGood(); + } else { + return Status::newFatal( "invalid-content-data" ); + } + } + + /** + * @see Content::getDeletionUpdates() + * + * @since WD.1 + * - * @param $title \Title the title of the deleted page ++ * @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( Title $title, ++ public function getDeletionUpdates( WikiPage $page, + ParserOutput $parserOutput = null ) + { + return array( - new LinksDeletionUpdate( $title ), ++ new LinksDeletionUpdate( $page ), + ); + } + + /** + * @see Content::matchMagicWord() + * + * This default implementation always returns false. Subclasses may override this to supply matching logic. + * + * @param MagicWord $word + * + * @return bool + */ + public function matchMagicWord( MagicWord $word ) { + return false; + } +} + +/** + * Content object implementation for representing flat text. + * + * TextContent instances are immutable + * + * @since WD.1 + */ +abstract class TextContent extends AbstractContent { + + public function __construct( $text, $model_id = null ) { + parent::__construct( $model_id ); + + $this->mText = $text; + } + + public function copy() { + return $this; # NOTE: this is ok since TextContent are immutable. + } + + public function getTextForSummary( $maxlength = 250 ) { + global $wgContLang; + + $text = $this->getNativeData(); + + $truncatedtext = $wgContLang->truncate( + preg_replace( "/[\n\r]/", ' ', $text ), + max( 0, $maxlength ) ); + + return $truncatedtext; + } + + /** + * returns the text's size in bytes. + * + * @return int The size + */ + public function getSize( ) { + $text = $this->getNativeData( ); + return strlen( $text ); + } + + /** + * Returns true if this content is not a redirect, and $wgArticleCountMethod + * is "any". + * + * @param $hasLinks Bool: if it is known whether this content contains links, + * provide this information here, to avoid redundant parsing to find out. + * + * @return bool True if the content is countable + */ + public function isCountable( $hasLinks = null ) { + global $wgArticleCountMethod; + + if ( $this->isRedirect( ) ) { + return false; + } + + if ( $wgArticleCountMethod === 'any' ) { + return true; + } + + return false; + } + + /** + * Returns the text represented by this Content object, as a string. + * + * @param the raw text + */ + public function getNativeData( ) { + $text = $this->mText; + return $text; + } + + /** + * Returns the text represented by this Content object, as a string. + * + * @param the raw text + */ + public function getTextForSearchIndex( ) { + return $this->getNativeData(); + } + + /** + * Returns the text represented by this Content object, as a string. + * + * @param the raw text + */ + public function getWikitextForTransclusion( ) { + return $this->getNativeData(); + } + + /** + * Diff this content object with another content object.. + * + * @since WD.diff + * + * @param $that Content the other content object to compare this content object to + * @param $lang Language the language object to use for text segmentation. + * If not given, $wgContentLang is used. + * + * @return DiffResult a diff representing the changes that would have to be + * made to this content object to make it equal to $that. + */ + public function diff( Content $that, Language $lang = null ) { + global $wgContLang; + + $this->checkModelID( $that->getModel() ); + + # @todo: could implement this in DifferenceEngine and just delegate here? + + if ( !$lang ) $lang = $wgContLang; + + $otext = $this->getNativeData(); + $ntext = $this->getNativeData(); + + # Note: Use native PHP diff, external engines don't give us abstract output + $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); + $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); + + $diff = new Diff( $ota, $nta ); + return $diff; + } + + + /** + * Returns a generic ParserOutput object, wrapping the HTML returned by + * getHtml(). + * + * @param $title Title Context title for parsing + * @param $revId int|null Revision ID (for {{REVISIONID}}) + * @param $options ParserOptions|null Parser options + * @param $generateHtml bool Whether or not to generate HTML + * + * @return ParserOutput representing the HTML form of the text + */ + public function getParserOutput( Title $title, + $revId = null, + ParserOptions $options = null, $generateHtml = true + ) { + # Generic implementation, relying on $this->getHtml() + + if ( $generateHtml ) { + $html = $this->getHtml(); + } else { + $html = ''; + } + + $po = new ParserOutput( $html ); + return $po; + } + + /** + * Generates an HTML version of the content, for display. Used by + * getParserOutput() to construct a ParserOutput object. + * + * This default implementation just calls getHighlightHtml(). Content + * models that have another mapping to HTML (as is the case for markup + * languages like wikitext) should override this method to generate the + * appropriate HTML. + * + * @return string An HTML representation of the content + */ + protected function getHtml() { + return $this->getHighlightHtml(); + } + + /** + * Generates a syntax-highlighted version of the content, as HTML. + * Used by the default implementation of getHtml(). + * + * @return string an HTML representation of the content's markup + */ + protected function getHighlightHtml( ) { + # TODO: make Highlighter interface, use highlighter here, if available + return htmlspecialchars( $this->getNativeData() ); + } +} + +/** + * @since WD.1 + */ +class WikitextContent extends TextContent { + + public function __construct( $text ) { + parent::__construct( $text, CONTENT_MODEL_WIKITEXT ); + } + + /** + * @see Content::getSection() + */ + public function getSection( $section ) { + global $wgParser; + + $text = $this->getNativeData(); + $sect = $wgParser->getSection( $text, $section, false ); + + return new WikitextContent( $sect ); + } + + /** + * @see Content::replaceSection() + */ + public function replaceSection( $section, Content $with, $sectionTitle = '' ) { + wfProfileIn( __METHOD__ ); + + $myModelId = $this->getModel(); + $sectionModelId = $with->getModel(); + + if ( $sectionModelId != $myModelId ) { + throw new MWException( "Incompatible content model for section: " . + "document uses $myModelId but " . + "section uses $sectionModelId." ); + } + + $oldtext = $this->getNativeData(); + $text = $with->getNativeData(); + + if ( $section === '' ) { + return $with; # XXX: copy first? + } if ( $section == 'new' ) { + # Inserting a new section + $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' ) + ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : ''; + if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) { + $text = strlen( trim( $oldtext ) ) > 0 + ? "{$oldtext}\n\n{$subject}{$text}" + : "{$subject}{$text}"; + } + } else { + # Replacing an existing section; roll out the big guns + global $wgParser; + + $text = $wgParser->replaceSection( $oldtext, $section, $text ); + } + + $newContent = new WikitextContent( $text ); + + wfProfileOut( __METHOD__ ); + return $newContent; + } + + /** + * Returns a new WikitextContent object with the given section heading + * prepended. + * + * @param $header string + * @return Content + */ + public function addSectionHeader( $header ) { + $text = wfMessage( 'newsectionheaderdefaultlevel' ) + ->inContentLanguage()->params( $header )->text(); + $text .= "\n\n"; + $text .= $this->getNativeData(); + + return new WikitextContent( $text ); + } + + /** + * 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; + + $text = $this->getNativeData(); + $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts ); + + return new WikitextContent( $pst ); + } + + /** + * Returns a Content object with preload transformations applied (or this + * object if no transformations apply). + * + * @param $title Title + * @param $popts ParserOptions + * @return Content + */ + public function preloadTransform( Title $title, ParserOptions $popts ) { + global $wgParser; + + $text = $this->getNativeData(); + $plt = $wgParser->getPreloadText( $text, $title, $popts ); + + return new WikitextContent( $plt ); + } + + /** + * Implement redirect extraction for wikitext. + * + * @return null|Title + * + * @note: migrated here from Title::newFromRedirectInternal() + * + * @see Content::getRedirectTarget + * @see AbstractContent::getRedirectTarget + */ + public function getRedirectTarget() { + global $wgMaxRedirects; + if ( $wgMaxRedirects < 1 ) { + // redirects are disabled, so quit early + return null; + } + $redir = MagicWord::get( 'redirect' ); + $text = trim( $this->getNativeData() ); + if ( $redir->matchStartAndRemove( $text ) ) { + // Extract the first link and see if it's usable + // Ensure that it really does come directly after #REDIRECT + // Some older redirects included a colon, so don't freak about that! + $m = array(); + if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) { + // Strip preceding colon used to "escape" categories, etc. + // and URL-decode links + if ( strpos( $m[1], '%' ) !== false ) { + // Match behavior of inline link parsing here; + $m[1] = rawurldecode( ltrim( $m[1], ':' ) ); + } + $title = Title::newFromText( $m[1] ); + // If the title is a redirect to bad special pages or is invalid, return null + if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) { + return null; + } + return $title; + } + } + return null; + } + + /** + * @see Content::updateRedirect() + * + * This implementation replaces the first link on the page with the given new target + * if this Content object is a redirect. Otherwise, this method returns $this. + * + * @since WD.1 + * + * @param Title $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 ) { + if ( !$this->isRedirect() ) { + return $this; + } + + # Fix the text + # Remember that redirect pages can have categories, templates, etc., + # so the regex has to be fairly general + $newText = preg_replace( '/ \[ \[ [^\]]* \] \] /x', + '[[' . $target->getFullText() . ']]', + $this->getNativeData(), 1 ); + + return new WikitextContent( $newText ); + } + + /** + * Returns true if this content is not a redirect, and this content's text + * is countable according to the criteria defined by $wgArticleCountMethod. + * + * @param $hasLinks Bool if it is known whether this content contains + * links, provide this information here, to avoid redundant parsing to + * find out. + * @param $title null|\Title + * + * @internal param \IContextSource $context context for parsing if necessary + * + * @return bool True if the content is countable + */ + public function isCountable( $hasLinks = null, Title $title = null ) { + global $wgArticleCountMethod; + + if ( $this->isRedirect( ) ) { + return false; + } + + $text = $this->getNativeData(); + + switch ( $wgArticleCountMethod ) { + case 'any': + return true; + case 'comma': + return strpos( $text, ',' ) !== false; + case 'link': + if ( $hasLinks === null ) { # not known, find out + if ( !$title ) { + $context = RequestContext::getMain(); + $title = $context->getTitle(); + } + + $po = $this->getParserOutput( $title, null, null, false ); + $links = $po->getLinks(); + $hasLinks = !empty( $links ); + } + + return $hasLinks; + } + + return false; + } + + public function getTextForSummary( $maxlength = 250 ) { + $truncatedtext = parent::getTextForSummary( $maxlength ); + + # clean up unfinished links + # XXX: make this optional? wasn't there in autosummary, but required for + # deletion summary. + $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext ); + + return $truncatedtext; + } + + + /** + * Returns a ParserOutput object resulting from parsing the content's text + * using $wgParser. + * + * @since WD.1 + * + * @param $content Content the content to render + * @param $title \Title + * @param $revId null + * @param $options null|ParserOptions + * @param $generateHtml bool + * + * @internal param \IContextSource|null $context + * @return ParserOutput representing the HTML form of the text + */ + public function getParserOutput( Title $title, + $revId = null, + ParserOptions $options = null, $generateHtml = true + ) { + global $wgParser; + + if ( !$options ) { + $options = new ParserOptions(); + } + + $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId ); + return $po; + } + + protected function getHtml() { + throw new MWException( + "getHtml() not implemented for wikitext. " + . "Use getParserOutput()->getText()." + ); + } + + /** + * @see Content::matchMagicWord() + * + * This implementation calls $word->match() on the this TextContent object's text. + * + * @param MagicWord $word + * + * @return bool whether this Content object matches the given magic word. + */ + public function matchMagicWord( MagicWord $word ) { + return $word->match( $this->getNativeData() ); + } +} + +/** + * Wrapper allowing us to handle a system message as a Content object. Note that this is generally *not* used + * to represent content from the MediaWiki namespace, and that there is no MessageContentHandler. MessageContent + * is just intended as glue for wrapping a message programatically. + * + * @since WD.1 + */ +class MessageContent extends AbstractContent { + + /** + * @var Message + */ + protected $mMessage; + + /** + * @param Message|String $msg A Message object, or a message key + * @param array|null $params An optional array of message parameters + */ + public function __construct( $msg, $params = null ) { + # XXX: messages may be wikitext, html or plain text! and maybe even something else entirely. + parent::__construct( CONTENT_MODEL_WIKITEXT ); + + if ( is_string( $msg ) ) { + $this->mMessage = wfMessage( $msg ); + } else { + $this->mMessage = clone $msg; + } + + if ( $params ) { + $this->mMessage = $this->mMessage->params( $params ); + } + } + + /** + * Returns the message as rendered HTML + * + * @return string The message text, parsed into html + */ + public function getHtml() { + return $this->mMessage->parse(); + } + + /** + * Returns the message as rendered HTML + * + * @return string The message text, parsed into html + */ + public function getWikitext() { + return $this->mMessage->text(); + } + + /** + * Returns the message object, with any parameters already substituted. + * + * @return Message The message object. + */ + public function getNativeData() { + //NOTE: Message objects are mutable. Cloning here makes MessageContent immutable. + return clone $this->mMessage; + } + + /** + * @see Content::getTextForSearchIndex + */ + public function getTextForSearchIndex() { + return $this->mMessage->plain(); + } + + /** + * @see Content::getWikitextForTransclusion + */ + public function getWikitextForTransclusion() { + return $this->getWikitext(); + } + + /** + * @see Content::getTextForSummary + */ + public function getTextForSummary( $maxlength = 250 ) { + return substr( $this->mMessage->plain(), 0, $maxlength ); + } + + /** + * @see Content::getSize + * + * @return int + */ + public function getSize() { + return strlen( $this->mMessage->plain() ); + } + + /** + * @see Content::copy + * + * @return Content. A copy of this object + */ + public function copy() { + // MessageContent is immutable (because getNativeData() returns a clone of the Message object) + return $this; + } + + /** + * @see Content::isCountable + * + * @return bool false + */ + public function isCountable( $hasLinks = null ) { + return false; + } + + /** + * @see Content::getParserOutput + * + * @return ParserOutput + */ + public function getParserOutput( + Title $title, $revId = null, + ParserOptions $options = null, $generateHtml = true + ) { + + if ( $generateHtml ) { + $html = $this->getHtml(); + } else { + $html = ''; + } + + $po = new ParserOutput( $html ); + return $po; + } +} + +/** + * @since WD.1 + */ +class JavaScriptContent extends TextContent { + public function __construct( $text ) { + parent::__construct( $text, CONTENT_MODEL_JAVASCRIPT ); + } + + /** + * Returns a Content object with pre-save transformations applied using + * Parser::preSaveTransform(). + * + * @param Title $title + * @param User $user + * @param ParserOptions $popts + * @return Content + */ + public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) { + global $wgParser; + // @todo: make pre-save transformation optional for script pages + // See bug #32858 + + $text = $this->getNativeData(); + $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts ); + + return new JavaScriptContent( $pst ); + } + + + protected function getHtml( ) { + $html = ""; + $html .= "
\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 --cc includes/LinksUpdate.php index 1188b9a0d7,87db4d60da..e2e860a150 --- a/includes/LinksUpdate.php +++ b/includes/LinksUpdate.php @@@ -823,14 -824,10 +825,14 @@@ class LinksDeletionUpdate extends SqlDa * * @param $page WikiPage Page we are updating */ - function __construct( Title $title ) { + function __construct( WikiPage $page ) { parent::__construct( false ); // no implicit transaction - $this->mTitle = $title; + $this->mPage = $page; + - if ( !$title->getArticleID() ) { - throw new MWException( "The Title object did not provide an article ID. Perhaps the page doesn't exist?" ); ++ if ( !$page->getId() ) { ++ throw new MWException( "Page ID not known, perhaps the page doesn't exist?" ); + } } /** diff --cc includes/OutputPage.php index 3ab68f436b,2c32c6abe0..926fbe3b6a --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@@ -1990,12 -1990,11 +1990,15 @@@ class OutputPage extends ContextSource wfProfileOut( 'Output-skin' ); } + // This hook allows last minute changes to final overall output by modifying output buffer + wfRunHooks( 'AfterFinalPageOutput', array( $this ) ); + $this->sendCacheControl(); + + wfRunHooks( 'AfterFinalPageOutput', array( &$this ) ); + ob_end_flush(); + wfProfileOut( __METHOD__ ); } diff --cc includes/Title.php index 15757b0a76,1b5e21d2ee..8caa557be1 --- a/includes/Title.php +++ b/includes/Title.php @@@ -4609,24 -4568,19 +4610,23 @@@ class Title * @return Language */ public function getPageViewLanguage() { - $pageLang = $this->getPageLanguage(); - // If this is nothing special (so the content is converted when viewed) - if ( !$this->isSpecialPage() - && !$this->isCssOrJsPage() && !$this->isCssJsSubpage() - && $this->getNamespace() !== NS_MEDIAWIKI - ) { + global $wgLang; + + if ( $this->isSpecialPage() ) { // 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 ); + $variant = $wgLang->getPreferredVariant(); + if ( $wgLang->getCode() !== $variant ) { + return Language::factory( $variant ); } + + return $wgLang; } + + //NOTE: can't be cached, depends on user settings + //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language! + $contentHandler = ContentHandler::getForTitle( $this ); + $pageLang = $contentHandler->getPageViewLanguage( $this ); - return $pageLang; } } diff --cc includes/WikiPage.php index f1ca07b991,74772f27d6..65ccc0bfa6 --- a/includes/WikiPage.php +++ b/includes/WikiPage.php @@@ -3167,31 -2979,6 +3171,31 @@@ class WikiPage extends Page implements global $wgUser; return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid ); } + + /** + * Returns a list of updates to be performed when this page is deleted. The updates should remove any information + * about this page from secondary data stores such as links tables. + * + * @param Content|null $content optional Content object for determining the necessary updates + * @return Array an array of DataUpdates objects + */ + public function getDeletionUpdates( Content $content = null ) { + if ( !$content ) { + // load content object, which may be used to determine the necessary updates + // XXX: the content may not be needed to determine the updates, then this would be overhead. + $content = $this->getContent( Revision::RAW ); + } + + if ( !$content ) { + $updates = array(); + } else { - $updates = $content->getDeletionUpdates( $this->mTitle ); ++ $updates = $content->getDeletionUpdates( $this ); + } + + wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) ); + return $updates; + } + } class PoolWorkArticleView extends PoolCounterWork { diff --cc includes/filerepo/file/LocalFile.php index 4801da81db,695c4e9eab..449cc45e68 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@@ -1177,12 -1186,17 +1186,18 @@@ 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'] ) ) { - $dbw->update( 'logging', array( 'log_page' => $status->value['revision']->getPage() ), array( 'log_id' => $logId ), __METHOD__ ); + if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction + $dbw->begin(); + $dbw->update( 'logging', + array( 'log_page' => $status->value['revision']->getPage() ), + array( 'log_id' => $logId ), + __METHOD__ + ); + $dbw->commit(); // commit before anything bad can happen } } wfProfileOut( __METHOD__ . '-edit' ); diff --cc includes/installer/MysqlUpdater.php index bf881d78aa,bd55cb4084..d92ab4a06c --- a/includes/installer/MysqlUpdater.php +++ b/includes/installer/MysqlUpdater.php @@@ -214,13 -213,7 +214,14 @@@ class MysqlUpdater extends DatabaseUpda array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ), 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 --cc includes/installer/SqliteUpdater.php index 7b7312cb72,bc5fcacfb5..1486683730 --- a/includes/installer/SqliteUpdater.php +++ b/includes/installer/SqliteUpdater.php @@@ -93,13 -92,7 +93,14 @@@ class SqliteUpdater extends DatabaseUpd array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ), 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 --cc tests/phpunit/includes/WikitextContentTest.php index 942e15273e,0000000000..7a7ba914b1 mode 100644,000000..100644 --- a/tests/phpunit/includes/WikitextContentTest.php +++ b/tests/phpunit/includes/WikitextContentTest.php @@@ -1,552 -1,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\'\' is foobar', + '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( $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" ); + } + } + } + +}