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