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 $this->getResult()->addValue( null, $this->getModuleName(), $vals );
174 * Guess an appropriate default Title and content model for this request
176 * Fills in $this->guessedTitle based on the first of 'fromrev',
177 * 'fromtitle', 'fromid', 'torev', 'totitle', and 'toid' that's present and
180 * Fills in $this->guessedModel based on the Revision or Title used to
181 * determine $this->guessedTitle, or the 'fromcontentmodel' or
182 * 'tocontentmodel' parameters if no title was guessed.
184 private function guessTitleAndModel() {
185 if ( $this->guessed
) {
189 $this->guessed
= true;
190 $params = $this->extractRequestParams();
192 foreach ( [ 'from', 'to' ] as $prefix ) {
193 if ( $params["{$prefix}rev"] !== null ) {
194 $revId = $params["{$prefix}rev"];
195 $rev = Revision
::newFromId( $revId );
197 // Titles of deleted revisions aren't secret, per T51088
198 $arQuery = Revision
::getArchiveQueryInfo();
199 $row = $this->getDB()->selectRow(
203 [ 'ar_namespace', 'ar_title' ]
205 [ 'ar_rev_id' => $revId ],
211 $rev = Revision
::newFromArchiveRow( $row );
215 $this->guessedTitle
= $rev->getTitle();
216 $this->guessedModel
= $rev->getContentModel();
221 if ( $params["{$prefix}title"] !== null ) {
222 $title = Title
::newFromText( $params["{$prefix}title"] );
223 if ( $title && !$title->isExternal() ) {
224 $this->guessedTitle
= $title;
229 if ( $params["{$prefix}id"] !== null ) {
230 $title = Title
::newFromID( $params["{$prefix}id"] );
232 $this->guessedTitle
= $title;
238 if ( !$this->guessedModel
) {
239 if ( $this->guessedTitle
) {
240 $this->guessedModel
= $this->guessedTitle
->getContentModel();
241 } elseif ( $params['fromcontentmodel'] !== null ) {
242 $this->guessedModel
= $params['fromcontentmodel'];
243 } elseif ( $params['tocontentmodel'] !== null ) {
244 $this->guessedModel
= $params['tocontentmodel'];
250 * Get the Revision and Content for one side of the diff
252 * This uses the appropriate set of 'rev', 'id', 'title', 'text', 'pst',
253 * 'contentmodel', and 'contentformat' parameters to determine what content
256 * Returns three values:
257 * - The revision used to retrieve the content, if any
258 * - The content to be diffed
259 * - The revision specified, if any, even if not used to retrieve the
262 * @param string $prefix 'from' or 'to'
263 * @param array $params
264 * @return array [ Revision|null, Content, Revision|null ]
266 private function getDiffContent( $prefix, array $params ) {
269 $suppliedContent = $params["{$prefix}text"] !== null;
271 // Get the revision and title, if applicable
273 if ( $params["{$prefix}rev"] !== null ) {
274 $revId = $params["{$prefix}rev"];
275 } elseif ( $params["{$prefix}title"] !== null ||
$params["{$prefix}id"] !== null ) {
276 if ( $params["{$prefix}title"] !== null ) {
277 $title = Title
::newFromText( $params["{$prefix}title"] );
278 if ( !$title ||
$title->isExternal() ) {
280 [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
284 $title = Title
::newFromID( $params["{$prefix}id"] );
286 $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
289 $revId = $title->getLatestRevID();
292 // Only die here if we're not using supplied text
293 if ( !$suppliedContent ) {
294 if ( $title->exists() ) {
296 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
300 [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
307 if ( $revId !== null ) {
308 $rev = Revision
::newFromId( $revId );
309 if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
310 // Try the 'archive' table
311 $arQuery = Revision
::getArchiveQueryInfo();
312 $row = $this->getDB()->selectRow(
316 [ 'ar_namespace', 'ar_title' ]
318 [ 'ar_rev_id' => $revId ],
324 $rev = Revision
::newFromArchiveRow( $row );
325 $rev->isArchive
= true;
329 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
331 $title = $rev->getTitle();
333 // If we don't have supplied content, return here. Otherwise,
334 // continue on below with the supplied content.
335 if ( !$suppliedContent ) {
336 $content = $rev->getContent( Revision
::FOR_THIS_USER
, $this->getUser() );
338 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' );
340 return [ $rev, $content, $rev ];
344 // Override $content based on supplied text
345 $model = $params["{$prefix}contentmodel"];
346 $format = $params["{$prefix}contentformat"];
348 if ( !$model && $rev ) {
349 $model = $rev->getContentModel();
351 if ( !$model && $title ) {
352 $model = $title->getContentModel();
355 $this->guessTitleAndModel();
356 $model = $this->guessedModel
;
359 $model = CONTENT_MODEL_WIKITEXT
;
360 $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
364 $this->guessTitleAndModel();
365 $title = $this->guessedTitle
;
369 $content = ContentHandler
::makeContent( $params["{$prefix}text"], $title, $model, $format );
370 } catch ( MWContentSerializationException
$ex ) {
371 $this->dieWithException( $ex, [
372 'wrap' => ApiMessage
::create( 'apierror-contentserializationexception', 'parseerror' )
376 if ( $params["{$prefix}pst"] ) {
378 $this->dieWithError( 'apierror-compare-no-title' );
380 $popts = ParserOptions
::newFromContext( $this->getContext() );
381 $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
384 return [ null, $content, $rev ];
388 * Set value fields from a Revision object
389 * @param array &$vals Result array to set data into
390 * @param string $prefix 'from' or 'to'
391 * @param Revision|null $rev
393 private function setVals( &$vals, $prefix, $rev ) {
395 $title = $rev->getTitle();
396 if ( isset( $this->props
['ids'] ) ) {
397 $vals["{$prefix}id"] = $title->getArticleId();
398 $vals["{$prefix}revid"] = $rev->getId();
400 if ( isset( $this->props
['title'] ) ) {
401 ApiQueryBase
::addTitleInfo( $vals, $title, $prefix );
403 if ( isset( $this->props
['size'] ) ) {
404 $vals["{$prefix}size"] = $rev->getSize();
408 if ( $rev->isDeleted( Revision
::DELETED_TEXT
) ) {
409 $vals["{$prefix}texthidden"] = true;
413 if ( $rev->isDeleted( Revision
::DELETED_USER
) ) {
414 $vals["{$prefix}userhidden"] = true;
417 if ( isset( $this->props
['user'] ) &&
418 $rev->userCan( Revision
::DELETED_USER
, $this->getUser() )
420 $vals["{$prefix}user"] = $rev->getUserText( Revision
::RAW
);
421 $vals["{$prefix}userid"] = $rev->getUser( Revision
::RAW
);
424 if ( $rev->isDeleted( Revision
::DELETED_COMMENT
) ) {
425 $vals["{$prefix}commenthidden"] = true;
428 if ( $rev->userCan( Revision
::DELETED_COMMENT
, $this->getUser() ) ) {
429 if ( isset( $this->props
['comment'] ) ) {
430 $vals["{$prefix}comment"] = $rev->getComment( Revision
::RAW
);
432 if ( isset( $this->props
['parsedcomment'] ) ) {
433 $vals["{$prefix}parsedcomment"] = Linker
::formatComment(
434 $rev->getComment( Revision
::RAW
),
441 $this->getMain()->setCacheMode( 'private' );
442 if ( $rev->isDeleted( Revision
::DELETED_RESTRICTED
) ) {
443 $vals["{$prefix}suppressed"] = true;
447 if ( !empty( $rev->isArchive
) ) {
448 $this->getMain()->setCacheMode( 'private' );
449 $vals["{$prefix}archive"] = true;
454 public function getAllowedParams() {
455 // Parameters for the 'from' and 'to' content
459 ApiBase
::PARAM_TYPE
=> 'integer'
462 ApiBase
::PARAM_TYPE
=> 'integer'
465 ApiBase
::PARAM_TYPE
=> 'text'
470 ApiBase
::PARAM_TYPE
=> ContentHandler
::getAllContentFormats(),
473 ApiBase
::PARAM_TYPE
=> ContentHandler
::getContentModels(),
478 foreach ( $fromToParams as $k => $v ) {
481 foreach ( $fromToParams as $k => $v ) {
485 $ret = wfArrayInsertAfter(
487 [ 'torelative' => [ ApiBase
::PARAM_TYPE
=> [ 'prev', 'next', 'cur' ], ] ],
492 ApiBase
::PARAM_DFLT
=> 'diff|ids|title',
493 ApiBase
::PARAM_TYPE
=> [
504 ApiBase
::PARAM_ISMULTI
=> true,
505 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [],
511 protected function getExamplesMessages() {
513 'action=compare&fromrev=1&torev=2'
514 => 'apihelp-compare-example-1',