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