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',