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