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