Merge "Add Content::matchMagicWord" into Wikidata
authordaniel <daniel.kinzler@wikimedia.de>
Tue, 28 Aug 2012 11:42:46 +0000 (11:42 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 28 Aug 2012 11:42:46 +0000 (11:42 +0000)
12 files changed:
includes/Article.php
includes/AutoLoader.php
includes/ContentHandler.php
includes/EditPage.php
includes/Revision.php
includes/WikiPage.php
includes/api/ApiEditPage.php
includes/api/ApiParse.php
includes/diff/DifferenceEngine.php
includes/installer/DatabaseUpdater.php
tests/phpunit/includes/ContentHandlerTest.php
tests/phpunit/includes/RevisionTest.php

index dc6294f..f791edf 100644 (file)
@@ -391,7 +391,7 @@ class Article extends Page {
                $content = $this->fetchContentObject();
 
                $this->mContent = ContentHandler::getContentText( $content ); #@todo: get rid of mContent everywhere!
-               wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); #BC cruft, deprecated!
+               ContentHandler::runLegacyHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
 
                wfProfileOut( __METHOD__ );
 
@@ -679,10 +679,16 @@ class Article extends Page {
                                                wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
                                                $this->showCssOrJsPage();
                                                $outputDone = true;
-                                       } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) {
+                                       } elseif( !wfRunHooks( 'ArticleContentViewCustom',
+                                                                                       array( $this->fetchContentObject(), $this->getTitle(),
+                                                                                                       $outputPage ) ) ) {
+
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
-                                       } elseif( Hooks::isRegistered( 'ArticleViewCustom' ) && !wfRunHooks( 'ArticleViewCustom', array( $this->fetchContent(), $this->getTitle(), $outputPage ) ) ) { #FIXME: fetchContent() is deprecated!
+                                       } elseif( !ContentHandler::runLegacyHooks( 'ArticleViewCustom',
+                                                                                                                               array( $this->fetchContentObject(), $this->getTitle(),
+                                                                                                                                               $outputPage ) ) ) {
+
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
                                        } else {
@@ -693,7 +699,8 @@ class Article extends Page {
                                                        # Viewing a redirect page (e.g. with parameter redirect=no)
                                                        $outputPage->addHTML( $this->viewRedirect( $rt ) );
                                                        # Parse just to get categories, displaytitle, etc.
-                                                       $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid, $parserOptions, false );
+                                                       $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid,
+                                                                                                                                                               $parserOptions, false );
                                                        $outputPage->addParserOutputNoText( $this->mParserOutput );
                                                        $outputDone = true;
                                                }
@@ -830,7 +837,7 @@ class Article extends Page {
                }
 
                // Give hooks a chance to customise the output
-               if ( !Hooks::isRegistered('ShowRawCssJs') || wfRunHooks( 'ShowRawCssJs', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated
+               if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->fetchContentObject(), $this->getTitle(), $wgOut ) ) ) {
                        $po = $this->mContentObject->getParserOutput( $this->getTitle() );
                        $wgOut->addHTML( $po->getText() );
                }
@@ -1704,8 +1711,10 @@ class Article extends Page {
         * @return ParserOutput or false if the given revsion ID is not found
         */
        public function getParserOutput( $oldid = null, User $user = null ) {
+               //XXX: bypasses mParserOptions and thus setParserOptions()
+
                $user = is_null( $user ) ? $this->getContext()->getUser() : $user;
-               $parserOptions = $this->mPage->makeParserOptions( $user ); //XXX: bypasses mParserOptions and thus setParserOptions()
+               $parserOptions = $this->mPage->makeParserOptions( $user );
 
                return $this->mPage->getParserOutput( $parserOptions, $oldid );
        }
index 9c18ced..6b3b27a 100644 (file)
@@ -293,6 +293,7 @@ $wgAutoloadLocalClasses = array(
        'AbstractContent' => 'includes/Content.php',
        'ContentHandler' => 'includes/ContentHandler.php',
        'CssContent' => 'includes/Content.php',
+       'TextContentHandler' => 'includes/ContentHandler.php',
        'CssContentHandler' => 'includes/ContentHandler.php',
        'JavaScriptContent' => 'includes/Content.php',
        'JavaScriptContentHandler' => 'includes/ContentHandler.php',
index e460eee..1f20682 100644 (file)
@@ -331,6 +331,12 @@ abstract class ContentHandler {
                else return wfMsg( $key );
        }
 
+       public static function getContentModels() {
+               global $wgContentHandlers;
+
+               return array_keys( $wgContentHandlers );
+       }
+
        public static function getAllContentFormats() {
                global $wgContentHandlers;
 
@@ -825,6 +831,62 @@ abstract class ContentHandler {
        public function supportsSections() {
                return false;
        }
+
+       /**
+        * Call a legacy hook that uses text instead of Content objects.
+        * Will log a warning when a matching hook function is registered.
+        * If the textual representation of the content is changed by the
+        * hook function, a new Content object is constructed from the new
+        * text.
+        *
+        * @param $event String: event name
+        * @param $args Array: parameters passed to hook functions
+        *
+        * @return Boolean True if no handler aborted the hook
+        */
+       public static function runLegacyHooks( $event, $args = array() ) {
+               if ( !Hooks::isRegistered( $event ) ) {
+                       return true; // nothing to do here
+               }
+
+               wfWarn( "Using obsolete hook $event" );
+
+               // convert Content objects to text
+               $contentObjects = array();
+               $contentTexts = array();
+
+               foreach ( $args as $k => $v ) {
+                       if ( $v instanceof Content ) {
+                               /* @var Content $v */
+
+                               $contentObjects[$k] = $v;
+
+                               $v = $v->serialize();
+                               $contentTexts[ $k ] = $v;
+                               $args[ $k ] = $v;
+                       }
+               }
+
+               // call the hook functions
+               $ok = wfRunHooks( $event, $args );
+
+               // see if the hook changed the text
+               foreach ( $contentTexts as $k => $orig ) {
+                       /* @var Content $content */
+
+                       $modified = $args[ $k ];
+                       $content = $contentObjects[$k];
+
+                       if ( $modified !== $orig ) {
+                               // text was changed, create updated Content object
+                               $content = $content->getContentHandler()->unserializeContent( $modified );
+                       }
+
+                       $args[ $k ] = $content;
+               }
+
+               return $ok;
+       }
 }
 
 /**
index 3e38fb8..09e5f15 100644 (file)
@@ -1457,8 +1457,10 @@ class EditPage {
                                }
 
                                // Run post-section-merge edit filter
-                               if ( !wfRunHooks( 'EditFilterMerged', array( $this, $content->serialize( $this->content_format ), &$this->hookError, $this->summary ) )
-                                               || !wfRunHooks( 'EditFilterMergedContent', array( $this, $content, &$this->hookError, $this->summary ) ) ) {
+                               $hook_args = array( $this, $content, &$this->hookError, $this->summary );
+
+                               if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged', $hook_args )
+                                               || !wfRunHooks( 'EditFilterMergedContent', $hook_args ) ) {
                                        # Error messages etc. could be handled within the hook...
                                        $status->fatal( 'hookaborted' );
                                        $status->value = self::AS_HOOK_ERROR;
@@ -2547,18 +2549,7 @@ HTML
                                                                                        $this->section, $textboxContent,
                                                                                        $this->summary, $this->edittime );
 
-               # hanlde legacy text-based hook
-               $newtext_orig = $newContent->serialize( $this->content_format );
-               $newtext = $newtext_orig; #clone
-               wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
-
-               if ( $newtext != $newtext_orig ) {
-                                               #if the hook changed the text, create a new Content object accordingly.
-                                               #XXX: handle parse errors ?
-                                               $newContent = ContentHandler::makeContent( $newtext, $this->getTitle(),
-                                                                                                                                       $newContent->getModel() );
-               }
-
+               ContentHandler::runLegacyHooks( 'EditPageGetDiffText', array( $this, &$newContent ) );
                wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
 
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
@@ -2862,17 +2853,9 @@ HTML
                                        $content = $content->addSectionHeader( $this->summary );
                                }
 
-                               $toparse_orig = $content->serialize( $this->content_format );
-                               $toparse = $toparse_orig;
-                               wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
-
-                               if ( $toparse !== $toparse_orig ) {
-                                       #hook changed the text, create new Content object
-                                       $content = ContentHandler::makeContent( $toparse, $this->getTitle(),
-                                                                                                                       $this->content_model, $this->content_format );
-                               }
-
-                               wfRunHooks( 'EditPageGetPreviewContent', array( $this, &$content ) );
+                               $hook_args = array( $this, &$content );
+                               ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
+                               wfRunHooks( 'EditPageGetPreviewContent', $hook_args );
 
                                $parserOptions->enableLimitReport();
 
index 30ffbea..0008b4e 100644 (file)
@@ -882,9 +882,10 @@ class Revision implements IDBAccessObject {
         *      Revision::RAW              get the text regardless of permissions
         * @param $user User object to check for, only if FOR_THIS_USER is passed
         *              to the $audience parameter
-        * @return String
+        *
         * @deprecated in 1.WD, use getContent() instead
         * @todo: replace usage in core
+        * @return String
         */
        public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
                wfDeprecated( __METHOD__, '1.WD' );
@@ -904,9 +905,8 @@ class Revision implements IDBAccessObject {
         *      Revision::RAW              get the text regardless of permissions
         * @param $user User object to check for, only if FOR_THIS_USER is passed
         *              to the $audience parameter
-        * @return Content
-        *
         * @since 1.WD
+        * @return Content
         */
        public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
                if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
@@ -946,14 +946,19 @@ class Revision implements IDBAccessObject {
        /**
         * Fetch original serialized data without regard for view restrictions
         *
-        * @return String
-        *
         * @since 1.WD
+        * @return String
         */
        public function getSerializedData() {
                return $this->mText;
        }
 
+       /**
+        * Gets the content object for the revision
+        *
+        * @since 1.WD
+        * @return Content
+        */
        protected function getContentInternal() {
                if( is_null( $this->mContent ) ) {
                        // Revision is immutable. Load on demand:
@@ -970,7 +975,7 @@ class Revision implements IDBAccessObject {
                        $this->mContent = is_null( $this->mText ) ? null : $handler->unserializeContent( $this->mText, $format );
                }
 
-               return $this->mContent;
+               return $this->mContent->copy(); // NOTE: copy() will return $this for immutable content objects
        }
 
        /**
index a4afda7..0318f2d 100644 (file)
@@ -1623,24 +1623,13 @@ class WikiPage extends Page implements IDBAccessObject {
 
                $flags = $this->checkFlags( $flags );
 
-               # call legacy hook
-               $hook_ok = wfRunHooks( 'ArticleContentSave', array( &$this, &$user, &$content, &$summary,
-                       $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
+               # handle hook
+               $hook_args = array( &$this, &$user, &$content, &$summary,
+                                                       $flags & EDIT_MINOR, null, null, &$flags, &$status );
 
-               if ( $hook_ok && Hooks::isRegistered( 'ArticleSave' ) ) { # avoid serialization overhead if the hook isn't present
-                       $content_text = $content->serialize();
-                       $txt = $content_text; # clone
+               if ( !wfRunHooks( 'ArticleContentSave', $hook_args )
+                       || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) {
 
-                       $hook_ok = wfRunHooks( 'ArticleSave', array( &$this, &$user, &$txt, &$summary,
-                               $flags & EDIT_MINOR, null, null, &$flags, &$status ) ); #TODO: survey extensions using this hook
-
-                       if ( $txt !== $content_text ) {
-                               # if the text changed, unserialize the new version to create an updated Content object.
-                               $content = $content->getContentHandler()->unserializeContent( $txt );
-                       }
-               }
-
-               if ( !$hook_ok ) {
                        wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
 
                        if ( $status->isOK() ) {
@@ -1869,11 +1858,11 @@ class WikiPage extends Page implements IDBAccessObject {
                        # Update links, etc.
                        $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
 
-                       wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $serialized, $summary,
-                               $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
+                       $hook_args = array( &$this, &$user, $content, $summary,
+                                                               $flags & EDIT_MINOR, null, null, &$flags, $revision );
 
-                       wfRunHooks( 'ArticleContentInsertComplete', array( &$this, &$user, $content, $summary,
-                               $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
+                       ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args );
+                       wfRunHooks( 'ArticleContentInsertComplete', $hook_args );
                }
 
                # Do updates right now unless deferral was requested
@@ -1884,11 +1873,11 @@ class WikiPage extends Page implements IDBAccessObject {
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
 
-               wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $serialized, $summary,
-                       $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
+               $hook_args = array( &$this, &$user, $content, $summary,
+                                                       $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
 
-               wfRunHooks( 'ArticleContentSaveComplete', array( &$this, &$user, $content, $summary,
-                       $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
+               ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
+               wfRunHooks( 'ArticleContentSaveComplete', $hook_args );
 
                # Promote user to any groups they meet the criteria for
                $user->addAutopromoteOnceGroups( 'onEdit' );
index 7574e6c..a611c9f 100644 (file)
@@ -54,7 +54,11 @@ class ApiEditPage extends ApiBase {
                        $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
                }
 
-               $contentHandler = $pageObj->getContentHandler();
+               if ( !isset( $params['contentmodel'] ) || $params['contentmodel'] == '' ) {
+                       $contentHandler = $pageObj->getContentHandler();
+               } else {
+                       $contentHandler = ContentHandler::getForModelID( $params['contentmodel'] );
+               }
 
                // @todo ask handler whether direct editing is supported at all! make allowFlatEdit() method or some such
 
@@ -139,7 +143,12 @@ class ApiEditPage extends ApiBase {
                                                $text = '';
                                        }
 
-                                       $content = ContentHandler::makeContent( $text, $this->getTitle() );
+                                       try {
+                                               $content = ContentHandler::makeContent( $text, $this->getTitle() );
+                                       } catch ( MWContentSerializationException $ex ) {
+                                               $this->dieUsage( $ex->getMessage(), 'parseerror' );
+                                               return;
+                                       }
                                }
                        }
 
@@ -195,12 +204,12 @@ class ApiEditPage extends ApiBase {
                                $this->dieUsageMsg( array( 'revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText() ) );
                        }
 
-                       $newContent = $contentHandler->getUndoContent( $undoRev, $undoafterRev );
+                       $newContent = $contentHandler->getUndoContent( $pageObj->getRevision(), $undoRev, $undoafterRev );
+
                        if ( !$newContent ) {
                                $this->dieUsageMsg( 'undo-failure' );
                        }
 
-                       $params['contentformat'] = $contentHandler->getDefaultFormat();
                        $params['text'] = $newContent->serialize( $params['contentformat'] );
 
                        // If no summary was given and we only undid one rev,
@@ -322,6 +331,9 @@ class ApiEditPage extends ApiBase {
                        case EditPage::AS_HOOK_ERROR_EXPECTED:
                                $this->dieUsageMsg( 'hookaborted' );
 
+                       case EditPage::AS_PARSE_ERROR:
+                               $this->dieUsage( $status->getMessage(), 'parseerror' );
+
                        case EditPage::AS_IMAGE_REDIRECT_ANON:
                                $this->dieUsageMsg( 'noimageredirect-anon' );
 
@@ -425,6 +437,7 @@ class ApiEditPage extends ApiBase {
                                array( 'undo-failure' ),
                                array( 'hashcheckfailed' ),
                                array( 'hookaborted' ),
+                               array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ),
                                array( 'noimageredirect-anon' ),
                                array( 'noimageredirect-logged' ),
                                array( 'spamdetected', 'spam' ),
@@ -512,6 +525,12 @@ class ApiEditPage extends ApiBase {
                                ApiBase::PARAM_TYPE => 'boolean',
                                ApiBase::PARAM_DFLT => false,
                        ),
+                       'contentformat' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+                       ),
+                       'contentmodel' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
+                       )
                );
        }
 
@@ -550,6 +569,8 @@ class ApiEditPage extends ApiBase {
                        'undo' => "Undo this revision. Overrides {$p}text, {$p}prependtext and {$p}appendtext",
                        'undoafter' => 'Undo all revisions from undo to this one. If not set, just undo one revision',
                        'redirect' => 'Automatically resolve redirects',
+                       'contentformat' => 'Content serialization format used for the input text',
+                       'contentmodel' => 'Content model of the new content',
                );
        }
 
index 3500795..702825a 100644 (file)
  * @ingroup API
  */
 class ApiParse extends ApiBase {
-       private $section, $text, $pstText = null;
+
+       /** @var String $section */
+       private $section = null;
+
+       /** @var Content $content */
+       private $content = null;
+
+       /** @var Content $pstContent */
+       private $pstContent = null;
 
        public function __construct( $main, $action ) {
                parent::__construct( $main, $action );
@@ -44,6 +52,9 @@ class ApiParse extends ApiBase {
                $pageid = $params['pageid'];
                $oldid = $params['oldid'];
 
+               $model = $params['contentmodel'];
+               $format = $params['contentformat'];
+
                if ( !is_null( $page ) && ( !is_null( $text ) || $title != 'API' ) ) {
                        $this->dieUsage( 'The page parameter cannot be used together with the text and title parameters', 'params' );
                }
@@ -95,17 +106,17 @@ class ApiParse extends ApiBase {
                                // If for some reason the "oldid" is actually the current revision, it may be cached
                                if ( $titleObj->getLatestRevID() === intval( $oldid ) )  {
                                        // May get from/save to parser cache
-                                       $p_result = $this->getParsedSectionOrText( $titleObj, $popts, $pageid,
-                                                isset( $prop['wikitext'] ) ) ;
+                                       $pageObj = WikiPage::factory( $titleObj );
+                                       $p_result = $this->getParsedContent( $pageObj, $popts, $pageid, isset( $prop['wikitext'] ) ) ;
                                } else { // This is an old revision, so get the text differently
-                                       $this->text = $rev->getText( Revision::FOR_THIS_USER, $this->getUser() );
+                                       $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
 
                                        if ( $this->section !== false ) {
-                                               $this->text = $this->getSectionText( $this->text, 'r' . $rev->getId() );
+                                               $this->content = $this->getSectionContent( $this->content, 'r' . $rev->getId() );
                                        }
 
                                        // Should we save old revision parses to the parser cache?
-                                       $p_result = $wgParser->parse( $this->text, $titleObj, $popts );
+                                       $p_result = $this->content->getParserOutput( $titleObj, $popts );
                                }
                        } else { // Not $oldid, but $pageid or $page
                                if ( $params['redirects'] ) {
@@ -153,44 +164,54 @@ class ApiParse extends ApiBase {
                                        $oldid = $titleObj->getLatestRevID();
                                }
 
+                               $pageObj = WikiPage::factory( $titleObj );
+
                                // Potentially cached
-                               $p_result = $this->getParsedSectionOrText( $titleObj, $popts, $pageid,
-                                        isset( $prop['wikitext'] ) ) ;
+                               $p_result = $this->getParsedContent( $pageObj, $popts, $pageid, isset( $prop['wikitext'] ) ) ;
                        }
                } else { // Not $oldid, $pageid, $page. Hence based on $text
-
-                       if ( is_null( $text ) ) {
-                               $this->dieUsage( 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params' );
-                       }
-                       $this->text = $text;
                        $titleObj = Title::newFromText( $title );
                        if ( !$titleObj ) {
                                $this->dieUsageMsg( array( 'invalidtitle', $title ) );
                        }
                        $wgTitle = $titleObj;
 
+                       if ( is_null( $text ) ) {
+                               $this->dieUsage( 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params' );
+                       }
+
+                       try {
+                               $this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format );
+                       } catch ( MWContentSerializationException $ex ) {
+                               $this->dieUsage( $ex->getMessage(), 'parseerror' );
+                       }
+
                        if ( $this->section !== false ) {
-                               $this->text = $this->getSectionText( $this->text, $titleObj->getText() );
+                               $this->content = $this->getSectionContent( $this->content, $titleObj->getText() );
                        }
 
                        if ( $params['pst'] || $params['onlypst'] ) {
-                               $this->pstText = $wgParser->preSaveTransform( $this->text, $titleObj, $this->getUser(), $popts );
+                               $this->pstContent = $this->content->preSaveTransform( $titleObj, $this->getUser(), $popts );
                        }
                        if ( $params['onlypst'] ) {
                                // Build a result and bail out
                                $result_array = array();
                                $result_array['text'] = array();
-                               $result->setContent( $result_array['text'], $this->pstText );
+                               $result->setContent( $result_array['text'], $this->pstContent->serialize( $format ) );
                                if ( isset( $prop['wikitext'] ) ) {
                                        $result_array['wikitext'] = array();
-                                       $result->setContent( $result_array['wikitext'], $this->text );
+                                       $result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
                                }
                                $result->addValue( null, $this->getModuleName(), $result_array );
                                return;
                        }
+
                        // Not cached (save or load)
-                       #FIXME: use Content object!
-                       $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts );
+                       if ( $params['pst'] ) {
+                               $p_result = $this->pstContent->getParserOutput( $titleObj, $popts );
+                       } else {
+                               $p_result = $this->content->getParserOutput( $titleObj, $popts );
+                       }
                }
 
                $result_array = array();
@@ -280,10 +301,10 @@ class ApiParse extends ApiBase {
 
                if ( isset( $prop['wikitext'] ) ) {
                        $result_array['wikitext'] = array();
-                       $result->setContent( $result_array['wikitext'], $this->text );
-                       if ( !is_null( $this->pstText ) ) {
+                       $result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
+                       if ( !is_null( $this->pstContent ) ) {
                                $result_array['psttext'] = array();
-                               $result->setContent( $result_array['psttext'], $this->pstText );
+                               $result->setContent( $result_array['psttext'], $this->pstContent->serialize( $format ) );
                        }
                }
                if ( isset( $prop['properties'] ) ) {
@@ -291,8 +312,12 @@ class ApiParse extends ApiBase {
                }
 
                if ( $params['generatexml'] ) {
+                       if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
+                               $this->dieUsage( "generatexml is only supported for wikitext content", "notwikitext" );
+                       }
+
                        $wgParser->startExternalParse( $titleObj, $popts, OT_PREPROCESS );
-                       $dom = $wgParser->preprocessToDom( $this->text );
+                       $dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
                        if ( is_callable( array( $dom, 'saveXML' ) ) ) {
                                $xml = $dom->saveXML();
                        } else {
@@ -324,23 +349,21 @@ class ApiParse extends ApiBase {
        }
 
        /**
-        * @param $titleObj Title
+        * @param $page WikiPage
         * @param $popts ParserOptions
         * @param $pageId Int
         * @param $getWikitext Bool
         * @return ParserOutput
         */
-       private function getParsedSectionOrText( $titleObj, $popts, $pageId = null, $getWikitext = false ) {
-               global $wgParser;
-
-               $page = WikiPage::factory( $titleObj );
+       private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) {
+               $this->content = $page->getContent( Revision::RAW ); //XXX: really raw?
 
-               if ( $this->section !== false ) { #FIXME: get section Content, get parser output, ...
-                       $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId )
-                                       ? 'page id ' . $pageId : $titleObj->getText() ); #FIXME: get section...
+               if ( $this->section !== false ) {
+                       $this->content = $this->getSectionContent( $this->content, !is_null( $pageId )
+                                                                                                               ? 'page id ' . $pageId : $page->getTitle()->getText() );
 
                        // Not cached (save or load)
-                       return $wgParser->parse( $this->text, $titleObj, $popts );
+                       return $this->content->getParserOutput( $page->getTitle(), $popts );
                } else {
                        // Try the parser cache first
                        // getParserOutput will save to Parser cache if able
@@ -349,21 +372,23 @@ class ApiParse extends ApiBase {
                                $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
                        }
                        if ( $getWikitext ) {
-                               $this->content = $page->getContent( Revision::RAW ); #FIXME: use $this->content everywhere
-                               $this->text = ContentHandler::getContentText( $this->content ); #FIXME: serialize, get format from params; or use object structure in result?
+                               $this->content = $page->getContent( Revision::RAW );
                        }
                        return $pout;
                }
        }
 
-       private function getSectionText( $text, $what ) { #FIXME: replace with Content::getSection
-               global $wgParser;
+       private function getSectionContent( Content $content, $what ) {
                // Not cached (save or load)
-               $text = $wgParser->getSection( $text, $this->section, false );
-               if ( $text === false ) {
+               $section = $content->getSection( $this->section );
+               if ( $section === false ) {
                        $this->dieUsage( "There is no section {$this->section} in " . $what, 'nosuchsection' );
                }
-               return $text;
+               if ( $section === null ) {
+                       $this->dieUsage( "Sections are not supported by " . $what, 'nosuchsection' );
+                       $section = false;
+               }
+               return $section;
        }
 
        private function formatLangLinks( $links ) {
@@ -556,6 +581,12 @@ class ApiParse extends ApiBase {
                        'section' => null,
                        'disablepp' => false,
                        'generatexml' => false,
+                       'contentformat' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+                       ),
+                       'contentmodel' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
+                       )
                );
        }
 
@@ -601,6 +632,8 @@ class ApiParse extends ApiBase {
                        'section' => 'Only retrieve the content of this section number',
                        'disablepp' => 'Disable the PP Report from the parser output',
                        'generatexml' => 'Generate XML parse tree',
+                       'contentformat' => 'Content serialization format used for the input text',
+                       'contentmodel' => 'Content model of the new content',
                );
        }
 
@@ -621,6 +654,8 @@ class ApiParse extends ApiBase {
                        array( 'code' => 'nosuchsection', 'info' => 'There is no section sectionnumber in page' ),
                        array( 'nosuchpageid' ),
                        array( 'invalidtitle', 'title' ),
+                       array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ),
+                       array( 'code' => 'notwikitext', 'info' => 'The requested operation is only supported on wikitext content.' ),
                ) );
        }
 
index f76d9f0..2030f1e 100644 (file)
@@ -223,7 +223,11 @@ class DifferenceEngine extends ContextSource {
                # If external diffs are enabled both globally and for the user,
                # we'll use the application/x-external-editor interface to call
                # an external diff tool like kompare, kdiff3, etc.
-               if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) { #FIXME: how to handle this for non-text content?
+               if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) {
+                       //TODO: come up with a good solution for non-text content here.
+                       //      at least, the content format needs to be passed to the client somehow.
+                       //      Currently, action=raw will just fail for non-text content.
+
                        $urls = array(
                                'File' => array( 'Extension' => 'wiki', 'URL' =>
                                        # This should be mOldPage, but it may not be set, see below.
@@ -510,13 +514,12 @@ class DifferenceEngine extends ContextSource {
                        $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
                        $out->setArticleFlag( true );
 
-                       #NOTE: only needed for B/C: custom rendering of JS/CSS via hook
+                       // NOTE: only needed for B/C: custom rendering of JS/CSS via hook
                        if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
                                // Stolen from Article::view --AG 2007-10-11
                                // Give hooks a chance to customise the output
                                // @TODO: standardize this crap into one function
-                               if ( !Hook::isRegistered( 'ShowRawCssJs' )
-                                       || wfRunHooks( 'ShowRawCssJs', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) {
+                               if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
                                        // NOTE: deprecated hook, B/C only
                                        // use the content object's own rendering
                                        $po = $this->mContentObject->getParserOutput();
@@ -524,8 +527,7 @@ class DifferenceEngine extends ContextSource {
                                }
                        } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
                                // Handled by extension
-                       } elseif( Hooks::isRegistered( 'ArticleViewCustom' )
-                                       && !wfRunHooks( 'ArticleViewCustom', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) {
+                       } elseif( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
                                // NOTE: deprecated hook, B/C only
                                // Handled by extension
                        } else {
@@ -711,13 +713,19 @@ class DifferenceEngine extends ContextSource {
         * @since 1.WD
         */
        function generateContentDiffBody( Content $old, Content $new ) {
-               #XXX: generate a warning if $old or $new are not instances of TextContent?
-               #XXX: fail if $old and $new don't have the same content model? or what?
+               if ( !( $old instanceof TextContent ) ) {
+                       throw new MWException( "Diff not implemented for " . get_class( $old ) . "; "
+                                                               . "override generateContentDiffBody to fix this." );
+               }
+
+               if ( !( $new instanceof TextContent ) ) {
+                       throw new MWException( "Diff not implemented for " . get_class( $new ) . "; "
+                               . "override generateContentDiffBody to fix this." );
+               }
 
                $otext = $old->serialize();
                $ntext = $new->serialize();
 
-               #XXX: text should be "already segmented". what does that mean?
                return $this->generateTextDiffBody( $otext, $ntext );
        }
 
@@ -998,9 +1006,9 @@ class DifferenceEngine extends ContextSource {
 
        /**
         * Use specified text instead of loading from the database
-        * @deprecated since 1.WD
+        * @deprecated since 1.WD, use setContent() instead.
         */
-       function setText( $oldText, $newText ) { #FIXME: no longer use this, use setContent()!
+       function setText( $oldText, $newText ) {
                wfDeprecated( __METHOD__, "1.WD" );
 
                $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() );
index 6483c4e..d344704 100644 (file)
@@ -561,15 +561,27 @@ abstract class DatabaseUpdater {
        }
 
        /**
+        * If the specified table exists, drop it, or execute the
+        * patch if one is provided.
+        *
+        * Public @since 1.20
+        *
         * @param $table string
-        * @param $patch string
+        * @param $patch string|false
         * @param $fullpath bool
         */
-       protected function dropTable( $table, $patch, $fullpath = false ) {
+       public function dropTable( $table, $patch = false, $fullpath = false ) {
                if ( $this->db->tableExists( $table, __METHOD__ ) ) {
-                       $this->output( "Dropping table $table... " );
-                       $this->applyPatch( $patch, $fullpath );
-                       $this->output( "done.\n" );
+                       $msg = "Dropping table $table";
+
+                       if ( $patch === false ) {
+                               $this->output( "$msg ..." );
+                               $this->db->dropTable( $table, __METHOD__ );
+                               $this->output( "done.\n" );
+                       }
+                       else {
+                               $this->applyPatch( $patch, $fullpath, $msg );
+                       }
                } else {
                        $this->output( "...$table doesn't exist.\n" );
                }
index afeddf8..01bba65 100644 (file)
@@ -236,6 +236,26 @@ class ContentHandlerTest extends MediaWikiTestCase {
        public function testSupportsSections() {
                $this->markTestIncomplete( "not yet implemented" );
        }
+
+       public function testRunLegacyHooks() {
+               Hooks::register( 'testRunLegacyHooks', __CLASS__ . '::dummyHookHandler' );
+
+               $content = new WikitextContent( 'test text' );
+               $ok = ContentHandler::runLegacyHooks( 'testRunLegacyHooks', array( 'foo', &$content, 'bar' ) );
+
+               $this->assertTrue( $ok, "runLegacyHooks should have returned true" );
+               $this->assertEquals( "TEST TEXT", $content->getNativeData() );
+       }
+
+       public static function dummyHookHandler( $foo, &$text, $bar ) {
+               if ( $text === null || $text === false ) {
+                       return false;
+               }
+
+               $text = strtoupper( $text );
+
+               return true;
+       }
 }
 
 class DummyContentHandlerForTesting extends ContentHandler {
@@ -389,4 +409,3 @@ class DummyContentForTesting extends AbstractContent {
                return new ParserOutput( $this->getNativeData() );
        }
 }
-
index f9a400c..5a98355 100644 (file)
@@ -31,6 +31,7 @@ class RevisionTest extends MediaWikiTestCase {
 
                $wgNamespaceContentModels[ 12312 ] = "testing";
                $wgContentHandlers[ "testing" ] = 'DummyContentHandlerForTesting';
+               $wgContentHandlers[ "RevisionTestModifyableContent" ] = 'RevisionTestModifyableContentHandler';
 
                MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
                $wgContLang->resetNamespaces(); # reset namespace cache
@@ -346,6 +347,87 @@ class RevisionTest extends MediaWikiTestCase {
                $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
        }
 
+       /**
+        * Tests whether $rev->getContent() returns a clone when needed.
+        *
+        * @group Database
+        */
+       function testGetContentClone( ) {
+               $content = new RevisionTestModifyableContent( "foo" );
+
+               $rev = new Revision(
+                       array(
+                               'id'         => 42,
+                               'page'       => 23,
+                               'title'      => Title::newFromText( "testGetContentClone_dummy" ),
+
+                               'content'    => $content,
+                               'length'     => $content->getSize(),
+                               'comment'    => "testing",
+                               'minor_edit' => false,
+                       )
+               );
+
+               $content = $rev->getContent( Revision::RAW );
+               $content->setText( "bar" );
+
+               $content2 = $rev->getContent( Revision::RAW );
+               $this->assertNotSame( $content, $content2, "expected a clone" ); // content is mutable, expect clone
+               $this->assertEquals( "foo", $content2->getText() ); // clone should contain the original text
+
+               $content2->setText( "bla bla" );
+               $this->assertEquals( "bar", $content->getText() ); // clones should be independent
+       }
+
+
+       /**
+        * Tests whether $rev->getContent() returns the same object repeatedly if appropriate.
+        *
+        * @group Database
+        */
+       function testGetContentUncloned() {
+               $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT );
+               $content = $rev->getContent( Revision::RAW );
+               $content2 = $rev->getContent( Revision::RAW );
+
+               // for immutable content like wikitext, this should be the same object
+               $this->assertSame( $content, $content2 );
+       }
+
+}
+
+class RevisionTestModifyableContent extends TextContent {
+       public function __construct( $text ) {
+               parent::__construct( $text, "RevisionTestModifyableContent" );
+       }
+
+       public function copy( ) {
+               return new RevisionTestModifyableContent( $this->mText );
+       }
+
+       public function getText() {
+               return $this->mText;
+       }
+
+       public function setText( $text ) {
+               $this->mText = $text;
+       }
+
 }
 
+class RevisionTestModifyableContentHandler extends TextContentHandler {
+
+       public function __construct( ) {
+               parent::__construct( "RevisionTestModifyableContent", array( CONTENT_FORMAT_TEXT ) );
+       }
 
+       public function unserializeContent( $text, $format = null ) {
+               $this->checkFormat( $format );
+
+               return new RevisionTestModifyableContent( $text );
+       }
+
+       public function makeEmptyContent() {
+               return new RevisionTestModifyableContent( '' );
+       }
+}