Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 460
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiHelp
0.00% covered (danger)
0.00%
0 / 459
0.00% covered (danger)
0.00%
0 / 12
8742
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
20
 getHelp
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
240
 fixHelpLinks
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
56
 wrap
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpInternal
0.00% covered (danger)
0.00%
0 / 308
0.00% covered (danger)
0.00%
0 / 1
3422
 shouldCheckMaxlag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isReadMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCustomPrinter
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2014 Wikimedia Foundation and contributors
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\Context\DerivativeContext;
12use MediaWiki\Context\IContextSource;
13use MediaWiki\Html\Html;
14use MediaWiki\Html\HtmlHelper;
15use MediaWiki\Json\FormatJson;
16use MediaWiki\MainConfigNames;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Message\Message;
19use MediaWiki\Output\OutputPage;
20use MediaWiki\Parser\Sanitizer;
21use MediaWiki\Skin\SkinFactory;
22use MediaWiki\SpecialPage\SpecialPage;
23use MediaWiki\Specials\SpecialVersion;
24use MediaWiki\Title\Title;
25use MediaWiki\Utils\ExtensionInfo;
26use Wikimedia\ParamValidator\ParamValidator;
27use Wikimedia\Parsoid\Core\SectionMetadata;
28use Wikimedia\Parsoid\Core\TOCData;
29use Wikimedia\RemexHtml\Serializer\SerializerNode;
30
31/**
32 * Class to output help for an API module
33 *
34 * @since 1.25 completely rewritten
35 * @ingroup API
36 */
37class ApiHelp extends ApiBase {
38    private SkinFactory $skinFactory;
39
40    public function __construct(
41        ApiMain $main,
42        string $action,
43        SkinFactory $skinFactory
44    ) {
45        parent::__construct( $main, $action );
46        $this->skinFactory = $skinFactory;
47    }
48
49    public function execute() {
50        $params = $this->extractRequestParams();
51        $modules = [];
52
53        foreach ( $params['modules'] as $path ) {
54            $modules[] = $this->getModuleFromPath( $path );
55        }
56
57        // Get the help
58        $context = new DerivativeContext( $this->getMain()->getContext() );
59        $context->setSkin( $this->skinFactory->makeSkin( 'apioutput' ) );
60        $context->setLanguage( $this->getMain()->getLanguage() );
61        $context->setTitle( SpecialPage::getTitleFor( 'ApiHelp' ) );
62        $out = new OutputPage( $context );
63        $out->setRobotPolicy( 'noindex,nofollow' );
64        $out->setCopyrightUrl( 'https://www.mediawiki.org/wiki/Special:MyLanguage/Copyright' );
65        $out->disallowUserJs();
66        $context->setOutput( $out );
67
68        self::getHelp( $context, $modules, $params );
69
70        // Grab the output from the skin
71        ob_start();
72        $context->getOutput()->output();
73        $html = ob_get_clean();
74
75        $result = $this->getResult();
76        if ( $params['wrap'] ) {
77            $data = [
78                'mime' => 'text/html',
79                'filename' => 'api-help.html',
80                'help' => $html,
81            ];
82            ApiResult::setSubelementsList( $data, 'help' );
83            $result->addValue( null, $this->getModuleName(), $data );
84        } else {
85            // Show any errors at the top of the HTML
86            $transform = [
87                'Types' => [ 'AssocAsObject' => true ],
88                'Strip' => 'all',
89            ];
90            $errors = array_filter( [
91                'errors' => $this->getResult()->getResultData( [ 'errors' ], $transform ),
92                'warnings' => $this->getResult()->getResultData( [ 'warnings' ], $transform ),
93            ] );
94            if ( $errors ) {
95                $json = FormatJson::encode( $errors, true, FormatJson::UTF8_OK );
96                // Escape any "--", some parsers might interpret that as end-of-comment.
97                // The above already escaped any "<" and ">".
98                $json = str_replace( '--', '-\u002D', $json );
99                $html = "<!-- API warnings and errors:\n$json\n-->\n$html";
100            }
101
102            $result->reset();
103            $result->addValue( null, 'text', $html, ApiResult::NO_SIZE_CHECK );
104            $result->addValue( null, 'mime', 'text/html', ApiResult::NO_SIZE_CHECK );
105            $result->addValue( null, 'filename', 'api-help.html', ApiResult::NO_SIZE_CHECK );
106        }
107    }
108
109    /**
110     * Generate help for the specified modules
111     *
112     * Help is placed into the OutputPage object returned by
113     * $context->getOutput().
114     *
115     * Recognized options include:
116     *  - headerlevel: (int) Header tag level
117     *  - nolead: (bool) Skip the inclusion of api-help-lead
118     *  - noheader: (bool) Skip the inclusion of the top-level section headers
119     *  - submodules: (bool) Include help for submodules of the current module
120     *  - recursivesubmodules: (bool) Include help for submodules recursively
121     *  - helptitle: (string) Title to link for additional modules' help. Should contain $1.
122     *  - toc: (bool) Include a table of contents
123     *
124     * @param IContextSource $context
125     * @param ApiBase[]|ApiBase $modules
126     * @param array $options Formatting options (described above)
127     */
128    public static function getHelp( IContextSource $context, $modules, array $options ) {
129        if ( !is_array( $modules ) ) {
130            $modules = [ $modules ];
131        }
132
133        $out = $context->getOutput();
134        $out->addModuleStyles( [
135            'mediawiki.hlist',
136            'mediawiki.apipretty',
137        ] );
138        $out->setPageTitleMsg( $context->msg( 'api-help-title' ) );
139
140        $services = MediaWikiServices::getInstance();
141        $cache = $services->getMainWANObjectCache();
142        $cacheKey = null;
143        if ( count( $modules ) == 1 && $modules[0] instanceof ApiMain &&
144            $options['recursivesubmodules'] &&
145            $context->getLanguage()->equals( $services->getContentLanguage() )
146        ) {
147            $cacheHelpTimeout = $context->getConfig()->get( MainConfigNames::APICacheHelpTimeout );
148            if ( $cacheHelpTimeout > 0 ) {
149                // Get help text from cache if present
150                $cacheKey = $cache->makeKey( 'apihelp', $modules[0]->getModulePath(),
151                    (int)!empty( $options['toc'] ),
152                    str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) );
153                $cached = $cache->get( $cacheKey );
154                if ( $cached ) {
155                    $out->addHTML( $cached );
156                    return;
157                }
158            }
159        }
160        if ( $out->getHTML() !== '' ) {
161            // Don't save to cache, there's someone else's content in the page
162            // already
163            $cacheKey = null;
164        }
165
166        $options['recursivesubmodules'] = !empty( $options['recursivesubmodules'] );
167        $options['submodules'] = $options['recursivesubmodules'] || !empty( $options['submodules'] );
168
169        // Prepend lead
170        if ( empty( $options['nolead'] ) ) {
171            $msg = $context->msg( 'api-help-lead' );
172            if ( !$msg->isDisabled() ) {
173                $out->addHTML( $msg->parseAsBlock() );
174            }
175        }
176
177        $haveModules = [];
178        $html = self::getHelpInternal( $context, $modules, $options, $haveModules );
179        if ( !empty( $options['toc'] ) && $haveModules ) {
180            $out->addTOCPlaceholder( new TOCData( ...array_values( $haveModules ) ) );
181        }
182        $out->addHTML( $html );
183
184        $helptitle = $options['helptitle'] ?? null;
185        $html = self::fixHelpLinks( $out->getHTML(), $helptitle, $haveModules );
186        $out->clearHTML();
187        $out->addHTML( $html );
188
189        if ( $cacheKey !== null ) {
190            // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $cacheHelpTimeout declared when $cacheKey is set
191            $cache->set( $cacheKey, $out->getHTML(), $cacheHelpTimeout );
192        }
193    }
194
195    /**
196     * Replace Special:ApiHelp links with links to api.php
197     *
198     * @param string $html
199     * @param string|null $helptitle Title to link to rather than api.php, must contain '$1'
200     * @param array $localModules Keys are modules to link within the current page, values are ignored
201     * @return string
202     */
203    public static function fixHelpLinks( $html, $helptitle = null, $localModules = [] ) {
204        return HtmlHelper::modifyElements(
205            $html,
206            static function ( SerializerNode $node ): bool {
207                return $node->name === 'a'
208                    && isset( $node->attrs['href'] )
209                    && !str_contains( $node->attrs['class'] ?? '', 'apihelp-linktrail' );
210            },
211            static function ( SerializerNode $node ) use ( $helptitle, $localModules ): SerializerNode {
212                $href = $node->attrs['href'];
213                // FIXME This can't be right to do this in a loop
214                do {
215                    $old = $href;
216                    $href = rawurldecode( $href );
217                } while ( $old !== $href );
218                if ( preg_match( '!Special:ApiHelp/([^&/|#]+)((?:#.*)?)!', $href, $m ) ) {
219                    if ( isset( $localModules[$m[1]] ) ) {
220                        $href = $m[2] === '' ? '#' . $m[1] : $m[2];
221                    } elseif ( $helptitle !== null ) {
222                        $href = Title::newFromText( str_replace( '$1', $m[1], $helptitle ) . $m[2] )
223                            ->getFullURL();
224                    } else {
225                        $href = wfAppendQuery( wfScript( 'api' ), [
226                            'action' => 'help',
227                            'modules' => $m[1],
228                        ] ) . $m[2];
229                    }
230                    $node->attrs['href'] = $href;
231                    unset( $node->attrs['title'] );
232                }
233
234                return $node;
235            }
236        );
237    }
238
239    /**
240     * Wrap a message in HTML with a class.
241     *
242     * @param Message $msg
243     * @param string $class
244     * @param string $tag
245     * @return string
246     */
247    private static function wrap( Message $msg, $class, $tag = 'span' ) {
248        return Html::rawElement( $tag, [ 'class' => $class ],
249            $msg->parse()
250        );
251    }
252
253    /**
254     * Recursively-called function to actually construct the help
255     *
256     * @param IContextSource $context
257     * @param ApiBase[] $modules
258     * @param array $options
259     * @param array &$haveModules
260     * @return string
261     */
262    private static function getHelpInternal( IContextSource $context, array $modules,
263        array $options, &$haveModules
264    ) {
265        $out = '';
266
267        $level = empty( $options['headerlevel'] ) ? 2 : $options['headerlevel'];
268        if ( empty( $options['tocnumber'] ) ) {
269            $tocnumber = [ 2 => 0 ];
270        } else {
271            $tocnumber = &$options['tocnumber'];
272        }
273
274        foreach ( $modules as $module ) {
275            $paramValidator = $module->getMain()->getParamValidator();
276            $tocnumber[$level]++;
277            $path = $module->getModulePath();
278            $module->setContext( $context );
279            $help = [
280                'header' => '',
281                'flags' => '',
282                'description' => '',
283                'help-urls' => '',
284                'parameters' => '',
285                'examples' => '',
286                'submodules' => '',
287            ];
288
289            if ( empty( $options['noheader'] ) || !empty( $options['toc'] ) ) {
290                $anchor = $path;
291                $i = 1;
292                while ( isset( $haveModules[$anchor] ) ) {
293                    $anchor = $path . '|' . ++$i;
294                }
295
296                if ( $module->isMain() ) {
297                    $headerContent = $context->msg( 'api-help-main-header' )->parse();
298                    $headerAttr = [
299                        'class' => 'apihelp-header',
300                    ];
301                } else {
302                    $name = $module->getModuleName();
303                    $headerContent = htmlspecialchars(
304                        $module->getParent()->getModuleManager()->getModuleGroup( $name ) . "=$name"
305                    );
306                    if ( $module->getModulePrefix() !== '' ) {
307                        $headerContent .= ' ' .
308                            $context->msg( 'parentheses', $module->getModulePrefix() )->parse();
309                    }
310                    // Module names are always in English and not localized,
311                    // so English language and direction must be set explicitly,
312                    // otherwise parentheses will get broken in RTL wikis
313                    $headerAttr = [
314                        'class' => [ 'apihelp-header', 'apihelp-module-name' ],
315                        'dir' => 'ltr',
316                        'lang' => 'en',
317                    ];
318                }
319
320                $headerAttr['id'] = $anchor;
321
322                $haveModules[$anchor] = new SectionMetadata(
323                    tocLevel: count( $tocnumber ),
324                    hLevel: $level,
325                    line: $headerContent,
326                    number: implode( '.', $tocnumber ),
327                    index: (string)( 1 + count( $haveModules ) ),
328                    anchor: $anchor,
329                    linkAnchor: Sanitizer::escapeIdForLink( $anchor ),
330                );
331                if ( empty( $options['noheader'] ) ) {
332                    $help['header'] .= Html::rawElement(
333                        'h' . min( 6, $level ),
334                        $headerAttr,
335                        $headerContent
336                    );
337                }
338            } else {
339                $haveModules[$path] = true;
340            }
341
342            $links = [];
343            $any = false;
344            for ( $m = $module; $m !== null; $m = $m->getParent() ) {
345                $name = $m->getModuleName();
346                if ( $name === 'main_int' ) {
347                    $name = 'main';
348                }
349
350                if ( count( $modules ) === 1 && $m === $modules[0] &&
351                    !( !empty( $options['submodules'] ) && $m->getModuleManager() )
352                ) {
353                    $link = Html::element( 'b', [ 'dir' => 'ltr', 'lang' => 'en' ], $name );
354                } else {
355                    $link = SpecialPage::getTitleFor( 'ApiHelp', $m->getModulePath() )->getLocalURL();
356                    $link = Html::element( 'a',
357                        [ 'href' => $link, 'class' => 'apihelp-linktrail', 'dir' => 'ltr', 'lang' => 'en' ],
358                        $name
359                    );
360                    $any = true;
361                }
362                array_unshift( $links, $link );
363            }
364            if ( $any ) {
365                $help['header'] .= self::wrap(
366                    $context->msg( 'parentheses' )
367                        ->rawParams( $context->getLanguage()->pipeList( $links ) ),
368                    'apihelp-linktrail', 'div'
369                );
370            }
371
372            $flags = $module->getHelpFlags();
373            $help['flags'] .= Html::openElement( 'div',
374                [ 'class' => [ 'apihelp-block', 'apihelp-flags' ] ] );
375            $msg = $context->msg( 'api-help-flags' );
376            if ( !$msg->isDisabled() ) {
377                $help['flags'] .= self::wrap(
378                    $msg->numParams( count( $flags ) ), 'apihelp-block-head', 'div'
379                );
380            }
381            $help['flags'] .= Html::openElement( 'ul' );
382            foreach ( $flags as $flag ) {
383                $help['flags'] .= Html::rawElement( 'li', [],
384                    // The follow classes are used here:
385                    // * apihelp-flag-generator
386                    // * apihelp-flag-internal
387                    // * apihelp-flag-mustbeposted
388                    // * apihelp-flag-readrights
389                    // * apihelp-flag-writerights
390                    self::wrap( $context->msg( "api-help-flag-$flag" ), "apihelp-flag-$flag" )
391                );
392            }
393            $sourceInfo = $module->getModuleSourceInfo();
394            if ( $sourceInfo ) {
395                if ( isset( $sourceInfo['namemsg'] ) ) {
396                    $extname = $context->msg( $sourceInfo['namemsg'] )->text();
397                } else {
398                    // Probably English, so wrap it.
399                    $extname = Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $sourceInfo['name'] );
400                }
401                $help['flags'] .= Html::rawElement( 'li', [],
402                    self::wrap(
403                        $context->msg( 'api-help-source', $extname, $sourceInfo['name'] ),
404                        'apihelp-source'
405                    )
406                );
407
408                $linkText = SpecialPage::getTitleFor( 'Version', 'License/' . $sourceInfo['name'] )
409                    ->getPrefixedText();
410                if ( isset( $sourceInfo['license-name'] ) ) {
411                    $msg = $context->msg( 'api-help-license', $linkText,
412                        Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $sourceInfo['license-name'] )
413                    );
414                } elseif ( ExtensionInfo::getLicenseFileNames( dirname( $sourceInfo['path'] ) ) ) {
415                    $msg = $context->msg( 'api-help-license-noname', $linkText );
416                } else {
417                    $msg = $context->msg( 'api-help-license-unknown' );
418                }
419                $help['flags'] .= Html::rawElement( 'li', [],
420                    self::wrap( $msg, 'apihelp-license' )
421                );
422            } else {
423                $help['flags'] .= Html::rawElement( 'li', [],
424                    self::wrap( $context->msg( 'api-help-source-unknown' ), 'apihelp-source' )
425                );
426                $help['flags'] .= Html::rawElement( 'li', [],
427                    self::wrap( $context->msg( 'api-help-license-unknown' ), 'apihelp-license' )
428                );
429            }
430            $help['flags'] .= Html::closeElement( 'ul' );
431            $help['flags'] .= Html::closeElement( 'div' );
432
433            foreach ( $module->getFinalDescription() as $msg ) {
434                $msg->setContext( $context );
435                $help['description'] .= $msg->parseAsBlock();
436            }
437
438            $urls = $module->getHelpUrls();
439            if ( $urls ) {
440                if ( !is_array( $urls ) ) {
441                    $urls = [ $urls ];
442                }
443                $help['help-urls'] .= Html::openElement( 'div',
444                    [ 'class' => [ 'apihelp-block', 'apihelp-help-urls' ] ]
445                );
446                $msg = $context->msg( 'api-help-help-urls' );
447                if ( !$msg->isDisabled() ) {
448                    $help['help-urls'] .= self::wrap(
449                        $msg->numParams( count( $urls ) ), 'apihelp-block-head', 'div'
450                    );
451                }
452                $help['help-urls'] .= Html::openElement( 'ul' );
453                foreach ( $urls as $url ) {
454                    $help['help-urls'] .= Html::rawElement( 'li', [],
455                        Html::element( 'a', [ 'href' => $url, 'dir' => 'ltr' ], $url )
456                    );
457                }
458                $help['help-urls'] .= Html::closeElement( 'ul' );
459                $help['help-urls'] .= Html::closeElement( 'div' );
460            }
461
462            $params = $module->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
463            $dynamicParams = $module->dynamicParameterDocumentation();
464            $groups = [];
465            if ( $params || $dynamicParams !== null ) {
466                $help['parameters'] .= Html::openElement( 'div',
467                    [ 'class' => [ 'apihelp-block', 'apihelp-parameters' ] ]
468                );
469                $msg = $context->msg( 'api-help-parameters' );
470                if ( !$msg->isDisabled() ) {
471                    $help['parameters'] .= self::wrap(
472                        $msg->numParams( count( $params ) ), 'apihelp-block-head', 'div'
473                    );
474                    if ( !$module->isMain() ) {
475                        // Add a note explaining that other parameters may exist.
476                        $help['parameters'] .= self::wrap(
477                            $context->msg( 'api-help-parameters-note' ), 'apihelp-block-header', 'div'
478                        );
479                    }
480                }
481                $help['parameters'] .= Html::openElement( 'dl' );
482
483                $descriptions = $module->getFinalParamDescription();
484
485                foreach ( $params as $name => $settings ) {
486                    $settings = $paramValidator->normalizeSettings( $settings );
487
488                    if ( $settings[ParamValidator::PARAM_TYPE] === 'submodule' ) {
489                        $groups[] = $name;
490                    }
491
492                    $encodedParamName = $module->encodeParamName( $name );
493                    $paramNameAttribs = [ 'dir' => 'ltr', 'lang' => 'en' ];
494                    if ( isset( $anchor ) ) {
495                        $paramNameAttribs['id'] = "$anchor:$encodedParamName";
496                    }
497                    $help['parameters'] .= Html::rawElement( 'dt', [],
498                        Html::element( 'span', $paramNameAttribs, $encodedParamName )
499                    );
500
501                    // Add description
502                    $description = [];
503                    if ( isset( $descriptions[$name] ) ) {
504                        foreach ( $descriptions[$name] as $msg ) {
505                            $msg->setContext( $context );
506                            $description[] = $msg->parseAsBlock();
507                        }
508                    }
509                    if ( !array_filter( $description ) ) {
510                        $description = [ self::wrap(
511                            $context->msg( 'api-help-param-no-description' ),
512                            'apihelp-empty'
513                        ) ];
514                    }
515
516                    // Add "deprecated" flag
517                    if ( !empty( $settings[ParamValidator::PARAM_DEPRECATED] ) ) {
518                        $help['parameters'] .= Html::openElement( 'dd',
519                            [ 'class' => 'info' ] );
520                        $help['parameters'] .= self::wrap(
521                            $context->msg( 'api-help-param-deprecated' ),
522                            'apihelp-deprecated', 'strong'
523                        );
524                        $help['parameters'] .= Html::closeElement( 'dd' );
525                    }
526
527                    if ( $description ) {
528                        $description = implode( '', $description );
529                        $description = preg_replace( '!\s*</([oud]l)>\s*<\1>\s*!', "\n", $description );
530                        $help['parameters'] .= Html::rawElement( 'dd',
531                            [ 'class' => 'description' ], $description );
532                    }
533
534                    // Add usage info
535                    $info = [];
536                    $paramHelp = $paramValidator->getHelpInfo( $module, $name, $settings, [] );
537
538                    unset( $paramHelp[ParamValidator::PARAM_DEPRECATED] );
539
540                    if ( isset( $paramHelp[ParamValidator::PARAM_REQUIRED] ) ) {
541                        $paramHelp[ParamValidator::PARAM_REQUIRED]->setContext( $context );
542                        $info[] = $paramHelp[ParamValidator::PARAM_REQUIRED];
543                        unset( $paramHelp[ParamValidator::PARAM_REQUIRED] );
544                    }
545
546                    // Custom info?
547                    if ( !empty( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) {
548                        foreach ( $settings[ApiBase::PARAM_HELP_MSG_INFO] as $i ) {
549                            $tag = array_shift( $i );
550                            $info[] = $context->msg( "apihelp-{$path}-paraminfo-{$tag}" )
551                                ->numParams( count( $i ) )
552                                ->params( $context->getLanguage()->commaList( $i ) )
553                                ->params( $module->getModulePrefix() )
554                                ->parse();
555                        }
556                    }
557
558                    // Templated?
559                    if ( !empty( $settings[ApiBase::PARAM_TEMPLATE_VARS] ) ) {
560                        $vars = [];
561                        $msg = 'api-help-param-templated-var-first';
562                        foreach ( $settings[ApiBase::PARAM_TEMPLATE_VARS] as $k => $v ) {
563                            $vars[] = $context->msg( $msg, $k, $module->encodeParamName( $v ) );
564                            $msg = 'api-help-param-templated-var';
565                        }
566                        $info[] = $context->msg( 'api-help-param-templated' )
567                            ->numParams( count( $vars ) )
568                            ->params( Message::listParam( $vars ) )
569                            ->parse();
570                    }
571
572                    // Type documentation
573                    foreach ( $paramHelp as $m ) {
574                        $m->setContext( $context );
575                        $info[] = $m->parse();
576                    }
577
578                    foreach ( $info as $i ) {
579                        $help['parameters'] .= Html::rawElement( 'dd', [ 'class' => 'info' ], $i );
580                    }
581                }
582
583                if ( $dynamicParams !== null ) {
584                    $dynamicParams = $context->msg(
585                        Message::newFromSpecifier( $dynamicParams ),
586                        $module->getModulePrefix(),
587                        $module->getModuleName(),
588                        $module->getModulePath()
589                    );
590                    $help['parameters'] .= Html::element( 'dt', [], '*' );
591                    $help['parameters'] .= Html::rawElement( 'dd',
592                        [ 'class' => 'description' ], $dynamicParams->parse() );
593                }
594
595                $help['parameters'] .= Html::closeElement( 'dl' );
596                $help['parameters'] .= Html::closeElement( 'div' );
597            }
598
599            $examples = $module->getExamplesMessages();
600            if ( $examples ) {
601                $help['examples'] .= Html::openElement( 'div',
602                    [ 'class' => [ 'apihelp-block', 'apihelp-examples' ] ] );
603                $msg = $context->msg( 'api-help-examples' );
604                if ( !$msg->isDisabled() ) {
605                    $help['examples'] .= self::wrap(
606                        $msg->numParams( count( $examples ) ), 'apihelp-block-head', 'div'
607                    );
608                }
609
610                $help['examples'] .= Html::openElement( 'dl' );
611                foreach ( $examples as $qs => $msg ) {
612                    $msg = $context->msg(
613                        Message::newFromSpecifier( $msg ),
614                        $module->getModulePrefix(),
615                        $module->getModuleName(),
616                        $module->getModulePath()
617                    );
618
619                    $link = wfAppendQuery( wfScript( 'api' ), $qs );
620                    $sandbox = SpecialPage::getTitleFor( 'ApiSandbox' )->getLocalURL() . '#' . $qs;
621                    $help['examples'] .= Html::rawElement( 'dt', [], $msg->parse() );
622                    $help['examples'] .= Html::rawElement( 'dd', [],
623                        Html::element( 'a', [
624                            'href' => $link,
625                            'dir' => 'ltr',
626                            'rel' => 'nofollow',
627                        ], "api.php?$qs" ) . ' ' .
628                        Html::rawElement( 'a', [ 'href' => $sandbox ],
629                            $context->msg( 'api-help-open-in-apisandbox' )->parse() )
630                    );
631                }
632                $help['examples'] .= Html::closeElement( 'dl' );
633                $help['examples'] .= Html::closeElement( 'div' );
634            }
635
636            $subtocnumber = $tocnumber;
637            $subtocnumber[$level + 1] = 0;
638            $suboptions = [
639                'submodules' => $options['recursivesubmodules'],
640                'headerlevel' => $level + 1,
641                'tocnumber' => &$subtocnumber,
642                'noheader' => false,
643            ] + $options;
644
645            // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
646            if ( $options['submodules'] && $module->getModuleManager() ) {
647                $manager = $module->getModuleManager();
648                $submodules = [];
649                foreach ( $groups as $group ) {
650                    $names = $manager->getNames( $group );
651                    sort( $names );
652                    foreach ( $names as $name ) {
653                        $submodules[] = $manager->getModule( $name );
654                    }
655                }
656                $help['submodules'] .= self::getHelpInternal(
657                    $context,
658                    $submodules,
659                    $suboptions,
660                    $haveModules
661                );
662            }
663
664            $module->modifyHelp( $help, $suboptions, $haveModules );
665
666            if ( $module->getHookContainer()->isRegistered( 'APIHelpModifyOutput' ) ) {
667                // XXX: we should probably deprecate this hook so that we can
668                // migrate the $haveModules format.
669                if ( !empty( $suboptions['toc'] ) ) {
670                    $haveModules = array_map(
671                        static fn ( $s )=>$s->toLegacy(), $haveModules
672                    );
673                }
674                $module->getHookRunner()->onAPIHelpModifyOutput(
675                    $module, $help, $suboptions, $haveModules
676                );
677                if ( !empty( $suboptions['toc'] ) ) {
678                    $haveModules = array_map(
679                        static fn ( $s )=>SectionMetadata::fromLegacy( $s ), $haveModules
680                    );
681                }
682            }
683
684            $out .= implode( "\n", $help );
685        }
686
687        return $out;
688    }
689
690    /** @inheritDoc */
691    public function shouldCheckMaxlag() {
692        return false;
693    }
694
695    /** @inheritDoc */
696    public function isReadMode() {
697        return false;
698    }
699
700    /** @inheritDoc */
701    public function getCustomPrinter() {
702        $params = $this->extractRequestParams();
703        if ( $params['wrap'] ) {
704            return null;
705        }
706
707        $main = $this->getMain();
708        $errorPrinter = $main->createPrinterByName( $main->getParameter( 'format' ) );
709        return new ApiFormatRaw( $main, $errorPrinter );
710    }
711
712    /** @inheritDoc */
713    public function getAllowedParams() {
714        return [
715            'modules' => [
716                ParamValidator::PARAM_DEFAULT => 'main',
717                ParamValidator::PARAM_ISMULTI => true,
718            ],
719            'submodules' => false,
720            'recursivesubmodules' => false,
721            'wrap' => false,
722            'toc' => false,
723        ];
724    }
725
726    /** @inheritDoc */
727    protected function getExamplesMessages() {
728        return [
729            'action=help'
730                => 'apihelp-help-example-main',
731            'action=help&modules=query&submodules=1'
732                => 'apihelp-help-example-submodules',
733            'action=help&recursivesubmodules=1'
734                => 'apihelp-help-example-recursive',
735            'action=help&modules=help'
736                => 'apihelp-help-example-help',
737            'action=help&modules=query+info|query+categorymembers'
738                => 'apihelp-help-example-query',
739        ];
740    }
741
742    /** @inheritDoc */
743    public function getHelpUrls() {
744        return [
745            'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Main_page',
746            'https://www.mediawiki.org/wiki/Special:MyLanguage/API:FAQ',
747            'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Quick_start_guide',
748        ];
749    }
750}
751
752/** @deprecated class alias since 1.43 */
753class_alias( ApiHelp::class, 'ApiHelp' );