Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 491
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryRevisionsBase
0.00% covered (danger)
0.00%
0 / 490
0.00% covered (danger)
0.00%
0 / 12
18906
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 executeGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 run
n/a
0 / 0
n/a
0 / 0
0
 parseParameters
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 1
1406
 checkRevDel
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 extractRevisionInfo
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 1
1260
 extractAllSlotInfo
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
132
 extractSlotInfo
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
210
 extractDeprecatedContent
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 1
756
 getUserForPreview
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getCacheMode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\CommentFormatter\CommentFormatter;
12use MediaWiki\Content\Content;
13use MediaWiki\Content\IContentHandlerFactory;
14use MediaWiki\Content\Renderer\ContentRenderer;
15use MediaWiki\Content\Transform\ContentTransformer;
16use MediaWiki\Content\WikitextContent;
17use MediaWiki\Context\DerivativeContext;
18use MediaWiki\Logger\LoggerFactory;
19use MediaWiki\MainConfigNames;
20use MediaWiki\MediaWikiServices;
21use MediaWiki\Parser\Parser;
22use MediaWiki\Parser\ParserFactory;
23use MediaWiki\Parser\ParserOptions;
24use MediaWiki\Revision\RevisionAccessException;
25use MediaWiki\Revision\RevisionRecord;
26use MediaWiki\Revision\RevisionStore;
27use MediaWiki\Revision\SlotRecord;
28use MediaWiki\Revision\SlotRoleRegistry;
29use MediaWiki\Title\Title;
30use MediaWiki\User\TempUser\TempUserCreator;
31use MediaWiki\User\UserFactory;
32use MediaWiki\User\UserIdentity;
33use MediaWiki\User\UserNameUtils;
34use stdClass;
35use Wikimedia\ParamValidator\ParamValidator;
36use Wikimedia\ParamValidator\TypeDef\EnumDef;
37use Wikimedia\ParamValidator\TypeDef\IntegerDef;
38use Wikimedia\Timestamp\TimestampFormat as TS;
39
40/**
41 * A base class for functions common to producing a list of revisions.
42 *
43 * @stable to extend
44 *
45 * @ingroup API
46 */
47abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
48
49    // region Constants for internal use. Don't use externally.
50    /** @name Constants for internal use. Don't use externally. */
51
52    // Bits to indicate the results of the revdel permission check on a revision,
53    // see self::checkRevDel()
54    private const IS_DELETED = 1; // Whether the field is revision-deleted
55    private const CANNOT_VIEW = 2; // Whether the user cannot view the field due to revdel
56
57    private const LIMIT_PARSE = 1;
58
59    // endregion
60
61    /** @var int|string|null */
62    protected $limit;
63    /** @var int|string|null */
64    protected $diffto;
65    /** @var string|null */
66    protected $difftotext;
67    /** @var bool */
68    protected $difftotextpst;
69    /** @var int|string|null */
70    protected $expandTemplates;
71    /** @var bool */
72    protected $generateXML;
73    /** @var int|string|null */
74    protected $section;
75    /** @var bool */
76    protected $parseContent;
77    /** @var bool */
78    protected $fetchContent;
79    /** @var string */
80    protected $contentFormat;
81    protected bool $setParsedLimit = true;
82    protected ?array $slotRoles = null;
83    /** @var string[] */
84    protected $slotContentFormats;
85    /** @var bool */
86    protected $needSlots;
87
88    protected bool $fld_ids = false;
89    protected bool $fld_flags = false;
90    protected bool $fld_timestamp = false;
91    protected bool $fld_size = false;
92    protected bool $fld_slotsize = false;
93    protected bool $fld_sha1 = false;
94    protected bool $fld_slotsha1 = false;
95    protected bool $fld_comment = false;
96    protected bool $fld_parsedcomment = false;
97    protected bool $fld_user = false;
98    protected bool $fld_userid = false;
99    protected bool $fld_content = false;
100    protected bool $fld_tags = false;
101    protected bool $fld_contentmodel = false;
102    protected bool $fld_roles = false;
103    protected bool $fld_parsetree = false;
104
105    /**
106     * The number of uncached diffs that had to be generated for this request.
107     * @var int
108     */
109    private $numUncachedDiffs = 0;
110
111    private RevisionStore $revisionStore;
112    private IContentHandlerFactory $contentHandlerFactory;
113    private ParserFactory $parserFactory;
114    private SlotRoleRegistry $slotRoleRegistry;
115    private ContentRenderer $contentRenderer;
116    private ContentTransformer $contentTransformer;
117    private CommentFormatter $commentFormatter;
118    private TempUserCreator $tempUserCreator;
119    private UserFactory $userFactory;
120    private UserNameUtils $userNameUtils;
121
122    /**
123     * @since 1.37 Support injection of services
124     * @stable to call
125     */
126    public function __construct(
127        ApiQuery $queryModule,
128        string $moduleName,
129        string $paramPrefix = '',
130        ?RevisionStore $revisionStore = null,
131        ?IContentHandlerFactory $contentHandlerFactory = null,
132        ?ParserFactory $parserFactory = null,
133        ?SlotRoleRegistry $slotRoleRegistry = null,
134        ?ContentRenderer $contentRenderer = null,
135        ?ContentTransformer $contentTransformer = null,
136        ?CommentFormatter $commentFormatter = null,
137        ?TempUserCreator $tempUserCreator = null,
138        ?UserFactory $userFactory = null,
139        ?UserNameUtils $userNameUtils = null
140    ) {
141        parent::__construct( $queryModule, $moduleName, $paramPrefix );
142        // This class is part of the stable interface and
143        // therefor fallback to global state, if services are not provided
144        $services = MediaWikiServices::getInstance();
145        $this->revisionStore = $revisionStore ?? $services->getRevisionStore();
146        $this->contentHandlerFactory = $contentHandlerFactory ?? $services->getContentHandlerFactory();
147        $this->parserFactory = $parserFactory ?? $services->getParserFactory();
148        $this->slotRoleRegistry = $slotRoleRegistry ?? $services->getSlotRoleRegistry();
149        $this->contentRenderer = $contentRenderer ?? $services->getContentRenderer();
150        $this->contentTransformer = $contentTransformer ?? $services->getContentTransformer();
151        $this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
152        $this->tempUserCreator = $tempUserCreator ?? $services->getTempUserCreator();
153        $this->userFactory = $userFactory ?? $services->getUserFactory();
154        $this->userNameUtils = $userNameUtils ?? $services->getUserNameUtils();
155    }
156
157    public function execute() {
158        $this->run();
159    }
160
161    /** @inheritDoc */
162    public function executeGenerator( $resultPageSet ) {
163        $this->run( $resultPageSet );
164    }
165
166    /**
167     * @param ApiPageSet|null $resultPageSet
168     * @return void
169     */
170    abstract protected function run( ?ApiPageSet $resultPageSet = null );
171
172    /**
173     * Parse the parameters into the various instance fields.
174     *
175     * @param array $params
176     */
177    protected function parseParameters( $params ) {
178        $prop = array_fill_keys( $params['prop'], true );
179
180        $this->fld_ids = isset( $prop['ids'] );
181        $this->fld_flags = isset( $prop['flags'] );
182        $this->fld_timestamp = isset( $prop['timestamp'] );
183        $this->fld_comment = isset( $prop['comment'] );
184        $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
185        $this->fld_size = isset( $prop['size'] );
186        $this->fld_slotsize = isset( $prop['slotsize'] );
187        $this->fld_sha1 = isset( $prop['sha1'] );
188        $this->fld_slotsha1 = isset( $prop['slotsha1'] );
189        $this->fld_content = isset( $prop['content'] );
190        $this->fld_contentmodel = isset( $prop['contentmodel'] );
191        $this->fld_userid = isset( $prop['userid'] );
192        $this->fld_user = isset( $prop['user'] );
193        $this->fld_tags = isset( $prop['tags'] );
194        $this->fld_roles = isset( $prop['roles'] );
195        $this->fld_parsetree = isset( $prop['parsetree'] );
196
197        $this->slotRoles = $params['slots'];
198
199        if ( $this->slotRoles !== null ) {
200            if ( $this->fld_parsetree ) {
201                $this->dieWithError( [
202                    'apierror-invalidparammix-cannotusewith',
203                    $this->encodeParamName( 'prop=parsetree' ),
204                    $this->encodeParamName( 'slots' ),
205                ], 'invalidparammix' );
206            }
207            foreach ( [
208                'expandtemplates', 'generatexml', 'parse', 'diffto', 'difftotext', 'difftotextpst',
209                'contentformat'
210            ] as $p ) {
211                if ( $params[$p] !== null && $params[$p] !== false ) {
212                    $this->dieWithError( [
213                        'apierror-invalidparammix-cannotusewith',
214                        $this->encodeParamName( $p ),
215                        $this->encodeParamName( 'slots' ),
216                    ], 'invalidparammix' );
217                }
218            }
219            $this->slotContentFormats = [];
220            foreach ( $this->slotRoles as $slotRole ) {
221                if ( isset( $params['contentformat-' . $slotRole] ) ) {
222                    $this->slotContentFormats[$slotRole] = $params['contentformat-' . $slotRole];
223                }
224            }
225        }
226
227        if ( !empty( $params['contentformat'] ) ) {
228            $this->contentFormat = $params['contentformat'];
229        }
230
231        $this->limit = $params['limit'];
232
233        if ( $params['difftotext'] !== null ) {
234            $this->difftotext = $params['difftotext'];
235            $this->difftotextpst = $params['difftotextpst'];
236        } elseif ( $params['diffto'] !== null ) {
237            if ( $params['diffto'] == 'cur' ) {
238                $params['diffto'] = 0;
239            }
240            if ( ( !ctype_digit( (string)$params['diffto'] ) || $params['diffto'] < 0 )
241                && $params['diffto'] != 'prev' && $params['diffto'] != 'next'
242            ) {
243                $p = $this->getModulePrefix();
244                $this->dieWithError( [ 'apierror-baddiffto', $p ], 'diffto' );
245            }
246            // Check whether the revision exists and is readable,
247            // DifferenceEngine returns a rather ambiguous empty
248            // string if that's not the case
249            if ( is_numeric( $params['diffto'] ) && $params['diffto'] != 0 ) {
250                $difftoRev = $this->revisionStore->getRevisionById( $params['diffto'] );
251                if ( !$difftoRev ) {
252                    $this->dieWithError( [ 'apierror-nosuchrevid', $params['diffto'] ] );
253                }
254                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
255                $revDel = $this->checkRevDel( $difftoRev, RevisionRecord::DELETED_TEXT );
256                if ( $revDel & self::CANNOT_VIEW ) {
257                    $this->addWarning( [ 'apiwarn-difftohidden', $difftoRev->getId() ] );
258                    $params['diffto'] = null;
259                }
260            }
261            $this->diffto = $params['diffto'];
262        }
263
264        $this->fetchContent = $this->fld_content || $this->diffto !== null
265            || $this->difftotext !== null || $this->fld_parsetree;
266
267        $smallLimit = false;
268        if ( $this->fetchContent ) {
269            $smallLimit = true;
270            $this->expandTemplates = $params['expandtemplates'];
271            $this->generateXML = $params['generatexml'];
272            $this->parseContent = $params['parse'];
273            if ( $this->parseContent ) {
274                // Must manually initialize unset limit
275                $this->limit ??= self::LIMIT_PARSE;
276            }
277            $this->section = $params['section'] ?? false;
278        }
279
280        $userMax = $this->parseContent ? self::LIMIT_PARSE :
281            ( $smallLimit ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 );
282        $botMax = $this->parseContent ? self::LIMIT_PARSE :
283            ( $smallLimit ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 );
284        if ( $this->limit == 'max' ) {
285            $this->limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
286            if ( $this->setParsedLimit ) {
287                $this->getResult()->addParsedLimit( $this->getModuleName(), $this->limit );
288            }
289        }
290
291        $this->limit = $this->getMain()->getParamValidator()->validateValue(
292            $this, 'limit', $this->limit ?? 10, [
293                ParamValidator::PARAM_TYPE => 'limit',
294                IntegerDef::PARAM_MIN => 1,
295                IntegerDef::PARAM_MAX => $userMax,
296                IntegerDef::PARAM_MAX2 => $botMax,
297                IntegerDef::PARAM_IGNORE_RANGE => true,
298            ]
299        );
300
301        $this->needSlots = $this->fetchContent || $this->fld_contentmodel ||
302            $this->fld_slotsize || $this->fld_slotsha1;
303        if ( $this->needSlots && $this->slotRoles === null ) {
304            $encParam = $this->encodeParamName( 'slots' );
305            $name = $this->getModuleName();
306            $parent = $this->getParent();
307            $parentParam = $parent->encodeParamName( $parent->getModuleManager()->getModuleGroup( $name ) );
308            $this->addDeprecation(
309                [ 'apiwarn-deprecation-missingparam', $encParam ],
310                "action=query&{$parentParam}={$name}&!{$encParam}"
311            );
312        }
313    }
314
315    /**
316     * Test revision deletion status
317     * @param RevisionRecord $revision Revision to check
318     * @param int $field One of the RevisionRecord::DELETED_* constants
319     * @return int Revision deletion status flags. Bitwise OR of
320     *  self::IS_DELETED and self::CANNOT_VIEW, as appropriate.
321     */
322    private function checkRevDel( RevisionRecord $revision, $field ) {
323        $ret = $revision->isDeleted( $field ) ? self::IS_DELETED : 0;
324        if ( $ret ) {
325            $canSee = $revision->userCan( $field, $this->getAuthority() );
326            $ret |= ( $canSee ? 0 : self::CANNOT_VIEW );
327        }
328        return $ret;
329    }
330
331    /**
332     * Extract information from the RevisionRecord
333     *
334     * @since 1.32, takes a RevisionRecord instead of a Revision
335     * @param RevisionRecord $revision
336     * @param stdClass $row Should have a field 'ts_tags' if $this->fld_tags is set
337     * @return array
338     */
339    protected function extractRevisionInfo( RevisionRecord $revision, $row ) {
340        $vals = [];
341        $anyHidden = false;
342
343        if ( $this->fld_ids ) {
344            $vals['revid'] = (int)$revision->getId();
345            if ( $revision->getParentId() !== null ) {
346                $vals['parentid'] = (int)$revision->getParentId();
347            }
348        }
349
350        if ( $this->fld_flags ) {
351            $vals['minor'] = $revision->isMinor();
352        }
353
354        if ( $this->fld_user || $this->fld_userid ) {
355            $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_USER );
356            if ( $revDel & self::IS_DELETED ) {
357                $vals['userhidden'] = true;
358                $anyHidden = true;
359            }
360            if ( !( $revDel & self::CANNOT_VIEW ) ) {
361                $u = $revision->getUser( RevisionRecord::RAW );
362                if ( $this->fld_user ) {
363                    $vals['user'] = $u->getName();
364                }
365                if ( $this->userNameUtils->isTemp( $u->getName() ) ) {
366                    $vals['temp'] = true;
367                }
368                if ( !$u->isRegistered() ) {
369                    $vals['anon'] = true;
370                }
371
372                if ( $this->fld_userid ) {
373                    $vals['userid'] = $u->getId();
374                }
375            }
376        }
377
378        if ( $this->fld_timestamp ) {
379            $vals['timestamp'] = wfTimestamp( TS::ISO_8601, $revision->getTimestamp() );
380        }
381
382        if ( $this->fld_size ) {
383            try {
384                $vals['size'] = (int)$revision->getSize();
385            } catch ( RevisionAccessException ) {
386                // Back compat: If there's no size, return 0.
387                // @todo: Gergő says to mention T198099 as a "todo" here.
388                $vals['size'] = 0;
389            }
390        }
391
392        if ( $this->fld_sha1 ) {
393            $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT );
394            if ( $revDel & self::IS_DELETED ) {
395                $vals['sha1hidden'] = true;
396                $anyHidden = true;
397            }
398            if ( !( $revDel & self::CANNOT_VIEW ) ) {
399                try {
400                    $vals['sha1'] = \Wikimedia\base_convert( $revision->getSha1(), 36, 16, 40 );
401                } catch ( RevisionAccessException ) {
402                    // Back compat: If there's no sha1, return empty string.
403                    // @todo: Gergő says to mention T198099 as a "todo" here.
404                    $vals['sha1'] = '';
405                }
406            }
407        }
408
409        try {
410            if ( $this->fld_roles ) {
411                $vals['roles'] = $revision->getSlotRoles();
412            }
413
414            if ( $this->needSlots ) {
415                $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT );
416                if ( ( $this->fld_slotsha1 || $this->fetchContent ) && ( $revDel & self::IS_DELETED ) ) {
417                    $anyHidden = true;
418                }
419                $vals = array_merge( $vals, $this->extractAllSlotInfo( $revision, $revDel ) );
420            }
421        } catch ( RevisionAccessException $ex ) {
422            // This is here so T212428 doesn't spam the log.
423            // TODO: find out why T212428 happens in the first place!
424            $vals['slotsmissing'] = true;
425
426            LoggerFactory::getInstance( 'api-warning' )->error(
427                'Failed to access revision slots',
428                [ 'revision' => $revision->getId(), 'exception' => $ex, ]
429            );
430        }
431
432        if ( $this->fld_comment || $this->fld_parsedcomment ) {
433            $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_COMMENT );
434            if ( $revDel & self::IS_DELETED ) {
435                $vals['commenthidden'] = true;
436                $anyHidden = true;
437            }
438            if ( !( $revDel & self::CANNOT_VIEW ) ) {
439                $comment = $revision->getComment( RevisionRecord::RAW );
440                $comment = $comment->text ?? '';
441
442                if ( $this->fld_comment ) {
443                    $vals['comment'] = $comment;
444                }
445
446                if ( $this->fld_parsedcomment ) {
447                    $vals['parsedcomment'] = $this->commentFormatter->format(
448                        $comment, $revision->getPageAsLinkTarget()
449                    );
450                }
451            }
452        }
453
454        if ( $this->fld_tags ) {
455            if ( $row->ts_tags ) {
456                $tags = explode( ',', $row->ts_tags );
457                ApiResult::setIndexedTagName( $tags, 'tag' );
458                $vals['tags'] = $tags;
459            } else {
460                $vals['tags'] = [];
461            }
462        }
463
464        if ( $anyHidden && $revision->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
465            $vals['suppressed'] = true;
466        }
467
468        return $vals;
469    }
470
471    /**
472     * Extracts information about all relevant slots.
473     *
474     * @param RevisionRecord $revision
475     * @param int $revDel
476     *
477     * @return array
478     * @throws ApiUsageException
479     */
480    private function extractAllSlotInfo( RevisionRecord $revision, $revDel ): array {
481        $vals = [];
482
483        if ( $this->slotRoles === null ) {
484            try {
485                $slot = $revision->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
486            } catch ( RevisionAccessException ) {
487                // Back compat: If there's no slot, there's no content, so set 'textmissing'
488                // @todo: Gergő says to mention T198099 as a "todo" here.
489                $vals['textmissing'] = true;
490                $slot = null;
491            }
492
493            if ( $slot ) {
494                $content = null;
495                $vals += $this->extractSlotInfo( $slot, $revDel, $content );
496                if ( !empty( $vals['nosuchsection'] ) ) {
497                    $this->dieWithError(
498                        [
499                            'apierror-nosuchsection-what',
500                            wfEscapeWikiText( $this->section ),
501                            $this->msg( 'revid', $revision->getId() )
502                        ],
503                        'nosuchsection'
504                    );
505                }
506                if ( $content ) {
507                    $vals += $this->extractDeprecatedContent( $content, $revision );
508                }
509            }
510        } else {
511            $roles = array_intersect( $this->slotRoles, $revision->getSlotRoles() );
512            $vals['slots'] = [
513                ApiResult::META_KVP_MERGE => true,
514            ];
515            foreach ( $roles as $role ) {
516                try {
517                    $slot = $revision->getSlot( $role, RevisionRecord::RAW );
518                } catch ( RevisionAccessException ) {
519                    // Don't error out here so the client can still process other slots/revisions.
520                    // @todo: Gergő says to mention T198099 as a "todo" here.
521                    $vals['slots'][$role]['missing'] = true;
522                    continue;
523                }
524                $content = null;
525                $vals['slots'][$role] = $this->extractSlotInfo( $slot, $revDel, $content );
526                // @todo Move this into extractSlotInfo() (and remove its $content parameter)
527                // when extractDeprecatedContent() is no more.
528                if ( $content && $this->getAuthority()->authorizeRead( 'read', $revision->getPage() ) ) {
529                    /** @var Content $content */
530                    $model = $content->getModel();
531                    $format = $this->slotContentFormats[$role] ?? $content->getDefaultFormat();
532                    if ( !$content->isSupportedFormat( $format ) ) {
533                        $this->addWarning( [
534                            'apierror-badformat',
535                            $format,
536                            $model,
537                            $this->msg( 'revid', $revision->getId() )
538                        ] );
539                        $vals['slots'][$role]['badcontentformat'] = true;
540                    } else {
541                        $vals['slots'][$role]['contentmodel'] = $model;
542                        $vals['slots'][$role]['contentformat'] = $format;
543                        ApiResult::setContentValue(
544                            $vals['slots'][$role],
545                            'content',
546                            $content->serialize( $format )
547                        );
548                    }
549                }
550            }
551            ApiResult::setArrayType( $vals['slots'], 'kvp', 'role' );
552            ApiResult::setIndexedTagName( $vals['slots'], 'slot' );
553        }
554        return $vals;
555    }
556
557    /**
558     * Extract information from the SlotRecord
559     *
560     * @param SlotRecord $slot
561     * @param int $revDel Revdel status flags, from self::checkRevDel()
562     * @param Content|null &$content Set to the slot's content, if available
563     *  and $this->fetchContent is true
564     * @return array
565     */
566    private function extractSlotInfo( SlotRecord $slot, $revDel, &$content = null ) {
567        $vals = [];
568        ApiResult::setArrayType( $vals, 'assoc' );
569
570        if ( $this->fld_slotsize ) {
571            $vals['size'] = (int)$slot->getSize();
572        }
573
574        if ( $this->fld_slotsha1 ) {
575            if ( $revDel & self::IS_DELETED ) {
576                $vals['sha1hidden'] = true;
577            }
578            if ( !( $revDel & self::CANNOT_VIEW ) ) {
579                if ( $slot->getSha1() != '' ) {
580                    $vals['sha1'] = \Wikimedia\base_convert( $slot->getSha1(), 36, 16, 40 );
581                } else {
582                    $vals['sha1'] = '';
583                }
584            }
585        }
586
587        if ( $this->fld_contentmodel ) {
588            $vals['contentmodel'] = $slot->getModel();
589        }
590
591        $content = null;
592        if ( $this->fetchContent ) {
593            if ( $revDel & self::IS_DELETED ) {
594                $vals['texthidden'] = true;
595            }
596            if ( !( $revDel & self::CANNOT_VIEW ) ) {
597                try {
598                    $content = $slot->getContent();
599                } catch ( RevisionAccessException ) {
600                    // @todo: Gergő says to mention T198099 as a "todo" here.
601                    $vals['textmissing'] = true;
602                }
603                // Expand templates after getting section content because
604                // template-added sections don't count and Parser::preprocess()
605                // will have less input
606                if ( $content && $this->section !== false ) {
607                    $content = $content->getSection( $this->section );
608                    if ( !$content ) {
609                        $vals['nosuchsection'] = true;
610                    }
611                }
612            }
613        }
614
615        return $vals;
616    }
617
618    /**
619     * Format a Content using deprecated options
620     * @param Content $content Content to format
621     * @param RevisionRecord $revision Revision being processed
622     * @return array
623     */
624    private function extractDeprecatedContent( Content $content, RevisionRecord $revision ) {
625        $vals = [];
626        $title = Title::newFromPageIdentity( $revision->getPage() );
627
628        if ( !$this->getAuthority()->authorizeRead( 'read', $title ) ) {
629            return [];
630        }
631
632        if ( $this->fld_parsetree || ( $this->fld_content && $this->generateXML ) ) {
633            if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
634                /** @var WikitextContent $content */
635                '@phan-var WikitextContent $content';
636                $t = $content->getText(); # note: don't set $text
637
638                $parser = $this->parserFactory->create();
639                $parser->startExternalParse(
640                    $title,
641                    ParserOptions::newFromContext( $this->getContext() ),
642                    Parser::OT_PREPROCESS
643                );
644                $dom = $parser->preprocessToDom( $t );
645                if ( is_callable( [ $dom, 'saveXML' ] ) ) {
646                    // @phan-suppress-next-line PhanUndeclaredMethod
647                    $xml = $dom->saveXML();
648                } else {
649                    // @phan-suppress-next-line PhanUndeclaredMethod
650                    $xml = $dom->__toString();
651                }
652                $vals['parsetree'] = $xml;
653            } else {
654                $vals['badcontentformatforparsetree'] = true;
655                $this->addWarning(
656                    [
657                        'apierror-parsetree-notwikitext-title',
658                        wfEscapeWikiText( $title->getPrefixedText() ),
659                        $content->getModel()
660                    ],
661                    'parsetree-notwikitext'
662                );
663            }
664        }
665
666        if ( $this->fld_content ) {
667            $text = null;
668
669            if ( $this->expandTemplates && !$this->parseContent ) {
670                if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
671                    /** @var WikitextContent $content */
672                    '@phan-var WikitextContent $content';
673                    $text = $content->getText();
674
675                    $text = $this->parserFactory->create()->preprocess(
676                        $text,
677                        $title,
678                        ParserOptions::newFromContext( $this->getContext() )
679                    );
680                } else {
681                    $this->addWarning( [
682                        'apierror-templateexpansion-notwikitext',
683                        wfEscapeWikiText( $title->getPrefixedText() ),
684                        $content->getModel()
685                    ] );
686                    $vals['badcontentformat'] = true;
687                    $text = false;
688                }
689            }
690            if ( $this->parseContent ) {
691                $popts = ParserOptions::newFromContext( $this->getContext() );
692                $po = $this->contentRenderer->getParserOutput(
693                    $content,
694                    $title,
695                    $revision,
696                    $popts
697                );
698                // TODO T371004 move runOutputPipeline out of $parserOutput
699                $text = $po->runOutputPipeline( $popts, [] )->getContentHolderText();
700            }
701
702            if ( $text === null ) {
703                $format = $this->contentFormat ?: $content->getDefaultFormat();
704                $model = $content->getModel();
705
706                if ( !$content->isSupportedFormat( $format ) ) {
707                    $name = wfEscapeWikiText( $title->getPrefixedText() );
708                    $this->addWarning( [ 'apierror-badformat', $this->contentFormat, $model, $name ] );
709                    $vals['badcontentformat'] = true;
710                    $text = false;
711                } else {
712                    $text = $content->serialize( $format );
713                    // always include format and model.
714                    // Format is needed to deserialize, model is needed to interpret.
715                    $vals['contentformat'] = $format;
716                    $vals['contentmodel'] = $model;
717                }
718            }
719
720            if ( $text !== false ) {
721                ApiResult::setContentValue( $vals, 'content', $text );
722            }
723        }
724
725        if ( $content && ( $this->diffto !== null || $this->difftotext !== null ) ) {
726            if ( $this->numUncachedDiffs < $this->getConfig()->get( MainConfigNames::APIMaxUncachedDiffs ) ) {
727                $vals['diff'] = [];
728                $context = new DerivativeContext( $this->getContext() );
729                $context->setTitle( $title );
730                $handler = $content->getContentHandler();
731
732                if ( $this->difftotext !== null ) {
733                    $model = $title->getContentModel();
734
735                    if ( $this->contentFormat
736                        && !$this->contentHandlerFactory->getContentHandler( $model )
737                            ->isSupportedFormat( $this->contentFormat )
738                    ) {
739                        $name = wfEscapeWikiText( $title->getPrefixedText() );
740                        $this->addWarning( [ 'apierror-badformat', $this->contentFormat, $model, $name ] );
741                        $vals['diff']['badcontentformat'] = true;
742                        $engine = null;
743                    } else {
744                        $difftocontent = $this->contentHandlerFactory->getContentHandler( $model )
745                            ->unserializeContent( $this->difftotext, $this->contentFormat );
746
747                        if ( $this->difftotextpst ) {
748                            $popts = ParserOptions::newFromContext( $this->getContext() );
749                            $difftocontent = $this->contentTransformer->preSaveTransform(
750                                $difftocontent,
751                                $title,
752                                $this->getUserForPreview(),
753                                $popts
754                            );
755                        }
756
757                        $engine = $handler->createDifferenceEngine( $context );
758                        $engine->setContent( $content, $difftocontent );
759                    }
760                } else {
761                    $engine = $handler->createDifferenceEngine( $context, $revision->getId(), $this->diffto );
762                    $vals['diff']['from'] = $engine->getOldid();
763                    $vals['diff']['to'] = $engine->getNewid();
764                }
765                if ( $engine ) {
766                    $difftext = $engine->getDiffBody();
767                    ApiResult::setContentValue( $vals['diff'], 'body', $difftext );
768                    if ( !$engine->wasCacheHit() ) {
769                        $this->numUncachedDiffs++;
770                    }
771                    foreach ( $engine->getRevisionLoadErrors() as $msg ) {
772                        $this->addWarning( $msg );
773                    }
774                }
775            } else {
776                $vals['diff']['notcached'] = true;
777            }
778        }
779
780        return $vals;
781    }
782
783    private function getUserForPreview(): UserIdentity {
784        $user = $this->getUser();
785        if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
786            return $this->userFactory->newUnsavedTempUser(
787                $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
788            );
789        }
790        return $user;
791    }
792
793    /**
794     * @stable to override
795     * @param array $params
796     *
797     * @return string
798     */
799    public function getCacheMode( $params ) {
800        if ( $this->userCanSeeRevDel() ) {
801            return 'private';
802        }
803
804        return 'public';
805    }
806
807    /**
808     * @stable to override
809     * @return array
810     */
811    public function getAllowedParams() {
812        $slotRoles = $this->slotRoleRegistry->getKnownRoles();
813        sort( $slotRoles, SORT_STRING );
814        $smallLimit = $this->getMain()->canApiHighLimits() ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_SML1;
815
816        return [
817            'prop' => [
818                ParamValidator::PARAM_ISMULTI => true,
819                ParamValidator::PARAM_DEFAULT => 'ids|timestamp|flags|comment|user',
820                ParamValidator::PARAM_TYPE => [
821                    'ids',
822                    'flags',
823                    'timestamp',
824                    'user',
825                    'userid',
826                    'size',
827                    'slotsize',
828                    'sha1',
829                    'slotsha1',
830                    'contentmodel',
831                    'comment',
832                    'parsedcomment',
833                    'content',
834                    'tags',
835                    'roles',
836                    'parsetree',
837                ],
838                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-prop',
839                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
840                    'ids' => 'apihelp-query+revisions+base-paramvalue-prop-ids',
841                    'flags' => 'apihelp-query+revisions+base-paramvalue-prop-flags',
842                    'timestamp' => 'apihelp-query+revisions+base-paramvalue-prop-timestamp',
843                    'user' => 'apihelp-query+revisions+base-paramvalue-prop-user',
844                    'userid' => 'apihelp-query+revisions+base-paramvalue-prop-userid',
845                    'size' => 'apihelp-query+revisions+base-paramvalue-prop-size',
846                    'slotsize' => 'apihelp-query+revisions+base-paramvalue-prop-slotsize',
847                    'sha1' => 'apihelp-query+revisions+base-paramvalue-prop-sha1',
848                    'slotsha1' => 'apihelp-query+revisions+base-paramvalue-prop-slotsha1',
849                    'contentmodel' => 'apihelp-query+revisions+base-paramvalue-prop-contentmodel',
850                    'comment' => 'apihelp-query+revisions+base-paramvalue-prop-comment',
851                    'parsedcomment' => 'apihelp-query+revisions+base-paramvalue-prop-parsedcomment',
852                    'content' => [ 'apihelp-query+revisions+base-paramvalue-prop-content', $smallLimit ],
853                    'tags' => 'apihelp-query+revisions+base-paramvalue-prop-tags',
854                    'roles' => 'apihelp-query+revisions+base-paramvalue-prop-roles',
855                    'parsetree' => [ 'apihelp-query+revisions+base-paramvalue-prop-parsetree',
856                        CONTENT_MODEL_WIKITEXT, $smallLimit ],
857                ],
858                EnumDef::PARAM_DEPRECATED_VALUES => [
859                    'parsetree' => true,
860                ],
861            ],
862            'slots' => [
863                ParamValidator::PARAM_TYPE => $slotRoles,
864                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-slots',
865                ParamValidator::PARAM_ISMULTI => true,
866                ParamValidator::PARAM_ALL => true,
867            ],
868            'contentformat-{slot}' => [
869                ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ],
870                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-contentformat-slot',
871                ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
872            ],
873            'limit' => [
874                ParamValidator::PARAM_TYPE => 'limit',
875                IntegerDef::PARAM_MIN => 1,
876                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
877                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
878                ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-limit',
879                    $smallLimit, self::LIMIT_PARSE ],
880            ],
881            'expandtemplates' => [
882                ParamValidator::PARAM_DEFAULT => false,
883                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-expandtemplates',
884                ParamValidator::PARAM_DEPRECATED => true,
885            ],
886            'generatexml' => [
887                ParamValidator::PARAM_DEFAULT => false,
888                ParamValidator::PARAM_DEPRECATED => true,
889                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-generatexml',
890            ],
891            'parse' => [
892                ParamValidator::PARAM_DEFAULT => false,
893                ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-parse', self::LIMIT_PARSE ],
894                ParamValidator::PARAM_DEPRECATED => true,
895            ],
896            'section' => [
897                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-section',
898            ],
899            'diffto' => [
900                ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-diffto', $smallLimit ],
901                ParamValidator::PARAM_DEPRECATED => true,
902            ],
903            'difftotext' => [
904                ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-difftotext', $smallLimit ],
905                ParamValidator::PARAM_DEPRECATED => true,
906            ],
907            'difftotextpst' => [
908                ParamValidator::PARAM_DEFAULT => false,
909                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-difftotextpst',
910                ParamValidator::PARAM_DEPRECATED => true,
911            ],
912            'contentformat' => [
913                ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
914                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-contentformat',
915                ParamValidator::PARAM_DEPRECATED => true,
916            ],
917        ];
918    }
919}
920
921/** @deprecated class alias since 1.43 */
922class_alias( ApiQueryRevisionsBase::class, 'ApiQueryRevisionsBase' );