summaryrefslogtreecommitdiff
path: root/www/wiki/includes/api/ApiComparePages.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/includes/api/ApiComparePages.php')
-rw-r--r--www/wiki/includes/api/ApiComparePages.php520
1 files changed, 520 insertions, 0 deletions
diff --git a/www/wiki/includes/api/ApiComparePages.php b/www/wiki/includes/api/ApiComparePages.php
new file mode 100644
index 00000000..93c35d3d
--- /dev/null
+++ b/www/wiki/includes/api/ApiComparePages.php
@@ -0,0 +1,520 @@
+<?php
+/**
+ *
+ * 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+class ApiComparePages extends ApiBase {
+
+ private $guessed = false, $guessedTitle, $guessedModel, $props;
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ // Parameter validation
+ $this->requireAtLeastOneParameter( $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext' );
+ $this->requireAtLeastOneParameter( $params, 'totitle', 'toid', 'torev', 'totext', 'torelative' );
+
+ $this->props = array_flip( $params['prop'] );
+
+ // 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 );
+
+ // 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 );
+ }
+
+ // Should never happen, but just in case...
+ if ( !$fromContent || !$toContent ) {
+ $this->dieWithError( 'apierror-baddiff' );
+ }
+
+ // Extract sections, if told to
+ if ( isset( $params['fromsection'] ) ) {
+ $fromContent = $fromContent->getSection( $params['fromsection'] );
+ if ( !$fromContent ) {
+ $this->dieWithError(
+ [ 'apierror-compare-nosuchfromsection', wfEscapeWikiText( $params['fromsection'] ) ],
+ 'nosuchfromsection'
+ );
+ }
+ }
+ if ( isset( $params['tosection'] ) ) {
+ $toContent = $toContent->getSection( $params['tosection'] );
+ if ( !$toContent ) {
+ $this->dieWithError(
+ [ 'apierror-compare-nosuchtosection', wfEscapeWikiText( $params['tosection'] ) ],
+ 'nosuchtosection'
+ );
+ }
+ }
+
+ // 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 );
+ }
+ }
+ $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' );
+ }
+
+ // Fill in the response
+ $vals = [];
+ $this->setVals( $vals, 'from', $fromRev );
+ $this->setVals( $vals, 'to', $toRev );
+
+ 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();
+ }
+ }
+ }
+
+ if ( isset( $this->props['diffsize'] ) ) {
+ $vals['diffsize'] = strlen( $difftext );
+ }
+ if ( isset( $this->props['diff'] ) ) {
+ ApiResult::setContentValue( $vals, 'body', $difftext );
+ }
+
+ // Diffs can be really big and there's little point in having
+ // ApiResult truncate it to an empty response since the diff is the
+ // whole reason this module exists. So pass NO_SIZE_CHECK here.
+ $this->getResult()->addValue( null, $this->getModuleName(), $vals, ApiResult::NO_SIZE_CHECK );
+ }
+
+ /**
+ * 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
+ $arQuery = Revision::getArchiveQueryInfo();
+ $row = $this->getDB()->selectRow(
+ $arQuery['tables'],
+ array_merge(
+ $arQuery['fields'],
+ [ 'ar_namespace', 'ar_title' ]
+ ),
+ [ 'ar_rev_id' => $revId ],
+ __METHOD__,
+ [],
+ $arQuery['joins']
+ );
+ 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 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
+ $arQuery = Revision::getArchiveQueryInfo();
+ $row = $this->getDB()->selectRow(
+ $arQuery['tables'],
+ array_merge(
+ $arQuery['fields'],
+ [ 'ar_namespace', 'ar_title' ]
+ ),
+ [ 'ar_rev_id' => $revId ],
+ __METHOD__,
+ [],
+ $arQuery['joins']
+ );
+ 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->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;
+ }
+ }
+
+ if ( !empty( $rev->isArchive ) ) {
+ $this->getMain()->setCacheMode( 'private' );
+ $vals["{$prefix}archive"] = true;
+ }
+ }
+ }
+
+ public function getAllowedParams() {
+ // Parameters for the 'from' and 'to' content
+ $fromToParams = [
+ 'title' => null,
+ 'id' => [
+ ApiBase::PARAM_TYPE => 'integer'
+ ],
+ 'rev' => [
+ ApiBase::PARAM_TYPE => 'integer'
+ ],
+ 'text' => [
+ ApiBase::PARAM_TYPE => 'text'
+ ],
+ 'section' => null,
+ '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 [
+ 'action=compare&fromrev=1&torev=2'
+ => 'apihelp-compare-example-1',
+ ];
+ }
+}