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