MediaWiki REL1_30
ApiComparePages.php
Go to the documentation of this file.
1<?php
22class ApiComparePages extends ApiBase {
23
25
26 public function execute() {
28
29 // Parameter validation
30 $this->requireAtLeastOneParameter( $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext' );
31 $this->requireAtLeastOneParameter( $params, 'totitle', 'toid', 'torev', 'totext', 'torelative' );
32
33 $this->props = array_flip( $params['prop'] );
34
35 // Cache responses publicly by default. This may be overridden later.
36 $this->getMain()->setCacheMode( 'public' );
37
38 // Get the 'from' Revision and Content
39 list( $fromRev, $fromContent, $relRev ) = $this->getDiffContent( 'from', $params );
40
41 // Get the 'to' Revision and Content
42 if ( $params['torelative'] !== null ) {
43 if ( !$relRev ) {
44 $this->dieWithError( 'apierror-compare-relative-to-nothing' );
45 }
46 switch ( $params['torelative'] ) {
47 case 'prev':
48 // Swap 'from' and 'to'
49 $toRev = $fromRev;
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 ) {
56 $this->dieWithError(
57 [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent'
58 );
59 }
60 break;
61
62 case 'next':
63 $toRev = $relRev->getNext();
64 $toContent = $toRev
65 ? $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
66 : $fromContent;
67 if ( !$toContent ) {
68 $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
69 }
70 break;
71
72 case 'cur':
73 $title = $relRev->getTitle();
74 $id = $title->getLatestRevID();
75 $toRev = $id ? Revision::newFromId( $id ) : null;
76 if ( !$toRev ) {
77 $this->dieWithError(
78 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
79 );
80 }
81 $toContent = $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
82 if ( !$toContent ) {
83 $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
84 }
85 break;
86 }
87 $relRev2 = null;
88 } else {
89 list( $toRev, $toContent, $relRev2 ) = $this->getDiffContent( 'to', $params );
90 }
91
92 // Should never happen, but just in case...
93 if ( !$fromContent || !$toContent ) {
94 $this->dieWithError( 'apierror-baddiff' );
95 }
96
97 // Get the diff
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() );
103 } else {
104 $this->guessTitleAndModel();
105 if ( $this->guessedTitle ) {
106 $context->setTitle( $this->guessedTitle );
107 }
108 }
109 $de = $fromContent->getContentHandler()->createDifferenceEngine(
110 $context,
111 $fromRev ? $fromRev->getId() : 0,
112 $toRev ? $toRev->getId() : 0,
113 /* $rcid = */ null,
114 /* $refreshCache = */ false,
115 /* $unhide = */ true
116 );
117 $de->setContent( $fromContent, $toContent );
118 $difftext = $de->getDiffBody();
119 if ( $difftext === false ) {
120 $this->dieWithError( 'apierror-baddiff' );
121 }
122
123 // Fill in the response
124 $vals = [];
125 $this->setVals( $vals, 'from', $fromRev );
126 $this->setVals( $vals, 'to', $toRev );
127
128 if ( isset( $this->props['rel'] ) ) {
129 if ( $fromRev ) {
130 $rev = $fromRev->getPrevious();
131 if ( $rev ) {
132 $vals['prev'] = $rev->getId();
133 }
134 }
135 if ( $toRev ) {
136 $rev = $toRev->getNext();
137 if ( $rev ) {
138 $vals['next'] = $rev->getId();
139 }
140 }
141 }
142
143 if ( isset( $this->props['diffsize'] ) ) {
144 $vals['diffsize'] = strlen( $difftext );
145 }
146 if ( isset( $this->props['diff'] ) ) {
147 ApiResult::setContentValue( $vals, 'body', $difftext );
148 }
149
150 $this->getResult()->addValue( null, $this->getModuleName(), $vals );
151 }
152
164 private function guessTitleAndModel() {
165 if ( $this->guessed ) {
166 return;
167 }
168
169 $this->guessed = true;
170 $params = $this->extractRequestParams();
171
172 foreach ( [ 'from', 'to' ] as $prefix ) {
173 if ( $params["{$prefix}rev"] !== null ) {
174 $revId = $params["{$prefix}rev"];
175 $rev = Revision::newFromId( $revId );
176 if ( !$rev ) {
177 // Titles of deleted revisions aren't secret, per T51088
178 $row = $this->getDB()->selectRow(
179 'archive',
180 array_merge(
182 [ 'ar_namespace', 'ar_title' ]
183 ),
184 [ 'ar_rev_id' => $revId ],
185 __METHOD__
186 );
187 if ( $row ) {
189 }
190 }
191 if ( $rev ) {
192 $this->guessedTitle = $rev->getTitle();
193 $this->guessedModel = $rev->getContentModel();
194 break;
195 }
196 }
197
198 if ( $params["{$prefix}title"] !== null ) {
199 $title = Title::newFromText( $params["{$prefix}title"] );
200 if ( $title && !$title->isExternal() ) {
201 $this->guessedTitle = $title;
202 break;
203 }
204 }
205
206 if ( $params["{$prefix}id"] !== null ) {
207 $title = Title::newFromID( $params["{$prefix}id"] );
208 if ( $title ) {
209 $this->guessedTitle = $title;
210 break;
211 }
212 }
213 }
214
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'];
222 }
223 }
224 }
225
243 private function getDiffContent( $prefix, array $params ) {
244 $title = null;
245 $rev = null;
246 $suppliedContent = $params["{$prefix}text"] !== null;
247
248 // Get the revision and title, if applicable
249 $revId = null;
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() ) {
256 $this->dieWithError(
257 [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
258 );
259 }
260 } else {
261 $title = Title::newFromID( $params["{$prefix}id"] );
262 if ( !$title ) {
263 $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
264 }
265 }
266 $revId = $title->getLatestRevID();
267 if ( !$revId ) {
268 $revId = null;
269 // Only die here if we're not using supplied text
270 if ( !$suppliedContent ) {
271 if ( $title->exists() ) {
272 $this->dieWithError(
273 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
274 );
275 } else {
276 $this->dieWithError(
277 [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
278 'missingtitle'
279 );
280 }
281 }
282 }
283 }
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(
289 'archive',
290 array_merge(
292 [ 'ar_namespace', 'ar_title' ]
293 ),
294 [ 'ar_rev_id' => $revId ],
295 __METHOD__
296 );
297 if ( $row ) {
299 $rev->isArchive = true;
300 }
301 }
302 if ( !$rev ) {
303 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
304 }
305 $title = $rev->getTitle();
306
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() );
311 if ( !$content ) {
312 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' );
313 }
314 return [ $rev, $content, $rev ];
315 }
316 }
317
318 // Override $content based on supplied text
319 $model = $params["{$prefix}contentmodel"];
320 $format = $params["{$prefix}contentformat"];
321
322 if ( !$model && $rev ) {
323 $model = $rev->getContentModel();
324 }
325 if ( !$model && $title ) {
326 $model = $title->getContentModel();
327 }
328 if ( !$model ) {
329 $this->guessTitleAndModel();
330 $model = $this->guessedModel;
331 }
332 if ( !$model ) {
333 $model = CONTENT_MODEL_WIKITEXT;
334 $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
335 }
336
337 if ( !$title ) {
338 $this->guessTitleAndModel();
339 $title = $this->guessedTitle;
340 }
341
342 try {
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' )
347 ] );
348 }
349
350 if ( $params["{$prefix}pst"] ) {
351 if ( !$title ) {
352 $this->dieWithError( 'apierror-compare-no-title' );
353 }
354 $popts = ParserOptions::newFromContext( $this->getContext() );
355 $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
356 }
357
358 return [ null, $content, $rev ];
359 }
360
367 private function setVals( &$vals, $prefix, $rev ) {
368 if ( $rev ) {
369 $title = $rev->getTitle();
370 if ( isset( $this->props['ids'] ) ) {
371 $vals["{$prefix}id"] = $title->getArticleId();
372 $vals["{$prefix}revid"] = $rev->getId();
373 }
374 if ( isset( $this->props['title'] ) ) {
375 ApiQueryBase::addTitleInfo( $vals, $title, $prefix );
376 }
377 if ( isset( $this->props['size'] ) ) {
378 $vals["{$prefix}size"] = $rev->getSize();
379 }
380
381 $anyHidden = false;
382 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
383 $vals["{$prefix}texthidden"] = true;
384 $anyHidden = true;
385 }
386
387 if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
388 $vals["{$prefix}userhidden"] = true;
389 $anyHidden = true;
390 }
391 if ( isset( $this->props['user'] ) &&
392 $rev->userCan( Revision::DELETED_USER, $this->getUser() )
393 ) {
394 $vals["{$prefix}user"] = $rev->getUserText( Revision::RAW );
395 $vals["{$prefix}userid"] = $rev->getUser( Revision::RAW );
396 }
397
398 if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
399 $vals["{$prefix}commenthidden"] = true;
400 $anyHidden = true;
401 }
402 if ( $rev->userCan( Revision::DELETED_COMMENT, $this->getUser() ) ) {
403 if ( isset( $this->props['comment'] ) ) {
404 $vals["{$prefix}comment"] = $rev->getComment( Revision::RAW );
405 }
406 if ( isset( $this->props['parsedcomment'] ) ) {
407 $vals["{$prefix}parsedcomment"] = Linker::formatComment(
408 $rev->getComment( Revision::RAW ),
409 $rev->getTitle()
410 );
411 }
412 }
413
414 if ( $anyHidden ) {
415 $this->getMain()->setCacheMode( 'private' );
416 if ( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
417 $vals["{$prefix}suppressed"] = true;
418 }
419 }
420
421 if ( !empty( $rev->isArchive ) ) {
422 $this->getMain()->setCacheMode( 'private' );
423 $vals["{$prefix}archive"] = true;
424 }
425 }
426 }
427
428 public function getAllowedParams() {
429 // Parameters for the 'from' and 'to' content
430 $fromToParams = [
431 'title' => null,
432 'id' => [
433 ApiBase::PARAM_TYPE => 'integer'
434 ],
435 'rev' => [
436 ApiBase::PARAM_TYPE => 'integer'
437 ],
438 'text' => [
439 ApiBase::PARAM_TYPE => 'text'
440 ],
441 'pst' => false,
442 'contentformat' => [
444 ],
445 'contentmodel' => [
447 ]
448 ];
449
450 $ret = [];
451 foreach ( $fromToParams as $k => $v ) {
452 $ret["from$k"] = $v;
453 }
454 foreach ( $fromToParams as $k => $v ) {
455 $ret["to$k"] = $v;
456 }
457
459 $ret,
460 [ 'torelative' => [ ApiBase::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
461 'torev'
462 );
463
464 $ret['prop'] = [
465 ApiBase::PARAM_DFLT => 'diff|ids|title',
467 'diff',
468 'diffsize',
469 'rel',
470 'ids',
471 'title',
472 'user',
473 'comment',
474 'parsedcomment',
475 'size',
476 ],
479 ];
480
481 return $ret;
482 }
483
484 protected function getExamplesMessages() {
485 return [
486 'action=compare&fromrev=1&torev=2'
487 => 'apihelp-compare-example-1',
488 ];
489 }
490}
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfArrayInsertAfter(array $array, array $insert, $after)
Insert array into another array after the specified KEY
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:41
getDB()
Gets a default replica DB connection object.
Definition ApiBase.php:660
dieWithError( $msg, $code=null, $data=null, $httpCode=null)
Abort execution with an error.
Definition ApiBase.php:1855
getMain()
Get the main module.
Definition ApiBase.php:528
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below.
Definition ApiBase.php:91
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition ApiBase.php:52
extractRequestParams( $parseLimit=true)
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:740
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition ApiBase.php:160
getResult()
Get the result object.
Definition ApiBase.php:632
dieWithException( $exception, array $options=[])
Abort execution with an error derived from an exception.
Definition ApiBase.php:1867
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition ApiBase.php:1779
requireAtLeastOneParameter( $params, $required)
Die if none of a certain set of parameters is set and not false.
Definition ApiBase.php:842
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:512
const PARAM_ISMULTI
(boolean) Accept multiple pipe-separated values for this parameter (e.g.
Definition ApiBase.php:55
guessTitleAndModel()
Guess an appropriate default Title and content model for this request.
getExamplesMessages()
Returns usage examples for this module.
setVals(&$vals, $prefix, $rev)
Set value fields from a Revision object.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
getDiffContent( $prefix, array $params)
Get the Revision and Content for one side of the diff.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
static setContentValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name and mark as META_CONTENT.
static getAllContentFormats()
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
static getContentModels()
getUser()
Get the User object.
IContextSource $context
getContext()
Get the base IContextSource object.
An IContextSource implementation which will inherit context from another source but allow individual ...
static formatComment( $comment, $title=null, $local=false, $wikiId=null)
This function is called by all recent changes variants, by the page history, and by the user contribu...
Definition Linker.php:1099
Exception representing a failure to serialize or unserialize a content object.
static selectArchiveFields()
Return the list of revision fields that should be selected to create a new revision from an archive r...
Definition Revision.php:486
static newFromArchiveRow( $row, $overrides=[])
Make a fake revision object from an archive table row.
Definition Revision.php:189
const DELETED_USER
Definition Revision.php:92
const DELETED_TEXT
Definition Revision.php:90
const DELETED_RESTRICTED
Definition Revision.php:93
const RAW
Definition Revision.php:100
const DELETED_COMMENT
Definition Revision.php:91
const FOR_THIS_USER
Definition Revision.php:99
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:116
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:962
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:1976
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:1975
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1760
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:236
$params