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