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