Merge "ResourceLoader: Deprecate ResourceLoader::makeConfigSetScript"
[mediawiki.git] / includes / api / ApiHelp.php
blob0490ac302cabce209bb7d1eeffb9661d04d7ecfb
1 <?php
2 /**
3 * Copyright © 2014 Wikimedia Foundation and contributors
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.
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.
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
20 * @file
23 namespace MediaWiki\Api;
25 use MediaWiki\Context\DerivativeContext;
26 use MediaWiki\Context\IContextSource;
27 use MediaWiki\Html\Html;
28 use MediaWiki\Html\HtmlHelper;
29 use MediaWiki\Json\FormatJson;
30 use MediaWiki\MainConfigNames;
31 use MediaWiki\MediaWikiServices;
32 use MediaWiki\Message\Message;
33 use MediaWiki\Output\OutputPage;
34 use MediaWiki\Parser\Parser;
35 use MediaWiki\Parser\ParserOutput;
36 use MediaWiki\Parser\ParserOutputFlags;
37 use MediaWiki\SpecialPage\SpecialPage;
38 use MediaWiki\Specials\SpecialVersion;
39 use MediaWiki\Title\Title;
40 use MediaWiki\Utils\ExtensionInfo;
41 use SkinFactory;
42 use Wikimedia\ParamValidator\ParamValidator;
43 use Wikimedia\Parsoid\Core\TOCData;
44 use Wikimedia\RemexHtml\Serializer\SerializerNode;
46 /**
47 * Class to output help for an API module
49 * @since 1.25 completely rewritten
50 * @ingroup API
52 class ApiHelp extends ApiBase {
53 private SkinFactory $skinFactory;
55 public function __construct(
56 ApiMain $main,
57 string $action,
58 SkinFactory $skinFactory
59 ) {
60 parent::__construct( $main, $action );
61 $this->skinFactory = $skinFactory;
64 public function execute() {
65 $params = $this->extractRequestParams();
66 $modules = [];
68 foreach ( $params['modules'] as $path ) {
69 $modules[] = $this->getModuleFromPath( $path );
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 );
83 self::getHelp( $context, $modules, $params );
85 // Grab the output from the skin
86 ob_start();
87 $context->getOutput()->output();
88 $html = ob_get_clean();
90 $result = $this->getResult();
91 if ( $params['wrap'] ) {
92 $data = [
93 'mime' => 'text/html',
94 'filename' => 'api-help.html',
95 'help' => $html,
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',
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";
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 );
125 * Generate help for the specified modules
127 * Help is placed into the OutputPage object returned by
128 * $context->getOutput().
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
139 * @param IContextSource $context
140 * @param ApiBase[]|ApiBase $modules
141 * @param array $options Formatting options (described above)
143 public static function getHelp( IContextSource $context, $modules, array $options ) {
144 if ( !is_array( $modules ) ) {
145 $modules = [ $modules ];
148 $out = $context->getOutput();
149 $out->addModuleStyles( [
150 'mediawiki.hlist',
151 'mediawiki.apipretty',
152 ] );
153 $out->setPageTitleMsg( $context->msg( 'api-help-title' ) );
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() )
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;
175 if ( $out->getHTML() !== '' ) {
176 // Don't save to cache, there's someone else's content in the page
177 // already
178 $cacheKey = null;
181 $options['recursivesubmodules'] = !empty( $options['recursivesubmodules'] );
182 $options['submodules'] = $options['recursivesubmodules'] || !empty( $options['submodules'] );
184 // Prepend lead
185 if ( empty( $options['nolead'] ) ) {
186 $msg = $context->msg( 'api-help-lead' );
187 if ( !$msg->isDisabled() ) {
188 $out->addHTML( $msg->parseAsBlock() );
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 );
201 $out->addHTML( $html );
203 $helptitle = $options['helptitle'] ?? null;
204 $html = self::fixHelpLinks( $out->getHTML(), $helptitle, $haveModules );
205 $out->clearHTML();
206 $out->addHTML( $html );
208 if ( $cacheKey !== null ) {
209 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $cacheHelpTimeout declared when $cacheKey is set
210 $cache->set( $cacheKey, $out->getHTML(), $cacheHelpTimeout );
215 * Replace Special:ApiHelp links with links to api.php
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
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' );
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];
249 $node->attrs['href'] = $href;
250 unset( $node->attrs['title'] );
253 return $node;
259 * Wrap a message in HTML with a class.
261 * @param Message $msg
262 * @param string $class
263 * @param string $tag
264 * @return string
266 private static function wrap( Message $msg, $class, $tag = 'span' ) {
267 return Html::rawElement( $tag, [ 'class' => $class ],
268 $msg->parse()
273 * Recursively-called function to actually construct the help
275 * @param IContextSource $context
276 * @param ApiBase[] $modules
277 * @param array $options
278 * @param array &$haveModules
279 * @return string
281 private static function getHelpInternal( IContextSource $context, array $modules,
282 array $options, &$haveModules
284 $out = '';
286 $level = empty( $options['headerlevel'] ) ? 2 : $options['headerlevel'];
287 if ( empty( $options['tocnumber'] ) ) {
288 $tocnumber = [ 2 => 0 ];
289 } else {
290 $tocnumber = &$options['tocnumber'];
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' => '',
308 if ( empty( $options['noheader'] ) || !empty( $options['toc'] ) ) {
309 $anchor = $path;
310 $i = 1;
311 while ( isset( $haveModules[$anchor] ) ) {
312 $anchor = $path . '|' . ++$i;
315 if ( $module->isMain() ) {
316 $headerContent = $context->msg( 'api-help-main-header' )->parse();
317 $headerAttr = [
318 'class' => 'apihelp-header',
320 } else {
321 $name = $module->getModuleName();
322 $headerContent = htmlspecialchars(
323 $module->getParent()->getModuleManager()->getModuleGroup( $name ) . "=$name"
325 if ( $module->getModulePrefix() !== '' ) {
326 $headerContent .= ' ' .
327 $context->msg( 'parentheses', $module->getModulePrefix() )->parse();
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',
339 $headerAttr['id'] = $anchor;
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' => '',
350 if ( empty( $options['noheader'] ) ) {
351 $help['header'] .= Html::rawElement(
352 'h' . min( 6, $level ),
353 $headerAttr,
354 $headerContent
357 } else {
358 $haveModules[$path] = true;
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';
369 if ( count( $modules ) === 1 && $m === $modules[0] &&
370 !( !empty( $options['submodules'] ) && $m->getModuleManager() )
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
379 $any = true;
381 array_unshift( $links, $link );
383 if ( $any ) {
384 $help['header'] .= self::wrap(
385 $context->msg( 'parentheses' )
386 ->rawParams( $context->getLanguage()->pipeList( $links ) ),
387 'apihelp-linktrail', 'div'
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'
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" )
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'] );
420 $help['flags'] .= Html::rawElement( 'li', [],
421 self::wrap(
422 $context->msg( 'api-help-source', $extname, $sourceInfo['name'] ),
423 'apihelp-source'
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'] )
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' );
437 $help['flags'] .= Html::rawElement( 'li', [],
438 self::wrap( $msg, 'apihelp-license' )
440 } else {
441 $help['flags'] .= Html::rawElement( 'li', [],
442 self::wrap( $context->msg( 'api-help-source-unknown' ), 'apihelp-source' )
444 $help['flags'] .= Html::rawElement( 'li', [],
445 self::wrap( $context->msg( 'api-help-license-unknown' ), 'apihelp-license' )
448 $help['flags'] .= Html::closeElement( 'ul' );
449 $help['flags'] .= Html::closeElement( 'div' );
451 foreach ( $module->getFinalDescription() as $msg ) {
452 $msg->setContext( $context );
453 $help['description'] .= $msg->parseAsBlock();
456 $urls = $module->getHelpUrls();
457 if ( $urls ) {
458 if ( !is_array( $urls ) ) {
459 $urls = [ $urls ];
461 $help['help-urls'] .= Html::openElement( 'div',
462 [ 'class' => [ 'apihelp-block', 'apihelp-help-urls' ] ]
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'
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 )
476 $help['help-urls'] .= Html::closeElement( 'ul' );
477 $help['help-urls'] .= Html::closeElement( 'div' );
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' ] ]
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'
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'
499 $help['parameters'] .= Html::openElement( 'dl' );
501 $descriptions = $module->getFinalParamDescription();
503 foreach ( $params as $name => $settings ) {
504 $settings = $paramValidator->normalizeSettings( $settings );
506 if ( $settings[ParamValidator::PARAM_TYPE] === 'submodule' ) {
507 $groups[] = $name;
510 $encodedParamName = $module->encodeParamName( $name );
511 $paramNameAttribs = [ 'dir' => 'ltr', 'lang' => 'en' ];
512 if ( isset( $anchor ) ) {
513 $paramNameAttribs['id'] = "$anchor:$encodedParamName";
515 $help['parameters'] .= Html::rawElement( 'dt', [],
516 Html::element( 'span', $paramNameAttribs, $encodedParamName )
519 // Add description
520 $description = [];
521 if ( isset( $descriptions[$name] ) ) {
522 foreach ( $descriptions[$name] as $msg ) {
523 $msg->setContext( $context );
524 $description[] = $msg->parseAsBlock();
527 if ( !array_filter( $description ) ) {
528 $description = [ self::wrap(
529 $context->msg( 'api-help-param-no-description' ),
530 'apihelp-empty'
531 ) ];
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'
542 $help['parameters'] .= Html::closeElement( 'dd' );
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 );
552 // Add usage info
553 $info = [];
554 $paramHelp = $paramValidator->getHelpInfo( $module, $name, $settings, [] );
556 unset( $paramHelp[ParamValidator::PARAM_DEPRECATED] );
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] );
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();
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';
584 $info[] = $context->msg( 'api-help-param-templated' )
585 ->numParams( count( $vars ) )
586 ->params( Message::listParam( $vars ) )
587 ->parse();
590 // Type documentation
591 foreach ( $paramHelp as $m ) {
592 $m->setContext( $context );
593 $info[] = $m->parse();
596 foreach ( $info as $i ) {
597 $help['parameters'] .= Html::rawElement( 'dd', [ 'class' => 'info' ], $i );
601 if ( $dynamicParams !== null ) {
602 $dynamicParams = $context->msg(
603 Message::newFromSpecifier( $dynamicParams ),
604 $module->getModulePrefix(),
605 $module->getModuleName(),
606 $module->getModulePath()
608 $help['parameters'] .= Html::element( 'dt', [], '*' );
609 $help['parameters'] .= Html::rawElement( 'dd',
610 [ 'class' => 'description' ], $dynamicParams->parse() );
613 $help['parameters'] .= Html::closeElement( 'dl' );
614 $help['parameters'] .= Html::closeElement( 'div' );
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'
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()
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() )
650 $help['examples'] .= Html::closeElement( 'dl' );
651 $help['examples'] .= Html::closeElement( 'div' );
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;
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 );
674 $help['submodules'] .= self::getHelpInternal(
675 $context,
676 $submodules,
677 $suboptions,
678 $haveModules
682 $module->modifyHelp( $help, $suboptions, $haveModules );
684 $module->getHookRunner()->onAPIHelpModifyOutput( $module, $help,
685 $suboptions, $haveModules );
687 $out .= implode( "\n", $help );
690 return $out;
693 public function shouldCheckMaxlag() {
694 return false;
697 public function isReadMode() {
698 return false;
701 public function getCustomPrinter() {
702 $params = $this->extractRequestParams();
703 if ( $params['wrap'] ) {
704 return null;
707 $main = $this->getMain();
708 $errorPrinter = $main->createPrinterByName( $main->getParameter( 'format' ) );
709 return new ApiFormatRaw( $main, $errorPrinter );
712 public function getAllowedParams() {
713 return [
714 'modules' => [
715 ParamValidator::PARAM_DEFAULT => 'main',
716 ParamValidator::PARAM_ISMULTI => true,
718 'submodules' => false,
719 'recursivesubmodules' => false,
720 'wrap' => false,
721 'toc' => false,
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',
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',
749 /** @deprecated class alias since 1.43 */
750 class_alias( ApiHelp::class, 'ApiHelp' );