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