4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
22 class ApiComparePages
extends ApiBase
{
24 private $guessed = false, $guessedTitle, $guessedModel, $props;
26 public function execute() {
27 $params = $this->extractRequestParams();
29 // Parameter validation
30 $this->requireAtLeastOneParameter( $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext' );
31 $this->requireAtLeastOneParameter( $params, 'totitle', 'toid', 'torev', 'totext', 'torelative' );
33 $this->props
= array_flip( $params['prop'] );
35 // Cache responses publicly by default. This may be overridden later.
36 $this->getMain()->setCacheMode( 'public' );
38 // Get the 'from' Revision and Content
39 list( $fromRev, $fromContent, $relRev ) = $this->getDiffContent( 'from', $params );
41 // Get the 'to' Revision and Content
42 if ( $params['torelative'] !== null ) {
44 $this->dieWithError( 'apierror-compare-relative-to-nothing' );
46 switch ( $params['torelative'] ) {
48 // Swap 'from' and 'to'
50 $toContent = $fromContent;
51 $fromRev = $relRev->getPrevious();
52 $fromContent = $fromRev
53 ?
$fromRev->getContent( Revision
::FOR_THIS_USER
, $this->getUser() )
54 : $toContent->getContentHandler()->makeEmptyContent();
55 if ( !$fromContent ) {
57 [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent'
63 $toRev = $relRev->getNext();
65 ?
$toRev->getContent( Revision
::FOR_THIS_USER
, $this->getUser() )
68 $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
73 $title = $relRev->getTitle();
74 $id = $title->getLatestRevID();
75 $toRev = $id ? Revision
::newFromId( $id ) : null;
78 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
81 $toContent = $toRev->getContent( Revision
::FOR_THIS_USER
, $this->getUser() );
83 $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
89 list( $toRev, $toContent, $relRev2 ) = $this->getDiffContent( 'to', $params );
92 // Should never happen, but just in case...
93 if ( !$fromContent ||
!$toContent ) {
94 $this->dieWithError( 'apierror-baddiff' );
97 // Extract sections, if told to
98 if ( isset( $params['fromsection'] ) ) {
99 $fromContent = $fromContent->getSection( $params['fromsection'] );
100 if ( !$fromContent ) {
102 [ 'apierror-compare-nosuchfromsection', wfEscapeWikiText( $params['fromsection'] ) ],
107 if ( isset( $params['tosection'] ) ) {
108 $toContent = $toContent->getSection( $params['tosection'] );
111 [ 'apierror-compare-nosuchtosection', wfEscapeWikiText( $params['tosection'] ) ],
118 $context = new DerivativeContext( $this->getContext() );
119 if ( $relRev && $relRev->getTitle() ) {
120 $context->setTitle( $relRev->getTitle() );
121 } elseif ( $relRev2 && $relRev2->getTitle() ) {
122 $context->setTitle( $relRev2->getTitle() );
124 $this->guessTitleAndModel();
125 if ( $this->guessedTitle
) {
126 $context->setTitle( $this->guessedTitle
);
129 $de = $fromContent->getContentHandler()->createDifferenceEngine(
131 $fromRev ?
$fromRev->getId() : 0,
132 $toRev ?
$toRev->getId() : 0,
134 /* $refreshCache = */ false,
137 $de->setContent( $fromContent, $toContent );
138 $difftext = $de->getDiffBody();
139 if ( $difftext === false ) {
140 $this->dieWithError( 'apierror-baddiff' );
143 // Fill in the response
145 $this->setVals( $vals, 'from', $fromRev );
146 $this->setVals( $vals, 'to', $toRev );
148 if ( isset( $this->props
['rel'] ) ) {
150 $rev = $fromRev->getPrevious();
152 $vals['prev'] = $rev->getId();
156 $rev = $toRev->getNext();
158 $vals['next'] = $rev->getId();
163 if ( isset( $this->props
['diffsize'] ) ) {
164 $vals['diffsize'] = strlen( $difftext );
166 if ( isset( $this->props
['diff'] ) ) {
167 ApiResult
::setContentValue( $vals, 'body', $difftext );
170 // Diffs can be really big and there's little point in having
171 // ApiResult truncate it to an empty response since the diff is the
172 // whole reason this module exists. So pass NO_SIZE_CHECK here.
173 $this->getResult()->addValue( null, $this->getModuleName(), $vals, ApiResult
::NO_SIZE_CHECK
);
177 * Guess an appropriate default Title and content model for this request
179 * Fills in $this->guessedTitle based on the first of 'fromrev',
180 * 'fromtitle', 'fromid', 'torev', 'totitle', and 'toid' that's present and
183 * Fills in $this->guessedModel based on the Revision or Title used to
184 * determine $this->guessedTitle, or the 'fromcontentmodel' or
185 * 'tocontentmodel' parameters if no title was guessed.
187 private function guessTitleAndModel() {
188 if ( $this->guessed
) {
192 $this->guessed
= true;
193 $params = $this->extractRequestParams();
195 foreach ( [ 'from', 'to' ] as $prefix ) {
196 if ( $params["{$prefix}rev"] !== null ) {
197 $revId = $params["{$prefix}rev"];
198 $rev = Revision
::newFromId( $revId );
200 // Titles of deleted revisions aren't secret, per T51088
201 $arQuery = Revision
::getArchiveQueryInfo();
202 $row = $this->getDB()->selectRow(
206 [ 'ar_namespace', 'ar_title' ]
208 [ 'ar_rev_id' => $revId ],
214 $rev = Revision
::newFromArchiveRow( $row );
218 $this->guessedTitle
= $rev->getTitle();
219 $this->guessedModel
= $rev->getContentModel();
224 if ( $params["{$prefix}title"] !== null ) {
225 $title = Title
::newFromText( $params["{$prefix}title"] );
226 if ( $title && !$title->isExternal() ) {
227 $this->guessedTitle
= $title;
232 if ( $params["{$prefix}id"] !== null ) {
233 $title = Title
::newFromID( $params["{$prefix}id"] );
235 $this->guessedTitle
= $title;
241 if ( !$this->guessedModel
) {
242 if ( $this->guessedTitle
) {
243 $this->guessedModel
= $this->guessedTitle
->getContentModel();
244 } elseif ( $params['fromcontentmodel'] !== null ) {
245 $this->guessedModel
= $params['fromcontentmodel'];
246 } elseif ( $params['tocontentmodel'] !== null ) {
247 $this->guessedModel
= $params['tocontentmodel'];
253 * Get the Revision and Content for one side of the diff
255 * This uses the appropriate set of 'rev', 'id', 'title', 'text', 'pst',
256 * 'contentmodel', and 'contentformat' parameters to determine what content
259 * Returns three values:
260 * - The revision used to retrieve the content, if any
261 * - The content to be diffed
262 * - The revision specified, if any, even if not used to retrieve the
265 * @param string $prefix 'from' or 'to'
266 * @param array $params
267 * @return array [ Revision|null, Content, Revision|null ]
269 private function getDiffContent( $prefix, array $params ) {
272 $suppliedContent = $params["{$prefix}text"] !== null;
274 // Get the revision and title, if applicable
276 if ( $params["{$prefix}rev"] !== null ) {
277 $revId = $params["{$prefix}rev"];
278 } elseif ( $params["{$prefix}title"] !== null ||
$params["{$prefix}id"] !== null ) {
279 if ( $params["{$prefix}title"] !== null ) {
280 $title = Title
::newFromText( $params["{$prefix}title"] );
281 if ( !$title ||
$title->isExternal() ) {
283 [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
287 $title = Title
::newFromID( $params["{$prefix}id"] );
289 $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
292 $revId = $title->getLatestRevID();
295 // Only die here if we're not using supplied text
296 if ( !$suppliedContent ) {
297 if ( $title->exists() ) {
299 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
303 [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
310 if ( $revId !== null ) {
311 $rev = Revision
::newFromId( $revId );
312 if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
313 // Try the 'archive' table
314 $arQuery = Revision
::getArchiveQueryInfo();
315 $row = $this->getDB()->selectRow(
319 [ 'ar_namespace', 'ar_title' ]
321 [ 'ar_rev_id' => $revId ],
327 $rev = Revision
::newFromArchiveRow( $row );
328 $rev->isArchive
= true;
332 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
334 $title = $rev->getTitle();
336 // If we don't have supplied content, return here. Otherwise,
337 // continue on below with the supplied content.
338 if ( !$suppliedContent ) {
339 $content = $rev->getContent( Revision
::FOR_THIS_USER
, $this->getUser() );
341 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' );
343 return [ $rev, $content, $rev ];
347 // Override $content based on supplied text
348 $model = $params["{$prefix}contentmodel"];
349 $format = $params["{$prefix}contentformat"];
351 if ( !$model && $rev ) {
352 $model = $rev->getContentModel();
354 if ( !$model && $title ) {
355 $model = $title->getContentModel();
358 $this->guessTitleAndModel();
359 $model = $this->guessedModel
;
362 $model = CONTENT_MODEL_WIKITEXT
;
363 $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
367 $this->guessTitleAndModel();
368 $title = $this->guessedTitle
;
372 $content = ContentHandler
::makeContent( $params["{$prefix}text"], $title, $model, $format );
373 } catch ( MWContentSerializationException
$ex ) {
374 $this->dieWithException( $ex, [
375 'wrap' => ApiMessage
::create( 'apierror-contentserializationexception', 'parseerror' )
379 if ( $params["{$prefix}pst"] ) {
381 $this->dieWithError( 'apierror-compare-no-title' );
383 $popts = ParserOptions
::newFromContext( $this->getContext() );
384 $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
387 return [ null, $content, $rev ];
391 * Set value fields from a Revision object
392 * @param array &$vals Result array to set data into
393 * @param string $prefix 'from' or 'to'
394 * @param Revision|null $rev
396 private function setVals( &$vals, $prefix, $rev ) {
398 $title = $rev->getTitle();
399 if ( isset( $this->props
['ids'] ) ) {
400 $vals["{$prefix}id"] = $title->getArticleID();
401 $vals["{$prefix}revid"] = $rev->getId();
403 if ( isset( $this->props
['title'] ) ) {
404 ApiQueryBase
::addTitleInfo( $vals, $title, $prefix );
406 if ( isset( $this->props
['size'] ) ) {
407 $vals["{$prefix}size"] = $rev->getSize();
411 if ( $rev->isDeleted( Revision
::DELETED_TEXT
) ) {
412 $vals["{$prefix}texthidden"] = true;
416 if ( $rev->isDeleted( Revision
::DELETED_USER
) ) {
417 $vals["{$prefix}userhidden"] = true;
420 if ( isset( $this->props
['user'] ) &&
421 $rev->userCan( Revision
::DELETED_USER
, $this->getUser() )
423 $vals["{$prefix}user"] = $rev->getUserText( Revision
::RAW
);
424 $vals["{$prefix}userid"] = $rev->getUser( Revision
::RAW
);
427 if ( $rev->isDeleted( Revision
::DELETED_COMMENT
) ) {
428 $vals["{$prefix}commenthidden"] = true;
431 if ( $rev->userCan( Revision
::DELETED_COMMENT
, $this->getUser() ) ) {
432 if ( isset( $this->props
['comment'] ) ) {
433 $vals["{$prefix}comment"] = $rev->getComment( Revision
::RAW
);
435 if ( isset( $this->props
['parsedcomment'] ) ) {
436 $vals["{$prefix}parsedcomment"] = Linker
::formatComment(
437 $rev->getComment( Revision
::RAW
),
444 $this->getMain()->setCacheMode( 'private' );
445 if ( $rev->isDeleted( Revision
::DELETED_RESTRICTED
) ) {
446 $vals["{$prefix}suppressed"] = true;
450 if ( !empty( $rev->isArchive
) ) {
451 $this->getMain()->setCacheMode( 'private' );
452 $vals["{$prefix}archive"] = true;
457 public function getAllowedParams() {
458 // Parameters for the 'from' and 'to' content
462 ApiBase
::PARAM_TYPE
=> 'integer'
465 ApiBase
::PARAM_TYPE
=> 'integer'
468 ApiBase
::PARAM_TYPE
=> 'text'
473 ApiBase
::PARAM_TYPE
=> ContentHandler
::getAllContentFormats(),
476 ApiBase
::PARAM_TYPE
=> ContentHandler
::getContentModels(),
481 foreach ( $fromToParams as $k => $v ) {
484 foreach ( $fromToParams as $k => $v ) {
488 $ret = wfArrayInsertAfter(
490 [ 'torelative' => [ ApiBase
::PARAM_TYPE
=> [ 'prev', 'next', 'cur' ], ] ],
495 ApiBase
::PARAM_DFLT
=> 'diff|ids|title',
496 ApiBase
::PARAM_TYPE
=> [
507 ApiBase
::PARAM_ISMULTI
=> true,
508 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [],
514 protected function getExamplesMessages() {
516 'action=compare&fromrev=1&torev=2'
517 => 'apihelp-compare-example-1',