X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=includes%2Fapi%2FApiComparePages.php;h=953bc10cc3e4b4ff0dd637da7b7c67a95a54a790;hp=7eb0bf3e81919184e1cbf09a52570de33780dc55;hb=12601ff7d2796752404bfb331fccc41083d31f9f;hpb=c340c41b37b5079ba90489f6b212bb8e4642031a diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index 7eb0bf3e81..953bc10cc3 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -1,9 +1,5 @@ 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' ); - $contentHandler = $revision->getContentHandler(); - $de = $contentHandler->createDifferenceEngine( $this->getContext(), - $rev1, - $rev2, - null, // rcid - true, - false ); + // Get the 'from' Revision and Content + list( $fromRev, $fromContent, $relRev ) = $this->getDiffContent( 'from', $params ); - $vals = []; - if ( isset( $params['fromtitle'] ) ) { - $vals['fromtitle'] = $params['fromtitle']; + // 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; + + 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( [ '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( [ '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(); + } + + $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; + } } - return $title->getLatestRevID(); + 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 [ - 'fromtitle' => null, - 'fromid' => [ + // Parameters for the 'from' and 'to' content + $fromToParams = [ + 'title' => null, + 'id' => [ ApiBase::PARAM_TYPE => 'integer' ], - 'fromrev' => [ + 'rev' => [ ApiBase::PARAM_TYPE => 'integer' ], - 'totitle' => null, - 'toid' => [ - ApiBase::PARAM_TYPE => 'integer' + 'text' => [ + ApiBase::PARAM_TYPE => 'text' ], - 'torev' => [ - ApiBase::PARAM_TYPE => 'integer' + '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() {