Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.78% covered (success)
92.78%
411 / 443
45.45% covered (danger)
45.45%
5 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiComparePages
92.99% covered (success)
92.99%
411 / 442
45.45% covered (danger)
45.45%
5 / 11
141.29
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 execute
97.27% covered (success)
97.27%
107 / 110
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.93% covered (warning)
89.93%
125 / 139
0.00% covered (danger)
0.00%
0 / 1
50.35
 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\ContentSerializationException;
12use MediaWiki\Content\IContentHandlerFactory;
13use MediaWiki\Content\Transform\ContentTransformer;
14use MediaWiki\Context\DerivativeContext;
15use MediaWiki\Diff\DifferenceEngine;
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\ArrayUtils\ArrayUtils;
29use Wikimedia\ParamValidator\ParamValidator;
30use Wikimedia\RequestTimeout\TimeoutException;
31use Wikimedia\Timestamp\TimestampFormat as TS;
32
33/**
34 * @ingroup API
35 */
36class ApiComparePages extends ApiBase {
37
38    /** @var Title|null|false */
39    private $guessedTitle = false;
40    /** @var array<string,true> */
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
65        $this->requireAtLeastOneParameter(
66            $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext', 'fromslots'
67        );
68        $this->requireAtLeastOneParameter(
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
242    /**
243     * Load a revision by ID
244     *
245     * Falls back to checking the archive table if appropriate.
246     *
247     * @param int $id
248     * @return RevisionRecord|null
249     */
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
268    /**
269     * Guess an appropriate default Title for this request
270     *
271     * @return Title|null
272     */
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
310    /**
311     * Guess an appropriate default content model for this request
312     * @param string $role Slot for which to guess the model
313     * @return string|null Guessed content model
314     */
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
351    /**
352     * Get the RevisionRecord for one side of the diff
353     *
354     * This uses the appropriate set of parameters to determine what content
355     * should be diffed.
356     *
357     * Returns three values:
358     * - A RevisionRecord holding the content
359     * - The revision specified, if any, even if content was supplied
360     * - The revision to pass to setVals(), if any
361     *
362     * @param string $prefix 'from' or 'to'
363     * @param array $params
364     * @return array [ RevisionRecord|null, RevisionRecord|null, RevisionRecord|null ]
365     */
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
577    /**
578     * Set value fields from a RevisionRecord object
579     *
580     * @param array &$vals Result array to set data into
581     * @param string $prefix 'from' or 'to'
582     * @param RevisionRecord|null $rev
583     */
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
662    /** @inheritDoc */
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
731        $ret = ArrayUtils::insertAfter(
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,
752            ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
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
769    /** @inheritDoc */
770    protected function getExamplesMessages() {
771        return [
772            'action=compare&fromrev=1&torev=2'
773                => 'apihelp-compare-example-1',
774        ];
775    }
776
777    /** @inheritDoc */
778    public function getHelpUrls() {
779        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Compare';
780    }
781}
782
783/** @deprecated class alias since 1.43 */
784class_alias( ApiComparePages::class, 'ApiComparePages' );