Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.03% covered (success)
95.03%
669 / 704
47.62% covered (danger)
47.62%
10 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiParse
95.16% covered (success)
95.16%
669 / 703
47.62% covered (danger)
47.62%
10 / 21
192
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPoolKey
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getContentParserOutput
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 getUserForPreview
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
2.50
 getPageParserOutput
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 execute
97.61% covered (success)
97.61%
367 / 376
0.00% covered (danger)
0.00%
0 / 1
124
 makeParserOptions
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 tweakParserOptions
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
7.01
 getParsedContent
97.06% covered (success)
97.06%
33 / 34
0.00% covered (danger)
0.00%
0 / 1
15
 getSectionContent
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 formatSummary
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 formatLangLinks
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
5
 formatCategoryLinks
96.67% covered (success)
96.67%
29 / 30
0.00% covered (danger)
0.00%
0 / 1
7
 formatLinks
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 formatIWLinks
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 formatHeadItems
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
2.75
 formatLimitReportData
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 setIndexedTagNames
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getAllowedParams
100.00% covered (success)
100.00%
117 / 117
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\CommentFormatter\CommentFormatter;
12use MediaWiki\Content\Content;
13use MediaWiki\Content\ContentSerializationException;
14use MediaWiki\Content\IContentHandlerFactory;
15use MediaWiki\Content\Renderer\ContentRenderer;
16use MediaWiki\Content\Transform\ContentTransformer;
17use MediaWiki\Context\DerivativeContext;
18use MediaWiki\EditPage\EditPage;
19use MediaWiki\Json\FormatJson;
20use MediaWiki\Json\JsonCodec;
21use MediaWiki\Language\LanguageNameUtils;
22use MediaWiki\Language\RawMessage;
23use MediaWiki\Message\Message;
24use MediaWiki\Output\OutputPage;
25use MediaWiki\Page\Article;
26use MediaWiki\Page\LinkBatchFactory;
27use MediaWiki\Page\LinkCache;
28use MediaWiki\Page\PageReference;
29use MediaWiki\Page\WikiPage;
30use MediaWiki\Page\WikiPageFactory;
31use MediaWiki\Parser\Parser;
32use MediaWiki\Parser\ParserFactory;
33use MediaWiki\Parser\ParserOptions;
34use MediaWiki\Parser\ParserOutput;
35use MediaWiki\Parser\ParserOutputFlags;
36use MediaWiki\Parser\ParserOutputLinkTypes;
37use MediaWiki\PoolCounter\PoolCounterWorkViaCallback;
38use MediaWiki\Request\FauxRequest;
39use MediaWiki\Revision\RevisionLookup;
40use MediaWiki\Revision\RevisionRecord;
41use MediaWiki\Revision\SlotRecord;
42use MediaWiki\Skin\Skin;
43use MediaWiki\Skin\SkinFactory;
44use MediaWiki\Title\Title;
45use MediaWiki\Title\TitleFormatter;
46use MediaWiki\Title\TitleValue;
47use MediaWiki\User\TempUser\TempUserCreator;
48use MediaWiki\User\UserFactory;
49use MediaWiki\User\UserIdentity;
50use MediaWiki\Utils\UrlUtils;
51use MediaWiki\WikiMap\WikiMap;
52use Wikimedia\ParamValidator\ParamValidator;
53use Wikimedia\ParamValidator\TypeDef\EnumDef;
54use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget;
55use Wikimedia\Parsoid\Core\TOCData;
56
57/**
58 * @ingroup API
59 */
60class ApiParse extends ApiBase {
61
62    /** @var string|false|null */
63    private $section = null;
64
65    /** @var Content|null */
66    private $content = null;
67
68    /** @var Content|null */
69    private $pstContent = null;
70
71    private bool $contentIsDeleted = false;
72    private bool $contentIsSuppressed = false;
73
74    public function __construct(
75        ApiMain $main,
76        string $action,
77        private readonly RevisionLookup $revisionLookup,
78        private readonly SkinFactory $skinFactory,
79        private readonly LanguageNameUtils $languageNameUtils,
80        private readonly LinkBatchFactory $linkBatchFactory,
81        private readonly LinkCache $linkCache,
82        private readonly IContentHandlerFactory $contentHandlerFactory,
83        private readonly ParserFactory $parserFactory,
84        private readonly WikiPageFactory $wikiPageFactory,
85        private readonly ContentRenderer $contentRenderer,
86        private readonly ContentTransformer $contentTransformer,
87        private readonly CommentFormatter $commentFormatter,
88        private readonly TempUserCreator $tempUserCreator,
89        private readonly UserFactory $userFactory,
90        private readonly UrlUtils $urlUtils,
91        private readonly TitleFormatter $titleFormatter,
92        private readonly JsonCodec $jsonCodec,
93    ) {
94        parent::__construct( $main, $action );
95    }
96
97    private function getPoolKey(): string {
98        $poolKey = WikiMap::getCurrentWikiDbDomain() . ':ApiParse:';
99        if ( !$this->getUser()->isRegistered() ) {
100            $poolKey .= 'a:' . $this->getUser()->getName();
101        } else {
102            $poolKey .= 'u:' . $this->getUser()->getId();
103        }
104        return $poolKey;
105    }
106
107    private function getContentParserOutput(
108        Content $content,
109        PageReference $page,
110        ?RevisionRecord $revision,
111        ParserOptions $popts
112    ): ParserOutput {
113        $worker = new PoolCounterWorkViaCallback( 'ApiParser', $this->getPoolKey(),
114            [
115                'doWork' => function () use ( $content, $page, $revision, $popts ) {
116                    return $this->contentRenderer->getParserOutput(
117                        $content, $page, $revision, $popts
118                    );
119                },
120                'error' => function (): never {
121                    $this->dieWithError( 'apierror-concurrency-limit' );
122                },
123            ]
124        );
125        return $worker->execute();
126    }
127
128    private function getUserForPreview(): UserIdentity {
129        $user = $this->getUser();
130        if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
131            return $this->userFactory->newUnsavedTempUser(
132                $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
133            );
134        }
135        return $user;
136    }
137
138    /** @return ParserOutput|false */
139    private function getPageParserOutput(
140        WikiPage $page,
141        ?int $revId,
142        ParserOptions $popts,
143        bool $suppressCache
144    ) {
145        $worker = new PoolCounterWorkViaCallback( 'ApiParser', $this->getPoolKey(),
146            [
147                'doWork' => static function () use ( $page, $revId, $popts, $suppressCache ) {
148                    return $page->getParserOutput( $popts, $revId, $suppressCache );
149                },
150                'error' => function (): never {
151                    $this->dieWithError( 'apierror-concurrency-limit' );
152                },
153            ]
154        );
155        return $worker->execute();
156    }
157
158    public function execute() {
159        // The data is hot but user-dependent, like page views, so we set vary cookies
160        $this->getMain()->setCacheMode( 'anon-public-user-private' );
161
162        // Get parameters
163        $params = $this->extractRequestParams();
164
165        // No easy way to say that text and title or revid are allowed together
166        // while the rest aren't, so just do it in three calls.
167        $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'text' );
168        $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'title' );
169        $this->requireMaxOneParameter( $params, 'page', 'pageid', 'oldid', 'revid' );
170
171        $text = $params['text'];
172        $title = $params['title'];
173        if ( $title === null ) {
174            $titleProvided = false;
175            // A title is needed for parsing, so arbitrarily choose one
176            $title = 'API';
177        } else {
178            $titleProvided = true;
179        }
180
181        $page = $params['page'];
182        $pageid = $params['pageid'];
183        $oldid = $params['oldid'];
184
185        $prop = array_fill_keys( $params['prop'], true );
186
187        if ( isset( $params['section'] ) ) {
188            $this->section = $params['section'];
189            if ( !preg_match( '/^((T-)?\d+|new)$/', $this->section ) ) {
190                $this->dieWithError( 'apierror-invalidsection' );
191            }
192        } else {
193            $this->section = false;
194        }
195
196        // The parser needs $wgTitle to be set, apparently the
197        // $title parameter in Parser::parse isn't enough *sigh*
198        // TODO: Does this still need $wgTitle?
199        // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
200        global $wgTitle;
201
202        $format = null;
203        $redirValues = null;
204
205        $needContent = isset( $prop['wikitext'] ) ||
206            isset( $prop['parsetree'] ) || $params['generatexml'];
207
208        // Return result
209        $result = $this->getResult();
210
211        if ( $oldid !== null || $pageid !== null || $page !== null ) {
212            if ( $this->section === 'new' ) {
213                $this->dieWithError( 'apierror-invalidparammix-parse-new-section', 'invalidparammix' );
214            }
215            if ( $oldid !== null ) {
216                // Don't use the parser cache
217                $rev = $this->revisionLookup->getRevisionById( $oldid );
218                if ( !$rev ) {
219                    $this->dieWithError( [ 'apierror-nosuchrevid', $oldid ] );
220                }
221
222                $this->checkTitleUserPermissions( $rev->getPage(), 'read' );
223
224                if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
225                    $this->dieWithError(
226                        [ 'apierror-permissiondenied', $this->msg( 'action-deletedtext' ) ]
227                    );
228                }
229
230                $revLinkTarget = $rev->getPageAsLinkTarget();
231                $titleObj = Title::newFromLinkTarget( $revLinkTarget );
232                $wgTitle = $titleObj;
233                $pageObj = $this->wikiPageFactory->newFromTitle( $titleObj );
234                [ $popts, $reset, $suppressCache ] = $this->makeParserOptions( $pageObj, $params );
235                $p_result = $this->getParsedContent(
236                    $pageObj, $popts, $suppressCache, $pageid, $rev, $needContent
237                );
238            } else { // Not $oldid, but $pageid or $page
239                if ( $params['redirects'] ) {
240                    $reqParams = [
241                        'redirects' => '',
242                    ];
243                    $pageParams = [];
244                    if ( $pageid !== null ) {
245                        $reqParams['pageids'] = $pageid;
246                        $pageParams['pageid'] = $pageid;
247                    } else { // $page
248                        $reqParams['titles'] = $page;
249                        $pageParams['title'] = $page;
250                    }
251                    $req = new FauxRequest( $reqParams );
252                    $main = new ApiMain( $req );
253                    $pageSet = new ApiPageSet( $main );
254                    $pageSet->execute();
255                    $redirValues = $pageSet->getRedirectTitlesAsResult( $this->getResult() );
256
257                    foreach ( $pageSet->getRedirectTargets() as $redirectTarget ) {
258                        $pageParams = [ 'title' => $this->titleFormatter->getFullText( $redirectTarget ) ];
259                    }
260                } elseif ( $pageid !== null ) {
261                    $pageParams = [ 'pageid' => $pageid ];
262                } else { // $page
263                    $pageParams = [ 'title' => $page ];
264                }
265
266                $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' );
267                $titleObj = $pageObj->getTitle();
268                if ( !$titleObj->exists() ) {
269                    $this->dieWithError( 'apierror-missingtitle' );
270                }
271
272                $this->checkTitleUserPermissions( $titleObj, 'read' );
273                $wgTitle = $titleObj;
274
275                if ( isset( $prop['revid'] ) ) {
276                    $oldid = $pageObj->getLatest();
277                }
278
279                [ $popts, $reset, $suppressCache ] = $this->makeParserOptions( $pageObj, $params );
280                $p_result = $this->getParsedContent(
281                    $pageObj, $popts, $suppressCache, $pageid, null, $needContent
282                );
283            }
284        } else { // Not $oldid, $pageid, $page. Hence based on $text
285            $model = $params['contentmodel'];
286            $format = $params['contentformat'];
287
288            $titleObj = Title::newFromText( $title );
289            if ( !$titleObj || $titleObj->isExternal() ) {
290                $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
291            }
292            $revid = $params['revid'];
293            $rev = null;
294            if ( $revid !== null ) {
295                $rev = $this->revisionLookup->getRevisionById( $revid );
296                if ( !$rev ) {
297                    $this->dieWithError( [ 'apierror-nosuchrevid', $revid ] );
298                }
299                $pTitleObj = $titleObj;
300                $titleObj = Title::newFromPageIdentity( $rev->getPage() );
301                if ( $titleProvided ) {
302                    if ( !$titleObj->equals( $pTitleObj ) ) {
303                        $this->addWarning( [ 'apierror-revwrongpage', $rev->getId(),
304                            wfEscapeWikiText( $pTitleObj->getPrefixedText() ) ] );
305                    }
306                } else {
307                    // Consider the title derived from the revid as having
308                    // been provided.
309                    $titleProvided = true;
310                }
311            }
312            $wgTitle = $titleObj;
313            if ( $titleObj->canExist() ) {
314                $pageObj = $this->wikiPageFactory->newFromTitle( $titleObj );
315                [ $popts, $reset ] = $this->makeParserOptions( $pageObj, $params );
316            } else {
317                // Allow parsing wikitext in the context of special pages (T51477)
318                $pageObj = null;
319                $popts = ParserOptions::newFromContext( $this->getContext() );
320                [ $popts, $reset ] = $this->tweakParserOptions( $popts, $titleObj, $params );
321            }
322
323            $textProvided = $text !== null;
324
325            if ( !$textProvided ) {
326                if ( $titleProvided && ( $prop || $params['generatexml'] ) ) {
327                    if ( $revid !== null ) {
328                        $this->addWarning( 'apiwarn-parse-revidwithouttext' );
329                    } else {
330                        $this->addWarning( 'apiwarn-parse-titlewithouttext' );
331                    }
332                }
333                // Prevent warning from ContentHandler::makeContent()
334                $text = '';
335            }
336
337            // If we are parsing text, do not use the content model of the default
338            // API title, but default to wikitext to keep BC.
339            if ( $textProvided && !$titleProvided && $model === null ) {
340                $model = CONTENT_MODEL_WIKITEXT;
341                $this->addWarning( [ 'apiwarn-parse-nocontentmodel', $model ] );
342            } elseif ( $model === null ) {
343                $model = $titleObj->getContentModel();
344            }
345
346            $contentHandler = $this->contentHandlerFactory->getContentHandler( $model );
347            // Not in the default format, check supported or not
348            if ( $format && !$contentHandler->isSupportedFormat( $format ) ) {
349                $this->dieWithError( [ 'apierror-badformat-generic', $format, $model ] );
350            }
351
352            try {
353                $this->content = $contentHandler->unserializeContent( $text, $format );
354            } catch ( ContentSerializationException $ex ) {
355                $this->dieWithException( $ex, [
356                    'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
357                ] );
358            }
359
360            if ( $this->section !== false ) {
361                if ( $this->section === 'new' ) {
362                    // Insert the section title above the content.
363                    if ( $params['sectiontitle'] !== null ) {
364                        $this->content = $this->content->addSectionHeader( $params['sectiontitle'] );
365                    }
366                } else {
367                    $this->content = $this->getSectionContent( $this->content, $titleObj->getPrefixedText() );
368                }
369            }
370
371            if ( $params['pst'] || $params['onlypst'] ) {
372                $this->pstContent = $this->contentTransformer->preSaveTransform(
373                    $this->content,
374                    $titleObj,
375                    $this->getUserForPreview(),
376                    $popts
377                );
378            }
379            if ( $params['onlypst'] ) {
380                // Build a result and bail out
381                $result_array = [];
382                $result_array['text'] = $this->pstContent->serialize( $format );
383                $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
384                if ( isset( $prop['wikitext'] ) ) {
385                    $result_array['wikitext'] = $this->content->serialize( $format );
386                    $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
387                }
388                if ( $params['summary'] !== null ||
389                    ( $params['sectiontitle'] !== null && $this->section === 'new' )
390                ) {
391                    $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
392                    $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
393                }
394
395                $result->addValue( null, $this->getModuleName(), $result_array );
396
397                return;
398            }
399
400            // Not cached (save or load)
401            if ( $params['pst'] ) {
402                $p_result = $this->getContentParserOutput( $this->pstContent, $titleObj, $rev, $popts );
403            } else {
404                $p_result = $this->getContentParserOutput( $this->content, $titleObj, $rev, $popts );
405            }
406        }
407
408        $result_array = [];
409
410        $result_array['title'] = $titleObj->getPrefixedText();
411        $result_array['pageid'] = $pageid ?: $titleObj->getArticleID();
412        if ( $this->contentIsDeleted ) {
413            $result_array['textdeleted'] = true;
414        }
415        if ( $this->contentIsSuppressed ) {
416            $result_array['textsuppressed'] = true;
417        }
418
419        if ( isset( $params['useskin'] ) ) {
420            $skin = $this->skinFactory->makeSkin( Skin::normalizeKey( $params['useskin'] ) );
421        } else {
422            $skin = null;
423        }
424
425        $outputPage = null;
426        $context = null;
427        if (
428            $skin || isset( $prop['subtitle'] ) || isset( $prop['headhtml'] ) || isset( $prop['categorieshtml'] ) ||
429            isset( $params['mobileformat'] )
430        ) {
431            // Enabling the skin via 'useskin', 'subtitle', 'headhtml', or 'categorieshtml'
432            // gets OutputPage and Skin involved, which (among others) applies
433            // these hooks:
434            // - Hook: LanguageLinks
435            // - Hook: SkinSubPageSubtitle
436            // - Hook: OutputPageParserOutput
437            // - Hook: OutputPageRenderCategoryLink
438            // - Hook: OutputPageBeforeHTML
439            // HACK Adding the 'mobileformat' parameter *also* enables the skin, for compatibility with legacy
440            // apps. This behavior should be considered deprecated so new users should not rely on this and
441            // always use the "useskin" parameter to enable "skin mode".
442            // Ideally this would be done with another hook so that MobileFrontend could enable skin mode, but
443            // as this is just for a deprecated feature, we are hard-coding this param into core.
444            $context = new DerivativeContext( $this->getContext() );
445            $context->setTitle( $titleObj );
446
447            if ( $pageObj ) {
448                $context->setWikiPage( $pageObj );
449            }
450            // Some hooks only apply to pages when action=view, which this API
451            // call is simulating.
452            $context->setRequest( new FauxRequest( [ 'action' => 'view' ] ) );
453
454            if ( $skin ) {
455                // Use the skin specified by 'useskin'
456                $context->setSkin( $skin );
457                // Context clones the skin, refetch to stay in sync. (T166022)
458                $skin = $context->getSkin();
459            } else {
460                // Make sure the context's skin refers to the context. Without this,
461                // $outputPage->getSkin()->getOutput() !== $outputPage which
462                // confuses some of the output.
463                $context->setSkin( $context->getSkin() );
464            }
465
466            $outputPage = new OutputPage( $context );
467            // Required for subtitle to appear
468            $outputPage->setArticleFlag( true );
469
470            // Some hooks are conditional on categories
471            $outputPage->addCategoryLinks( $p_result->getCategoryMap() );
472
473            if ( $this->content ) {
474                $outputPage->addContentOverride( $titleObj, $this->content );
475            }
476            $context->setOutput( $outputPage );
477
478            if ( $skin ) {
479                // Based on OutputPage::output()
480                $outputPage->loadSkinModules( $skin );
481            }
482        }
483
484        if ( $oldid !== null ) {
485            $result_array['revid'] = (int)$oldid;
486        }
487
488        if ( $params['redirects'] && $redirValues !== null ) {
489            $result_array['redirects'] = $redirValues;
490        }
491
492        // Parsoid displaytitle is set during the postprocessing pipeline
493        if ( isset( $prop['text'] ) || isset( $prop['displaytitle'] ) ) {
494            $skin = $context ? $context->getSkin() : null;
495            $skinOptions = $skin ? $skin->getOptions() : [
496                'toc' => true,
497            ];
498            // TODO T371004 move runOutputPipeline out of $parserOutput
499            // TODO T371022 it should be reasonably straightforward to move this to a clone, but it requires
500            // careful checking of the clone and of what happens on the boundary of OutputPage. Leaving this as
501            // "getText-equivalent" for now; will fix in a later, independent patch.
502            $oldText = $p_result->getContentHolderText();
503            $newText = $p_result->runOutputPipeline( $popts, [
504                // This will have side effects on $p_result (T371022)
505                'allowClone' => false,
506                'allowTOC' => !$params['disabletoc'],
507                'injectTOC' => $skinOptions['toc'],
508                'wrapperDivClass' => $params['wrapoutputclass'],
509                'deduplicateStyles' => !$params['disablestylededuplication'],
510                'userLang' => $context ? $context->getLanguage() : null,
511                'skin' => $skin,
512                'includeDebugInfo' => !$params['disablepp'] && !$params['disablelimitreport']
513            ] )->getContentHolderText();
514            $p_result->setContentHolderText( $oldText );
515            if ( isset( $prop['text'] ) ) {
516                $result_array['text'] = $newText;
517                $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
518                if ( $context ) {
519                    $this->getHookRunner()->onOutputPageBeforeHTML( $context->getOutput(), $result_array['text'] );
520                }
521            }
522        }
523
524        if ( $outputPage ) {
525            // This needs to happen after running the OutputTransform pipeline so that the metadata inserted by
526            // the pipeline is also added to the OutputPage
527            $outputPage->addParserOutputMetadata( $p_result );
528
529            $this->getHookRunner()->onApiParseMakeOutputPage( $this, $outputPage );
530        }
531
532        if ( $params['summary'] !== null ||
533            ( $params['sectiontitle'] !== null && $this->section === 'new' )
534        ) {
535            $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
536            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
537        }
538
539        if ( isset( $prop['langlinks'] ) ) {
540            if ( $outputPage ) {
541                $langlinks = $outputPage->getLanguageLinks();
542            } else {
543                $langlinks = array_map(
544                    static fn ( $item ) => $item['link'],
545                    $p_result->getLinkList( ParserOutputLinkTypes::LANGUAGE )
546                );
547                // The deprecated 'effectivelanglinks' option pre-dates OutputPage
548                // support via 'useskin'. If not already applied, then run just this
549                // one hook of OutputPage::addParserOutputMetadata here.
550                if ( $params['effectivelanglinks'] ) {
551                    # for compatibility with old hook, convert to string[]
552                    $compat = [];
553                    foreach ( $langlinks as $link ) {
554                        $s = $link->getInterwiki() . ':' . $link->getText();
555                        if ( $link->hasFragment() ) {
556                            $s .= '#' . $link->getFragment();
557                        }
558                        $compat[] = $s;
559                    }
560                    $langlinks = $compat;
561                    $linkFlags = [];
562                    sort( $langlinks );
563                    $this->getHookRunner()->onLanguageLinks( $titleObj, $langlinks, $linkFlags );
564                }
565            }
566
567            $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
568        }
569        if ( isset( $prop['categories'] ) ) {
570            $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategoryMap() );
571        }
572        if ( isset( $prop['categorieshtml'] ) ) {
573            $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories();
574            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
575        }
576        if ( isset( $prop['links'] ) ) {
577
578            $result_array['links'] = $this->formatLinks( $p_result->getLinkList( ParserOutputLinkTypes::LOCAL ) );
579        }
580        if ( isset( $prop['templates'] ) ) {
581            $result_array['templates'] = $this->formatLinks(
582                $p_result->getLinkList( ParserOutputLinkTypes::TEMPLATE )
583            );
584        }
585        if ( isset( $prop['images'] ) ) {
586            $result_array['images'] = array_map(
587                static fn ( $item ) => $item['link']->getDBkey(),
588                $p_result->getLinkList( ParserOutputLinkTypes::MEDIA )
589            );
590        }
591        if ( isset( $prop['externallinks'] ) ) {
592            $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
593        }
594        if ( isset( $prop['sections'] ) ) {
595            $result_array['sections'] = $p_result->getSections();
596        }
597        if ( isset( $prop['tocdata'] ) ) {
598            $result_array['tocdata'] = $this->jsonCodec->toJsonArray(
599                $p_result->getTOCData(), TOCData::class
600            );
601        }
602        if ( isset( $prop['parseroutput'] ) ) {
603            $result_array['parseroutput'] = $this->jsonCodec->toJsonArray(
604                $p_result, ParserOutput::class
605            );
606        }
607        if ( isset( $prop['sections'] ) || isset( $prop['tocdata'] ) ) {
608            $result_array['showtoc'] = $p_result->getOutputFlag( ParserOutputFlags::SHOW_TOC );
609        }
610        if ( isset( $prop['parsewarnings'] ) || isset( $prop['parsewarningshtml'] ) ) {
611            $warningMsgs = array_map(
612                static fn ( $mv ) => Message::newFromSpecifier( $mv )
613                    ->page( $titleObj )
614                    // Note that we use ContentLanguage here
615                    ->inContentLanguage(),
616                $p_result->getWarningMsgs()
617            );
618            if ( $warningMsgs === [] ) {
619                // Backward compatibility with cached ParserOutput from
620                // MW <= 1.45 which didn't store the MessageValues (T343048)
621                $warningMsgs = array_map(
622                    static fn ( $warning ) => new RawMessage( '$1', [ $warning ] ),
623                    $p_result->getWarnings()
624                );
625            }
626            if ( isset( $prop['parsewarnings'] ) ) {
627                $warnings = array_map( static fn ( $msg ) => $msg->text(), $warningMsgs );
628                $result_array['parsewarnings'] = $warnings;
629            }
630            if ( isset( $prop['parsewarningshtml'] ) ) {
631                $warningsHtml = array_map( static fn ( $msg ) => $msg->parse(), $warningMsgs );
632                $result_array['parsewarningshtml'] = $warningsHtml;
633            }
634        }
635
636        if ( isset( $prop['displaytitle'] ) ) {
637            $result_array['displaytitle'] = $p_result->getDisplayTitle() !== false
638                ? $p_result->getDisplayTitle()
639                : htmlspecialchars( $titleObj->getPrefixedText(), ENT_NOQUOTES );
640        }
641
642        if ( isset( $prop['subtitle'] ) ) {
643            // Get the subtitle without its container element to support UI refreshing
644            $result_array['subtitle'] = $context->getSkin()->prepareSubtitle( false );
645        }
646
647        if ( isset( $prop['headitems'] ) ) {
648            if ( $outputPage ) {
649                $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() );
650            } else {
651                $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
652            }
653        }
654
655        if ( isset( $prop['headhtml'] ) ) {
656            $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
657            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
658        }
659
660        if ( isset( $prop['modules'] ) ) {
661            if ( $outputPage ) {
662                $result_array['modules'] = $outputPage->getModules();
663                // Deprecated since 1.32 (T188689)
664                $result_array['modulescripts'] = [];
665                $result_array['modulestyles'] = $outputPage->getModuleStyles();
666            } else {
667                $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
668                // Deprecated since 1.32 (T188689)
669                $result_array['modulescripts'] = [];
670                $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
671            }
672        }
673
674        if ( isset( $prop['jsconfigvars'] ) ) {
675            $showStrategyKeys = (bool)( $params['showstrategykeys'] );
676            $jsconfigvars = $outputPage ? $outputPage->getJsConfigVars() :
677                $p_result->getJsConfigVars( $showStrategyKeys );
678            $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars );
679        }
680
681        if ( isset( $prop['encodedjsconfigvars'] ) ) {
682            $jsconfigvars = $outputPage ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
683            $result_array['encodedjsconfigvars'] = FormatJson::encode(
684                $jsconfigvars,
685                false,
686                FormatJson::ALL_OK
687            );
688            $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
689        }
690
691        if ( isset( $prop['modules'] ) &&
692            !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
693            $this->addWarning( 'apiwarn-moduleswithoutvars' );
694        }
695
696        if ( isset( $prop['indicators'] ) ) {
697            if ( $skin ) {
698                $result_array['indicators'] = $outputPage->getIndicators();
699            } else {
700                $result_array['indicators'] = $p_result->getIndicators();
701            }
702            ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
703        }
704
705        if ( isset( $prop['iwlinks'] ) ) {
706            $links = array_map(
707                static fn ( $item ) => $item['link'],
708                $p_result->getLinkList( ParserOutputLinkTypes::INTERWIKI )
709            );
710            $result_array['iwlinks'] = $this->formatIWLinks( $links );
711        }
712
713        if ( isset( $prop['wikitext'] ) ) {
714            $result_array['wikitext'] = $this->content->serialize( $format );
715            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
716            // @phan-suppress-next-line PhanImpossibleTypeComparison
717            if ( $this->pstContent !== null ) {
718                $result_array['psttext'] = $this->pstContent->serialize( $format );
719                $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
720            }
721        }
722        if ( isset( $prop['properties'] ) ) {
723            $result_array['properties'] = $p_result->getPageProperties();
724            ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
725        }
726
727        if ( isset( $prop['limitreportdata'] ) ) {
728            $result_array['limitreportdata'] =
729                $this->formatLimitReportData( $p_result->getLimitReportData() );
730        }
731        if ( isset( $prop['limitreporthtml'] ) ) {
732            $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
733            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
734        }
735
736        if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
737            if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
738                $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
739            }
740
741            $parser = $this->parserFactory->getInstance();
742            $parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
743            // @phan-suppress-next-line PhanUndeclaredMethod
744            $xml = $parser->preprocessToDom( $this->content->getText() )->__toString();
745            $result_array['parsetree'] = $xml;
746            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
747        }
748
749        $result_mapping = [
750            'redirects' => 'r',
751            'langlinks' => 'll',
752            'categories' => 'cl',
753            'links' => 'pl',
754            'templates' => 'tl',
755            'images' => 'img',
756            'externallinks' => 'el',
757            'iwlinks' => 'iw',
758            'sections' => 's',
759            'tocdata' => 'toc',
760            'headitems' => 'hi',
761            'modules' => 'm',
762            'indicators' => 'ind',
763            'modulescripts' => 'm',
764            'modulestyles' => 'm',
765            'properties' => 'pp',
766            'limitreportdata' => 'lr',
767            'parsewarnings' => 'pw',
768            'parsewarningshtml' => 'pw',
769        ];
770        $this->setIndexedTagNames( $result_array, $result_mapping );
771        $result->addValue( null, $this->getModuleName(), $result_array );
772    }
773
774    /**
775     * Constructs a ParserOptions object
776     *
777     * @param WikiPage $pageObj
778     * @param array $params
779     *
780     * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ]
781     */
782    private function makeParserOptions( WikiPage $pageObj, array $params ) {
783        if ( $params['usearticle'] ) {
784            # T349037: The ArticleParserOptions hook should be broadened to take
785            # a WikiPage (aka $pageObj) instead of an Article.  But for now
786            # fake the Article.
787            $article = Article::newFromWikiPage( $pageObj, $this->getContext() );
788            # Allow extensions to vary parser options used for article rendering,
789            # in the same way Article does
790            $popts = $article->getParserOptions();
791        } else {
792            $popts = $pageObj->makeParserOptions( $this->getContext() );
793        }
794        // Overwrite render reason
795        $popts->setRenderReason( 'api-parse' );
796        return $this->tweakParserOptions( $popts, $pageObj->getTitle(), $params );
797    }
798
799    /**
800     * Tweaks a ParserOptions object
801     *
802     * @param ParserOptions $popts
803     * @param Title $title
804     * @param array $params
805     *
806     * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ]
807     */
808    private function tweakParserOptions( ParserOptions $popts, Title $title, array $params ) {
809        $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
810        $popts->setIsSectionPreview( $params['sectionpreview'] );
811
812        if ( $params['wrapoutputclass'] !== '' ) {
813            $popts->setWrapOutputClass( $params['wrapoutputclass'] );
814        }
815        if ( $params['parsoid'] || $params['parser'] === 'parsoid' ) {
816            $popts->setUseParsoid( true );
817        }
818        if ( $params['parser'] === 'legacy' ) {
819            $popts->setUseParsoid( false );
820        }
821        if ( $params['disableeditsection'] ) {
822            $popts->setSuppressSectionEditLinks();
823        }
824
825        $reset = null;
826        $suppressCache = false;
827        $this->getHookRunner()->onApiMakeParserOptions( $popts, $title,
828            $params, $this, $reset, $suppressCache );
829
830        return [ $popts, $reset, $suppressCache ];
831    }
832
833    /**
834     * @param WikiPage $page
835     * @param ParserOptions $popts
836     * @param bool $suppressCache
837     * @param int $pageId
838     * @param RevisionRecord|null $rev
839     * @param bool $getContent
840     * @return ParserOutput
841     */
842    private function getParsedContent(
843        WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
844    ) {
845        $revId = $rev ? $rev->getId() : null;
846        $isDeleted = $rev && $rev->isDeleted( RevisionRecord::DELETED_TEXT );
847
848        if ( $getContent || $this->section !== false || $isDeleted ) {
849            if ( $rev ) {
850                $this->content = $rev->getContent(
851                    SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $this->getAuthority()
852                );
853                if ( !$this->content ) {
854                    $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
855                }
856            } else {
857                $this->content = $page->getContent( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
858                if ( !$this->content ) {
859                    $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] );
860                }
861            }
862            $this->contentIsDeleted = $isDeleted;
863            $this->contentIsSuppressed = $rev &&
864                $rev->isDeleted( RevisionRecord::DELETED_TEXT | RevisionRecord::DELETED_RESTRICTED );
865        }
866
867        if ( $this->section !== false ) {
868            $this->content = $this->getSectionContent(
869                $this->content,
870                $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
871            );
872            return $this->getContentParserOutput(
873                $this->content, $page->getTitle(),
874                $rev,
875                $popts
876            );
877        }
878
879        if ( $isDeleted ) {
880            // getParserOutput can't do revdeled revisions
881
882            $pout = $this->getContentParserOutput(
883                $this->content, $page->getTitle(),
884                $rev,
885                $popts
886            );
887        } else {
888            // getParserOutput will save to Parser cache if able
889            $pout = $this->getPageParserOutput( $page, $revId, $popts, $suppressCache );
890        }
891        if ( !$pout ) {
892            // @codeCoverageIgnoreStart
893            $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
894            // @codeCoverageIgnoreEnd
895        }
896
897        return $pout;
898    }
899
900    /**
901     * Extract the requested section from the given Content
902     *
903     * @param Content $content
904     * @param string|Message $what Identifies the content in error messages, e.g. page title.
905     * @return Content
906     */
907    private function getSectionContent( Content $content, $what ) {
908        // Not cached (save or load)
909        $section = $content->getSection( $this->section );
910        if ( $section === false ) {
911            $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
912        }
913        if ( $section === null ) {
914            $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
915        }
916
917        return $section;
918    }
919
920    /**
921     * This mimics the behavior of EditPage in formatting a summary
922     *
923     * @param Title $title of the page being parsed
924     * @param array $params The API parameters of the request
925     * @return string HTML
926     */
927    private function formatSummary( $title, $params ) {
928        $summary = $params['summary'] ?? '';
929        $sectionTitle = $params['sectiontitle'] ?? '';
930
931        if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
932            if ( $sectionTitle !== '' ) {
933                $summary = $params['sectiontitle'];
934            }
935            if ( $summary !== '' ) {
936                $summary = $this->msg( 'newsectionsummary' )
937                    ->plaintextParams( $this->parserFactory->getMainInstance()->stripSectionName( $summary ) )
938                    ->inContentLanguage()->text();
939            }
940        }
941        return $this->commentFormatter->format( $summary, $title, $this->section === 'new' );
942    }
943
944    /**
945     * @param string[]|ParsoidLinkTarget[] $links
946     * @return array
947     */
948    private function formatLangLinks( $links ): array {
949        $result = [];
950        foreach ( $links as $link ) {
951            $entry = [];
952            if ( is_string( $link ) ) {
953                [ $lang, $titleWithFrag ] = explode( ':', $link, 2 );
954                [ $title, $frag ] = array_pad( explode( '#', $titleWithFrag, 2 ), 2, '' );
955                $title = TitleValue::tryNew( NS_MAIN, $title, $frag, $lang );
956            } else {
957                $title = $link;
958                $lang = $link->getInterwiki();
959                $titleWithFrag = $link->getText();
960                if ( $link->hasFragment() ) {
961                    $titleWithFrag .= '#' . $link->getFragment();
962                }
963            }
964            $title = Title::castFromLinkTarget( $title );
965
966            $entry['lang'] = $lang;
967            if ( $title ) {
968                $entry['url'] = (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT );
969                // title validity implies language code validity
970                // localised language name in 'uselang' language
971                $entry['langname'] = $this->languageNameUtils->getLanguageName(
972                    $lang,
973                    $this->getLanguage()->getCode()
974                );
975
976                // native language name
977                $entry['autonym'] = $this->languageNameUtils->getLanguageName( $lang );
978            }
979            ApiResult::setContentValue( $entry, 'title', $titleWithFrag );
980            $result[] = $entry;
981        }
982
983        return $result;
984    }
985
986    private function formatCategoryLinks( array $links ): array {
987        $result = [];
988
989        if ( !$links ) {
990            return $result;
991        }
992
993        // Fetch hiddencat property
994        $lb = $this->linkBatchFactory->newLinkBatch();
995        $lb->setArray( [ NS_CATEGORY => $links ] );
996        $db = $this->getDB();
997        $res = $db->newSelectQueryBuilder()
998            ->select( [ 'page_title', 'pp_propname' ] )
999            ->from( 'page' )
1000            ->where( $lb->constructSet( 'page', $db ) )
1001            ->leftJoin( 'page_props', null, [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ] )
1002            ->caller( __METHOD__ )
1003            ->fetchResultSet();
1004        $hiddencats = [];
1005        foreach ( $res as $row ) {
1006            $hiddencats[$row->page_title] = isset( $row->pp_propname );
1007        }
1008
1009        foreach ( $links as $link => $sortkey ) {
1010            $entry = [];
1011            $entry['sortkey'] = $sortkey;
1012            // array keys will cast numeric category names to ints, so cast back to string
1013            ApiResult::setContentValue( $entry, 'category', (string)$link );
1014            if ( !isset( $hiddencats[$link] ) ) {
1015                $entry['missing'] = true;
1016
1017                // We already know the link doesn't exist in the database, so
1018                // tell LinkCache that before calling $title->isKnown().
1019                $title = Title::makeTitle( NS_CATEGORY, $link );
1020                $this->linkCache->addBadLinkObj( $title );
1021                if ( $title->isKnown() ) {
1022                    $entry['known'] = true;
1023                }
1024            } elseif ( $hiddencats[$link] ) {
1025                $entry['hidden'] = true;
1026            }
1027            $result[] = $entry;
1028        }
1029
1030        return $result;
1031    }
1032
1033    /**
1034     * @param list<array{link:ParsoidLinkTarget,pageid?:int}> $links
1035     * @return array
1036     */
1037    private function formatLinks( array $links ): array {
1038        $result = [];
1039        foreach ( $links as [ 'link' => $link, 'pageid' => $id ] ) {
1040            $entry = [];
1041            $entry['ns'] = $link->getNamespace();
1042            ApiResult::setContentValue( $entry, 'title', Title::newFromLinkTarget( $link )->getFullText() );
1043            $entry['exists'] = $id != 0;
1044            $result[] = $entry;
1045        }
1046
1047        return $result;
1048    }
1049
1050    private function formatIWLinks( array $iw ): array {
1051        $result = [];
1052        foreach ( $iw as $linkTarget ) {
1053            $entry = [];
1054            $entry['prefix'] = $linkTarget->getInterwiki();
1055            $title = Title::newFromLinkTarget( $linkTarget );
1056            if ( $title ) {
1057                $entry['url'] = (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT );
1058
1059                ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
1060            }
1061            $result[] = $entry;
1062        }
1063
1064        return $result;
1065    }
1066
1067    private function formatHeadItems( array $headItems ): array {
1068        $result = [];
1069        foreach ( $headItems as $tag => $content ) {
1070            $entry = [];
1071            $entry['tag'] = $tag;
1072            ApiResult::setContentValue( $entry, 'content', $content );
1073            $result[] = $entry;
1074        }
1075
1076        return $result;
1077    }
1078
1079    private function formatLimitReportData( array $limitReportData ): array {
1080        $result = [];
1081
1082        foreach ( $limitReportData as $name => $value ) {
1083            $entry = [];
1084            $entry['name'] = $name;
1085            if ( !is_array( $value ) ) {
1086                $value = [ $value ];
1087            }
1088            ApiResult::setIndexedTagNameRecursive( $value, 'param' );
1089            $entry = array_merge( $entry, $value );
1090            $result[] = $entry;
1091        }
1092
1093        return $result;
1094    }
1095
1096    private function setIndexedTagNames( array &$array, array $mapping ) {
1097        foreach ( $mapping as $key => $name ) {
1098            if ( isset( $array[$key] ) ) {
1099                ApiResult::setIndexedTagName( $array[$key], $name );
1100            }
1101        }
1102    }
1103
1104    /** @inheritDoc */
1105    public function getAllowedParams() {
1106        return [
1107            'title' => null,
1108            'text' => [
1109                ParamValidator::PARAM_TYPE => 'text',
1110            ],
1111            'revid' => [
1112                ParamValidator::PARAM_TYPE => 'integer',
1113            ],
1114            'summary' => null,
1115            'page' => null,
1116            'pageid' => [
1117                ParamValidator::PARAM_TYPE => 'integer',
1118            ],
1119            'redirects' => false,
1120            'oldid' => [
1121                ParamValidator::PARAM_TYPE => 'integer',
1122            ],
1123            'prop' => [
1124                ParamValidator::PARAM_DEFAULT => 'text|langlinks|categories|links|templates|' .
1125                    'images|externallinks|sections|tocdata|revid|displaytitle|iwlinks|' .
1126                    'properties|parsewarnings',
1127                ParamValidator::PARAM_ISMULTI => true,
1128                ParamValidator::PARAM_TYPE => [
1129                    'text',
1130                    'langlinks',
1131                    'categories',
1132                    'categorieshtml',
1133                    'links',
1134                    'templates',
1135                    'images',
1136                    'externallinks',
1137                    'sections',
1138                    'tocdata',
1139                    'revid',
1140                    'displaytitle',
1141                    'subtitle',
1142                    'headhtml',
1143                    'modules',
1144                    'jsconfigvars',
1145                    'encodedjsconfigvars',
1146                    'indicators',
1147                    'iwlinks',
1148                    'wikitext',
1149                    'properties',
1150                    'limitreportdata',
1151                    'limitreporthtml',
1152                    'parseroutput',
1153                    'parsetree',
1154                    'parsewarnings',
1155                    'parsewarningshtml',
1156                    'headitems',
1157                ],
1158                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
1159                    'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
1160                ],
1161                EnumDef::PARAM_DEPRECATED_VALUES => [
1162                    'headitems' => 'apiwarn-deprecation-parse-headitems',
1163                    // deprecated since 1.46: T319141
1164                    'sections' => [ 'apiwarn-deprecation-withreplacement', 'prop=sections', 'prop=tocdata' ],
1165                ],
1166                EnumDef::PARAM_INTERNAL_VALUES => [
1167                    'parseroutput' => true, // since 1.47: T428786
1168                ],
1169            ],
1170            'wrapoutputclass' => 'mw-parser-output',
1171            'usearticle' => false, // since 1.43
1172            'parsoid' => [ // since 1.41
1173                ParamValidator::PARAM_TYPE => 'boolean',
1174                ParamValidator::PARAM_DEFAULT => false,
1175                ParamValidator::PARAM_DEPRECATED => true,
1176            ],
1177            'parser' => [ // since 1.45
1178                ParamValidator::PARAM_TYPE => [
1179                    'parsoid',
1180                    'default',
1181                    'legacy'
1182                ],
1183                ParamValidator::PARAM_DEFAULT => 'default',
1184                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
1185            ],
1186            'pst' => false,
1187            'onlypst' => false,
1188            'effectivelanglinks' => [
1189                ParamValidator::PARAM_DEFAULT => false,
1190                ParamValidator::PARAM_DEPRECATED => true,
1191            ],
1192            'section' => null,
1193            'sectiontitle' => [
1194                ParamValidator::PARAM_TYPE => 'string',
1195            ],
1196            'disablepp' => [
1197                ParamValidator::PARAM_DEFAULT => false,
1198                ParamValidator::PARAM_DEPRECATED => true,
1199            ],
1200            'disablelimitreport' => false,
1201            'disableeditsection' => false,
1202            'disablestylededuplication' => false,
1203            'showstrategykeys' => false,
1204            'generatexml' => [
1205                ParamValidator::PARAM_DEFAULT => false,
1206                ApiBase::PARAM_HELP_MSG => [
1207                    'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
1208                ],
1209                ParamValidator::PARAM_DEPRECATED => true,
1210            ],
1211            'preview' => false,
1212            'sectionpreview' => false,
1213            'disabletoc' => false,
1214            'useskin' => [
1215                // T237856; We use all installed skins here to allow hidden (but usable) skins
1216                // to continue working correctly with some features such as Live Preview
1217                ParamValidator::PARAM_TYPE => array_keys( $this->skinFactory->getInstalledSkins() ),
1218            ],
1219            'contentformat' => [
1220                ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
1221            ],
1222            'contentmodel' => [
1223                ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
1224            ],
1225        ];
1226    }
1227
1228    /** @inheritDoc */
1229    protected function getExamplesMessages() {
1230        return [
1231            'action=parse&page=Project:Sandbox'
1232                => 'apihelp-parse-example-page',
1233            'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
1234                => 'apihelp-parse-example-text',
1235            'action=parse&text={{PAGENAME}}&title=Test'
1236                => 'apihelp-parse-example-texttitle',
1237            'action=parse&summary=Some+[[link]]&prop='
1238                => 'apihelp-parse-example-summary',
1239        ];
1240    }
1241
1242    /** @inheritDoc */
1243    public function getHelpUrls() {
1244        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext';
1245    }
1246}
1247
1248/** @deprecated class alias since 1.43 */
1249class_alias( ApiParse::class, 'ApiParse' );