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