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