merged master (2012-09-11)
authordaniel <daniel.kinzler@wikimedia.de>
Tue, 11 Sep 2012 09:43:02 +0000 (11:43 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Tue, 11 Sep 2012 09:43:02 +0000 (11:43 +0200)
Change-Id: I8e953eaa22f9d331b0af5e780fbeff6d702b23e3

21 files changed:
1  2 
docs/hooks.txt
includes/Article.php
includes/Content.php
includes/EditPage.php
includes/GlobalFunctions.php
includes/ImagePage.php
includes/LinksUpdate.php
includes/OutputPage.php
includes/Title.php
includes/WikiPage.php
includes/api/ApiMain.php
includes/filerepo/file/LocalFile.php
includes/installer/MysqlUpdater.php
includes/installer/SqliteUpdater.php
includes/search/SearchEngine.php
languages/messages/MessagesDe.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/tables.sql
tests/phpunit/includes/WikitextContentTest.php
tests/phpunit/maintenance/backupTextPassTest.php

diff --cc docs/hooks.txt
Simple merge
Simple merge
index 1b879c1,0000000..3bce48e
mode 100644,000000..100644
--- /dev/null
@@@ -1,1482 -1,0 +1,1482 @@@
-        * @param $title \Title the title of the deleted page
 +<?php
 +/**
 + * A content object represents page content, e.g. the text to show on a page.
 + * Content objects have no knowledge about how they relate to wiki pages.
 + *
 + * @since 1.WD
 + */
 +interface Content {
 +
 +      /**
 +       * @since WD.1
 +       *
 +       * @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.
 +       *
 +       * @todo: test that this actually works
 +       * @todo: make sure this also works with LuceneSearch / WikiSearch
 +       */
 +      public function getTextForSearchIndex( );
 +
 +      /**
 +       * @since WD.1
 +       *
 +       * @return string The wikitext to include when another page includes this
 +       * content, or false if the content is not includable in a wikitext page.
 +       *
 +       * @TODO: allow native handling, bypassing wikitext representation, like
 +       *    for includable special pages.
 +       * @TODO: allow transclusion into other content models than Wikitext!
 +       * @TODO: used in WikiPage and MessageCache to get message text. Not so
 +       *    nice. What should we use instead?!
 +       */
 +      public function getWikitextForTransclusion( );
 +
 +      /**
 +       * Returns a textual representation of the content suitable for use in edit
 +       * summaries and log messages.
 +       *
 +       * @since WD.1
 +       *
 +       * @param $maxlength int Maximum length of the summary text
 +       * @return   The summary text
 +       */
 +      public function getTextForSummary( $maxlength = 250 );
 +
 +      /**
 +       * Returns native representation of the data. Interpretation depends on
 +       * the data model used, as given by getDataModel().
 +       *
 +       * @since WD.1
 +       *
 +       * @return mixed The native representation of the content. Could be a
 +       *    string, a nested array structure, an object, a binary blob...
 +       *    anything, really.
 +       *
 +       * @NOTE: review all calls carefully, caller must be aware of content model!
 +       */
 +      public function getNativeData( );
 +
 +      /**
 +       * Returns the content's nominal size in bogo-bytes.
 +       *
 +       * @return int
 +       */
 +      public function getSize( );
 +
 +      /**
 +       * Returns the ID of the content model used by this Content object.
 +       * Corresponds to the CONTENT_MODEL_XXX constants.
 +       *
 +       * @since WD.1
 +       *
 +       * @return String The model id
 +       */
 +      public function getModel();
 +
 +      /**
 +       * Convenience method that returns the ContentHandler singleton for handling
 +       * the content model that this Content object uses.
 +       *
 +       * Shorthand for ContentHandler::getForContent( $this )
 +       *
 +       * @since WD.1
 +       *
 +       * @return ContentHandler
 +       */
 +      public function getContentHandler();
 +
 +      /**
 +       * Convenience method that returns the default serialization format for the
 +       * content model that this Content object uses.
 +       *
 +       * Shorthand for $this->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
 +       *
-       public function getDeletionUpdates( Title $title,
++       * @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.
 +       */
-        * @param $title \Title the title of the deleted page
++      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] <vvv> Hooks are ugly; make CodeHighlighter interface and a 
 +      #   config to set the class which handles syntax highlighting
 +      #   [12:00] <vvv> And default it to a DummyHighlighter
 +
 +      # TODO: make sure we cover the external editor interface (does anyone actually use that?!)
 +
 +      # TODO: tie into API to provide contentModel for Revisions
 +      # TODO: tie into API to provide serialized version and contentFormat for Revisions
 +      # TODO: tie into API edit interface
 +      # 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
 +       *
-       public function getDeletionUpdates( Title $title,
++       * @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.
 +       */
-                       new LinksDeletionUpdate( $title ),
++      public function getDeletionUpdates( WikiPage $page,
 +              ParserOutput $parserOutput = null )
 +      {
 +              return array(
++                      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 .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
 +              $html .= $this->getHighlightHtml( );
 +              $html .= "\n</pre>\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 .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
 +              $html .= $this->getHighlightHtml( );
 +              $html .= "\n</pre>\n";
 +
 +              return $html;
 +      }
 +}
Simple merge
Simple merge
Simple merge
@@@ -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?" );
 +              }
        }
  
        /**
@@@ -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__ );
        }
  
@@@ -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;
        }
  }
@@@ -3167,31 -2979,6 +3171,31 @@@ class WikiPage extends Page implements 
                global $wgUser;
                return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid );
        }
-                       $updates = $content->getDeletionUpdates( $this->mTitle );
 +
 +      /**
 +       * 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 );
 +              }
 +
 +              wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) );
 +              return $updates;
 +      }
 +
  }
  
  class PoolWorkArticleView extends PoolCounterWork {
Simple merge
@@@ -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' );
@@@ -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' ),
                );
        }
  
@@@ -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' ),
                );
        }
  
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
index 942e152,0000000..7a7ba91
mode 100644,000000..100644
--- /dev/null
@@@ -1,552 -1,0 +1,552 @@@
-               $updates = $content->getDeletionUpdates( $title );
 +<?php
 +
 +/**
 + * @group ContentHandler
 + *
 + * @group Database
 + *        ^--- needed, because we do need the database to test link updates
 + */
 +class WikitextContentTest extends MediaWikiTestCase {
 +
 +      public function setup() {
 +              global $wgUser;
 +
 +              // anon user
 +              $wgUser = new User();
 +              $wgUser->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", "<p>hello <i>world</i>\n</p>"),
 +                      // @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 <nowiki>~~~</nowiki>',
 +                             'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
 +                      ),
 +              );
 +      }
 +
 +      /**
 +       * @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 <noinclude>foo</noinclude><includeonly>bar</includeonly>',
 +                             '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" );
 +                      }
 +              }
 +      }
 +
 +}