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