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