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