Merge "Convert article delete to use OOUI"
[lhc/web/wiklou.git] / includes / api / ApiComparePages.php
index 2300912..953bc10 100644 (file)
@@ -1,9 +1,5 @@
 <?php
 /**
- *
- * Created on May 1, 2011
- *
- * Copyright © 2011 Sam Reed
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 
 class ApiComparePages extends ApiBase {
 
+       private $guessed = false, $guessedTitle, $guessedModel, $props;
+
        public function execute() {
                $params = $this->extractRequestParams();
 
-               $rev1 = $this->revisionOrTitleOrId( $params['fromrev'], $params['fromtitle'], $params['fromid'] );
-               $rev2 = $this->revisionOrTitleOrId( $params['torev'], $params['totitle'], $params['toid'] );
+               // Parameter validation
+               $this->requireAtLeastOneParameter( $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext' );
+               $this->requireAtLeastOneParameter( $params, 'totitle', 'toid', 'torev', 'totext', 'torelative' );
 
-               $revision = Revision::newFromId( $rev1 );
+               $this->props = array_flip( $params['prop'] );
 
-               if ( !$revision ) {
-                       $this->dieUsage( 'The diff cannot be retrieved, ' .
-                               'one revision does not exist or you do not have permission to view it.', 'baddiff' );
-               }
+               // Cache responses publicly by default. This may be overridden later.
+               $this->getMain()->setCacheMode( 'public' );
+
+               // Get the 'from' Revision and Content
+               list( $fromRev, $fromContent, $relRev ) = $this->getDiffContent( 'from', $params );
 
-               $contentHandler = $revision->getContentHandler();
-               $de = $contentHandler->createDifferenceEngine( $this->getContext(),
-                       $rev1,
-                       $rev2,
-                       null, // rcid
-                       true,
-                       false );
+               // Get the 'to' Revision and Content
+               if ( $params['torelative'] !== null ) {
+                       if ( !$relRev ) {
+                               $this->dieWithError( 'apierror-compare-relative-to-nothing' );
+                       }
+                       switch ( $params['torelative'] ) {
+                               case 'prev':
+                                       // Swap 'from' and 'to'
+                                       $toRev = $fromRev;
+                                       $toContent = $fromContent;
+                                       $fromRev = $relRev->getPrevious();
+                                       $fromContent = $fromRev
+                                               ? $fromRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
+                                               : $toContent->getContentHandler()->makeEmptyContent();
+                                       if ( !$fromContent ) {
+                                               $this->dieWithError(
+                                                       [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent'
+                                               );
+                                       }
+                                       break;
 
-               $vals = array();
-               if ( isset( $params['fromtitle'] ) ) {
-                       $vals['fromtitle'] = $params['fromtitle'];
+                               case 'next':
+                                       $toRev = $relRev->getNext();
+                                       $toContent = $toRev
+                                               ? $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
+                                               : $fromContent;
+                                       if ( !$toContent ) {
+                                               $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
+                                       }
+                                       break;
+
+                               case 'cur':
+                                       $title = $relRev->getTitle();
+                                       $id = $title->getLatestRevID();
+                                       $toRev = $id ? Revision::newFromId( $id ) : null;
+                                       if ( !$toRev ) {
+                                               $this->dieWithError(
+                                                       [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
+                                               );
+                                       }
+                                       $toContent = $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+                                       if ( !$toContent ) {
+                                               $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
+                                       }
+                                       break;
+                       }
+                       $relRev2 = null;
+               } else {
+                       list( $toRev, $toContent, $relRev2 ) = $this->getDiffContent( 'to', $params );
                }
-               if ( isset( $params['fromid'] ) ) {
-                       $vals['fromid'] = $params['fromid'];
+
+               // Should never happen, but just in case...
+               if ( !$fromContent || !$toContent ) {
+                       $this->dieWithError( 'apierror-baddiff' );
                }
-               $vals['fromrevid'] = $rev1;
-               if ( isset( $params['totitle'] ) ) {
-                       $vals['totitle'] = $params['totitle'];
+
+               // Get the diff
+               $context = new DerivativeContext( $this->getContext() );
+               if ( $relRev && $relRev->getTitle() ) {
+                       $context->setTitle( $relRev->getTitle() );
+               } elseif ( $relRev2 && $relRev2->getTitle() ) {
+                       $context->setTitle( $relRev2->getTitle() );
+               } else {
+                       $this->guessTitleAndModel();
+                       if ( $this->guessedTitle ) {
+                               $context->setTitle( $this->guessedTitle );
+                       }
                }
-               if ( isset( $params['toid'] ) ) {
-                       $vals['toid'] = $params['toid'];
+               $de = $fromContent->getContentHandler()->createDifferenceEngine(
+                       $context,
+                       $fromRev ? $fromRev->getId() : 0,
+                       $toRev ? $toRev->getId() : 0,
+                       /* $rcid = */ null,
+                       /* $refreshCache = */ false,
+                       /* $unhide = */ true
+               );
+               $de->setContent( $fromContent, $toContent );
+               $difftext = $de->getDiffBody();
+               if ( $difftext === false ) {
+                       $this->dieWithError( 'apierror-baddiff' );
                }
-               $vals['torevid'] = $rev2;
 
-               $difftext = $de->getDiffBody();
+               // Fill in the response
+               $vals = [];
+               $this->setVals( $vals, 'from', $fromRev );
+               $this->setVals( $vals, 'to', $toRev );
 
-               if ( $difftext === false ) {
-                       $this->dieUsage(
-                               'The diff cannot be retrieved. Maybe one or both revisions do ' .
-                                       'not exist or you do not have permission to view them.',
-                               'baddiff'
-                       );
+               if ( isset( $this->props['rel'] ) ) {
+                       if ( $fromRev ) {
+                               $rev = $fromRev->getPrevious();
+                               if ( $rev ) {
+                                       $vals['prev'] = $rev->getId();
+                               }
+                       }
+                       if ( $toRev ) {
+                               $rev = $toRev->getNext();
+                               if ( $rev ) {
+                                       $vals['next'] = $rev->getId();
+                               }
+                       }
                }
 
-               ApiResult::setContentValue( $vals, 'body', $difftext );
+               if ( isset( $this->props['diffsize'] ) ) {
+                       $vals['diffsize'] = strlen( $difftext );
+               }
+               if ( isset( $this->props['diff'] ) ) {
+                       ApiResult::setContentValue( $vals, 'body', $difftext );
+               }
 
                $this->getResult()->addValue( null, $this->getModuleName(), $vals );
        }
 
        /**
-        * @param int $revision
-        * @param string $titleText
-        * @param int $titleId
-        * @return int
+        * Guess an appropriate default Title and content model for this request
+        *
+        * Fills in $this->guessedTitle based on the first of 'fromrev',
+        * 'fromtitle', 'fromid', 'torev', 'totitle', and 'toid' that's present and
+        * valid.
+        *
+        * Fills in $this->guessedModel based on the Revision or Title used to
+        * determine $this->guessedTitle, or the 'fromcontentmodel' or
+        * 'tocontentmodel' parameters if no title was guessed.
+        */
+       private function guessTitleAndModel() {
+               if ( $this->guessed ) {
+                       return;
+               }
+
+               $this->guessed = true;
+               $params = $this->extractRequestParams();
+
+               foreach ( [ 'from', 'to' ] as $prefix ) {
+                       if ( $params["{$prefix}rev"] !== null ) {
+                               $revId = $params["{$prefix}rev"];
+                               $rev = Revision::newFromId( $revId );
+                               if ( !$rev ) {
+                                       // Titles of deleted revisions aren't secret, per T51088
+                                       $row = $this->getDB()->selectRow(
+                                               'archive',
+                                               array_merge(
+                                                       Revision::selectArchiveFields(),
+                                                       [ 'ar_namespace', 'ar_title' ]
+                                               ),
+                                               [ 'ar_rev_id' => $revId ],
+                                               __METHOD__
+                                       );
+                                       if ( $row ) {
+                                               $rev = Revision::newFromArchiveRow( $row );
+                                       }
+                               }
+                               if ( $rev ) {
+                                       $this->guessedTitle = $rev->getTitle();
+                                       $this->guessedModel = $rev->getContentModel();
+                                       break;
+                               }
+                       }
+
+                       if ( $params["{$prefix}title"] !== null ) {
+                               $title = Title::newFromText( $params["{$prefix}title"] );
+                               if ( $title && !$title->isExternal() ) {
+                                       $this->guessedTitle = $title;
+                                       break;
+                               }
+                       }
+
+                       if ( $params["{$prefix}id"] !== null ) {
+                               $title = Title::newFromID( $params["{$prefix}id"] );
+                               if ( $title ) {
+                                       $this->guessedTitle = $title;
+                                       break;
+                               }
+                       }
+               }
+
+               if ( !$this->guessedModel ) {
+                       if ( $this->guessedTitle ) {
+                               $this->guessedModel = $this->guessedTitle->getContentModel();
+                       } elseif ( $params['fromcontentmodel'] !== null ) {
+                               $this->guessedModel = $params['fromcontentmodel'];
+                       } elseif ( $params['tocontentmodel'] !== null ) {
+                               $this->guessedModel = $params['tocontentmodel'];
+                       }
+               }
+       }
+
+       /**
+        * Get the Revision and Content for one side of the diff
+        *
+        * This uses the appropriate set of 'rev', 'id', 'title', 'text', 'pst',
+        * 'contentmodel', and 'contentformat' parameters to determine what content
+        * should be diffed.
+        *
+        * Returns three values:
+        * - The revision used to retrieve the content, if any
+        * - The content to be diffed
+        * - The revision specified, if any, even if not used to retrieve the
+        *   Content
+        *
+        * @param string $prefix 'from' or 'to'
+        * @param array $params
+        * @return array [ Revision|null, Content, Revision|null ]
         */
-       private function revisionOrTitleOrId( $revision, $titleText, $titleId ) {
-               if ( $revision ) {
-                       return $revision;
-               } elseif ( $titleText ) {
-                       $title = Title::newFromText( $titleText );
-                       if ( !$title || $title->isExternal() ) {
-                               $this->dieUsageMsg( array( 'invalidtitle', $titleText ) );
-                       }
-
-                       return $title->getLatestRevID();
-               } elseif ( $titleId ) {
-                       $title = Title::newFromID( $titleId );
+       private function getDiffContent( $prefix, array $params ) {
+               $title = null;
+               $rev = null;
+               $suppliedContent = $params["{$prefix}text"] !== null;
+
+               // Get the revision and title, if applicable
+               $revId = null;
+               if ( $params["{$prefix}rev"] !== null ) {
+                       $revId = $params["{$prefix}rev"];
+               } elseif ( $params["{$prefix}title"] !== null || $params["{$prefix}id"] !== null ) {
+                       if ( $params["{$prefix}title"] !== null ) {
+                               $title = Title::newFromText( $params["{$prefix}title"] );
+                               if ( !$title || $title->isExternal() ) {
+                                       $this->dieWithError(
+                                               [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
+                                       );
+                               }
+                       } else {
+                               $title = Title::newFromID( $params["{$prefix}id"] );
+                               if ( !$title ) {
+                                       $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
+                               }
+                       }
+                       $revId = $title->getLatestRevID();
+                       if ( !$revId ) {
+                               $revId = null;
+                               // Only die here if we're not using supplied text
+                               if ( !$suppliedContent ) {
+                                       if ( $title->exists() ) {
+                                               $this->dieWithError(
+                                                       [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
+                                               );
+                                       } else {
+                                               $this->dieWithError(
+                                                       [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
+                                                       'missingtitle'
+                                               );
+                                       }
+                               }
+                       }
+               }
+               if ( $revId !== null ) {
+                       $rev = Revision::newFromId( $revId );
+                       if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
+                               // Try the 'archive' table
+                               $row = $this->getDB()->selectRow(
+                                       'archive',
+                                       array_merge(
+                                               Revision::selectArchiveFields(),
+                                               [ 'ar_namespace', 'ar_title' ]
+                                       ),
+                                       [ 'ar_rev_id' => $revId ],
+                                       __METHOD__
+                               );
+                               if ( $row ) {
+                                       $rev = Revision::newFromArchiveRow( $row );
+                                       $rev->isArchive = true;
+                               }
+                       }
+                       if ( !$rev ) {
+                               $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
+                       }
+                       $title = $rev->getTitle();
+
+                       // If we don't have supplied content, return here. Otherwise,
+                       // continue on below with the supplied content.
+                       if ( !$suppliedContent ) {
+                               $content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+                               if ( !$content ) {
+                                       $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' );
+                               }
+                               return [ $rev, $content, $rev ];
+                       }
+               }
+
+               // Override $content based on supplied text
+               $model = $params["{$prefix}contentmodel"];
+               $format = $params["{$prefix}contentformat"];
+
+               if ( !$model && $rev ) {
+                       $model = $rev->getContentModel();
+               }
+               if ( !$model && $title ) {
+                       $model = $title->getContentModel();
+               }
+               if ( !$model ) {
+                       $this->guessTitleAndModel();
+                       $model = $this->guessedModel;
+               }
+               if ( !$model ) {
+                       $model = CONTENT_MODEL_WIKITEXT;
+                       $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
+               }
+
+               if ( !$title ) {
+                       $this->guessTitleAndModel();
+                       $title = $this->guessedTitle;
+               }
+
+               try {
+                       $content = ContentHandler::makeContent( $params["{$prefix}text"], $title, $model, $format );
+               } catch ( MWContentSerializationException $ex ) {
+                       $this->dieWithException( $ex, [
+                               'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
+                       ] );
+               }
+
+               if ( $params["{$prefix}pst"] ) {
                        if ( !$title ) {
-                               $this->dieUsageMsg( array( 'nosuchpageid', $titleId ) );
+                               $this->dieWithError( 'apierror-compare-no-title' );
+                       }
+                       $popts = ParserOptions::newFromContext( $this->getContext() );
+                       $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
+               }
+
+               return [ null, $content, $rev ];
+       }
+
+       /**
+        * Set value fields from a Revision object
+        * @param array &$vals Result array to set data into
+        * @param string $prefix 'from' or 'to'
+        * @param Revision|null $rev
+        */
+       private function setVals( &$vals, $prefix, $rev ) {
+               if ( $rev ) {
+                       $title = $rev->getTitle();
+                       if ( isset( $this->props['ids'] ) ) {
+                               $vals["{$prefix}id"] = $title->getArticleId();
+                               $vals["{$prefix}revid"] = $rev->getId();
+                       }
+                       if ( isset( $this->props['title'] ) ) {
+                               ApiQueryBase::addTitleInfo( $vals, $title, $prefix );
+                       }
+                       if ( isset( $this->props['size'] ) ) {
+                               $vals["{$prefix}size"] = $rev->getSize();
                        }
 
-                       return $title->getLatestRevID();
+                       $anyHidden = false;
+                       if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+                               $vals["{$prefix}texthidden"] = true;
+                               $anyHidden = true;
+                       }
+
+                       if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
+                               $vals["{$prefix}userhidden"] = true;
+                               $anyHidden = true;
+                       }
+                       if ( isset( $this->props['user'] ) &&
+                               $rev->userCan( Revision::DELETED_USER, $this->getUser() )
+                       ) {
+                               $vals["{$prefix}user"] = $rev->getUserText( Revision::RAW );
+                               $vals["{$prefix}userid"] = $rev->getUser( Revision::RAW );
+                       }
+
+                       if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
+                               $vals["{$prefix}commenthidden"] = true;
+                               $anyHidden = true;
+                       }
+                       if ( $rev->userCan( Revision::DELETED_COMMENT, $this->getUser() ) ) {
+                               if ( isset( $this->props['comment'] ) ) {
+                                       $vals["{$prefix}comment"] = $rev->getComment( Revision::RAW );
+                               }
+                               if ( isset( $this->props['parsedcomment'] ) ) {
+                                       $vals["{$prefix}parsedcomment"] = Linker::formatComment(
+                                               $rev->getComment( Revision::RAW ),
+                                               $rev->getTitle()
+                                       );
+                               }
+                       }
+
+                       if ( $anyHidden ) {
+                               $this->getMain()->setCacheMode( 'private' );
+                               if ( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
+                                       $vals["{$prefix}suppressed"] = true;
+                               }
+                       }
+
+                       if ( !empty( $rev->isArchive ) ) {
+                               $this->getMain()->setCacheMode( 'private' );
+                               $vals["{$prefix}archive"] = true;
+                       }
                }
-               $this->dieUsage(
-                       'A title, a page ID, or a revision number is needed for both the from and the to parameters',
-                       'inputneeded'
-               );
        }
 
        public function getAllowedParams() {
-               return array(
-                       'fromtitle' => null,
-                       'fromid' => array(
-                               ApiBase::PARAM_TYPE => 'integer'
-                       ),
-                       'fromrev' => array(
-                               ApiBase::PARAM_TYPE => 'integer'
-                       ),
-                       'totitle' => null,
-                       'toid' => array(
+               // Parameters for the 'from' and 'to' content
+               $fromToParams = [
+                       'title' => null,
+                       'id' => [
                                ApiBase::PARAM_TYPE => 'integer'
-                       ),
-                       'torev' => array(
+                       ],
+                       'rev' => [
                                ApiBase::PARAM_TYPE => 'integer'
-                       ),
+                       ],
+                       'text' => [
+                               ApiBase::PARAM_TYPE => 'text'
+                       ],
+                       'pst' => false,
+                       'contentformat' => [
+                               ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+                       ],
+                       'contentmodel' => [
+                               ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
+                       ]
+               ];
+
+               $ret = [];
+               foreach ( $fromToParams as $k => $v ) {
+                       $ret["from$k"] = $v;
+               }
+               foreach ( $fromToParams as $k => $v ) {
+                       $ret["to$k"] = $v;
+               }
+
+               $ret = wfArrayInsertAfter(
+                       $ret,
+                       [ 'torelative' => [ ApiBase::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
+                       'torev'
                );
+
+               $ret['prop'] = [
+                       ApiBase::PARAM_DFLT => 'diff|ids|title',
+                       ApiBase::PARAM_TYPE => [
+                               'diff',
+                               'diffsize',
+                               'rel',
+                               'ids',
+                               'title',
+                               'user',
+                               'comment',
+                               'parsedcomment',
+                               'size',
+                       ],
+                       ApiBase::PARAM_ISMULTI => true,
+                       ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
+               ];
+
+               return $ret;
        }
 
        protected function getExamplesMessages() {
-               return array(
+               return [
                        'action=compare&fromrev=1&torev=2'
                                => 'apihelp-compare-example-1',
-               );
+               ];
        }
 }