Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 482
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 / 481
0.00% covered (danger)
0.00%
0 / 12
18360
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 / 91
0.00% covered (danger)
0.00%
0 / 1
1260
 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                $revDel = $this->checkRevDel( $difftoRev, RevisionRecord::DELETED_TEXT );
255                if ( $revDel & self::CANNOT_VIEW ) {
256                    $this->addWarning( [ 'apiwarn-difftohidden', $difftoRev->getId() ] );
257                    $params['diffto'] = null;
258                }
259            }
260            $this->diffto = $params['diffto'];
261        }
262
263        $this->fetchContent = $this->fld_content || $this->diffto !== null
264            || $this->difftotext !== null || $this->fld_parsetree;
265
266        $smallLimit = false;
267        if ( $this->fetchContent ) {
268            $smallLimit = true;
269            $this->expandTemplates = $params['expandtemplates'];
270            $this->generateXML = $params['generatexml'];
271            $this->parseContent = $params['parse'];
272            if ( $this->parseContent ) {
273                // Must manually initialize unset limit
274                $this->limit ??= self::LIMIT_PARSE;
275            }
276            $this->section = $params['section'] ?? false;
277        }
278
279        $userMax = $this->parseContent ? self::LIMIT_PARSE :
280            ( $smallLimit ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 );
281        $botMax = $this->parseContent ? self::LIMIT_PARSE :
282            ( $smallLimit ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 );
283        if ( $this->limit == 'max' ) {
284            $this->limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
285            if ( $this->setParsedLimit ) {
286                $this->getResult()->addParsedLimit( $this->getModuleName(), $this->limit );
287            }
288        }
289
290        $this->limit = $this->getMain()->getParamValidator()->validateValue(
291            $this, 'limit', $this->limit ?? 10, [
292                ParamValidator::PARAM_TYPE => 'limit',
293                IntegerDef::PARAM_MIN => 1,
294                IntegerDef::PARAM_MAX => $userMax,
295                IntegerDef::PARAM_MAX2 => $botMax,
296                IntegerDef::PARAM_IGNORE_RANGE => true,
297            ]
298        );
299
300        $this->needSlots = $this->fetchContent || $this->fld_contentmodel ||
301            $this->fld_slotsize || $this->fld_slotsha1;
302    }
303
304    /**
305     * Test revision deletion status
306     * @param RevisionRecord $revision Revision to check
307     * @param int $field One of the RevisionRecord::DELETED_* constants
308     * @return int Revision deletion status flags. Bitwise OR of
309     *  self::IS_DELETED and self::CANNOT_VIEW, as appropriate.
310     */
311    private function checkRevDel( RevisionRecord $revision, $field ) {
312        $ret = $revision->isDeleted( $field ) ? self::IS_DELETED : 0;
313        if ( $ret ) {
314            $canSee = $revision->userCan( $field, $this->getAuthority() );
315            $ret |= ( $canSee ? 0 : self::CANNOT_VIEW );
316        }
317        return $ret;
318    }
319
320    /**
321     * Extract information from the RevisionRecord
322     *
323     * @since 1.32, takes a RevisionRecord instead of a Revision
324     * @param RevisionRecord $revision
325     * @param stdClass $row Should have a field 'ts_tags' if $this->fld_tags is set
326     * @return array
327     */
328    protected function extractRevisionInfo( RevisionRecord $revision, $row ) {
329        $vals = [];
330        $anyHidden = false;
331
332        if ( $this->fld_ids ) {
333            $vals['revid'] = (int)$revision->getId();
334            if ( $revision->getParentId() !== null ) {
335                $vals['parentid'] = (int)$revision->getParentId();
336            }
337        }
338
339        if ( $this->fld_flags ) {
340            $vals['minor'] = $revision->isMinor();
341        }
342
343        if ( $this->fld_user || $this->fld_userid ) {
344            $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_USER );
345            if ( $revDel & self::IS_DELETED ) {
346                $vals['userhidden'] = true;
347                $anyHidden = true;
348            }
349            if ( !( $revDel & self::CANNOT_VIEW ) ) {
350                $u = $revision->getUser( RevisionRecord::RAW );
351                if ( $this->fld_user ) {
352                    $vals['user'] = $u->getName();
353                }
354                if ( $this->userNameUtils->isTemp( $u->getName() ) ) {
355                    $vals['temp'] = true;
356                }
357                if ( !$u->isRegistered() ) {
358                    $vals['anon'] = true;
359                }
360
361                if ( $this->fld_userid ) {
362                    $vals['userid'] = $u->getId();
363                }
364            }
365        }
366
367        if ( $this->fld_timestamp ) {
368            $vals['timestamp'] = wfTimestamp( TS::ISO_8601, $revision->getTimestamp() );
369        }
370
371        if ( $this->fld_size ) {
372            try {
373                $vals['size'] = (int)$revision->getSize();
374            } catch ( RevisionAccessException ) {
375                // Back compat: If there's no size, return 0.
376                // @todo: Gergő says to mention T198099 as a "todo" here.
377                $vals['size'] = 0;
378            }
379        }
380
381        if ( $this->fld_sha1 ) {
382            $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT );
383            if ( $revDel & self::IS_DELETED ) {
384                $vals['sha1hidden'] = true;
385                $anyHidden = true;
386            }
387            if ( !( $revDel & self::CANNOT_VIEW ) ) {
388                try {
389                    $vals['sha1'] = \Wikimedia\base_convert( $revision->getSha1(), 36, 16, 40 );
390                } catch ( RevisionAccessException ) {
391                    // Back compat: If there's no sha1, return empty string.
392                    // @todo: Gergő says to mention T198099 as a "todo" here.
393                    $vals['sha1'] = '';
394                }
395            }
396        }
397
398        try {
399            if ( $this->fld_roles ) {
400                $vals['roles'] = $revision->getSlotRoles();
401            }
402
403            if ( $this->needSlots ) {
404                $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_TEXT );
405                if ( ( $this->fld_slotsha1 || $this->fetchContent ) && ( $revDel & self::IS_DELETED ) ) {
406                    $anyHidden = true;
407                }
408                $vals = array_merge( $vals, $this->extractAllSlotInfo( $revision, $revDel ) );
409            }
410        } catch ( RevisionAccessException $ex ) {
411            // This is here so T212428 doesn't spam the log.
412            // TODO: find out why T212428 happens in the first place!
413            $vals['slotsmissing'] = true;
414
415            LoggerFactory::getInstance( 'api-warning' )->error(
416                'Failed to access revision slots',
417                [ 'revision' => $revision->getId(), 'exception' => $ex, ]
418            );
419        }
420
421        if ( $this->fld_comment || $this->fld_parsedcomment ) {
422            $revDel = $this->checkRevDel( $revision, RevisionRecord::DELETED_COMMENT );
423            if ( $revDel & self::IS_DELETED ) {
424                $vals['commenthidden'] = true;
425                $anyHidden = true;
426            }
427            if ( !( $revDel & self::CANNOT_VIEW ) ) {
428                $comment = $revision->getComment( RevisionRecord::RAW );
429                $comment = $comment->text ?? '';
430
431                if ( $this->fld_comment ) {
432                    $vals['comment'] = $comment;
433                }
434
435                if ( $this->fld_parsedcomment ) {
436                    $vals['parsedcomment'] = $this->commentFormatter->format(
437                        $comment, $revision->getPageAsLinkTarget()
438                    );
439                }
440            }
441        }
442
443        if ( $this->fld_tags ) {
444            if ( $row->ts_tags ) {
445                $tags = explode( ',', $row->ts_tags );
446                ApiResult::setIndexedTagName( $tags, 'tag' );
447                $vals['tags'] = $tags;
448            } else {
449                $vals['tags'] = [];
450            }
451        }
452
453        if ( $anyHidden && $revision->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
454            $vals['suppressed'] = true;
455        }
456
457        return $vals;
458    }
459
460    /**
461     * Extracts information about all relevant slots.
462     *
463     * @param RevisionRecord $revision
464     * @param int $revDel
465     *
466     * @return array
467     * @throws ApiUsageException
468     */
469    private function extractAllSlotInfo( RevisionRecord $revision, $revDel ): array {
470        $vals = [];
471
472        if ( $this->slotRoles === null ) {
473            try {
474                $slot = $revision->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
475            } catch ( RevisionAccessException ) {
476                // Back compat: If there's no slot, there's no content, so set 'textmissing'
477                // @todo: Gergő says to mention T198099 as a "todo" here.
478                $vals['textmissing'] = true;
479                $slot = null;
480            }
481
482            if ( $slot ) {
483                $content = null;
484                $vals += $this->extractSlotInfo( $slot, $revDel, $content );
485                if ( !empty( $vals['nosuchsection'] ) ) {
486                    $this->dieWithError(
487                        [
488                            'apierror-nosuchsection-what',
489                            wfEscapeWikiText( $this->section ),
490                            $this->msg( 'revid', $revision->getId() )
491                        ],
492                        'nosuchsection'
493                    );
494                }
495                if ( $content ) {
496                    $vals += $this->extractDeprecatedContent( $content, $revision );
497                }
498            }
499        } else {
500            $roles = array_intersect( $this->slotRoles, $revision->getSlotRoles() );
501            $vals['slots'] = [
502                ApiResult::META_KVP_MERGE => true,
503            ];
504            foreach ( $roles as $role ) {
505                try {
506                    $slot = $revision->getSlot( $role, RevisionRecord::RAW );
507                } catch ( RevisionAccessException ) {
508                    // Don't error out here so the client can still process other slots/revisions.
509                    // @todo: Gergő says to mention T198099 as a "todo" here.
510                    $vals['slots'][$role]['missing'] = true;
511                    continue;
512                }
513                $content = null;
514                $vals['slots'][$role] = $this->extractSlotInfo( $slot, $revDel, $content );
515                // @todo Move this into extractSlotInfo() (and remove its $content parameter)
516                // when extractDeprecatedContent() is no more.
517                if ( $content && $this->getAuthority()->authorizeRead( 'read', $revision->getPage() ) ) {
518                    /** @var Content $content */
519                    $model = $content->getModel();
520                    $format = $this->slotContentFormats[$role] ?? $content->getDefaultFormat();
521                    if ( !$content->isSupportedFormat( $format ) ) {
522                        $this->addWarning( [
523                            'apierror-badformat',
524                            $format,
525                            $model,
526                            $this->msg( 'revid', $revision->getId() )
527                        ] );
528                        $vals['slots'][$role]['badcontentformat'] = true;
529                    } else {
530                        $vals['slots'][$role]['contentmodel'] = $model;
531                        $vals['slots'][$role]['contentformat'] = $format;
532                        ApiResult::setContentValue(
533                            $vals['slots'][$role],
534                            'content',
535                            $content->serialize( $format )
536                        );
537                    }
538                }
539            }
540            ApiResult::setArrayType( $vals['slots'], 'kvp', 'role' );
541            ApiResult::setIndexedTagName( $vals['slots'], 'slot' );
542        }
543        return $vals;
544    }
545
546    /**
547     * Extract information from the SlotRecord
548     *
549     * @param SlotRecord $slot
550     * @param int $revDel Revdel status flags, from self::checkRevDel()
551     * @param Content|null &$content Set to the slot's content, if available
552     *  and $this->fetchContent is true
553     * @return array
554     */
555    private function extractSlotInfo( SlotRecord $slot, $revDel, &$content = null ) {
556        $vals = [];
557        ApiResult::setArrayType( $vals, 'assoc' );
558
559        if ( $this->fld_slotsize ) {
560            $vals['size'] = (int)$slot->getSize();
561        }
562
563        if ( $this->fld_slotsha1 ) {
564            if ( $revDel & self::IS_DELETED ) {
565                $vals['sha1hidden'] = true;
566            }
567            if ( !( $revDel & self::CANNOT_VIEW ) ) {
568                if ( $slot->getSha1() != '' ) {
569                    $vals['sha1'] = \Wikimedia\base_convert( $slot->getSha1(), 36, 16, 40 );
570                } else {
571                    $vals['sha1'] = '';
572                }
573            }
574        }
575
576        if ( $this->fld_contentmodel ) {
577            $vals['contentmodel'] = $slot->getModel();
578        }
579
580        $content = null;
581        if ( $this->fetchContent ) {
582            if ( $revDel & self::IS_DELETED ) {
583                $vals['texthidden'] = true;
584            }
585            if ( !( $revDel & self::CANNOT_VIEW ) ) {
586                try {
587                    $content = $slot->getContent();
588                } catch ( RevisionAccessException ) {
589                    // @todo: Gergő says to mention T198099 as a "todo" here.
590                    $vals['textmissing'] = true;
591                }
592                // Expand templates after getting section content because
593                // template-added sections don't count and Parser::preprocess()
594                // will have less input
595                if ( $content && $this->section !== false ) {
596                    $content = $content->getSection( $this->section );
597                    if ( !$content ) {
598                        $vals['nosuchsection'] = true;
599                    }
600                }
601            }
602        }
603
604        return $vals;
605    }
606
607    /**
608     * Format a Content using deprecated options
609     * @param Content $content Content to format
610     * @param RevisionRecord $revision Revision being processed
611     * @return array
612     */
613    private function extractDeprecatedContent( Content $content, RevisionRecord $revision ) {
614        $vals = [];
615        $title = Title::newFromPageIdentity( $revision->getPage() );
616
617        if ( !$this->getAuthority()->authorizeRead( 'read', $title ) ) {
618            return [];
619        }
620
621        if ( $this->fld_parsetree || ( $this->fld_content && $this->generateXML ) ) {
622            if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
623                /** @var WikitextContent $content */
624                '@phan-var WikitextContent $content';
625                $t = $content->getText(); # note: don't set $text
626
627                $parser = $this->parserFactory->create();
628                $parser->startExternalParse(
629                    $title,
630                    ParserOptions::newFromContext( $this->getContext() ),
631                    Parser::OT_PREPROCESS
632                );
633                $dom = $parser->preprocessToDom( $t );
634                if ( is_callable( [ $dom, 'saveXML' ] ) ) {
635                    // @phan-suppress-next-line PhanUndeclaredMethod
636                    $xml = $dom->saveXML();
637                } else {
638                    // @phan-suppress-next-line PhanUndeclaredMethod
639                    $xml = $dom->__toString();
640                }
641                $vals['parsetree'] = $xml;
642            } else {
643                $vals['badcontentformatforparsetree'] = true;
644                $this->addWarning(
645                    [
646                        'apierror-parsetree-notwikitext-title',
647                        wfEscapeWikiText( $title->getPrefixedText() ),
648                        $content->getModel()
649                    ],
650                    'parsetree-notwikitext'
651                );
652            }
653        }
654
655        if ( $this->fld_content ) {
656            $text = null;
657
658            if ( $this->expandTemplates && !$this->parseContent ) {
659                if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
660                    /** @var WikitextContent $content */
661                    '@phan-var WikitextContent $content';
662                    $text = $content->getText();
663
664                    $text = $this->parserFactory->create()->preprocess(
665                        $text,
666                        $title,
667                        ParserOptions::newFromContext( $this->getContext() )
668                    );
669                } else {
670                    $this->addWarning( [
671                        'apierror-templateexpansion-notwikitext',
672                        wfEscapeWikiText( $title->getPrefixedText() ),
673                        $content->getModel()
674                    ] );
675                    $vals['badcontentformat'] = true;
676                    $text = false;
677                }
678            }
679            if ( $this->parseContent ) {
680                $popts = ParserOptions::newFromContext( $this->getContext() );
681                $po = $this->contentRenderer->getParserOutput(
682                    $content,
683                    $title,
684                    $revision,
685                    $popts
686                );
687                // TODO T371004 move runOutputPipeline out of $parserOutput
688                $text = $po->runOutputPipeline( $popts, [] )->getContentHolderText();
689            }
690
691            if ( $text === null ) {
692                $format = $this->contentFormat ?: $content->getDefaultFormat();
693                $model = $content->getModel();
694
695                if ( !$content->isSupportedFormat( $format ) ) {
696                    $name = wfEscapeWikiText( $title->getPrefixedText() );
697                    $this->addWarning( [ 'apierror-badformat', $this->contentFormat, $model, $name ] );
698                    $vals['badcontentformat'] = true;
699                    $text = false;
700                } else {
701                    $text = $content->serialize( $format );
702                    // always include format and model.
703                    // Format is needed to deserialize, model is needed to interpret.
704                    $vals['contentformat'] = $format;
705                    $vals['contentmodel'] = $model;
706                }
707            }
708
709            if ( $text !== false ) {
710                ApiResult::setContentValue( $vals, 'content', $text );
711            }
712        }
713
714        if ( $content && ( $this->diffto !== null || $this->difftotext !== null ) ) {
715            if ( $this->numUncachedDiffs < $this->getConfig()->get( MainConfigNames::APIMaxUncachedDiffs ) ) {
716                $vals['diff'] = [];
717                $context = new DerivativeContext( $this->getContext() );
718                $context->setTitle( $title );
719                $handler = $content->getContentHandler();
720
721                if ( $this->difftotext !== null ) {
722                    $model = $title->getContentModel();
723
724                    if ( $this->contentFormat
725                        && !$this->contentHandlerFactory->getContentHandler( $model )
726                            ->isSupportedFormat( $this->contentFormat )
727                    ) {
728                        $name = wfEscapeWikiText( $title->getPrefixedText() );
729                        $this->addWarning( [ 'apierror-badformat', $this->contentFormat, $model, $name ] );
730                        $vals['diff']['badcontentformat'] = true;
731                        $engine = null;
732                    } else {
733                        $difftocontent = $this->contentHandlerFactory->getContentHandler( $model )
734                            ->unserializeContent( $this->difftotext, $this->contentFormat );
735
736                        if ( $this->difftotextpst ) {
737                            $popts = ParserOptions::newFromContext( $this->getContext() );
738                            $difftocontent = $this->contentTransformer->preSaveTransform(
739                                $difftocontent,
740                                $title,
741                                $this->getUserForPreview(),
742                                $popts
743                            );
744                        }
745
746                        $engine = $handler->createDifferenceEngine( $context );
747                        $engine->setContent( $content, $difftocontent );
748                    }
749                } else {
750                    $engine = $handler->createDifferenceEngine( $context, $revision->getId(), $this->diffto );
751                    $vals['diff']['from'] = $engine->getOldid();
752                    $vals['diff']['to'] = $engine->getNewid();
753                }
754                if ( $engine ) {
755                    $difftext = $engine->getDiffBody();
756                    ApiResult::setContentValue( $vals['diff'], 'body', $difftext );
757                    if ( !$engine->wasCacheHit() ) {
758                        $this->numUncachedDiffs++;
759                    }
760                    foreach ( $engine->getRevisionLoadErrors() as $msg ) {
761                        $this->addWarning( $msg );
762                    }
763                }
764            } else {
765                $vals['diff']['notcached'] = true;
766            }
767        }
768
769        return $vals;
770    }
771
772    private function getUserForPreview(): UserIdentity {
773        $user = $this->getUser();
774        if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
775            return $this->userFactory->newUnsavedTempUser(
776                $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
777            );
778        }
779        return $user;
780    }
781
782    /**
783     * @stable to override
784     * @param array $params
785     *
786     * @return string
787     */
788    public function getCacheMode( $params ) {
789        if ( $this->userCanSeeRevDel() ) {
790            return 'private';
791        }
792
793        return 'public';
794    }
795
796    /**
797     * @stable to override
798     * @return array
799     */
800    public function getAllowedParams() {
801        $slotRoles = $this->slotRoleRegistry->getKnownRoles();
802        sort( $slotRoles, SORT_STRING );
803        $smallLimit = $this->getMain()->canApiHighLimits() ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_SML1;
804
805        return [
806            'prop' => [
807                ParamValidator::PARAM_ISMULTI => true,
808                ParamValidator::PARAM_DEFAULT => 'ids|timestamp|flags|comment|user',
809                ParamValidator::PARAM_TYPE => [
810                    'ids',
811                    'flags',
812                    'timestamp',
813                    'user',
814                    'userid',
815                    'size',
816                    'slotsize',
817                    'sha1',
818                    'slotsha1',
819                    'contentmodel',
820                    'comment',
821                    'parsedcomment',
822                    'content',
823                    'tags',
824                    'roles',
825                    'parsetree',
826                ],
827                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-prop',
828                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
829                    'ids' => 'apihelp-query+revisions+base-paramvalue-prop-ids',
830                    'flags' => 'apihelp-query+revisions+base-paramvalue-prop-flags',
831                    'timestamp' => 'apihelp-query+revisions+base-paramvalue-prop-timestamp',
832                    'user' => 'apihelp-query+revisions+base-paramvalue-prop-user',
833                    'userid' => 'apihelp-query+revisions+base-paramvalue-prop-userid',
834                    'size' => 'apihelp-query+revisions+base-paramvalue-prop-size',
835                    'slotsize' => 'apihelp-query+revisions+base-paramvalue-prop-slotsize',
836                    'sha1' => 'apihelp-query+revisions+base-paramvalue-prop-sha1',
837                    'slotsha1' => 'apihelp-query+revisions+base-paramvalue-prop-slotsha1',
838                    'contentmodel' => 'apihelp-query+revisions+base-paramvalue-prop-contentmodel',
839                    'comment' => 'apihelp-query+revisions+base-paramvalue-prop-comment',
840                    'parsedcomment' => 'apihelp-query+revisions+base-paramvalue-prop-parsedcomment',
841                    'content' => [ 'apihelp-query+revisions+base-paramvalue-prop-content', $smallLimit ],
842                    'tags' => 'apihelp-query+revisions+base-paramvalue-prop-tags',
843                    'roles' => 'apihelp-query+revisions+base-paramvalue-prop-roles',
844                    'parsetree' => [ 'apihelp-query+revisions+base-paramvalue-prop-parsetree',
845                        CONTENT_MODEL_WIKITEXT, $smallLimit ],
846                ],
847                EnumDef::PARAM_DEPRECATED_VALUES => [
848                    'parsetree' => true,
849                ],
850            ],
851            'slots' => [
852                ParamValidator::PARAM_TYPE => $slotRoles,
853                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-slots',
854                ParamValidator::PARAM_ISMULTI => true,
855                ParamValidator::PARAM_ALL => true,
856            ],
857            'contentformat-{slot}' => [
858                ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ],
859                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-contentformat-slot',
860                ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
861            ],
862            'limit' => [
863                ParamValidator::PARAM_TYPE => 'limit',
864                IntegerDef::PARAM_MIN => 1,
865                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
866                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
867                ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-limit',
868                    $smallLimit, self::LIMIT_PARSE ],
869            ],
870            'expandtemplates' => [
871                ParamValidator::PARAM_DEFAULT => false,
872                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-expandtemplates',
873                ParamValidator::PARAM_DEPRECATED => true,
874            ],
875            'generatexml' => [
876                ParamValidator::PARAM_DEFAULT => false,
877                ParamValidator::PARAM_DEPRECATED => true,
878                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-generatexml',
879            ],
880            'parse' => [
881                ParamValidator::PARAM_DEFAULT => false,
882                ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-parse', self::LIMIT_PARSE ],
883                ParamValidator::PARAM_DEPRECATED => true,
884            ],
885            'section' => [
886                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-section',
887            ],
888            'diffto' => [
889                ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-diffto', $smallLimit ],
890                ParamValidator::PARAM_DEPRECATED => true,
891            ],
892            'difftotext' => [
893                ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+revisions+base-param-difftotext', $smallLimit ],
894                ParamValidator::PARAM_DEPRECATED => true,
895            ],
896            'difftotextpst' => [
897                ParamValidator::PARAM_DEFAULT => false,
898                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-difftotextpst',
899                ParamValidator::PARAM_DEPRECATED => true,
900            ],
901            'contentformat' => [
902                ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
903                ApiBase::PARAM_HELP_MSG => 'apihelp-query+revisions+base-param-contentformat',
904                ParamValidator::PARAM_DEPRECATED => true,
905            ],
906        ];
907    }
908}
909
910/** @deprecated class alias since 1.43 */
911class_alias( ApiQueryRevisionsBase::class, 'ApiQueryRevisionsBase' );