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' );
98 $context = new DerivativeContext( $this->getContext() );
99 if ( $relRev && $relRev->getTitle() ) {
100 $context->setTitle( $relRev->getTitle() );
101 } elseif ( $relRev2 && $relRev2->getTitle() ) {
102 $context->setTitle( $relRev2->getTitle() );
104 $this->guessTitleAndModel();
105 if ( $this->guessedTitle
) {
106 $context->setTitle( $this->guessedTitle
);
109 $de = $fromContent->getContentHandler()->createDifferenceEngine(
111 $fromRev ?
$fromRev->getId() : 0,
112 $toRev ?
$toRev->getId() : 0,
114 /* $refreshCache = */ false,
117 $de->setContent( $fromContent, $toContent );
118 $difftext = $de->getDiffBody();
119 if ( $difftext === false ) {
120 $this->dieWithError( 'apierror-baddiff' );
123 // Fill in the response
125 $this->setVals( $vals, 'from', $fromRev );
126 $this->setVals( $vals, 'to', $toRev );
128 if ( isset( $this->props
['rel'] ) ) {
130 $rev = $fromRev->getPrevious();
132 $vals['prev'] = $rev->getId();
136 $rev = $toRev->getNext();
138 $vals['next'] = $rev->getId();
143 if ( isset( $this->props
['diffsize'] ) ) {
144 $vals['diffsize'] = strlen( $difftext );
146 if ( isset( $this->props
['diff'] ) ) {
147 ApiResult
::setContentValue( $vals, 'body', $difftext );
150 $this->getResult()->addValue( null, $this->getModuleName(), $vals );
154 * Guess an appropriate default Title and content model for this request
156 * Fills in $this->guessedTitle based on the first of 'fromrev',
157 * 'fromtitle', 'fromid', 'torev', 'totitle', and 'toid' that's present and
160 * Fills in $this->guessedModel based on the Revision or Title used to
161 * determine $this->guessedTitle, or the 'fromcontentmodel' or
162 * 'tocontentmodel' parameters if no title was guessed.
164 private function guessTitleAndModel() {
165 if ( $this->guessed
) {
169 $this->guessed
= true;
170 $params = $this->extractRequestParams();
172 foreach ( [ 'from', 'to' ] as $prefix ) {
173 if ( $params["{$prefix}rev"] !== null ) {
174 $revId = $params["{$prefix}rev"];
175 $rev = Revision
::newFromId( $revId );
177 // Titles of deleted revisions aren't secret, per T51088
178 $row = $this->getDB()->selectRow(
181 Revision
::selectArchiveFields(),
182 [ 'ar_namespace', 'ar_title' ]
184 [ 'ar_rev_id' => $revId ],
188 $rev = Revision
::newFromArchiveRow( $row );
192 $this->guessedTitle
= $rev->getTitle();
193 $this->guessedModel
= $rev->getContentModel();
198 if ( $params["{$prefix}title"] !== null ) {
199 $title = Title
::newFromText( $params["{$prefix}title"] );
200 if ( $title && !$title->isExternal() ) {
201 $this->guessedTitle
= $title;
206 if ( $params["{$prefix}id"] !== null ) {
207 $title = Title
::newFromID( $params["{$prefix}id"] );
209 $this->guessedTitle
= $title;
215 if ( !$this->guessedModel
) {
216 if ( $this->guessedTitle
) {
217 $this->guessedModel
= $this->guessedTitle
->getContentModel();
218 } elseif ( $params['fromcontentmodel'] !== null ) {
219 $this->guessedModel
= $params['fromcontentmodel'];
220 } elseif ( $params['tocontentmodel'] !== null ) {
221 $this->guessedModel
= $params['tocontentmodel'];
227 * Get the Revision and Content for one side of the diff
229 * This uses the appropriate set of 'rev', 'id', 'title', 'text', 'pst',
230 * 'contentmodel', and 'contentformat' parameters to determine what content
233 * Returns three values:
234 * - The revision used to retrieve the content, if any
235 * - The content to be diffed
236 * - The revision specified, if any, even if not used to retrieve the
239 * @param string $prefix 'from' or 'to'
240 * @param array $params
241 * @return array [ Revision|null, Content, Revision|null ]
243 private function getDiffContent( $prefix, array $params ) {
246 $suppliedContent = $params["{$prefix}text"] !== null;
248 // Get the revision and title, if applicable
250 if ( $params["{$prefix}rev"] !== null ) {
251 $revId = $params["{$prefix}rev"];
252 } elseif ( $params["{$prefix}title"] !== null ||
$params["{$prefix}id"] !== null ) {
253 if ( $params["{$prefix}title"] !== null ) {
254 $title = Title
::newFromText( $params["{$prefix}title"] );
255 if ( !$title ||
$title->isExternal() ) {
257 [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
261 $title = Title
::newFromID( $params["{$prefix}id"] );
263 $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
266 $revId = $title->getLatestRevID();
269 // Only die here if we're not using supplied text
270 if ( !$suppliedContent ) {
271 if ( $title->exists() ) {
273 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
277 [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
284 if ( $revId !== null ) {
285 $rev = Revision
::newFromId( $revId );
286 if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
287 // Try the 'archive' table
288 $row = $this->getDB()->selectRow(
291 Revision
::selectArchiveFields(),
292 [ 'ar_namespace', 'ar_title' ]
294 [ 'ar_rev_id' => $revId ],
298 $rev = Revision
::newFromArchiveRow( $row );
299 $rev->isArchive
= true;
303 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
305 $title = $rev->getTitle();
307 // If we don't have supplied content, return here. Otherwise,
308 // continue on below with the supplied content.
309 if ( !$suppliedContent ) {
310 $content = $rev->getContent( Revision
::FOR_THIS_USER
, $this->getUser() );
312 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' );
314 return [ $rev, $content, $rev ];
318 // Override $content based on supplied text
319 $model = $params["{$prefix}contentmodel"];
320 $format = $params["{$prefix}contentformat"];
322 if ( !$model && $rev ) {
323 $model = $rev->getContentModel();
325 if ( !$model && $title ) {
326 $model = $title->getContentModel();
329 $this->guessTitleAndModel();
330 $model = $this->guessedModel
;
333 $model = CONTENT_MODEL_WIKITEXT
;
334 $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
338 $this->guessTitleAndModel();
339 $title = $this->guessedTitle
;
343 $content = ContentHandler
::makeContent( $params["{$prefix}text"], $title, $model, $format );
344 } catch ( MWContentSerializationException
$ex ) {
345 $this->dieWithException( $ex, [
346 'wrap' => ApiMessage
::create( 'apierror-contentserializationexception', 'parseerror' )
350 if ( $params["{$prefix}pst"] ) {
352 $this->dieWithError( 'apierror-compare-no-title' );
354 $popts = ParserOptions
::newFromContext( $this->getContext() );
355 $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
358 return [ null, $content, $rev ];
362 * Set value fields from a Revision object
363 * @param array &$vals Result array to set data into
364 * @param string $prefix 'from' or 'to'
365 * @param Revision|null $rev
367 private function setVals( &$vals, $prefix, $rev ) {
369 $title = $rev->getTitle();
370 if ( isset( $this->props
['ids'] ) ) {
371 $vals["{$prefix}id"] = $title->getArticleId();
372 $vals["{$prefix}revid"] = $rev->getId();
374 if ( isset( $this->props
['title'] ) ) {
375 ApiQueryBase
::addTitleInfo( $vals, $title, $prefix );
377 if ( isset( $this->props
['size'] ) ) {
378 $vals["{$prefix}size"] = $rev->getSize();
382 if ( $rev->isDeleted( Revision
::DELETED_TEXT
) ) {
383 $vals["{$prefix}texthidden"] = true;
387 if ( $rev->isDeleted( Revision
::DELETED_USER
) ) {
388 $vals["{$prefix}userhidden"] = true;
391 if ( isset( $this->props
['user'] ) &&
392 $rev->userCan( Revision
::DELETED_USER
, $this->getUser() )
394 $vals["{$prefix}user"] = $rev->getUserText( Revision
::RAW
);
395 $vals["{$prefix}userid"] = $rev->getUser( Revision
::RAW
);
398 if ( $rev->isDeleted( Revision
::DELETED_COMMENT
) ) {
399 $vals["{$prefix}commenthidden"] = true;
402 if ( $rev->userCan( Revision
::DELETED_COMMENT
, $this->getUser() ) ) {
403 if ( isset( $this->props
['comment'] ) ) {
404 $vals["{$prefix}comment"] = $rev->getComment( Revision
::RAW
);
406 if ( isset( $this->props
['parsedcomment'] ) ) {
407 $vals["{$prefix}parsedcomment"] = Linker
::formatComment(
408 $rev->getComment( Revision
::RAW
),
415 $this->getMain()->setCacheMode( 'private' );
416 if ( $rev->isDeleted( Revision
::DELETED_RESTRICTED
) ) {
417 $vals["{$prefix}suppressed"] = true;
421 if ( !empty( $rev->isArchive
) ) {
422 $this->getMain()->setCacheMode( 'private' );
423 $vals["{$prefix}archive"] = true;
428 public function getAllowedParams() {
429 // Parameters for the 'from' and 'to' content
433 ApiBase
::PARAM_TYPE
=> 'integer'
436 ApiBase
::PARAM_TYPE
=> 'integer'
439 ApiBase
::PARAM_TYPE
=> 'text'
443 ApiBase
::PARAM_TYPE
=> ContentHandler
::getAllContentFormats(),
446 ApiBase
::PARAM_TYPE
=> ContentHandler
::getContentModels(),
451 foreach ( $fromToParams as $k => $v ) {
454 foreach ( $fromToParams as $k => $v ) {
458 $ret = wfArrayInsertAfter(
460 [ 'torelative' => [ ApiBase
::PARAM_TYPE
=> [ 'prev', 'next', 'cur' ], ] ],
465 ApiBase
::PARAM_DFLT
=> 'diff|ids|title',
466 ApiBase
::PARAM_TYPE
=> [
477 ApiBase
::PARAM_ISMULTI
=> true,
478 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [],
484 protected function getExamplesMessages() {
486 'action=compare&fromrev=1&torev=2'
487 => 'apihelp-compare-example-1',