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