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