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