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