cleanup and fixes for secondary data updates
[lhc/web/wiklou.git] / includes / Content.php
index 1629203..490bb8c 100644 (file)
@@ -1,5 +1,4 @@
 <?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.
@@ -12,17 +11,24 @@ abstract class Content {
         * Name of the content model this Content object represents.
         * Use with CONTENT_MODEL_XXX constants
         *
-        * @var String $model_name
+        * @var String $model_id
         */
-       protected $model_name;
+       protected $model_id;
 
        /**
+        * @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 abstract 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.
         *
@@ -34,15 +40,19 @@ abstract class Content {
        /**
         * Returns a textual representation of the content suitable for use in edit summaries and log messages.
         *
+        * @since WD.1
+        *
         * @param int $maxlength maximum length of the summary text
         * @return String the summary text
         */
        public abstract function getTextForSummary( $maxlength = 250 );
 
        /**
-        * Returns native represenation of the data. Interpretation depends on the data model used,
+        * 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.
         *
@@ -58,40 +68,49 @@ abstract class Content {
        public abstract function getSize( );
 
        /**
-        * @param $model_name
+        * @param int $model_id
         */
-       public function __construct( $model_name = null ) {
-               $this->model_name = $model_name;
+       public function __construct( $model_id = null ) {
+               $this->model_id = $model_id;
        }
 
        /**
-        * Returns the name of the content model used by this content objects.
+        * Returns the id of the content model used by this content objects.
         * Corresponds to the CONTENT_MODEL_XXX constants.
         *
-        * @return String the model name
+        * @since WD.1
+        *
+        * @return int the model id
         */
-       public function getModelName() {
-               return $this->model_name;
+       public function getModel() {
+               return $this->model_id;
        }
 
        /**
-        * Throws an MWException if $model_name is not the name of the content model
+        * Throws an MWException if $model_id is not the id of the content model
         * supported by this Content object.
         *
-        * @param String $model_name the model to check
+        * @param int $model_id the model to check
+        *
+        * @throws MWException
         */
-       protected function checkModelName( $model_name ) {
-               if ( $model_name !== $this->model_name ) {
-                       throw new MWException( "Bad content model: expected " . $this->model_name . " but got found " . $model_name );
+       protected function checkModelID( $model_id ) {
+               if ( $model_id !== $this->model_id ) {
+                       $model_name = ContentHandler::getContentModelName( $model_id );
+                       $own_model_name = ContentHandler::getContentModelName( $this->model_id );
+
+                       throw new MWException( "Bad content model: expected {$this->model_id} ($own_model_name) but got found $model_id ($model_name)." );
                }
        }
 
        /**
-        * Conveniance method that returns the ContentHandler singleton for handling the content
+        * Convenience method that returns the ContentHandler singleton for handling the content
         * model this Content object uses.
         *
         * Shorthand for ContentHandler::getForContent( $this )
         *
+        * @since WD.1
+        *
         * @return ContentHandler
         */
        public function getContentHandler() {
@@ -99,11 +118,13 @@ abstract class Content {
        }
 
        /**
-        * Conveniance method that returns the default serialization format for the content model
+        * Convenience method that returns the default serialization format for the content model
         * model this Content object uses.
         *
         * Shorthand for $this->getContentHandler()->getDefaultFormat()
         *
+        * @since WD.1
+        *
         * @return ContentHandler
         */
        public function getDefaultFormat() {
@@ -111,11 +132,13 @@ abstract class Content {
        }
 
        /**
-        * Conveniance method that returns the list of serialization formats supported
+        * Convenience method that returns the list of serialization formats supported
         * for the content model model this Content object uses.
         *
         * Shorthand for $this->getContentHandler()->getSupportedFormats()
         *
+        * @since WD.1
+        *
         * @return array of supported serialization formats
         */
        public function getSupportedFormats() {
@@ -131,6 +154,8 @@ abstract class Content {
         *
         * Shorthand for $this->getContentHandler()->isSupportedFormat( $format )
         *
+        * @since WD.1
+        *
         * @param String $format the format to check
         * @return bool whether the format is supported
         */
@@ -150,15 +175,17 @@ abstract class Content {
         */
        protected function checkFormat( $format ) {
                if ( !$this->isSupportedFormat( $format ) ) {
-                       throw new MWException( "Format $format is not supported for content model " . $this->getModelName() );
+                       throw new MWException( "Format $format is not supported for content model " . $this->getModel() );
                }
        }
 
        /**
-        * Conveniance method for serializing this Content object.
+        * Convenience method for serializing this Content object.
         *
         * Shorthand for $this->getContentHandler()->serializeContent( $this, $format )
         *
+        * @since WD.1
+        *
         * @param null|String $format the desired serialization format (or null for the default format).
         * @return String serialized form of this Content object
         */
@@ -169,26 +196,44 @@ abstract class Content {
        /**
         * Returns true if this Content object represents empty content.
         *
+        * @since WD.1
+        *
         * @return bool whether this Content object is empty
         */
        public function isEmpty() {
                return $this->getSize() == 0;
        }
 
+       /**
+        * Returns if the content is valid. This is intended for local validity checks, not considering global consistency.
+        * It needs to be valid before it can be saved.
+        *
+        * This default implementation always returns true.
+        *
+        * @since WD.1
+        *
+        * @return boolean
+        */
+       public function isValid() {
+               return true;
+       }
+
        /**
         * Returns true if this Content objects is conceptually equivalent to the given Content object.
         *
         * Will returns false if $that is null.
         * Will return true if $that === $this.
-        * Will return false if $that->getModleName() != $this->getModelName().
+        * 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( $b )
+        * * $a->equals( $b ) <=> $b->equals( $a )
         * * $a->equals( $b ) &&  $b->equals( $c ) ==> $a->equals( $c )
         *
+        * @since WD.1
+        *
         * @param Content $that the Content object to compare to
         * @return bool true if this Content object is euqual to $that, false otherwise.
         */
@@ -201,7 +246,7 @@ abstract class Content {
                        return true;
                }
 
-               if ( $that->getModelName() !== $this->getModelName() ) {
+               if ( $that->getModel() !== $this->getModel() ) {
                        return false;
                }
 
@@ -213,13 +258,15 @@ abstract class Content {
         * if $copy = $original->copy()
         *
         * * get_class($original) === get_class($copy)
-        * * $original->getModelName() === $copy->getModelName()
+        * * $original->getModel() === $copy->getModel()
         * * $original->equals( $copy )
         *
-        * If and only if the Content object is imutable, the copy() method can and should
-        * return $this. That is,  $copy === $original may be true, but only for imutable content
+        * 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 abstract function copy( );
@@ -228,6 +275,8 @@ abstract class Content {
         * 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
@@ -235,16 +284,26 @@ abstract class Content {
        public abstract function isCountable( $hasLinks = null ) ;
 
        /**
-        * @param IContextSource $context
+        * Convenience method, shorthand for
+        * $this->getContentHandler()->getParserOutput( $this, $title, $revId, $options, $generateHtml )
+        *
+        * @note: subclasses should NOT override this to provide custom rendering.
+        *        Override ContentHandler::getParserOutput() instead!
+        *
+        * @param Title $title
         * @param null $revId
         * @param null|ParserOptions $options
         * @param Boolean $generateHtml whether to generate Html (default: true). If false,
         *        the result of calling getText() on the ParserOutput object returned by
         *        this method is undefined.
         *
+        * @since WD.1
+        *
         * @return ParserOutput
         */
-       public abstract function getParserOutput( IContextSource $context, $revId = null, ParserOptions $options = NULL, $generateHtml = true );
+       public function getParserOutput( Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
+               return $this->getContentHandler()->getParserOutput( $this, $title, $revId, $options, $generateHtml );
+       }
 
        /**
         * Construct the redirect destination from this content and return an
@@ -252,6 +311,8 @@ abstract class Content {
         * 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() {
@@ -259,11 +320,13 @@ abstract class Content {
        }
 
        /**
-        * Construct the redirect destination from this content and return an
-        * array of Titles, or null if this content doesn't represent a redirect.
+        * 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() {
@@ -276,14 +339,21 @@ abstract class Content {
         * 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.
         *
+        * @since WD.1
+        *
         * @return Title
         */
        public function getUltimateRedirectTarget() {
                return null;
        }
 
+       /**
+        * @since WD.1
+        *
+        * @return bool
+        */
        public function isRedirect() {
-               return $this->getRedirectTarget() != null;
+               return $this->getRedirectTarget() !== null;
        }
 
        /**
@@ -291,8 +361,10 @@ abstract class Content {
         *
         * The default implementation returns null.
         *
+        * @since WD.1
+        *
         * @param String $sectionId 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 (inluded) and the second heading (excluded), etc.
+        *          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 ) {
@@ -302,6 +374,8 @@ abstract class Content {
        /**
         * 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'
@@ -314,6 +388,8 @@ abstract class Content {
        /**
         * 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 null|ParserOptions $popts
@@ -327,6 +403,8 @@ abstract class Content {
         * 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
         */
@@ -337,6 +415,8 @@ abstract class Content {
        /**
         * Returns a Content object with preload transformations applied (or this object if no transformations apply).
         *
+        * @since WD.1
+        *
         * @param Title $title
         * @param null|ParserOptions $popts
         * @return Content
@@ -366,18 +446,20 @@ abstract class Content {
 /**
  * Content object implementation for representing flat text.
  *
- * TextContent instances are imutable
+ * TextContent instances are immutable
+ *
+ * @since WD.1
  */
 abstract class TextContent extends Content {
 
-       public function __construct( $text, $model_name = null ) {
-               parent::__construct( $model_name );
+       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 imutable.
+               return $this; #NOTE: this is ok since TextContent are immutable.
        }
 
        public function getTextForSummary( $maxlength = 250 ) {
@@ -453,63 +535,54 @@ abstract class TextContent extends Content {
        }
 
        /**
-        * Returns a generic ParserOutput object, wrapping the HTML returned by getHtml().
+        * Diff this content object with another content object..
+        *
+        * @since WD.diff
+        *
+        * @param Content $that the other content object to compare this content object to
+        * @param Language $lang the language object to use for text segmentation. If not given, $wgContentLang is used.
         *
-        * @return ParserOutput representing the HTML form of the text
+        * @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 getParserOutput( IContextSource $context, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
-               # generic implementation, relying on $this->getHtml()
+       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 ( $generateHtml ) $html = $this->getHtml( $options );
-               else $html = '';
+               if ( !$lang ) $lang = $wgContLang;
 
-               $po = new ParserOutput( $html );
+               $otext = $this->getNativeData();
+               $ntext = $this->getNativeData();
 
-               return $po;
+               # 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;
        }
 
-       protected abstract function getHtml( );
 
 }
 
+/**
+ * @since WD.1
+ */
 class WikitextContent extends TextContent {
 
        public function __construct( $text ) {
                parent::__construct($text, CONTENT_MODEL_WIKITEXT);
        }
 
-       protected function getHtml( ) {
-               throw new MWException( "getHtml() not implemented for wikitext. Use getParserOutput()->getText()." );
-       }
-
-       /**
-        * Returns a ParserOutput object resulting from parsing the content's text using $wgParser.
-        *
-        * @since WikiData1
-        *
-        * @param IContextSource|null $context
-        * @param null $revId
-        * @param null|ParserOptions $options
-        * @param bool $generateHtml
-        *
-        * @return ParserOutput representing the HTML form of the text
-        */
-       public function getParserOutput( IContextSource $context, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
-               global $wgParser;
-
-               if ( !$options ) {
-                       $options = ParserOptions::newFromUserAndLang( $context->getUser(), $context->getLanguage() );
-               }
-
-               $po = $wgParser->parse( $this->mText, $context->getTitle(), $options, true, true, $revId );
-
-               return $po;
-       }
-
        /**
         * Returns the section with the given id.
         *
-        * @param String $sectionId the section's id
+        * @param String $section
+        *
+        * @internal param String $sectionId the section's id
         * @return Content|false|null the section, or false if no such section exist, or null if sections are not supported
         */
        public function getSection( $section ) {
@@ -524,19 +597,25 @@ class WikitextContent extends TextContent {
        /**
         * Replaces a section in the wikitext
         *
-        * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
-        * @param $with Content: new content of the section
+        * @param $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'
+        *
+        * @throws MWException
         * @return Content Complete article content, or null if error
         */
        public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
                wfProfileIn( __METHOD__ );
 
-               $myModelName = $this->getModelName();
-               $sectionModelName = $with->getModelName();
+               $myModelId = $this->getModel();
+               $sectionModelId = $with->getModel();
 
-               if ( $sectionModelName != $myModelName  ) {
-                       throw new MWException( "Incompatible content model for section: document uses $myModelName, section uses $sectionModelName." );
+               if ( $sectionModelId != $myModelId  ) {
+                       $myModelName = ContentHandler::getContentModelName( $myModelId );
+                       $sectionModelName = ContentHandler::getContentModelName( $sectionModelId );
+
+                       throw new MWException( "Incompatible content model for section: document uses $myModelId ($myModelName), "
+                                                               . "section uses $sectionModelId ($sectionModelName)." );
                }
 
                $oldtext = $this->getNativeData();
@@ -585,7 +664,7 @@ class WikitextContent extends TextContent {
         * @param ParserOptions $popts
         * @return Content
         */
-       public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+       public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) { #FIXME: also needed for JS/CSS!
                global $wgParser, $wgConteLang;
 
                $text = $this->getNativeData();
@@ -627,15 +706,17 @@ class WikitextContent extends TextContent {
 
        /**
         * Returns true if this content is not a redirect, and this content's text is countable according to
-        * the criteria defiend by $wgArticleCountMethod.
+        * the criteria defined by $wgArticleCountMethod.
         *
-        * @param Bool $hasLinks if it is known whether this content contains links, provide this information here,
-        *                        to avoid redundant parsing to find out.
-        * @param IContextSource $context context for parsing if necessary
+        * @param Bool        $hasLinks  if it is known whether this content contains links, provide this information here,
+        *                               to avoid redundant parsing to find out.
+        * @param null|\Title $title
+        *
+        * @internal param \IContextSource $context context for parsing if necessary
         *
         * @return bool true if the content is countable
         */
-       public function isCountable( $hasLinks = null, IContextSource $context = null ) {
+       public function isCountable( $hasLinks = null, Title $title = null ) {
                global $wgArticleCountMethod, $wgRequest;
 
                if ( $this->isRedirect( ) ) {
@@ -651,18 +732,20 @@ class WikitextContent extends TextContent {
                                return strpos( $text,  ',' ) !== false;
                        case 'link':
                                if ( $hasLinks === null ) { # not known, find out
-                                       if ( !$context ) { # make dummy context
-                                               //XXX: caller of this method often knows the title, but not a context...
-                                               $context = new RequestContext( $wgRequest );
+                                       if ( !$title ) {
+                                               $context = RequestContext::getMain();
+                                               $title = $context->getTitle();
                                        }
 
-                                       $po = $this->getParserOutput( $context, null, null, false );
+                                       $po = $this->getParserOutput( $title, null, null, false );
                                        $links = $po->getLinks();
                                        $hasLinks = !empty( $links );
                                }
 
                                return $hasLinks;
                }
+
+               return false;
        }
 
        public function getTextForSummary( $maxlength = 250 ) {
@@ -677,6 +760,9 @@ class WikitextContent extends TextContent {
 
 }
 
+/**
+ * @since WD.1
+ */
 class MessageContent extends TextContent {
        public function __construct( $msg_key, $params = null, $options = null ) {
                parent::__construct(null, CONTENT_MODEL_WIKITEXT); #XXX: messages may be wikitext, html or plain text! and maybe even something else entirely.
@@ -693,14 +779,13 @@ class MessageContent extends TextContent {
                }
 
                $this->mOptions = $options;
-
-               $this->mHtmlOptions = null;
        }
 
        /**
         * Returns the message as rendered HTML, using the options supplied to the constructor plus "parse".
+        * @return String the message text, parsed
         */
-       protected function getHtml(  ) {
+       public function getHtml(  ) {
                $opt = array_merge( $this->mOptions, array('parse') );
 
                return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
@@ -709,6 +794,8 @@ class MessageContent extends TextContent {
 
        /**
         * Returns the message as raw text, using the options supplied to the constructor minus "parse" and "parseinline".
+        *
+        * @return String the message text, unparsed.
         */
        public function getNativeData( ) {
                $opt = array_diff( $this->mOptions, array('parse', 'parseinline') );
@@ -718,34 +805,21 @@ class MessageContent extends TextContent {
 
 }
 
-
+/**
+ * @since WD.1
+ */
 class JavaScriptContent extends TextContent {
        public function __construct( $text ) {
                parent::__construct($text, CONTENT_MODEL_JAVASCRIPT);
        }
 
-       protected function getHtml( ) {
-               $html = "";
-               $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
-               $html .= htmlspecialchars( $this->getNativeData() );
-               $html .= "\n</pre>\n";
-
-               return $html;
-       }
-
 }
 
+/**
+ * @since WD.1
+ */
 class CssContent extends TextContent {
        public function __construct( $text ) {
                parent::__construct($text, CONTENT_MODEL_CSS);
        }
-
-       protected function getHtml( ) {
-               $html = "";
-               $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
-               $html .= htmlspecialchars( $this->getNativeData() );
-               $html .= "\n</pre>\n";
-
-               return $html;
-       }
 }