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