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