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