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