MediaWiki master
ApiComparePages.php
Go to the documentation of this file.
1<?php
35use Wikimedia\RequestTimeout\TimeoutException;
36
40class ApiComparePages extends ApiBase {
41
42 private RevisionStore $revisionStore;
43 private ArchivedRevisionLookup $archivedRevisionLookup;
44 private SlotRoleRegistry $slotRoleRegistry;
45
47 private $guessedTitle = false;
48 private $props;
49
50 private IContentHandlerFactory $contentHandlerFactory;
51 private ContentTransformer $contentTransformer;
52 private CommentFormatter $commentFormatter;
53 private TempUserCreator $tempUserCreator;
54 private UserFactory $userFactory;
55 private DifferenceEngine $differenceEngine;
56
69 public function __construct(
70 ApiMain $mainModule,
71 $moduleName,
72 RevisionStore $revisionStore,
73 ArchivedRevisionLookup $archivedRevisionLookup,
74 SlotRoleRegistry $slotRoleRegistry,
75 IContentHandlerFactory $contentHandlerFactory,
76 ContentTransformer $contentTransformer,
77 CommentFormatter $commentFormatter,
78 TempUserCreator $tempUserCreator,
79 UserFactory $userFactory
80 ) {
81 parent::__construct( $mainModule, $moduleName );
82 $this->revisionStore = $revisionStore;
83 $this->archivedRevisionLookup = $archivedRevisionLookup;
84 $this->slotRoleRegistry = $slotRoleRegistry;
85 $this->contentHandlerFactory = $contentHandlerFactory;
86 $this->contentTransformer = $contentTransformer;
87 $this->commentFormatter = $commentFormatter;
88 $this->tempUserCreator = $tempUserCreator;
89 $this->userFactory = $userFactory;
90 $this->differenceEngine = new DifferenceEngine;
91 }
92
93 public function execute() {
94 $params = $this->extractRequestParams();
95
96 // Parameter validation
98 $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext', 'fromslots'
99 );
101 $params, 'totitle', 'toid', 'torev', 'totext', 'torelative', 'toslots'
102 );
103
104 $this->props = array_fill_keys( $params['prop'], true );
105
106 // Cache responses publicly by default. This may be overridden later.
107 $this->getMain()->setCacheMode( 'public' );
108
109 // Get the 'from' RevisionRecord
110 [ $fromRev, $fromRelRev, $fromValsRev ] = $this->getDiffRevision( 'from', $params );
111
112 // Get the 'to' RevisionRecord
113 if ( $params['torelative'] !== null ) {
114 if ( !$fromRelRev ) {
115 $this->dieWithError( 'apierror-compare-relative-to-nothing' );
116 }
117 if ( $params['torelative'] !== 'cur' && $fromRelRev instanceof RevisionArchiveRecord ) {
118 // RevisionStore's getPreviousRevision/getNextRevision blow up
119 // when passed an RevisionArchiveRecord for a deleted page
120 $this->dieWithError( [ 'apierror-compare-relative-to-deleted', $params['torelative'] ] );
121 }
122 switch ( $params['torelative'] ) {
123 case 'prev':
124 // Swap 'from' and 'to'
125 [ $toRev, $toRelRev, $toValsRev ] = [ $fromRev, $fromRelRev, $fromValsRev ];
126 $fromRev = $this->revisionStore->getPreviousRevision( $toRelRev );
127 $fromRelRev = $fromRev;
128 $fromValsRev = $fromRev;
129 if ( !$fromRev ) {
130 $title = Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() );
131 $this->addWarning( [
132 'apiwarn-compare-no-prev',
133 wfEscapeWikiText( $title->getPrefixedText() ),
134 $toRelRev->getId()
135 ] );
136
137 // (T203433) Create an empty dummy revision as the "previous".
138 // The main slot has to exist, the rest will be handled by DifferenceEngine.
139 $fromRev = new MutableRevisionRecord(
140 $title ?: $toRev->getPage()
141 );
142 $fromRev->setContent(
143 SlotRecord::MAIN,
144 $toRelRev->getContent( SlotRecord::MAIN, RevisionRecord::RAW )
145 ->getContentHandler()
146 ->makeEmptyContent()
147 );
148 }
149 break;
150
151 case 'next':
152 $toRev = $this->revisionStore->getNextRevision( $fromRelRev );
153 $toRelRev = $toRev;
154 $toValsRev = $toRev;
155 if ( !$toRev ) {
156 $title = Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() );
157 $this->addWarning( [
158 'apiwarn-compare-no-next',
159 wfEscapeWikiText( $title->getPrefixedText() ),
160 $fromRelRev->getId()
161 ] );
162
163 // (T203433) The web UI treats "next" as "cur" in this case.
164 // Avoid repeating metadata by making a MutableRevisionRecord with no changes.
165 $toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev );
166 }
167 break;
168
169 case 'cur':
170 $title = $fromRelRev->getPageAsLinkTarget();
171 $toRev = $this->revisionStore->getRevisionByTitle( $title );
172 if ( !$toRev ) {
173 $title = Title::newFromLinkTarget( $title );
174 $this->dieWithError(
175 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ],
176 'nosuchrevid'
177 );
178 }
179 $toRelRev = $toRev;
180 $toValsRev = $toRev;
181 break;
182 }
183 } else {
184 [ $toRev, $toRelRev, $toValsRev ] = $this->getDiffRevision( 'to', $params );
185 }
186
187 // Handle missing from or to revisions (should never happen)
188 // @codeCoverageIgnoreStart
189 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
190 if ( !$fromRev || !$toRev ) {
191 $this->dieWithError( 'apierror-baddiff' );
192 }
193 // @codeCoverageIgnoreEnd
194
195 // Handle revdel
196 if ( !$fromRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
197 $this->dieWithError( [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent' );
198 }
199 if ( !$toRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
200 $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
201 }
202
203 // Get the diff
204 $context = new DerivativeContext( $this->getContext() );
205 if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) {
206 $context->setTitle( Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) );
207 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
208 } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
209 $context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) );
210 } else {
211 $guessedTitle = $this->guessTitle();
212 if ( $guessedTitle ) {
213 $context->setTitle( $guessedTitle );
214 }
215 }
216 $this->differenceEngine->setContext( $context );
217 $this->differenceEngine->setSlotDiffOptions( [ 'diff-type' => $params['difftype'] ] );
218 $this->differenceEngine->setRevisions( $fromRev, $toRev );
219 if ( $params['slots'] === null ) {
220 $difftext = $this->differenceEngine->getDiffBody();
221 if ( $difftext === false ) {
222 $this->dieWithError( 'apierror-baddiff' );
223 }
224 } else {
225 $difftext = [];
226 foreach ( $params['slots'] as $role ) {
227 $difftext[$role] = $this->differenceEngine->getDiffBodyForRole( $role );
228 }
229 }
230 foreach ( $this->differenceEngine->getRevisionLoadErrors() as $msg ) {
231 $this->addWarning( $msg );
232 }
233
234 // Fill in the response
235 $vals = [];
236 $this->setVals( $vals, 'from', $fromValsRev );
237 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
238 $this->setVals( $vals, 'to', $toValsRev );
239
240 if ( isset( $this->props['rel'] ) ) {
241 if ( !$fromRev instanceof MutableRevisionRecord ) {
242 $rev = $this->revisionStore->getPreviousRevision( $fromRev );
243 if ( $rev ) {
244 $vals['prev'] = $rev->getId();
245 }
246 }
247 if ( !$toRev instanceof MutableRevisionRecord ) {
248 $rev = $this->revisionStore->getNextRevision( $toRev );
249 if ( $rev ) {
250 $vals['next'] = $rev->getId();
251 }
252 }
253 }
254
255 if ( isset( $this->props['diffsize'] ) ) {
256 $vals['diffsize'] = 0;
257 foreach ( (array)$difftext as $text ) {
258 $vals['diffsize'] += strlen( $text );
259 }
260 }
261 if ( isset( $this->props['diff'] ) ) {
262 if ( is_array( $difftext ) ) {
263 ApiResult::setArrayType( $difftext, 'kvp', 'diff' );
264 $vals['bodies'] = $difftext;
265 } else {
266 ApiResult::setContentValue( $vals, 'body', $difftext );
267 }
268 }
269
270 // Diffs can be really big and there's little point in having
271 // ApiResult truncate it to an empty response since the diff is the
272 // whole reason this module exists. So pass NO_SIZE_CHECK here.
273 $this->getResult()->addValue( null, $this->getModuleName(), $vals, ApiResult::NO_SIZE_CHECK );
274 }
275
284 private function getRevisionById( $id ) {
285 $rev = $this->revisionStore->getRevisionById( $id );
286 if ( !$rev && $this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
287 // Try the 'archive' table
288 $rev = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id );
289 }
290 return $rev;
291 }
292
298 private function guessTitle() {
299 if ( $this->guessedTitle !== false ) {
300 return $this->guessedTitle;
301 }
302
303 $this->guessedTitle = null;
304 $params = $this->extractRequestParams();
305
306 foreach ( [ 'from', 'to' ] as $prefix ) {
307 if ( $params["{$prefix}rev"] !== null ) {
308 $rev = $this->getRevisionById( $params["{$prefix}rev"] );
309 if ( $rev ) {
310 $this->guessedTitle = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
311 break;
312 }
313 }
314
315 if ( $params["{$prefix}title"] !== null ) {
316 $title = Title::newFromText( $params["{$prefix}title"] );
317 if ( $title && !$title->isExternal() ) {
318 $this->guessedTitle = $title;
319 break;
320 }
321 }
322
323 if ( $params["{$prefix}id"] !== null ) {
324 $title = Title::newFromID( $params["{$prefix}id"] );
325 if ( $title ) {
326 $this->guessedTitle = $title;
327 break;
328 }
329 }
330 }
331
332 return $this->guessedTitle;
333 }
334
340 private function guessModel( $role ) {
341 $params = $this->extractRequestParams();
342
343 foreach ( [ 'from', 'to' ] as $prefix ) {
344 if ( $params["{$prefix}rev"] !== null ) {
345 $rev = $this->getRevisionById( $params["{$prefix}rev"] );
346 if ( $rev && $rev->hasSlot( $role ) ) {
347 return $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
348 }
349 }
350 }
351
352 $guessedTitle = $this->guessTitle();
353 if ( $guessedTitle ) {
354 return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle );
355 }
356
357 if ( isset( $params["fromcontentmodel-$role"] ) ) {
358 return $params["fromcontentmodel-$role"];
359 }
360 if ( isset( $params["tocontentmodel-$role"] ) ) {
361 return $params["tocontentmodel-$role"];
362 }
363
364 if ( $role === SlotRecord::MAIN ) {
365 if ( isset( $params['fromcontentmodel'] ) ) {
366 return $params['fromcontentmodel'];
367 }
368 if ( isset( $params['tocontentmodel'] ) ) {
369 return $params['tocontentmodel'];
370 }
371 }
372
373 return null;
374 }
375
391 private function getDiffRevision( $prefix, array $params ) {
392 // Back compat params
393 $this->requireMaxOneParameter( $params, "{$prefix}text", "{$prefix}slots" );
394 $this->requireMaxOneParameter( $params, "{$prefix}section", "{$prefix}slots" );
395 if ( $params["{$prefix}text"] !== null ) {
396 $params["{$prefix}slots"] = [ SlotRecord::MAIN ];
397 $params["{$prefix}text-main"] = $params["{$prefix}text"];
398 $params["{$prefix}section-main"] = null;
399 $params["{$prefix}contentmodel-main"] = $params["{$prefix}contentmodel"];
400 $params["{$prefix}contentformat-main"] = $params["{$prefix}contentformat"];
401 }
402
403 $title = null;
404 $rev = null;
405 $suppliedContent = $params["{$prefix}slots"] !== null;
406
407 // Get the revision and title, if applicable
408 $revId = null;
409 if ( $params["{$prefix}rev"] !== null ) {
410 $revId = $params["{$prefix}rev"];
411 } elseif ( $params["{$prefix}title"] !== null || $params["{$prefix}id"] !== null ) {
412 if ( $params["{$prefix}title"] !== null ) {
413 $title = Title::newFromText( $params["{$prefix}title"] );
414 if ( !$title || $title->isExternal() ) {
415 $this->dieWithError(
416 [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
417 );
418 }
419 } else {
420 $title = Title::newFromID( $params["{$prefix}id"] );
421 if ( !$title ) {
422 $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
423 }
424 }
425 $revId = $title->getLatestRevID();
426 if ( !$revId ) {
427 $revId = null;
428 // Only die here if we're not using supplied text
429 if ( !$suppliedContent ) {
430 if ( $title->exists() ) {
431 $this->dieWithError(
432 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ],
433 'nosuchrevid'
434 );
435 } else {
436 $this->dieWithError(
437 [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
438 'missingtitle'
439 );
440 }
441 }
442 }
443 }
444 if ( $revId !== null ) {
445 $rev = $this->getRevisionById( $revId );
446 if ( !$rev ) {
447 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
448 }
449 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
450
451 // If we don't have supplied content, return here. Otherwise,
452 // continue on below with the supplied content.
453 if ( !$suppliedContent ) {
454 $newRev = $rev;
455
456 // Deprecated 'fromsection'/'tosection'
457 if ( isset( $params["{$prefix}section"] ) ) {
458 $section = $params["{$prefix}section"];
459 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
460 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
461 $content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
462 $this->getAuthority() );
463 if ( !$content ) {
464 $this->dieWithError(
465 [ 'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ], 'missingcontent'
466 );
467 }
468 $content = $content->getSection( $section );
469 if ( !$content ) {
470 $this->dieWithError(
471 [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
472 "nosuch{$prefix}section"
473 );
474 }
475 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
476 $newRev->setContent( SlotRecord::MAIN, $content );
477 }
478
479 return [ $newRev, $rev, $rev ];
480 }
481 }
482
483 // Override $content based on supplied text
484 if ( !$title ) {
485 $title = $this->guessTitle();
486 }
487 if ( $rev ) {
488 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
489 } else {
490 $newRev = new MutableRevisionRecord( $title ?: Title::newMainPage() );
491 }
492 foreach ( $params["{$prefix}slots"] as $role ) {
493 $text = $params["{$prefix}text-{$role}"];
494 if ( $text === null ) {
495 // The SlotRecord::MAIN role can't be deleted
496 if ( $role === SlotRecord::MAIN ) {
497 $this->dieWithError( [ 'apierror-compare-maintextrequired', $prefix ] );
498 }
499
500 // These parameters make no sense without text. Reject them to avoid
501 // confusion.
502 foreach ( [ 'section', 'contentmodel', 'contentformat' ] as $param ) {
503 if ( isset( $params["{$prefix}{$param}-{$role}"] ) ) {
504 $this->dieWithError( [
505 'apierror-compare-notext',
506 wfEscapeWikiText( "{$prefix}{$param}-{$role}" ),
507 wfEscapeWikiText( "{$prefix}text-{$role}" ),
508 ] );
509 }
510 }
511
512 $newRev->removeSlot( $role );
513 continue;
514 }
515
516 $model = $params["{$prefix}contentmodel-{$role}"];
517 $format = $params["{$prefix}contentformat-{$role}"];
518
519 if ( !$model && $rev && $rev->hasSlot( $role ) ) {
520 $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
521 }
522 if ( !$model && $title && $role === SlotRecord::MAIN ) {
523 // @todo: Use SlotRoleRegistry and do this for all slots
524 $model = $title->getContentModel();
525 }
526 if ( !$model ) {
527 $model = $this->guessModel( $role );
528 }
529 if ( !$model ) {
530 $model = CONTENT_MODEL_WIKITEXT;
531 $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
532 }
533
534 try {
535 $content = $this->contentHandlerFactory
536 ->getContentHandler( $model )
537 ->unserializeContent( $text, $format );
538 } catch ( MWContentSerializationException $ex ) {
539 $this->dieWithException( $ex, [
540 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
541 ] );
542 }
543
544 if ( $params["{$prefix}pst"] ) {
545 if ( !$title ) {
546 $this->dieWithError( 'apierror-compare-no-title' );
547 }
548 $popts = ParserOptions::newFromContext( $this->getContext() );
549 $content = $this->contentTransformer->preSaveTransform(
550 $content,
551 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
552 $title,
553 $this->getUserForPreview(),
554 $popts
555 );
556 }
557
558 $section = $params["{$prefix}section-{$role}"];
559 if ( $section !== null && $section !== '' ) {
560 if ( !$rev ) {
561 $this->dieWithError( "apierror-compare-no{$prefix}revision" );
562 }
563 $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
564 if ( !$oldContent ) {
565 $this->dieWithError(
566 [ 'apierror-missingcontent-revid-role', $rev->getId(), wfEscapeWikiText( $role ) ],
567 'missingcontent'
568 );
569 }
570 if ( !$oldContent->getContentHandler()->supportsSections() ) {
571 $this->dieWithError( [ 'apierror-sectionsnotsupported', $content->getModel() ] );
572 }
573 try {
574 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
575 $content = $oldContent->replaceSection( $section, $content, '' );
576 } catch ( TimeoutException $e ) {
577 throw $e;
578 } catch ( Exception $ex ) {
579 // Probably a content model mismatch.
580 $content = null;
581 }
582 if ( !$content ) {
583 $this->dieWithError( [ 'apierror-sectionreplacefailed' ] );
584 }
585 }
586
587 // Deprecated 'fromsection'/'tosection'
588 if ( $role === SlotRecord::MAIN && isset( $params["{$prefix}section"] ) ) {
589 $section = $params["{$prefix}section"];
590 $content = $content->getSection( $section );
591 if ( !$content ) {
592 $this->dieWithError(
593 [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
594 "nosuch{$prefix}section"
595 );
596 }
597 }
598
599 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
600 $newRev->setContent( $role, $content );
601 }
602 return [ $newRev, $rev, null ];
603 }
604
612 private function setVals( &$vals, $prefix, $rev ) {
613 if ( $rev ) {
614 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
615 if ( isset( $this->props['ids'] ) ) {
616 $vals["{$prefix}id"] = $title->getArticleID();
617 $vals["{$prefix}revid"] = $rev->getId();
618 }
619 if ( isset( $this->props['title'] ) ) {
620 ApiQueryBase::addTitleInfo( $vals, $title, $prefix );
621 }
622 if ( isset( $this->props['size'] ) ) {
623 $vals["{$prefix}size"] = $rev->getSize();
624 }
625 if ( isset( $this->props['timestamp'] ) ) {
626 $revTimestamp = $rev->getTimestamp();
627 if ( $revTimestamp ) {
628 $vals["{$prefix}timestamp"] = wfTimestamp( TS_ISO_8601, $revTimestamp );
629 }
630 }
631
632 $anyHidden = false;
633 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
634 $vals["{$prefix}texthidden"] = true;
635 $anyHidden = true;
636 }
637
638 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
639 $vals["{$prefix}userhidden"] = true;
640 $anyHidden = true;
641 }
642 if ( isset( $this->props['user'] ) ) {
643 $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
644 if ( $user ) {
645 $vals["{$prefix}user"] = $user->getName();
646 $vals["{$prefix}userid"] = $user->getId();
647 }
648 }
649
650 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
651 $vals["{$prefix}commenthidden"] = true;
652 $anyHidden = true;
653 }
654 if ( isset( $this->props['comment'] ) || isset( $this->props['parsedcomment'] ) ) {
655 $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
656 if ( $comment !== null ) {
657 if ( isset( $this->props['comment'] ) ) {
658 $vals["{$prefix}comment"] = $comment->text;
659 }
660 $vals["{$prefix}parsedcomment"] = $this->commentFormatter->format(
661 $comment->text, $title
662 );
663 }
664 }
665
666 if ( $anyHidden ) {
667 $this->getMain()->setCacheMode( 'private' );
668 if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
669 $vals["{$prefix}suppressed"] = true;
670 }
671 }
672
673 if ( $rev instanceof RevisionArchiveRecord ) {
674 $this->getMain()->setCacheMode( 'private' );
675 $vals["{$prefix}archive"] = true;
676 }
677 }
678 }
679
680 private function getUserForPreview() {
681 $user = $this->getUser();
682 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
683 return $this->userFactory->newUnsavedTempUser(
684 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
685 );
686 }
687 return $user;
688 }
689
690 public function getAllowedParams() {
691 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
692 sort( $slotRoles, SORT_STRING );
693
694 // Parameters for the 'from' and 'to' content
695 $fromToParams = [
696 'title' => null,
697 'id' => [
698 ParamValidator::PARAM_TYPE => 'integer'
699 ],
700 'rev' => [
701 ParamValidator::PARAM_TYPE => 'integer'
702 ],
703
704 'slots' => [
705 ParamValidator::PARAM_TYPE => $slotRoles,
706 ParamValidator::PARAM_ISMULTI => true,
707 ],
708 'text-{slot}' => [
709 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
710 ParamValidator::PARAM_TYPE => 'text',
711 ],
712 'section-{slot}' => [
713 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
714 ParamValidator::PARAM_TYPE => 'string',
715 ],
716 'contentformat-{slot}' => [
717 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
718 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
719 ],
720 'contentmodel-{slot}' => [
721 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
722 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
723 ],
724 'pst' => false,
725
726 'text' => [
727 ParamValidator::PARAM_TYPE => 'text',
728 ParamValidator::PARAM_DEPRECATED => true,
729 ],
730 'contentformat' => [
731 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
732 ParamValidator::PARAM_DEPRECATED => true,
733 ],
734 'contentmodel' => [
735 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
736 ParamValidator::PARAM_DEPRECATED => true,
737 ],
738 'section' => [
739 ParamValidator::PARAM_DEFAULT => null,
740 ParamValidator::PARAM_DEPRECATED => true,
741 ],
742 ];
743
744 $ret = [];
745 foreach ( $fromToParams as $k => $v ) {
746 if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
747 $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'fromslots';
748 }
749 $ret["from$k"] = $v;
750 }
751 foreach ( $fromToParams as $k => $v ) {
752 if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
753 $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'toslots';
754 }
755 $ret["to$k"] = $v;
756 }
757
758 $ret = wfArrayInsertAfter(
759 $ret,
760 [ 'torelative' => [ ParamValidator::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
761 'torev'
762 );
763
764 $ret['prop'] = [
765 ParamValidator::PARAM_DEFAULT => 'diff|ids|title',
766 ParamValidator::PARAM_TYPE => [
767 'diff',
768 'diffsize',
769 'rel',
770 'ids',
771 'title',
772 'user',
773 'comment',
774 'parsedcomment',
775 'size',
776 'timestamp',
777 ],
778 ParamValidator::PARAM_ISMULTI => true,
780 ];
781
782 $ret['slots'] = [
783 ParamValidator::PARAM_TYPE => $slotRoles,
784 ParamValidator::PARAM_ISMULTI => true,
785 ParamValidator::PARAM_ALL => true,
786 ];
787
788 $ret['difftype'] = [
789 ParamValidator::PARAM_TYPE => $this->differenceEngine->getSupportedFormats(),
790 ParamValidator::PARAM_DEFAULT => 'table',
791 ];
792
793 return $ret;
794 }
795
796 protected function getExamplesMessages() {
797 return [
798 'action=compare&fromrev=1&torev=2'
799 => 'apihelp-compare-example-1',
800 ];
801 }
802
803 public function getHelpUrls() {
804 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Compare';
805 }
806}
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:218
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfArrayInsertAfter(array $array, array $insert, $after)
Insert an array into another array after the specified key.
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:64
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1533
getMain()
Get the main module.
Definition ApiBase.php:550
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition ApiBase.php:211
requireAtLeastOneParameter( $params,... $required)
Die if 0 of a certain set of parameters is set and not false.
Definition ApiBase.php:1010
requireMaxOneParameter( $params,... $required)
Dies if more than one parameter from a certain set of parameters are set and not false.
Definition ApiBase.php:985
const PARAM_TEMPLATE_VARS
(array) Indicate that this is a templated parameter, and specify replacements.
Definition ApiBase.php:229
getResult()
Get the result object.
Definition ApiBase.php:671
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:811
addWarning( $msg, $code=null, $data=null)
Add a warning for this module.
Definition ApiBase.php:1451
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:532
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition ApiBase.php:1546
getHelpUrls()
Return links to more detailed help pages about the module.
getExamplesMessages()
Returns usage examples for this module.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
__construct(ApiMain $mainModule, $moduleName, RevisionStore $revisionStore, ArchivedRevisionLookup $archivedRevisionLookup, SlotRoleRegistry $slotRoleRegistry, IContentHandlerFactory $contentHandlerFactory, ContentTransformer $contentTransformer, CommentFormatter $commentFormatter, TempUserCreator $tempUserCreator, UserFactory $userFactory)
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:65
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
Exception representing a failure to serialize or unserialize a content object.
This is the main service interface for converting single-line comments from various DB comment fields...
getContext()
Get the base IContextSource object.
setContent( $role, Content $content)
Sets the content for the slot with the given role.
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Page revision base class.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Represents a title within MediaWiki.
Definition Title.php:78
Service for temporary user creation.
Creates User objects.
static newFromContext(IContextSource $context)
Get a ParserOptions object from a IContextSource object.
Service for formatting and validating API parameters.