Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.09% covered (success)
95.09%
658 / 692
47.62% covered (danger)
47.62%
10 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiParse
95.22% covered (success)
95.22%
658 / 691
47.62% covered (danger)
47.62%
10 / 21
189
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.83% covered (success)
97.83%
360 / 368
0.00% covered (danger)
0.00%
0 / 1
121
 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%
113 / 113
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\IContentHandlerFactory;
14use MediaWiki\Content\Renderer\ContentRenderer;
15use MediaWiki\Content\Transform\ContentTransformer;
16use MediaWiki\Context\DerivativeContext;
17use MediaWiki\EditPage\EditPage;
18use MediaWiki\Exception\MWContentSerializationException;
19use MediaWiki\Json\FormatJson;
20use MediaWiki\Json\JsonCodec;
21use MediaWiki\Language\RawMessage;
22use MediaWiki\Languages\LanguageNameUtils;
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 RevisionLookup $revisionLookup,
78        private SkinFactory $skinFactory,
79        private LanguageNameUtils $languageNameUtils,
80        private LinkBatchFactory $linkBatchFactory,
81        private LinkCache $linkCache,
82        private IContentHandlerFactory $contentHandlerFactory,
83        private ParserFactory $parserFactory,
84        private WikiPageFactory $wikiPageFactory,
85        private ContentRenderer $contentRenderer,
86        private ContentTransformer $contentTransformer,
87        private CommentFormatter $commentFormatter,
88        private TempUserCreator $tempUserCreator,
89        private UserFactory $userFactory,
90        private UrlUtils $urlUtils,
91        private TitleFormatter $titleFormatter,
92        private 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 ( MWContentSerializationException $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        if ( isset( $prop['text'] ) ) {
493            $skin = $context ? $context->getSkin() : null;
494            $skinOptions = $skin ? $skin->getOptions() : [
495                'toc' => true,
496            ];
497            // TODO T371004 move runOutputPipeline out of $parserOutput
498            // TODO T371022 it should be reasonably straightforward to move this to a clone, but it requires
499            // careful checking of the clone and of what happens on the boundary of OutputPage. Leaving this as
500            // "getText-equivalent" for now; will fix in a later, independent patch.
501            $oldText = $p_result->getRawText();
502            $result_array['text'] = $p_result->runOutputPipeline( $popts, [
503                'allowClone' => false,
504                'allowTOC' => !$params['disabletoc'],
505                'injectTOC' => $skinOptions['toc'],
506                'wrapperDivClass' => $params['wrapoutputclass'],
507                'deduplicateStyles' => !$params['disablestylededuplication'],
508                'userLang' => $context ? $context->getLanguage() : null,
509                'skin' => $skin,
510                'includeDebugInfo' => !$params['disablepp'] && !$params['disablelimitreport']
511            ] )->getContentHolderText();
512            $p_result->setRawText( $oldText );
513            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
514            if ( $context ) {
515                $this->getHookRunner()->onOutputPageBeforeHTML( $context->getOutput(), $result_array['text'] );
516            }
517        }
518
519        if ( $outputPage ) {
520            // This needs to happen after running the OutputTransform pipeline so that the metadata inserted by
521            // the pipeline is also added to the OutputPage
522            $outputPage->addParserOutputMetadata( $p_result );
523
524            $this->getHookRunner()->onApiParseMakeOutputPage( $this, $outputPage );
525        }
526
527        if ( $params['summary'] !== null ||
528            ( $params['sectiontitle'] !== null && $this->section === 'new' )
529        ) {
530            $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
531            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
532        }
533
534        if ( isset( $prop['langlinks'] ) ) {
535            if ( $skin ) {
536                $langlinks = $outputPage->getLanguageLinks();
537            } else {
538                $langlinks = array_map(
539                    static fn ( $item ) => $item['link'],
540                    $p_result->getLinkList( ParserOutputLinkTypes::LANGUAGE )
541                );
542                // The deprecated 'effectivelanglinks' option pre-dates OutputPage
543                // support via 'useskin'. If not already applied, then run just this
544                // one hook of OutputPage::addParserOutputMetadata here.
545                if ( $params['effectivelanglinks'] ) {
546                    # for compatibility with old hook, convert to string[]
547                    $compat = [];
548                    foreach ( $langlinks as $link ) {
549                        $s = $link->getInterwiki() . ':' . $link->getText();
550                        if ( $link->hasFragment() ) {
551                            $s .= '#' . $link->getFragment();
552                        }
553                        $compat[] = $s;
554                    }
555                    $langlinks = $compat;
556                    $linkFlags = [];
557                    $this->getHookRunner()->onLanguageLinks( $titleObj, $langlinks, $linkFlags );
558                }
559            }
560
561            $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
562        }
563        if ( isset( $prop['categories'] ) ) {
564            $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategoryMap() );
565        }
566        if ( isset( $prop['categorieshtml'] ) ) {
567            $result_array['categorieshtml'] = $outputPage->getSkin()->getCategories();
568            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
569        }
570        if ( isset( $prop['links'] ) ) {
571
572            $result_array['links'] = $this->formatLinks( $p_result->getLinkList( ParserOutputLinkTypes::LOCAL ) );
573        }
574        if ( isset( $prop['templates'] ) ) {
575            $result_array['templates'] = $this->formatLinks(
576                $p_result->getLinkList( ParserOutputLinkTypes::TEMPLATE )
577            );
578        }
579        if ( isset( $prop['images'] ) ) {
580            $result_array['images'] = array_map(
581                static fn ( $item ) => $item['link']->getDBkey(),
582                $p_result->getLinkList( ParserOutputLinkTypes::MEDIA )
583            );
584        }
585        if ( isset( $prop['externallinks'] ) ) {
586            $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
587        }
588        if ( isset( $prop['sections'] ) ) {
589            $result_array['sections'] = $p_result->getSections();
590        }
591        if ( isset( $prop['tocdata'] ) ) {
592            $result_array['tocdata'] = $this->jsonCodec->toJsonArray(
593                $p_result->getTOCData(), TOCData::class
594            );
595        }
596        if ( isset( $prop['sections'] ) || isset( $prop['tocdata'] ) ) {
597            $result_array['showtoc'] = $p_result->getOutputFlag( ParserOutputFlags::SHOW_TOC );
598        }
599        if ( isset( $prop['parsewarnings'] ) || isset( $prop['parsewarningshtml'] ) ) {
600            $warningMsgs = array_map(
601                static fn ( $mv ) => Message::newFromSpecifier( $mv )
602                    ->page( $titleObj )
603                    // Note that we use ContentLanguage here
604                    ->inContentLanguage(),
605                $p_result->getWarningMsgs()
606            );
607            if ( $warningMsgs === [] ) {
608                // Backward compatibilty with cached ParserOutput from
609                // MW <= 1.45 which didn't store the MessageValues (T343048)
610                $warningMsgs = array_map(
611                    static fn ( $warning ) => new RawMessage( '$1', [ $warning ] ),
612                    $p_result->getWarnings()
613                );
614            }
615            if ( isset( $prop['parsewarnings'] ) ) {
616                $warnings = array_map( static fn ( $msg ) => $msg->text(), $warningMsgs );
617                $result_array['parsewarnings'] = $warnings;
618            }
619            if ( isset( $prop['parsewarningshtml'] ) ) {
620                $warningsHtml = array_map( static fn ( $msg ) => $msg->parse(), $warningMsgs );
621                $result_array['parsewarningshtml'] = $warningsHtml;
622            }
623        }
624
625        if ( isset( $prop['displaytitle'] ) ) {
626            $result_array['displaytitle'] = $p_result->getDisplayTitle() !== false
627                ? $p_result->getDisplayTitle()
628                : htmlspecialchars( $titleObj->getPrefixedText(), ENT_NOQUOTES );
629        }
630
631        if ( isset( $prop['subtitle'] ) ) {
632            // Get the subtitle without its container element to support UI refreshing
633            $result_array['subtitle'] = $context->getSkin()->prepareSubtitle( false );
634        }
635
636        if ( isset( $prop['headitems'] ) ) {
637            if ( $skin ) {
638                $result_array['headitems'] = $this->formatHeadItems( $outputPage->getHeadItemsArray() );
639            } else {
640                $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
641            }
642        }
643
644        if ( isset( $prop['headhtml'] ) ) {
645            $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
646            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
647        }
648
649        if ( isset( $prop['modules'] ) ) {
650            if ( $skin ) {
651                $result_array['modules'] = $outputPage->getModules();
652                // Deprecated since 1.32 (T188689)
653                $result_array['modulescripts'] = [];
654                $result_array['modulestyles'] = $outputPage->getModuleStyles();
655            } else {
656                $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
657                // Deprecated since 1.32 (T188689)
658                $result_array['modulescripts'] = [];
659                $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
660            }
661        }
662
663        if ( isset( $prop['jsconfigvars'] ) ) {
664            $showStrategyKeys = (bool)( $params['showstrategykeys'] );
665            $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars( $showStrategyKeys );
666            $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars );
667        }
668
669        if ( isset( $prop['encodedjsconfigvars'] ) ) {
670            $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
671            $result_array['encodedjsconfigvars'] = FormatJson::encode(
672                $jsconfigvars,
673                false,
674                FormatJson::ALL_OK
675            );
676            $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
677        }
678
679        if ( isset( $prop['modules'] ) &&
680            !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
681            $this->addWarning( 'apiwarn-moduleswithoutvars' );
682        }
683
684        if ( isset( $prop['indicators'] ) ) {
685            if ( $skin ) {
686                $result_array['indicators'] = $outputPage->getIndicators();
687            } else {
688                $result_array['indicators'] = $p_result->getIndicators();
689            }
690            ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
691        }
692
693        if ( isset( $prop['iwlinks'] ) ) {
694            $links = array_map(
695                static fn ( $item ) => $item['link'],
696                $p_result->getLinkList( ParserOutputLinkTypes::INTERWIKI )
697            );
698            $result_array['iwlinks'] = $this->formatIWLinks( $links );
699        }
700
701        if ( isset( $prop['wikitext'] ) ) {
702            $result_array['wikitext'] = $this->content->serialize( $format );
703            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
704            // @phan-suppress-next-line PhanImpossibleTypeComparison
705            if ( $this->pstContent !== null ) {
706                $result_array['psttext'] = $this->pstContent->serialize( $format );
707                $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
708            }
709        }
710        if ( isset( $prop['properties'] ) ) {
711            $result_array['properties'] = $p_result->getPageProperties();
712            ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
713        }
714
715        if ( isset( $prop['limitreportdata'] ) ) {
716            $result_array['limitreportdata'] =
717                $this->formatLimitReportData( $p_result->getLimitReportData() );
718        }
719        if ( isset( $prop['limitreporthtml'] ) ) {
720            $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
721            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
722        }
723
724        if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
725            if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
726                $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
727            }
728
729            $parser = $this->parserFactory->getInstance();
730            $parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
731            // @phan-suppress-next-line PhanUndeclaredMethod
732            $xml = $parser->preprocessToDom( $this->content->getText() )->__toString();
733            $result_array['parsetree'] = $xml;
734            $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
735        }
736
737        $result_mapping = [
738            'redirects' => 'r',
739            'langlinks' => 'll',
740            'categories' => 'cl',
741            'links' => 'pl',
742            'templates' => 'tl',
743            'images' => 'img',
744            'externallinks' => 'el',
745            'iwlinks' => 'iw',
746            'sections' => 's',
747            'tocdata' => 'toc',
748            'headitems' => 'hi',
749            'modules' => 'm',
750            'indicators' => 'ind',
751            'modulescripts' => 'm',
752            'modulestyles' => 'm',
753            'properties' => 'pp',
754            'limitreportdata' => 'lr',
755            'parsewarnings' => 'pw',
756            'parsewarningshtml' => 'pw',
757        ];
758        $this->setIndexedTagNames( $result_array, $result_mapping );
759        $result->addValue( null, $this->getModuleName(), $result_array );
760    }
761
762    /**
763     * Constructs a ParserOptions object
764     *
765     * @param WikiPage $pageObj
766     * @param array $params
767     *
768     * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ]
769     */
770    private function makeParserOptions( WikiPage $pageObj, array $params ) {
771        $popts = $pageObj->makeParserOptions( $this->getContext() );
772        $popts->setRenderReason( 'api-parse' );
773        if ( $params['usearticle'] ) {
774            # T349037: The ArticleParserOptions hook should be broadened to take
775            # a WikiPage (aka $pageObj) instead of an Article.  But for now
776            # fake the Article.
777            $article = Article::newFromWikiPage( $pageObj, $this->getContext() );
778            # Allow extensions to vary parser options used for article rendering,
779            # in the same way Article does
780            $this->getHookRunner()->onArticleParserOptions( $article, $popts );
781        }
782        return $this->tweakParserOptions( $popts, $pageObj->getTitle(), $params );
783    }
784
785    /**
786     * Tweaks a ParserOptions object
787     *
788     * @param ParserOptions $popts
789     * @param Title $title
790     * @param array $params
791     *
792     * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ]
793     */
794    private function tweakParserOptions( ParserOptions $popts, Title $title, array $params ) {
795        $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
796        $popts->setIsSectionPreview( $params['sectionpreview'] );
797
798        if ( $params['wrapoutputclass'] !== '' ) {
799            $popts->setWrapOutputClass( $params['wrapoutputclass'] );
800        }
801        if ( $params['parsoid'] || $params['parser'] === 'parsoid' ) {
802            $popts->setUseParsoid( true );
803        }
804        if ( $params['parser'] === 'legacy' ) {
805            $popts->setUseParsoid( false );
806        }
807        if ( $params['disableeditsection'] ) {
808            $popts->setSuppressSectionEditLinks();
809        }
810
811        $reset = null;
812        $suppressCache = false;
813        $this->getHookRunner()->onApiMakeParserOptions( $popts, $title,
814            $params, $this, $reset, $suppressCache );
815
816        return [ $popts, $reset, $suppressCache ];
817    }
818
819    /**
820     * @param WikiPage $page
821     * @param ParserOptions $popts
822     * @param bool $suppressCache
823     * @param int $pageId
824     * @param RevisionRecord|null $rev
825     * @param bool $getContent
826     * @return ParserOutput
827     */
828    private function getParsedContent(
829        WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
830    ) {
831        $revId = $rev ? $rev->getId() : null;
832        $isDeleted = $rev && $rev->isDeleted( RevisionRecord::DELETED_TEXT );
833
834        if ( $getContent || $this->section !== false || $isDeleted ) {
835            if ( $rev ) {
836                $this->content = $rev->getContent(
837                    SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $this->getAuthority()
838                );
839                if ( !$this->content ) {
840                    $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
841                }
842            } else {
843                $this->content = $page->getContent( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
844                if ( !$this->content ) {
845                    $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ] );
846                }
847            }
848            $this->contentIsDeleted = $isDeleted;
849            $this->contentIsSuppressed = $rev &&
850                $rev->isDeleted( RevisionRecord::DELETED_TEXT | RevisionRecord::DELETED_RESTRICTED );
851        }
852
853        if ( $this->section !== false ) {
854            $this->content = $this->getSectionContent(
855                $this->content,
856                $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
857            );
858            return $this->getContentParserOutput(
859                $this->content, $page->getTitle(),
860                $rev,
861                $popts
862            );
863        }
864
865        if ( $isDeleted ) {
866            // getParserOutput can't do revdeled revisions
867
868            $pout = $this->getContentParserOutput(
869                $this->content, $page->getTitle(),
870                $rev,
871                $popts
872            );
873        } else {
874            // getParserOutput will save to Parser cache if able
875            $pout = $this->getPageParserOutput( $page, $revId, $popts, $suppressCache );
876        }
877        if ( !$pout ) {
878            // @codeCoverageIgnoreStart
879            $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
880            // @codeCoverageIgnoreEnd
881        }
882
883        return $pout;
884    }
885
886    /**
887     * Extract the requested section from the given Content
888     *
889     * @param Content $content
890     * @param string|Message $what Identifies the content in error messages, e.g. page title.
891     * @return Content
892     */
893    private function getSectionContent( Content $content, $what ) {
894        // Not cached (save or load)
895        $section = $content->getSection( $this->section );
896        if ( $section === false ) {
897            $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
898        }
899        if ( $section === null ) {
900            $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
901        }
902
903        // @phan-suppress-next-line PhanTypeMismatchReturnNullable T240141
904        return $section;
905    }
906
907    /**
908     * This mimics the behavior of EditPage in formatting a summary
909     *
910     * @param Title $title of the page being parsed
911     * @param array $params The API parameters of the request
912     * @return string HTML
913     */
914    private function formatSummary( $title, $params ) {
915        $summary = $params['summary'] ?? '';
916        $sectionTitle = $params['sectiontitle'] ?? '';
917
918        if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
919            if ( $sectionTitle !== '' ) {
920                $summary = $params['sectiontitle'];
921            }
922            if ( $summary !== '' ) {
923                $summary = $this->msg( 'newsectionsummary' )
924                    ->rawParams( $this->parserFactory->getMainInstance()->stripSectionName( $summary ) )
925                    ->inContentLanguage()->text();
926            }
927        }
928        return $this->commentFormatter->format( $summary, $title, $this->section === 'new' );
929    }
930
931    /**
932     * @param string[]|ParsoidLinkTarget[] $links
933     * @return array
934     */
935    private function formatLangLinks( $links ): array {
936        $result = [];
937        foreach ( $links as $link ) {
938            $entry = [];
939            if ( is_string( $link ) ) {
940                [ $lang, $titleWithFrag ] = explode( ':', $link, 2 );
941                [ $title, $frag ] = array_pad( explode( '#', $titleWithFrag, 2 ), 2, '' );
942                $title = TitleValue::tryNew( NS_MAIN, $title, $frag, $lang );
943            } else {
944                $title = $link;
945                $lang = $link->getInterwiki();
946                $titleWithFrag = $link->getText();
947                if ( $link->hasFragment() ) {
948                    $titleWithFrag .= '#' . $link->getFragment();
949                }
950            }
951            $title = Title::castFromLinkTarget( $title );
952
953            $entry['lang'] = $lang;
954            if ( $title ) {
955                $entry['url'] = (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT );
956                // title validity implies language code validity
957                // localised language name in 'uselang' language
958                $entry['langname'] = $this->languageNameUtils->getLanguageName(
959                    $lang,
960                    $this->getLanguage()->getCode()
961                );
962
963                // native language name
964                $entry['autonym'] = $this->languageNameUtils->getLanguageName( $lang );
965            }
966            ApiResult::setContentValue( $entry, 'title', $titleWithFrag );
967            $result[] = $entry;
968        }
969
970        return $result;
971    }
972
973    private function formatCategoryLinks( array $links ): array {
974        $result = [];
975
976        if ( !$links ) {
977            return $result;
978        }
979
980        // Fetch hiddencat property
981        $lb = $this->linkBatchFactory->newLinkBatch();
982        $lb->setArray( [ NS_CATEGORY => $links ] );
983        $db = $this->getDB();
984        $res = $db->newSelectQueryBuilder()
985            ->select( [ 'page_title', 'pp_propname' ] )
986            ->from( 'page' )
987            ->where( $lb->constructSet( 'page', $db ) )
988            ->leftJoin( 'page_props', null, [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ] )
989            ->caller( __METHOD__ )
990            ->fetchResultSet();
991        $hiddencats = [];
992        foreach ( $res as $row ) {
993            $hiddencats[$row->page_title] = isset( $row->pp_propname );
994        }
995
996        foreach ( $links as $link => $sortkey ) {
997            $entry = [];
998            $entry['sortkey'] = $sortkey;
999            // array keys will cast numeric category names to ints, so cast back to string
1000            ApiResult::setContentValue( $entry, 'category', (string)$link );
1001            if ( !isset( $hiddencats[$link] ) ) {
1002                $entry['missing'] = true;
1003
1004                // We already know the link doesn't exist in the database, so
1005                // tell LinkCache that before calling $title->isKnown().
1006                $title = Title::makeTitle( NS_CATEGORY, $link );
1007                $this->linkCache->addBadLinkObj( $title );
1008                if ( $title->isKnown() ) {
1009                    $entry['known'] = true;
1010                }
1011            } elseif ( $hiddencats[$link] ) {
1012                $entry['hidden'] = true;
1013            }
1014            $result[] = $entry;
1015        }
1016
1017        return $result;
1018    }
1019
1020    /**
1021     * @param list<array{link:ParsoidLinkTarget,pageid?:int}> $links
1022     * @return array
1023     */
1024    private function formatLinks( array $links ): array {
1025        $result = [];
1026        foreach ( $links as [ 'link' => $link, 'pageid' => $id ] ) {
1027            $entry = [];
1028            $entry['ns'] = $link->getNamespace();
1029            ApiResult::setContentValue( $entry, 'title', Title::newFromLinkTarget( $link )->getFullText() );
1030            $entry['exists'] = $id != 0;
1031            $result[] = $entry;
1032        }
1033
1034        return $result;
1035    }
1036
1037    private function formatIWLinks( array $iw ): array {
1038        $result = [];
1039        foreach ( $iw as $linkTarget ) {
1040            $entry = [];
1041            $entry['prefix'] = $linkTarget->getInterwiki();
1042            $title = Title::newFromLinkTarget( $linkTarget );
1043            if ( $title ) {
1044                $entry['url'] = (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT );
1045
1046                ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
1047            }
1048            $result[] = $entry;
1049        }
1050
1051        return $result;
1052    }
1053
1054    private function formatHeadItems( array $headItems ): array {
1055        $result = [];
1056        foreach ( $headItems as $tag => $content ) {
1057            $entry = [];
1058            $entry['tag'] = $tag;
1059            ApiResult::setContentValue( $entry, 'content', $content );
1060            $result[] = $entry;
1061        }
1062
1063        return $result;
1064    }
1065
1066    private function formatLimitReportData( array $limitReportData ): array {
1067        $result = [];
1068
1069        foreach ( $limitReportData as $name => $value ) {
1070            $entry = [];
1071            $entry['name'] = $name;
1072            if ( !is_array( $value ) ) {
1073                $value = [ $value ];
1074            }
1075            ApiResult::setIndexedTagNameRecursive( $value, 'param' );
1076            $entry = array_merge( $entry, $value );
1077            $result[] = $entry;
1078        }
1079
1080        return $result;
1081    }
1082
1083    private function setIndexedTagNames( array &$array, array $mapping ) {
1084        foreach ( $mapping as $key => $name ) {
1085            if ( isset( $array[$key] ) ) {
1086                ApiResult::setIndexedTagName( $array[$key], $name );
1087            }
1088        }
1089    }
1090
1091    /** @inheritDoc */
1092    public function getAllowedParams() {
1093        return [
1094            'title' => null,
1095            'text' => [
1096                ParamValidator::PARAM_TYPE => 'text',
1097            ],
1098            'revid' => [
1099                ParamValidator::PARAM_TYPE => 'integer',
1100            ],
1101            'summary' => null,
1102            'page' => null,
1103            'pageid' => [
1104                ParamValidator::PARAM_TYPE => 'integer',
1105            ],
1106            'redirects' => false,
1107            'oldid' => [
1108                ParamValidator::PARAM_TYPE => 'integer',
1109            ],
1110            'prop' => [
1111                ParamValidator::PARAM_DEFAULT => 'text|langlinks|categories|links|templates|' .
1112                    'images|externallinks|sections|tocdata|revid|displaytitle|iwlinks|' .
1113                    'properties|parsewarnings',
1114                ParamValidator::PARAM_ISMULTI => true,
1115                ParamValidator::PARAM_TYPE => [
1116                    'text',
1117                    'langlinks',
1118                    'categories',
1119                    'categorieshtml',
1120                    'links',
1121                    'templates',
1122                    'images',
1123                    'externallinks',
1124                    'sections',
1125                    'tocdata',
1126                    'revid',
1127                    'displaytitle',
1128                    'subtitle',
1129                    'headhtml',
1130                    'modules',
1131                    'jsconfigvars',
1132                    'encodedjsconfigvars',
1133                    'indicators',
1134                    'iwlinks',
1135                    'wikitext',
1136                    'properties',
1137                    'limitreportdata',
1138                    'limitreporthtml',
1139                    'parsetree',
1140                    'parsewarnings',
1141                    'parsewarningshtml',
1142                    'headitems',
1143                ],
1144                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
1145                    'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
1146                ],
1147                EnumDef::PARAM_DEPRECATED_VALUES => [
1148                    'headitems' => 'apiwarn-deprecation-parse-headitems',
1149                    // deprecated since 1.46: T319141
1150                    'sections' => 'apiwarn-deprecation-parse-sections',
1151                ],
1152            ],
1153            'wrapoutputclass' => 'mw-parser-output',
1154            'usearticle' => false, // since 1.43
1155            'parsoid' => [ // since 1.41
1156                ParamValidator::PARAM_TYPE => 'boolean',
1157                ParamValidator::PARAM_DEFAULT => false,
1158                ParamValidator::PARAM_DEPRECATED => true,
1159            ],
1160            'parser' => [ // since 1.45
1161                ParamValidator::PARAM_TYPE => [
1162                    'parsoid',
1163                    'default',
1164                    'legacy'
1165                ],
1166                ParamValidator::PARAM_DEFAULT => 'default',
1167                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
1168            ],
1169            'pst' => false,
1170            'onlypst' => false,
1171            'effectivelanglinks' => [
1172                ParamValidator::PARAM_DEFAULT => false,
1173                ParamValidator::PARAM_DEPRECATED => true,
1174            ],
1175            'section' => null,
1176            'sectiontitle' => [
1177                ParamValidator::PARAM_TYPE => 'string',
1178            ],
1179            'disablepp' => [
1180                ParamValidator::PARAM_DEFAULT => false,
1181                ParamValidator::PARAM_DEPRECATED => true,
1182            ],
1183            'disablelimitreport' => false,
1184            'disableeditsection' => false,
1185            'disablestylededuplication' => false,
1186            'showstrategykeys' => false,
1187            'generatexml' => [
1188                ParamValidator::PARAM_DEFAULT => false,
1189                ApiBase::PARAM_HELP_MSG => [
1190                    'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
1191                ],
1192                ParamValidator::PARAM_DEPRECATED => true,
1193            ],
1194            'preview' => false,
1195            'sectionpreview' => false,
1196            'disabletoc' => false,
1197            'useskin' => [
1198                // T237856; We use all installed skins here to allow hidden (but usable) skins
1199                // to continue working correctly with some features such as Live Preview
1200                ParamValidator::PARAM_TYPE => array_keys( $this->skinFactory->getInstalledSkins() ),
1201            ],
1202            'contentformat' => [
1203                ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
1204            ],
1205            'contentmodel' => [
1206                ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
1207            ],
1208        ];
1209    }
1210
1211    /** @inheritDoc */
1212    protected function getExamplesMessages() {
1213        return [
1214            'action=parse&page=Project:Sandbox'
1215                => 'apihelp-parse-example-page',
1216            'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
1217                => 'apihelp-parse-example-text',
1218            'action=parse&text={{PAGENAME}}&title=Test'
1219                => 'apihelp-parse-example-texttitle',
1220            'action=parse&summary=Some+[[link]]&prop='
1221                => 'apihelp-parse-example-summary',
1222        ];
1223    }
1224
1225    /** @inheritDoc */
1226    public function getHelpUrls() {
1227        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext';
1228    }
1229}
1230
1231/** @deprecated class alias since 1.43 */
1232class_alias( ApiParse::class, 'ApiParse' );