ApiComparePages: Don't try to find next/prev of a deleted revision
[lhc/web/wiklou.git] / includes / api / ApiComparePages.php
1 <?php
2 /**
3 *
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.
8 *
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.
13 *
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
18 *
19 * @file
20 */
21
22 use MediaWiki\MediaWikiServices;
23 use MediaWiki\Revision\MutableRevisionRecord;
24 use MediaWiki\Revision\RevisionRecord;
25 use MediaWiki\Revision\RevisionArchiveRecord;
26 use MediaWiki\Revision\RevisionStore;
27 use MediaWiki\Revision\SlotRecord;
28
29 class ApiComparePages extends ApiBase {
30
31 /** @var RevisionStore */
32 private $revisionStore;
33
34 private $guessedTitle = false, $props;
35
36 public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
37 parent::__construct( $mainModule, $moduleName, $modulePrefix );
38 $this->revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
39 }
40
41 public function execute() {
42 $params = $this->extractRequestParams();
43
44 // Parameter validation
45 $this->requireAtLeastOneParameter(
46 $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext', 'fromslots'
47 );
48 $this->requireAtLeastOneParameter(
49 $params, 'totitle', 'toid', 'torev', 'totext', 'torelative', 'toslots'
50 );
51
52 $this->props = array_flip( $params['prop'] );
53
54 // Cache responses publicly by default. This may be overridden later.
55 $this->getMain()->setCacheMode( 'public' );
56
57 // Get the 'from' RevisionRecord
58 list( $fromRev, $fromRelRev, $fromValsRev ) = $this->getDiffRevision( 'from', $params );
59
60 // Get the 'to' RevisionRecord
61 if ( $params['torelative'] !== null ) {
62 if ( !$fromRelRev ) {
63 $this->dieWithError( 'apierror-compare-relative-to-nothing' );
64 }
65 if ( $params['torelative'] !== 'cur' && $fromRelRev instanceof RevisionArchiveRecord ) {
66 // RevisionStore's getPreviousRevision/getNextRevision blow up
67 // when passed an RevisionArchiveRecord for a deleted page
68 $this->dieWithError( [ 'apierror-compare-relative-to-deleted', $params['torelative'] ] );
69 }
70 switch ( $params['torelative'] ) {
71 case 'prev':
72 // Swap 'from' and 'to'
73 list( $toRev, $toRelRev2, $toValsRev ) = [ $fromRev, $fromRelRev, $fromValsRev ];
74 $fromRev = $this->revisionStore->getPreviousRevision( $fromRelRev );
75 $fromRelRev = $fromRev;
76 $fromValsRev = $fromRev;
77 break;
78
79 case 'next':
80 $toRev = $this->revisionStore->getNextRevision( $fromRelRev );
81 $toRelRev = $toRev;
82 $toValsRev = $toRev;
83 break;
84
85 case 'cur':
86 $title = $fromRelRev->getPageAsLinkTarget();
87 $toRev = $this->revisionStore->getRevisionByTitle( $title );
88 if ( !$toRev ) {
89 $title = Title::newFromLinkTarget( $title );
90 $this->dieWithError(
91 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
92 );
93 }
94 $toRelRev = $toRev;
95 $toValsRev = $toRev;
96 break;
97 }
98 } else {
99 list( $toRev, $toRelRev, $toValsRev ) = $this->getDiffRevision( 'to', $params );
100 }
101
102 // Handle missing from or to revisions
103 if ( !$fromRev || !$toRev ) {
104 $this->dieWithError( 'apierror-baddiff' );
105 }
106
107 // Handle revdel
108 if ( !$fromRev->audienceCan(
109 RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_THIS_USER, $this->getUser()
110 ) ) {
111 $this->dieWithError( [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent' );
112 }
113 if ( !$toRev->audienceCan(
114 RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_THIS_USER, $this->getUser()
115 ) ) {
116 $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
117 }
118
119 // Get the diff
120 $context = new DerivativeContext( $this->getContext() );
121 if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) {
122 $context->setTitle( Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) );
123 } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
124 $context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) );
125 } else {
126 $guessedTitle = $this->guessTitle();
127 if ( $guessedTitle ) {
128 $context->setTitle( $guessedTitle );
129 }
130 }
131 $de = new DifferenceEngine( $context );
132 $de->setRevisions( $fromRev, $toRev );
133 if ( $params['slots'] === null ) {
134 $difftext = $de->getDiffBody();
135 if ( $difftext === false ) {
136 $this->dieWithError( 'apierror-baddiff' );
137 }
138 } else {
139 $difftext = [];
140 foreach ( $params['slots'] as $role ) {
141 $difftext[$role] = $de->getDiffBodyForRole( $role );
142 }
143 }
144
145 // Fill in the response
146 $vals = [];
147 $this->setVals( $vals, 'from', $fromValsRev );
148 $this->setVals( $vals, 'to', $toValsRev );
149
150 if ( isset( $this->props['rel'] ) ) {
151 if ( !$fromRev instanceof MutableRevisionRecord ) {
152 $rev = $this->revisionStore->getPreviousRevision( $fromRev );
153 if ( $rev ) {
154 $vals['prev'] = $rev->getId();
155 }
156 }
157 if ( !$toRev instanceof MutableRevisionRecord ) {
158 $rev = $this->revisionStore->getNextRevision( $toRev );
159 if ( $rev ) {
160 $vals['next'] = $rev->getId();
161 }
162 }
163 }
164
165 if ( isset( $this->props['diffsize'] ) ) {
166 $vals['diffsize'] = 0;
167 foreach ( (array)$difftext as $text ) {
168 $vals['diffsize'] += strlen( $text );
169 }
170 }
171 if ( isset( $this->props['diff'] ) ) {
172 if ( is_array( $difftext ) ) {
173 ApiResult::setArrayType( $difftext, 'kvp', 'diff' );
174 $vals['bodies'] = $difftext;
175 } else {
176 ApiResult::setContentValue( $vals, 'body', $difftext );
177 }
178 }
179
180 // Diffs can be really big and there's little point in having
181 // ApiResult truncate it to an empty response since the diff is the
182 // whole reason this module exists. So pass NO_SIZE_CHECK here.
183 $this->getResult()->addValue( null, $this->getModuleName(), $vals, ApiResult::NO_SIZE_CHECK );
184 }
185
186 /**
187 * Load a revision by ID
188 *
189 * Falls back to checking the archive table if appropriate.
190 *
191 * @param int $id
192 * @return RevisionRecord|null
193 */
194 private function getRevisionById( $id ) {
195 $rev = $this->revisionStore->getRevisionById( $id );
196 if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
197 // Try the 'archive' table
198 $arQuery = $this->revisionStore->getArchiveQueryInfo();
199 $row = $this->getDB()->selectRow(
200 $arQuery['tables'],
201 array_merge(
202 $arQuery['fields'],
203 [ 'ar_namespace', 'ar_title' ]
204 ),
205 [ 'ar_rev_id' => $id ],
206 __METHOD__,
207 [],
208 $arQuery['joins']
209 );
210 if ( $row ) {
211 $rev = $this->revisionStore->newRevisionFromArchiveRow( $row );
212 $rev->isArchive = true;
213 }
214 }
215 return $rev;
216 }
217
218 /**
219 * Guess an appropriate default Title for this request
220 *
221 * @return Title|null
222 */
223 private function guessTitle() {
224 if ( $this->guessedTitle !== false ) {
225 return $this->guessedTitle;
226 }
227
228 $this->guessedTitle = null;
229 $params = $this->extractRequestParams();
230
231 foreach ( [ 'from', 'to' ] as $prefix ) {
232 if ( $params["{$prefix}rev"] !== null ) {
233 $rev = $this->getRevisionById( $params["{$prefix}rev"] );
234 if ( $rev ) {
235 $this->guessedTitle = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
236 break;
237 }
238 }
239
240 if ( $params["{$prefix}title"] !== null ) {
241 $title = Title::newFromText( $params["{$prefix}title"] );
242 if ( $title && !$title->isExternal() ) {
243 $this->guessedTitle = $title;
244 break;
245 }
246 }
247
248 if ( $params["{$prefix}id"] !== null ) {
249 $title = Title::newFromID( $params["{$prefix}id"] );
250 if ( $title ) {
251 $this->guessedTitle = $title;
252 break;
253 }
254 }
255 }
256
257 return $this->guessedTitle;
258 }
259
260 /**
261 * Guess an appropriate default content model for this request
262 * @param string $role Slot for which to guess the model
263 * @return string|null Guessed content model
264 */
265 private function guessModel( $role ) {
266 $params = $this->extractRequestParams();
267
268 $title = null;
269 foreach ( [ 'from', 'to' ] as $prefix ) {
270 if ( $params["{$prefix}rev"] !== null ) {
271 $rev = $this->getRevisionById( $params["{$prefix}rev"] );
272 if ( $rev ) {
273 if ( $rev->hasSlot( $role ) ) {
274 return $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
275 }
276 }
277 }
278 }
279
280 $guessedTitle = $this->guessTitle();
281 if ( $guessedTitle && $role === SlotRecord::MAIN ) {
282 // @todo: Use SlotRoleRegistry and do this for all slots
283 return $guessedTitle->getContentModel();
284 }
285
286 if ( isset( $params["fromcontentmodel-$role"] ) ) {
287 return $params["fromcontentmodel-$role"];
288 }
289 if ( isset( $params["tocontentmodel-$role"] ) ) {
290 return $params["tocontentmodel-$role"];
291 }
292
293 if ( $role === SlotRecord::MAIN ) {
294 if ( isset( $params['fromcontentmodel'] ) ) {
295 return $params['fromcontentmodel'];
296 }
297 if ( isset( $params['tocontentmodel'] ) ) {
298 return $params['tocontentmodel'];
299 }
300 }
301
302 return null;
303 }
304
305 /**
306 * Get the RevisionRecord for one side of the diff
307 *
308 * This uses the appropriate set of parameters to determine what content
309 * should be diffed.
310 *
311 * Returns three values:
312 * - A RevisionRecord holding the content
313 * - The revision specified, if any, even if content was supplied
314 * - The revision to pass to setVals(), if any
315 *
316 * @param string $prefix 'from' or 'to'
317 * @param array $params
318 * @return array [ RevisionRecord|null, RevisionRecord|null, RevisionRecord|null ]
319 */
320 private function getDiffRevision( $prefix, array $params ) {
321 // Back compat params
322 $this->requireMaxOneParameter( $params, "{$prefix}text", "{$prefix}slots" );
323 $this->requireMaxOneParameter( $params, "{$prefix}section", "{$prefix}slots" );
324 if ( $params["{$prefix}text"] !== null ) {
325 $params["{$prefix}slots"] = [ SlotRecord::MAIN ];
326 $params["{$prefix}text-main"] = $params["{$prefix}text"];
327 $params["{$prefix}section-main"] = null;
328 $params["{$prefix}contentmodel-main"] = $params["{$prefix}contentmodel"];
329 $params["{$prefix}contentformat-main"] = $params["{$prefix}contentformat"];
330 }
331
332 $title = null;
333 $rev = null;
334 $suppliedContent = $params["{$prefix}slots"] !== null;
335
336 // Get the revision and title, if applicable
337 $revId = null;
338 if ( $params["{$prefix}rev"] !== null ) {
339 $revId = $params["{$prefix}rev"];
340 } elseif ( $params["{$prefix}title"] !== null || $params["{$prefix}id"] !== null ) {
341 if ( $params["{$prefix}title"] !== null ) {
342 $title = Title::newFromText( $params["{$prefix}title"] );
343 if ( !$title || $title->isExternal() ) {
344 $this->dieWithError(
345 [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
346 );
347 }
348 } else {
349 $title = Title::newFromID( $params["{$prefix}id"] );
350 if ( !$title ) {
351 $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
352 }
353 }
354 $revId = $title->getLatestRevID();
355 if ( !$revId ) {
356 $revId = null;
357 // Only die here if we're not using supplied text
358 if ( !$suppliedContent ) {
359 if ( $title->exists() ) {
360 $this->dieWithError(
361 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
362 );
363 } else {
364 $this->dieWithError(
365 [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
366 'missingtitle'
367 );
368 }
369 }
370 }
371 }
372 if ( $revId !== null ) {
373 $rev = $this->getRevisionById( $revId );
374 if ( !$rev ) {
375 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
376 }
377 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
378
379 // If we don't have supplied content, return here. Otherwise,
380 // continue on below with the supplied content.
381 if ( !$suppliedContent ) {
382 $newRev = $rev;
383
384 // Deprecated 'fromsection'/'tosection'
385 if ( isset( $params["{$prefix}section"] ) ) {
386 $section = $params["{$prefix}section"];
387 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
388 $content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
389 $this->getUser() );
390 if ( !$content ) {
391 $this->dieWithError(
392 [ 'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ], 'missingcontent'
393 );
394 }
395 $content = $content ? $content->getSection( $section ) : null;
396 if ( !$content ) {
397 $this->dieWithError(
398 [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
399 "nosuch{$prefix}section"
400 );
401 }
402 $newRev->setContent( SlotRecord::MAIN, $content );
403 }
404
405 return [ $newRev, $rev, $rev ];
406 }
407 }
408
409 // Override $content based on supplied text
410 if ( !$title ) {
411 $title = $this->guessTitle();
412 }
413 if ( $rev ) {
414 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
415 } else {
416 $newRev = $this->revisionStore->newMutableRevisionFromArray( [
417 'title' => $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . __METHOD__ )
418 ] );
419 }
420 foreach ( $params["{$prefix}slots"] as $role ) {
421 $text = $params["{$prefix}text-{$role}"];
422 if ( $text === null ) {
423 // The SlotRecord::MAIN role can't be deleted
424 if ( $role === SlotRecord::MAIN ) {
425 $this->dieWithError( [ 'apierror-compare-maintextrequired', $prefix ] );
426 }
427
428 // These parameters make no sense without text. Reject them to avoid
429 // confusion.
430 foreach ( [ 'section', 'contentmodel', 'contentformat' ] as $param ) {
431 if ( isset( $params["{$prefix}{$param}-{$role}"] ) ) {
432 $this->dieWithError( [
433 'apierror-compare-notext',
434 wfEscapeWikiText( "{$prefix}{$param}-{$role}" ),
435 wfEscapeWikiText( "{$prefix}text-{$role}" ),
436 ] );
437 }
438 }
439
440 $newRev->removeSlot( $role );
441 continue;
442 }
443
444 $model = $params["{$prefix}contentmodel-{$role}"];
445 $format = $params["{$prefix}contentformat-{$role}"];
446
447 if ( !$model && $rev && $rev->hasSlot( $role ) ) {
448 $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
449 }
450 if ( !$model && $title && $role === SlotRecord::MAIN ) {
451 // @todo: Use SlotRoleRegistry and do this for all slots
452 $model = $title->getContentModel();
453 }
454 if ( !$model ) {
455 $model = $this->guessModel( $role );
456 }
457 if ( !$model ) {
458 $model = CONTENT_MODEL_WIKITEXT;
459 $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
460 }
461
462 try {
463 $content = ContentHandler::makeContent( $text, $title, $model, $format );
464 } catch ( MWContentSerializationException $ex ) {
465 $this->dieWithException( $ex, [
466 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
467 ] );
468 }
469
470 if ( $params["{$prefix}pst"] ) {
471 if ( !$title ) {
472 $this->dieWithError( 'apierror-compare-no-title' );
473 }
474 $popts = ParserOptions::newFromContext( $this->getContext() );
475 $content = $content->preSaveTransform( $title, $this->getUser(), $popts );
476 }
477
478 $section = $params["{$prefix}section-{$role}"];
479 if ( $section !== null && $section !== '' ) {
480 if ( !$rev ) {
481 $this->dieWithError( "apierror-compare-no{$prefix}revision" );
482 }
483 $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->getUser() );
484 if ( !$oldContent ) {
485 $this->dieWithError(
486 [ 'apierror-missingcontent-revid-role', $rev->getId(), wfEscapeWikiText( $role ) ],
487 'missingcontent'
488 );
489 }
490 if ( !$oldContent->getContentHandler()->supportsSections() ) {
491 $this->dieWithError( [ 'apierror-sectionsnotsupported', $content->getModel() ] );
492 }
493 try {
494 $content = $oldContent->replaceSection( $section, $content, '' );
495 } catch ( Exception $ex ) {
496 // Probably a content model mismatch.
497 $content = null;
498 }
499 if ( !$content ) {
500 $this->dieWithError( [ 'apierror-sectionreplacefailed' ] );
501 }
502 }
503
504 // Deprecated 'fromsection'/'tosection'
505 if ( $role === SlotRecord::MAIN && isset( $params["{$prefix}section"] ) ) {
506 $section = $params["{$prefix}section"];
507 $content = $content->getSection( $section );
508 if ( !$content ) {
509 $this->dieWithError(
510 [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
511 "nosuch{$prefix}section"
512 );
513 }
514 }
515
516 $newRev->setContent( $role, $content );
517 }
518 return [ $newRev, $rev, null ];
519 }
520
521 /**
522 * Set value fields from a RevisionRecord object
523 *
524 * @param array &$vals Result array to set data into
525 * @param string $prefix 'from' or 'to'
526 * @param RevisionRecord|null $rev
527 */
528 private function setVals( &$vals, $prefix, $rev ) {
529 if ( $rev ) {
530 $title = $rev->getPageAsLinkTarget();
531 if ( isset( $this->props['ids'] ) ) {
532 $vals["{$prefix}id"] = $title->getArticleID();
533 $vals["{$prefix}revid"] = $rev->getId();
534 }
535 if ( isset( $this->props['title'] ) ) {
536 ApiQueryBase::addTitleInfo( $vals, $title, $prefix );
537 }
538 if ( isset( $this->props['size'] ) ) {
539 $vals["{$prefix}size"] = $rev->getSize();
540 }
541
542 $anyHidden = false;
543 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
544 $vals["{$prefix}texthidden"] = true;
545 $anyHidden = true;
546 }
547
548 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
549 $vals["{$prefix}userhidden"] = true;
550 $anyHidden = true;
551 }
552 if ( isset( $this->props['user'] ) ) {
553 $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->getUser() );
554 if ( $user ) {
555 $vals["{$prefix}user"] = $user->getName();
556 $vals["{$prefix}userid"] = $user->getId();
557 }
558 }
559
560 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
561 $vals["{$prefix}commenthidden"] = true;
562 $anyHidden = true;
563 }
564 if ( isset( $this->props['comment'] ) || isset( $this->props['parsedcomment'] ) ) {
565 $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->getUser() );
566 if ( $comment !== null ) {
567 if ( isset( $this->props['comment'] ) ) {
568 $vals["{$prefix}comment"] = $comment->text;
569 }
570 $vals["{$prefix}parsedcomment"] = Linker::formatComment(
571 $comment->text, Title::newFromLinkTarget( $title )
572 );
573 }
574 }
575
576 if ( $anyHidden ) {
577 $this->getMain()->setCacheMode( 'private' );
578 if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
579 $vals["{$prefix}suppressed"] = true;
580 }
581 }
582
583 if ( !empty( $rev->isArchive ) ) {
584 $this->getMain()->setCacheMode( 'private' );
585 $vals["{$prefix}archive"] = true;
586 }
587 }
588 }
589
590 public function getAllowedParams() {
591 $slotRoles = MediaWikiServices::getInstance()->getSlotRoleStore()->getMap();
592 if ( !in_array( SlotRecord::MAIN, $slotRoles, true ) ) {
593 $slotRoles[] = SlotRecord::MAIN;
594 }
595 sort( $slotRoles, SORT_STRING );
596
597 // Parameters for the 'from' and 'to' content
598 $fromToParams = [
599 'title' => null,
600 'id' => [
601 ApiBase::PARAM_TYPE => 'integer'
602 ],
603 'rev' => [
604 ApiBase::PARAM_TYPE => 'integer'
605 ],
606
607 'slots' => [
608 ApiBase::PARAM_TYPE => $slotRoles,
609 ApiBase::PARAM_ISMULTI => true,
610 ],
611 'text-{slot}' => [
612 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
613 ApiBase::PARAM_TYPE => 'text',
614 ],
615 'section-{slot}' => [
616 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
617 ApiBase::PARAM_TYPE => 'string',
618 ],
619 'contentformat-{slot}' => [
620 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
621 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
622 ],
623 'contentmodel-{slot}' => [
624 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
625 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
626 ],
627 'pst' => false,
628
629 'text' => [
630 ApiBase::PARAM_TYPE => 'text',
631 ApiBase::PARAM_DEPRECATED => true,
632 ],
633 'contentformat' => [
634 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
635 ApiBase::PARAM_DEPRECATED => true,
636 ],
637 'contentmodel' => [
638 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
639 ApiBase::PARAM_DEPRECATED => true,
640 ],
641 'section' => [
642 ApiBase::PARAM_DFLT => null,
643 ApiBase::PARAM_DEPRECATED => true,
644 ],
645 ];
646
647 $ret = [];
648 foreach ( $fromToParams as $k => $v ) {
649 if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
650 $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'fromslots';
651 }
652 $ret["from$k"] = $v;
653 }
654 foreach ( $fromToParams as $k => $v ) {
655 if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
656 $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'toslots';
657 }
658 $ret["to$k"] = $v;
659 }
660
661 $ret = wfArrayInsertAfter(
662 $ret,
663 [ 'torelative' => [ ApiBase::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
664 'torev'
665 );
666
667 $ret['prop'] = [
668 ApiBase::PARAM_DFLT => 'diff|ids|title',
669 ApiBase::PARAM_TYPE => [
670 'diff',
671 'diffsize',
672 'rel',
673 'ids',
674 'title',
675 'user',
676 'comment',
677 'parsedcomment',
678 'size',
679 ],
680 ApiBase::PARAM_ISMULTI => true,
681 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
682 ];
683
684 $ret['slots'] = [
685 ApiBase::PARAM_TYPE => $slotRoles,
686 ApiBase::PARAM_ISMULTI => true,
687 ApiBase::PARAM_ALL => true,
688 ];
689
690 return $ret;
691 }
692
693 protected function getExamplesMessages() {
694 return [
695 'action=compare&fromrev=1&torev=2'
696 => 'apihelp-compare-example-1',
697 ];
698 }
699 }