Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / api / ApiComparePages.php
blob00d204d12e7ee7e691f8a8bb040c530c26d5d57c
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
18 * @file
21 namespace MediaWiki\Api;
23 use DifferenceEngine;
24 use Exception;
25 use MediaWiki\CommentFormatter\CommentFormatter;
26 use MediaWiki\Content\IContentHandlerFactory;
27 use MediaWiki\Content\Transform\ContentTransformer;
28 use MediaWiki\Context\DerivativeContext;
29 use MediaWiki\Parser\ParserOptions;
30 use MediaWiki\Revision\ArchivedRevisionLookup;
31 use MediaWiki\Revision\MutableRevisionRecord;
32 use MediaWiki\Revision\RevisionArchiveRecord;
33 use MediaWiki\Revision\RevisionRecord;
34 use MediaWiki\Revision\RevisionStore;
35 use MediaWiki\Revision\SlotRecord;
36 use MediaWiki\Revision\SlotRoleRegistry;
37 use MediaWiki\Title\Title;
38 use MediaWiki\User\TempUser\TempUserCreator;
39 use MediaWiki\User\UserFactory;
40 use MWContentSerializationException;
41 use Wikimedia\ParamValidator\ParamValidator;
42 use Wikimedia\RequestTimeout\TimeoutException;
44 /**
45 * @ingroup API
47 class ApiComparePages extends ApiBase {
49 private RevisionStore $revisionStore;
50 private ArchivedRevisionLookup $archivedRevisionLookup;
51 private SlotRoleRegistry $slotRoleRegistry;
53 /** @var Title|null|false */
54 private $guessedTitle = false;
55 /** @var array<string,true> */
56 private $props;
58 private IContentHandlerFactory $contentHandlerFactory;
59 private ContentTransformer $contentTransformer;
60 private CommentFormatter $commentFormatter;
61 private TempUserCreator $tempUserCreator;
62 private UserFactory $userFactory;
63 private DifferenceEngine $differenceEngine;
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;
89 public function execute() {
90 $params = $this->extractRequestParams();
92 // Parameter validation
93 $this->requireAtLeastOneParameter(
94 $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext', 'fromslots'
96 $this->requireAtLeastOneParameter(
97 $params, 'totitle', 'toid', 'torev', 'totext', 'torelative', 'toslots'
100 $this->props = array_fill_keys( $params['prop'], true );
102 // Cache responses publicly by default. This may be overridden later.
103 $this->getMain()->setCacheMode( 'public' );
105 // Get the 'from' RevisionRecord
106 [ $fromRev, $fromRelRev, $fromValsRev ] = $this->getDiffRevision( 'from', $params );
108 // Get the 'to' RevisionRecord
109 if ( $params['torelative'] !== null ) {
110 if ( !$fromRelRev ) {
111 $this->dieWithError( 'apierror-compare-relative-to-nothing' );
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'] ] );
118 switch ( $params['torelative'] ) {
119 case 'prev':
120 // Swap 'from' and 'to'
121 [ $toRev, $toRelRev, $toValsRev ] = [ $fromRev, $fromRelRev, $fromValsRev ];
122 $fromRev = $this->revisionStore->getPreviousRevision( $toRelRev );
123 $fromRelRev = $fromRev;
124 $fromValsRev = $fromRev;
125 if ( !$fromRev ) {
126 $title = Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() );
127 $this->addWarning( [
128 'apiwarn-compare-no-prev',
129 wfEscapeWikiText( $title->getPrefixedText() ),
130 $toRelRev->getId()
131 ] );
133 // (T203433) Create an empty dummy revision as the "previous".
134 // The main slot has to exist, the rest will be handled by DifferenceEngine.
135 $fromRev = new MutableRevisionRecord(
136 $title ?: $toRev->getPage()
138 $fromRev->setContent(
139 SlotRecord::MAIN,
140 $toRelRev->getMainContentRaw()
141 ->getContentHandler()
142 ->makeEmptyContent()
145 break;
147 case 'next':
148 $toRev = $this->revisionStore->getNextRevision( $fromRelRev );
149 $toRelRev = $toRev;
150 $toValsRev = $toRev;
151 if ( !$toRev ) {
152 $title = Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() );
153 $this->addWarning( [
154 'apiwarn-compare-no-next',
155 wfEscapeWikiText( $title->getPrefixedText() ),
156 $fromRelRev->getId()
157 ] );
159 // (T203433) The web UI treats "next" as "cur" in this case.
160 // Avoid repeating metadata by making a MutableRevisionRecord with no changes.
161 $toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev );
163 break;
165 case 'cur':
166 $title = $fromRelRev->getPageAsLinkTarget();
167 $toRev = $this->revisionStore->getRevisionByTitle( $title );
168 if ( !$toRev ) {
169 $title = Title::newFromLinkTarget( $title );
170 $this->dieWithError(
171 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ],
172 'nosuchrevid'
175 $toRelRev = $toRev;
176 $toValsRev = $toRev;
177 break;
179 } else {
180 [ $toRev, $toRelRev, $toValsRev ] = $this->getDiffRevision( 'to', $params );
183 // Handle missing from or to revisions (should never happen)
184 // @codeCoverageIgnoreStart
185 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
186 if ( !$fromRev || !$toRev ) {
187 $this->dieWithError( 'apierror-baddiff' );
189 // @codeCoverageIgnoreEnd
191 // Handle revdel
192 if ( !$fromRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
193 $this->dieWithError( [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent' );
195 if ( !$toRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
196 $this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
199 // Get the diff
200 $context = new DerivativeContext( $this->getContext() );
201 if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) {
202 $context->setTitle( Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) );
203 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
204 } elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
205 $context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) );
206 } else {
207 $guessedTitle = $this->guessTitle();
208 if ( $guessedTitle ) {
209 $context->setTitle( $guessedTitle );
212 $this->differenceEngine->setContext( $context );
213 $this->differenceEngine->setSlotDiffOptions( [ 'diff-type' => $params['difftype'] ] );
214 $this->differenceEngine->setRevisions( $fromRev, $toRev );
215 if ( $params['slots'] === null ) {
216 $difftext = $this->differenceEngine->getDiffBody();
217 if ( $difftext === false ) {
218 $this->dieWithError( 'apierror-baddiff' );
220 } else {
221 $difftext = [];
222 foreach ( $params['slots'] as $role ) {
223 $difftext[$role] = $this->differenceEngine->getDiffBodyForRole( $role );
226 foreach ( $this->differenceEngine->getRevisionLoadErrors() as $msg ) {
227 $this->addWarning( $msg );
230 // Fill in the response
231 $vals = [];
232 $this->setVals( $vals, 'from', $fromValsRev );
233 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
234 $this->setVals( $vals, 'to', $toValsRev );
236 if ( isset( $this->props['rel'] ) ) {
237 if ( !$fromRev instanceof MutableRevisionRecord ) {
238 $rev = $this->revisionStore->getPreviousRevision( $fromRev );
239 if ( $rev ) {
240 $vals['prev'] = $rev->getId();
243 if ( !$toRev instanceof MutableRevisionRecord ) {
244 $rev = $this->revisionStore->getNextRevision( $toRev );
245 if ( $rev ) {
246 $vals['next'] = $rev->getId();
251 if ( isset( $this->props['diffsize'] ) ) {
252 $vals['diffsize'] = 0;
253 foreach ( (array)$difftext as $text ) {
254 $vals['diffsize'] += strlen( $text );
257 if ( isset( $this->props['diff'] ) ) {
258 if ( is_array( $difftext ) ) {
259 ApiResult::setArrayType( $difftext, 'kvp', 'diff' );
260 $vals['bodies'] = $difftext;
261 } else {
262 ApiResult::setContentValue( $vals, 'body', $difftext );
266 // Diffs can be really big and there's little point in having
267 // ApiResult truncate it to an empty response since the diff is the
268 // whole reason this module exists. So pass NO_SIZE_CHECK here.
269 $this->getResult()->addValue( null, $this->getModuleName(), $vals, ApiResult::NO_SIZE_CHECK );
273 * Load a revision by ID
275 * Falls back to checking the archive table if appropriate.
277 * @param int $id
278 * @return RevisionRecord|null
280 private function getRevisionById( $id ) {
281 $rev = $this->revisionStore->getRevisionById( $id );
282 if ( !$rev && $this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
283 // Try the 'archive' table
284 $rev = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id );
286 return $rev;
290 * Guess an appropriate default Title for this request
292 * @return Title|null
294 private function guessTitle() {
295 if ( $this->guessedTitle !== false ) {
296 return $this->guessedTitle;
299 $this->guessedTitle = null;
300 $params = $this->extractRequestParams();
302 foreach ( [ 'from', 'to' ] as $prefix ) {
303 if ( $params["{$prefix}rev"] !== null ) {
304 $rev = $this->getRevisionById( $params["{$prefix}rev"] );
305 if ( $rev ) {
306 $this->guessedTitle = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
307 break;
311 if ( $params["{$prefix}title"] !== null ) {
312 $title = Title::newFromText( $params["{$prefix}title"] );
313 if ( $title && !$title->isExternal() ) {
314 $this->guessedTitle = $title;
315 break;
319 if ( $params["{$prefix}id"] !== null ) {
320 $title = Title::newFromID( $params["{$prefix}id"] );
321 if ( $title ) {
322 $this->guessedTitle = $title;
323 break;
328 return $this->guessedTitle;
332 * Guess an appropriate default content model for this request
333 * @param string $role Slot for which to guess the model
334 * @return string|null Guessed content model
336 private function guessModel( $role ) {
337 $params = $this->extractRequestParams();
339 foreach ( [ 'from', 'to' ] as $prefix ) {
340 if ( $params["{$prefix}rev"] !== null ) {
341 $rev = $this->getRevisionById( $params["{$prefix}rev"] );
342 if ( $rev && $rev->hasSlot( $role ) ) {
343 return $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
348 $guessedTitle = $this->guessTitle();
349 if ( $guessedTitle ) {
350 return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle );
353 if ( isset( $params["fromcontentmodel-$role"] ) ) {
354 return $params["fromcontentmodel-$role"];
356 if ( isset( $params["tocontentmodel-$role"] ) ) {
357 return $params["tocontentmodel-$role"];
360 if ( $role === SlotRecord::MAIN ) {
361 if ( isset( $params['fromcontentmodel'] ) ) {
362 return $params['fromcontentmodel'];
364 if ( isset( $params['tocontentmodel'] ) ) {
365 return $params['tocontentmodel'];
369 return null;
373 * Get the RevisionRecord for one side of the diff
375 * This uses the appropriate set of parameters to determine what content
376 * should be diffed.
378 * Returns three values:
379 * - A RevisionRecord holding the content
380 * - The revision specified, if any, even if content was supplied
381 * - The revision to pass to setVals(), if any
383 * @param string $prefix 'from' or 'to'
384 * @param array $params
385 * @return array [ RevisionRecord|null, RevisionRecord|null, RevisionRecord|null ]
387 private function getDiffRevision( $prefix, array $params ) {
388 // Back compat params
389 $this->requireMaxOneParameter( $params, "{$prefix}text", "{$prefix}slots" );
390 $this->requireMaxOneParameter( $params, "{$prefix}section", "{$prefix}slots" );
391 if ( $params["{$prefix}text"] !== null ) {
392 $params["{$prefix}slots"] = [ SlotRecord::MAIN ];
393 $params["{$prefix}text-main"] = $params["{$prefix}text"];
394 $params["{$prefix}section-main"] = null;
395 $params["{$prefix}contentmodel-main"] = $params["{$prefix}contentmodel"];
396 $params["{$prefix}contentformat-main"] = $params["{$prefix}contentformat"];
399 $title = null;
400 $rev = null;
401 $suppliedContent = $params["{$prefix}slots"] !== null;
403 // Get the revision and title, if applicable
404 $revId = null;
405 if ( $params["{$prefix}rev"] !== null ) {
406 $revId = $params["{$prefix}rev"];
407 } elseif ( $params["{$prefix}title"] !== null || $params["{$prefix}id"] !== null ) {
408 if ( $params["{$prefix}title"] !== null ) {
409 $title = Title::newFromText( $params["{$prefix}title"] );
410 if ( !$title || $title->isExternal() ) {
411 $this->dieWithError(
412 [ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
415 } else {
416 $title = Title::newFromID( $params["{$prefix}id"] );
417 if ( !$title ) {
418 $this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
421 $revId = $title->getLatestRevID();
422 if ( !$revId ) {
423 $revId = null;
424 // Only die here if we're not using supplied text
425 if ( !$suppliedContent ) {
426 if ( $title->exists() ) {
427 $this->dieWithError(
428 [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ],
429 'nosuchrevid'
431 } else {
432 $this->dieWithError(
433 [ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
434 'missingtitle'
440 if ( $revId !== null ) {
441 $rev = $this->getRevisionById( $revId );
442 if ( !$rev ) {
443 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
445 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
447 // If we don't have supplied content, return here. Otherwise,
448 // continue on below with the supplied content.
449 if ( !$suppliedContent ) {
450 $newRev = $rev;
452 // Deprecated 'fromsection'/'tosection'
453 if ( isset( $params["{$prefix}section"] ) ) {
454 $section = $params["{$prefix}section"];
455 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
456 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
457 $content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
458 $this->getAuthority() );
459 if ( !$content ) {
460 $this->dieWithError(
461 [ 'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ], 'missingcontent'
464 $content = $content->getSection( $section );
465 if ( !$content ) {
466 $this->dieWithError(
467 [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
468 "nosuch{$prefix}section"
471 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
472 $newRev->setContent( SlotRecord::MAIN, $content );
475 return [ $newRev, $rev, $rev ];
479 // Override $content based on supplied text
480 if ( !$title ) {
481 $title = $this->guessTitle();
483 if ( $rev ) {
484 $newRev = MutableRevisionRecord::newFromParentRevision( $rev );
485 } else {
486 $newRev = new MutableRevisionRecord( $title ?: Title::newMainPage() );
488 foreach ( $params["{$prefix}slots"] as $role ) {
489 $text = $params["{$prefix}text-{$role}"];
490 if ( $text === null ) {
491 // The SlotRecord::MAIN role can't be deleted
492 if ( $role === SlotRecord::MAIN ) {
493 $this->dieWithError( [ 'apierror-compare-maintextrequired', $prefix ] );
496 // These parameters make no sense without text. Reject them to avoid
497 // confusion.
498 foreach ( [ 'section', 'contentmodel', 'contentformat' ] as $param ) {
499 if ( isset( $params["{$prefix}{$param}-{$role}"] ) ) {
500 $this->dieWithError( [
501 'apierror-compare-notext',
502 wfEscapeWikiText( "{$prefix}{$param}-{$role}" ),
503 wfEscapeWikiText( "{$prefix}text-{$role}" ),
504 ] );
508 $newRev->removeSlot( $role );
509 continue;
512 $model = $params["{$prefix}contentmodel-{$role}"];
513 $format = $params["{$prefix}contentformat-{$role}"];
515 if ( !$model && $rev && $rev->hasSlot( $role ) ) {
516 $model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
518 if ( !$model && $title && $role === SlotRecord::MAIN ) {
519 // @todo: Use SlotRoleRegistry and do this for all slots
520 $model = $title->getContentModel();
522 if ( !$model ) {
523 $model = $this->guessModel( $role );
525 if ( !$model ) {
526 $model = CONTENT_MODEL_WIKITEXT;
527 $this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
530 try {
531 $content = $this->contentHandlerFactory
532 ->getContentHandler( $model )
533 ->unserializeContent( $text, $format );
534 } catch ( MWContentSerializationException $ex ) {
535 $this->dieWithException( $ex, [
536 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
537 ] );
540 if ( $params["{$prefix}pst"] ) {
541 if ( !$title ) {
542 $this->dieWithError( 'apierror-compare-no-title' );
544 $popts = ParserOptions::newFromContext( $this->getContext() );
545 $content = $this->contentTransformer->preSaveTransform(
546 $content,
547 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
548 $title,
549 $this->getUserForPreview(),
550 $popts
554 $section = $params["{$prefix}section-{$role}"];
555 if ( $section !== null && $section !== '' ) {
556 if ( !$rev ) {
557 $this->dieWithError( "apierror-compare-no{$prefix}revision" );
559 $oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
560 if ( !$oldContent ) {
561 $this->dieWithError(
562 [ 'apierror-missingcontent-revid-role', $rev->getId(), wfEscapeWikiText( $role ) ],
563 'missingcontent'
566 if ( !$oldContent->getContentHandler()->supportsSections() ) {
567 $this->dieWithError( [ 'apierror-sectionsnotsupported', $content->getModel() ] );
569 try {
570 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
571 $content = $oldContent->replaceSection( $section, $content, '' );
572 } catch ( TimeoutException $e ) {
573 throw $e;
574 } catch ( Exception $ex ) {
575 // Probably a content model mismatch.
576 $content = null;
578 if ( !$content ) {
579 $this->dieWithError( [ 'apierror-sectionreplacefailed' ] );
583 // Deprecated 'fromsection'/'tosection'
584 if ( $role === SlotRecord::MAIN && isset( $params["{$prefix}section"] ) ) {
585 $section = $params["{$prefix}section"];
586 $content = $content->getSection( $section );
587 if ( !$content ) {
588 $this->dieWithError(
589 [ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
590 "nosuch{$prefix}section"
595 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
596 $newRev->setContent( $role, $content );
598 return [ $newRev, $rev, null ];
602 * Set value fields from a RevisionRecord object
604 * @param array &$vals Result array to set data into
605 * @param string $prefix 'from' or 'to'
606 * @param RevisionRecord|null $rev
608 private function setVals( &$vals, $prefix, $rev ) {
609 if ( $rev ) {
610 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
611 if ( isset( $this->props['ids'] ) ) {
612 $vals["{$prefix}id"] = $title->getArticleID();
613 $vals["{$prefix}revid"] = $rev->getId();
615 if ( isset( $this->props['title'] ) ) {
616 ApiQueryBase::addTitleInfo( $vals, $title, $prefix );
618 if ( isset( $this->props['size'] ) ) {
619 $vals["{$prefix}size"] = $rev->getSize();
621 if ( isset( $this->props['timestamp'] ) ) {
622 $revTimestamp = $rev->getTimestamp();
623 if ( $revTimestamp ) {
624 $vals["{$prefix}timestamp"] = wfTimestamp( TS_ISO_8601, $revTimestamp );
628 $anyHidden = false;
629 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
630 $vals["{$prefix}texthidden"] = true;
631 $anyHidden = true;
634 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
635 $vals["{$prefix}userhidden"] = true;
636 $anyHidden = true;
638 if ( isset( $this->props['user'] ) ) {
639 $user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
640 if ( $user ) {
641 $vals["{$prefix}user"] = $user->getName();
642 $vals["{$prefix}userid"] = $user->getId();
646 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
647 $vals["{$prefix}commenthidden"] = true;
648 $anyHidden = true;
650 if ( isset( $this->props['comment'] ) || isset( $this->props['parsedcomment'] ) ) {
651 $comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
652 if ( $comment !== null ) {
653 if ( isset( $this->props['comment'] ) ) {
654 $vals["{$prefix}comment"] = $comment->text;
656 $vals["{$prefix}parsedcomment"] = $this->commentFormatter->format(
657 $comment->text, $title
662 if ( $anyHidden ) {
663 $this->getMain()->setCacheMode( 'private' );
664 if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
665 $vals["{$prefix}suppressed"] = true;
669 if ( $rev instanceof RevisionArchiveRecord ) {
670 $this->getMain()->setCacheMode( 'private' );
671 $vals["{$prefix}archive"] = true;
676 private function getUserForPreview() {
677 $user = $this->getUser();
678 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
679 return $this->userFactory->newUnsavedTempUser(
680 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
683 return $user;
686 public function getAllowedParams() {
687 $slotRoles = $this->slotRoleRegistry->getKnownRoles();
688 sort( $slotRoles, SORT_STRING );
690 // Parameters for the 'from' and 'to' content
691 $fromToParams = [
692 'title' => null,
693 'id' => [
694 ParamValidator::PARAM_TYPE => 'integer'
696 'rev' => [
697 ParamValidator::PARAM_TYPE => 'integer'
700 'slots' => [
701 ParamValidator::PARAM_TYPE => $slotRoles,
702 ParamValidator::PARAM_ISMULTI => true,
704 'text-{slot}' => [
705 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
706 ParamValidator::PARAM_TYPE => 'text',
708 'section-{slot}' => [
709 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
710 ParamValidator::PARAM_TYPE => 'string',
712 'contentformat-{slot}' => [
713 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
714 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
716 'contentmodel-{slot}' => [
717 ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
718 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
720 'pst' => false,
722 'text' => [
723 ParamValidator::PARAM_TYPE => 'text',
724 ParamValidator::PARAM_DEPRECATED => true,
726 'contentformat' => [
727 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
728 ParamValidator::PARAM_DEPRECATED => true,
730 'contentmodel' => [
731 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
732 ParamValidator::PARAM_DEPRECATED => true,
734 'section' => [
735 ParamValidator::PARAM_DEFAULT => null,
736 ParamValidator::PARAM_DEPRECATED => true,
740 $ret = [];
741 foreach ( $fromToParams as $k => $v ) {
742 if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
743 $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'fromslots';
745 $ret["from$k"] = $v;
747 foreach ( $fromToParams as $k => $v ) {
748 if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
749 $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'toslots';
751 $ret["to$k"] = $v;
754 $ret = wfArrayInsertAfter(
755 $ret,
756 [ 'torelative' => [ ParamValidator::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
757 'torev'
760 $ret['prop'] = [
761 ParamValidator::PARAM_DEFAULT => 'diff|ids|title',
762 ParamValidator::PARAM_TYPE => [
763 'diff',
764 'diffsize',
765 'rel',
766 'ids',
767 'title',
768 'user',
769 'comment',
770 'parsedcomment',
771 'size',
772 'timestamp',
774 ParamValidator::PARAM_ISMULTI => true,
775 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
778 $ret['slots'] = [
779 ParamValidator::PARAM_TYPE => $slotRoles,
780 ParamValidator::PARAM_ISMULTI => true,
781 ParamValidator::PARAM_ALL => true,
784 $ret['difftype'] = [
785 ParamValidator::PARAM_TYPE => $this->differenceEngine->getSupportedFormats(),
786 ParamValidator::PARAM_DEFAULT => 'table',
789 return $ret;
792 protected function getExamplesMessages() {
793 return [
794 'action=compare&fromrev=1&torev=2'
795 => 'apihelp-compare-example-1',
799 public function getHelpUrls() {
800 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Compare';
804 /** @deprecated class alias since 1.43 */
805 class_alias( ApiComparePages::class, 'ApiComparePages' );