ApiHelp::getHelpUrls()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 *
4
 *
5
 * Created on Aug 29, 2014
6
 *
7
 * Copyright © 2014 Brad Jorsch <[email protected]>
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 2 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License along
20
 * with this program; if not, write to the Free Software Foundation, Inc.,
21
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
 * http://www.gnu.org/copyleft/gpl.html
23
 *
24
 * @file
25
 */
26
27
use HtmlFormatter\HtmlFormatter;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, HtmlFormatter.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
28
29
/**
30
 * Class to output help for an API module
31
 *
32
 * @since 1.25 completely rewritten
33
 * @ingroup API
34
 */
35
class ApiHelp extends ApiBase {
36
	public function execute() {
37
		$params = $this->extractRequestParams();
38
		$modules = [];
39
40
		foreach ( $params['modules'] as $path ) {
41
			$modules[] = $this->getModuleFromPath( $path );
42
		}
43
44
		// Get the help
45
		$context = new DerivativeContext( $this->getMain()->getContext() );
46
		$context->setSkin( SkinFactory::getDefaultInstance()->makeSkin( 'apioutput' ) );
47
		$context->setLanguage( $this->getMain()->getLanguage() );
48
		$context->setTitle( SpecialPage::getTitleFor( 'ApiHelp' ) );
49
		$out = new OutputPage( $context );
50
		$out->setCopyrightUrl( 'https://www.mediawiki.org/wiki/Special:MyLanguage/Copyright' );
51
		$context->setOutput( $out );
52
53
		self::getHelp( $context, $modules, $params );
54
55
		// Grab the output from the skin
56
		ob_start();
57
		$context->getOutput()->output();
58
		$html = ob_get_clean();
59
60
		$result = $this->getResult();
61
		if ( $params['wrap'] ) {
62
			$data = [
63
				'mime' => 'text/html',
64
				'help' => $html,
65
			];
66
			ApiResult::setSubelementsList( $data, 'help' );
67
			$result->addValue( null, $this->getModuleName(), $data );
68
		} else {
69
			$result->reset();
70
			$result->addValue( null, 'text', $html, ApiResult::NO_SIZE_CHECK );
71
			$result->addValue( null, 'mime', 'text/html', ApiResult::NO_SIZE_CHECK );
72
		}
73
	}
74
75
	/**
76
	 * Generate help for the specified modules
77
	 *
78
	 * Help is placed into the OutputPage object returned by
79
	 * $context->getOutput().
80
	 *
81
	 * Recognized options include:
82
	 *  - headerlevel: (int) Header tag level
83
	 *  - nolead: (bool) Skip the inclusion of api-help-lead
84
	 *  - noheader: (bool) Skip the inclusion of the top-level section headers
85
	 *  - submodules: (bool) Include help for submodules of the current module
86
	 *  - recursivesubmodules: (bool) Include help for submodules recursively
87
	 *  - helptitle: (string) Title to link for additional modules' help. Should contain $1.
88
	 *  - toc: (bool) Include a table of contents
89
	 *
90
	 * @param IContextSource $context
91
	 * @param ApiBase[]|ApiBase $modules
92
	 * @param array $options Formatting options (described above)
93
	 * @return string
94
	 */
95
	public static function getHelp( IContextSource $context, $modules, array $options ) {
96
		global $wgContLang;
97
98
		if ( !is_array( $modules ) ) {
99
			$modules = [ $modules ];
100
		}
101
102
		$out = $context->getOutput();
103
		$out->addModuleStyles( [
104
			'mediawiki.hlist',
105
			'mediawiki.apihelp',
106
		] );
107
		if ( !empty( $options['toc'] ) ) {
108
			$out->addModules( 'mediawiki.toc' );
109
		}
110
		$out->setPageTitle( $context->msg( 'api-help-title' ) );
111
112
		$cache = ObjectCache::getMainWANInstance();
113
		$cacheKey = null;
114
		if ( count( $modules ) == 1 && $modules[0] instanceof ApiMain &&
115
			$options['recursivesubmodules'] && $context->getLanguage() === $wgContLang
116
		) {
117
			$cacheHelpTimeout = $context->getConfig()->get( 'APICacheHelpTimeout' );
118
			if ( $cacheHelpTimeout > 0 ) {
119
				// Get help text from cache if present
120
				$cacheKey = wfMemcKey( 'apihelp', $modules[0]->getModulePath(),
121
					(int)!empty( $options['toc'] ),
122
					str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) );
123
				$cached = $cache->get( $cacheKey );
124
				if ( $cached ) {
125
					$out->addHTML( $cached );
126
					return;
127
				}
128
			}
129
		}
130
		if ( $out->getHTML() !== '' ) {
131
			// Don't save to cache, there's someone else's content in the page
132
			// already
133
			$cacheKey = null;
134
		}
135
136
		$options['recursivesubmodules'] = !empty( $options['recursivesubmodules'] );
137
		$options['submodules'] = $options['recursivesubmodules'] || !empty( $options['submodules'] );
138
139
		// Prepend lead
140
		if ( empty( $options['nolead'] ) ) {
141
			$msg = $context->msg( 'api-help-lead' );
142
			if ( !$msg->isDisabled() ) {
143
				$out->addHTML( $msg->parseAsBlock() );
144
			}
145
		}
146
147
		$haveModules = [];
148
		$html = self::getHelpInternal( $context, $modules, $options, $haveModules );
149
		if ( !empty( $options['toc'] ) && $haveModules ) {
150
			$out->addHTML( Linker::generateTOC( $haveModules, $context->getLanguage() ) );
151
		}
152
		$out->addHTML( $html );
153
154
		$helptitle = isset( $options['helptitle'] ) ? $options['helptitle'] : null;
155
		$html = self::fixHelpLinks( $out->getHTML(), $helptitle, $haveModules );
0 ignored issues
show
Bug introduced by
It seems like $helptitle defined by isset($options['helptitl...ons['helptitle'] : null on line 154 can also be of type boolean; however, ApiHelp::fixHelpLinks() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
156
		$out->clearHTML();
157
		$out->addHTML( $html );
158
159
		if ( $cacheKey !== null ) {
160
			$cache->set( $cacheKey, $out->getHTML(), $cacheHelpTimeout );
0 ignored issues
show
Bug introduced by
The variable $cacheHelpTimeout does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
161
		}
162
	}
163
164
	/**
165
	 * Replace Special:ApiHelp links with links to api.php
166
	 *
167
	 * @param string $html
168
	 * @param string|null $helptitle Title to link to rather than api.php, must contain '$1'
169
	 * @param array $localModules Keys are modules to link within the current page, values are ignored
170
	 * @return string
171
	 */
172
	public static function fixHelpLinks( $html, $helptitle = null, $localModules = [] ) {
173
		$formatter = new HtmlFormatter( $html );
174
		$doc = $formatter->getDoc();
175
		$xpath = new DOMXPath( $doc );
176
		$nodes = $xpath->query( '//a[@href][not(contains(@class,\'apihelp-linktrail\'))]' );
177
		foreach ( $nodes as $node ) {
178
			$href = $node->getAttribute( 'href' );
179
			do {
180
				$old = $href;
181
				$href = rawurldecode( $href );
182
			} while ( $old !== $href );
183
			if ( preg_match( '!Special:ApiHelp/([^&/|#]+)((?:#.*)?)!', $href, $m ) ) {
184
				if ( isset( $localModules[$m[1]] ) ) {
185
					$href = $m[2] === '' ? '#' . $m[1] : $m[2];
186
				} elseif ( $helptitle !== null ) {
187
					$href = Title::newFromText( str_replace( '$1', $m[1], $helptitle ) . $m[2] )
188
						->getFullURL();
189
				} else {
190
					$href = wfAppendQuery( wfScript( 'api' ), [
191
						'action' => 'help',
192
						'modules' => $m[1],
193
					] ) . $m[2];
194
				}
195
				$node->setAttribute( 'href', $href );
196
				$node->removeAttribute( 'title' );
197
			}
198
		}
199
200
		return $formatter->getText();
201
	}
202
203
	/**
204
	 * Wrap a message in HTML with a class.
205
	 *
206
	 * @param Message $msg
207
	 * @param string $class
208
	 * @param string $tag
209
	 * @return string
210
	 */
211
	private static function wrap( Message $msg, $class, $tag = 'span' ) {
212
		return Html::rawElement( $tag, [ 'class' => $class ],
213
			$msg->parse()
214
		);
215
	}
216
217
	/**
218
	 * Recursively-called function to actually construct the help
219
	 *
220
	 * @param IContextSource $context
221
	 * @param ApiBase[] $modules
222
	 * @param array $options
223
	 * @param array &$haveModules
224
	 * @return string
225
	 */
226
	private static function getHelpInternal( IContextSource $context, array $modules,
227
		array $options, &$haveModules
228
	) {
229
		$out = '';
230
231
		$level = empty( $options['headerlevel'] ) ? 2 : $options['headerlevel'];
232
		if ( empty( $options['tocnumber'] ) ) {
233
			$tocnumber = [ 2 => 0 ];
234
		} else {
235
			$tocnumber = &$options['tocnumber'];
236
		}
237
238
		foreach ( $modules as $module ) {
239
			$tocnumber[$level]++;
240
			$path = $module->getModulePath();
241
			$module->setContext( $context );
242
			$help = [
243
				'header' => '',
244
				'flags' => '',
245
				'description' => '',
246
				'help-urls' => '',
247
				'parameters' => '',
248
				'examples' => '',
249
				'submodules' => '',
250
			];
251
252
			if ( empty( $options['noheader'] ) || !empty( $options['toc'] ) ) {
253
				$anchor = $path;
254
				$i = 1;
255
				while ( isset( $haveModules[$anchor] ) ) {
256
					$anchor = $path . '|' . ++$i;
257
				}
258
259
				if ( $module->isMain() ) {
260
					$headerContent = $context->msg( 'api-help-main-header' )->parse();
261
					$headerAttr = [
262
						'class' => 'apihelp-header',
263
					];
264
				} else {
265
					$name = $module->getModuleName();
266
					$headerContent = $module->getParent()->getModuleManager()->getModuleGroup( $name ) .
267
						"=$name";
268
					if ( $module->getModulePrefix() !== '' ) {
269
						$headerContent .= ' ' .
270
							$context->msg( 'parentheses', $module->getModulePrefix() )->parse();
271
					}
272
					// Module names are always in English and not localized,
273
					// so English language and direction must be set explicitly,
274
					// otherwise parentheses will get broken in RTL wikis
275
					$headerAttr = [
276
						'class' => 'apihelp-header apihelp-module-name',
277
						'dir' => 'ltr',
278
						'lang' => 'en',
279
					];
280
				}
281
282
				$headerAttr['id'] = $anchor;
283
284
				$haveModules[$anchor] = [
285
					'toclevel' => count( $tocnumber ),
286
					'level' => $level,
287
					'anchor' => $anchor,
288
					'line' => $headerContent,
289
					'number' => implode( '.', $tocnumber ),
290
					'index' => false,
291
				];
292
				if ( empty( $options['noheader'] ) ) {
293
					$help['header'] .= Html::element(
294
						'h' . min( 6, $level ),
295
						$headerAttr,
296
						$headerContent
297
					);
298
				}
299
			} else {
300
				$haveModules[$path] = true;
301
			}
302
303
			$links = [];
304
			$any = false;
305
			for ( $m = $module; $m !== null; $m = $m->getParent() ) {
306
				$name = $m->getModuleName();
307
				if ( $name === 'main_int' ) {
308
					$name = 'main';
309
				}
310
311
				if ( count( $modules ) === 1 && $m === $modules[0] &&
312
					!( !empty( $options['submodules'] ) && $m->getModuleManager() )
313
				) {
314
					$link = Html::element( 'b', [ 'dir' => 'ltr', 'lang' => 'en' ], $name );
315
				} else {
316
					$link = SpecialPage::getTitleFor( 'ApiHelp', $m->getModulePath() )->getLocalURL();
317
					$link = Html::element( 'a',
318
						[ 'href' => $link, 'class' => 'apihelp-linktrail', 'dir' => 'ltr', 'lang' => 'en' ],
319
						$name
320
					);
321
					$any = true;
322
				}
323
				array_unshift( $links, $link );
324
			}
325
			if ( $any ) {
326
				$help['header'] .= self::wrap(
327
					$context->msg( 'parentheses' )
328
						->rawParams( $context->getLanguage()->pipeList( $links ) ),
329
					'apihelp-linktrail', 'div'
330
				);
331
			}
332
333
			$flags = $module->getHelpFlags();
334
			$help['flags'] .= Html::openElement( 'div',
335
				[ 'class' => 'apihelp-block apihelp-flags' ] );
336
			$msg = $context->msg( 'api-help-flags' );
337
			if ( !$msg->isDisabled() ) {
338
				$help['flags'] .= self::wrap(
339
					$msg->numParams( count( $flags ) ), 'apihelp-block-head', 'div'
340
				);
341
			}
342
			$help['flags'] .= Html::openElement( 'ul' );
343
			foreach ( $flags as $flag ) {
344
				$help['flags'] .= Html::rawElement( 'li', null,
345
					self::wrap( $context->msg( "api-help-flag-$flag" ), "apihelp-flag-$flag" )
346
				);
347
			}
348
			$sourceInfo = $module->getModuleSourceInfo();
349
			if ( $sourceInfo ) {
350
				if ( isset( $sourceInfo['namemsg'] ) ) {
351
					$extname = $context->msg( $sourceInfo['namemsg'] )->text();
352
				} else {
353
					// Probably English, so wrap it.
354
					$extname = Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $sourceInfo['name'] );
355
				}
356
				$help['flags'] .= Html::rawElement( 'li', null,
357
					self::wrap(
358
						$context->msg( 'api-help-source', $extname, $sourceInfo['name'] ),
359
						'apihelp-source'
360
					)
361
				);
362
363
				$link = SpecialPage::getTitleFor( 'Version', 'License/' . $sourceInfo['name'] );
364
				if ( isset( $sourceInfo['license-name'] ) ) {
365
					$msg = $context->msg( 'api-help-license', $link,
366
						Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $sourceInfo['license-name'] )
367
					);
368
				} elseif ( SpecialVersion::getExtLicenseFileName( dirname( $sourceInfo['path'] ) ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \SpecialVersion::getExtL...e($sourceInfo['path'])) of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
369
					$msg = $context->msg( 'api-help-license-noname', $link );
370
				} else {
371
					$msg = $context->msg( 'api-help-license-unknown' );
372
				}
373
				$help['flags'] .= Html::rawElement( 'li', null,
374
					self::wrap( $msg, 'apihelp-license' )
375
				);
376
			} else {
377
				$help['flags'] .= Html::rawElement( 'li', null,
378
					self::wrap( $context->msg( 'api-help-source-unknown' ), 'apihelp-source' )
379
				);
380
				$help['flags'] .= Html::rawElement( 'li', null,
381
					self::wrap( $context->msg( 'api-help-license-unknown' ), 'apihelp-license' )
382
				);
383
			}
384
			$help['flags'] .= Html::closeElement( 'ul' );
385
			$help['flags'] .= Html::closeElement( 'div' );
386
387
			foreach ( $module->getFinalDescription() as $msg ) {
388
				$msg->setContext( $context );
389
				$help['description'] .= $msg->parseAsBlock();
390
			}
391
392
			$urls = $module->getHelpUrls();
393
			if ( $urls ) {
394
				$help['help-urls'] .= Html::openElement( 'div',
395
					[ 'class' => 'apihelp-block apihelp-help-urls' ]
396
				);
397
				$msg = $context->msg( 'api-help-help-urls' );
398
				if ( !$msg->isDisabled() ) {
399
					$help['help-urls'] .= self::wrap(
400
						$msg->numParams( count( $urls ) ), 'apihelp-block-head', 'div'
401
					);
402
				}
403
				if ( !is_array( $urls ) ) {
404
					$urls = [ $urls ];
405
				}
406
				$help['help-urls'] .= Html::openElement( 'ul' );
407
				foreach ( $urls as $url ) {
408
					$help['help-urls'] .= Html::rawElement( 'li', null,
409
						Html::element( 'a', [ 'href' => $url, 'dir' => 'ltr' ], $url )
410
					);
411
				}
412
				$help['help-urls'] .= Html::closeElement( 'ul' );
413
				$help['help-urls'] .= Html::closeElement( 'div' );
414
			}
415
416
			$params = $module->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
417
			$dynamicParams = $module->dynamicParameterDocumentation();
418
			$groups = [];
419
			if ( $params || $dynamicParams !== null ) {
420
				$help['parameters'] .= Html::openElement( 'div',
421
					[ 'class' => 'apihelp-block apihelp-parameters' ]
422
				);
423
				$msg = $context->msg( 'api-help-parameters' );
424
				if ( !$msg->isDisabled() ) {
425
					$help['parameters'] .= self::wrap(
426
						$msg->numParams( count( $params ) ), 'apihelp-block-head', 'div'
427
					);
428
				}
429
				$help['parameters'] .= Html::openElement( 'dl' );
430
431
				$descriptions = $module->getFinalParamDescription();
432
433
				foreach ( $params as $name => $settings ) {
0 ignored issues
show
Bug introduced by
The expression $params of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
434
					if ( !is_array( $settings ) ) {
435
						$settings = [ ApiBase::PARAM_DFLT => $settings ];
436
					}
437
438
					$help['parameters'] .= Html::rawElement( 'dt', null,
439
						Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $module->encodeParamName( $name ) )
440
					);
441
442
					// Add description
443
					$description = [];
444
					if ( isset( $descriptions[$name] ) ) {
445
						foreach ( $descriptions[$name] as $msg ) {
446
							$msg->setContext( $context );
447
							$description[] = $msg->parseAsBlock();
448
						}
449
					}
450
451
					// Add usage info
452
					$info = [];
453
454
					// Required?
455
					if ( !empty( $settings[ApiBase::PARAM_REQUIRED] ) ) {
456
						$info[] = $context->msg( 'api-help-param-required' )->parse();
457
					}
458
459
					// Custom info?
460
					if ( !empty( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) {
461
						foreach ( $settings[ApiBase::PARAM_HELP_MSG_INFO] as $i ) {
462
							$tag = array_shift( $i );
463
							$info[] = $context->msg( "apihelp-{$path}-paraminfo-{$tag}" )
464
								->numParams( count( $i ) )
465
								->params( $context->getLanguage()->commaList( $i ) )
466
								->params( $module->getModulePrefix() )
467
								->parse();
468
						}
469
					}
470
471
					// Type documentation
472 View Code Duplication
					if ( !isset( $settings[ApiBase::PARAM_TYPE] ) ) {
473
						$dflt = isset( $settings[ApiBase::PARAM_DFLT] )
474
							? $settings[ApiBase::PARAM_DFLT]
475
							: null;
476
						if ( is_bool( $dflt ) ) {
477
							$settings[ApiBase::PARAM_TYPE] = 'boolean';
478
						} elseif ( is_string( $dflt ) || is_null( $dflt ) ) {
479
							$settings[ApiBase::PARAM_TYPE] = 'string';
480
						} elseif ( is_int( $dflt ) ) {
481
							$settings[ApiBase::PARAM_TYPE] = 'integer';
482
						}
483
					}
484
					if ( isset( $settings[ApiBase::PARAM_TYPE] ) ) {
485
						$type = $settings[ApiBase::PARAM_TYPE];
486
						$multi = !empty( $settings[ApiBase::PARAM_ISMULTI] );
487
						$hintPipeSeparated = true;
488
						$count = ApiBase::LIMIT_SML2 + 1;
489
490
						if ( is_array( $type ) ) {
491
							$count = count( $type );
492
							$links = isset( $settings[ApiBase::PARAM_VALUE_LINKS] )
493
								? $settings[ApiBase::PARAM_VALUE_LINKS]
494
								: [];
495
							$values = array_map( function ( $v ) use ( $links ) {
496
								// We can't know whether this contains LTR or RTL text.
497
								$ret = $v === '' ? $v : Html::element( 'span', [ 'dir' => 'auto' ], $v );
498
								if ( isset( $links[$v] ) ) {
499
									$ret = "[[{$links[$v]}|$ret]]";
500
								}
501
								return $ret;
502
							}, $type );
503
							$i = array_search( '', $type, true );
504
							if ( $i === false ) {
505
								$values = $context->getLanguage()->commaList( $values );
506
							} else {
507
								unset( $values[$i] );
508
								$values = $context->msg( 'api-help-param-list-can-be-empty' )
509
									->numParams( count( $values ) )
510
									->params( $context->getLanguage()->commaList( $values ) )
511
									->parse();
512
							}
513
							$info[] = $context->msg( 'api-help-param-list' )
514
								->params( $multi ? 2 : 1 )
515
								->params( $values )
516
								->parse();
517
							$hintPipeSeparated = false;
518
						} else {
519
							switch ( $type ) {
520
								case 'submodule':
521
									$groups[] = $name;
522
									if ( isset( $settings[ApiBase::PARAM_SUBMODULE_MAP] ) ) {
523
										$map = $settings[ApiBase::PARAM_SUBMODULE_MAP];
524
										ksort( $map );
525
										$submodules = [];
526
										foreach ( $map as $v => $m ) {
527
											$submodules[] = "[[Special:ApiHelp/{$m}|{$v}]]";
528
										}
529
									} else {
530
										$submodules = $module->getModuleManager()->getNames( $name );
531
										sort( $submodules );
532
										$prefix = $module->isMain()
533
											? '' : ( $module->getModulePath() . '+' );
534
										$submodules = array_map( function ( $name ) use ( $prefix ) {
535
											$text = Html::element( 'span', [ 'dir' => 'ltr', 'lang' => 'en' ], $name );
536
											return "[[Special:ApiHelp/{$prefix}{$name}|{$text}]]";
537
										}, $submodules );
538
									}
539
									$count = count( $submodules );
540
									$info[] = $context->msg( 'api-help-param-list' )
541
										->params( $multi ? 2 : 1 )
542
										->params( $context->getLanguage()->commaList( $submodules ) )
543
										->parse();
544
									$hintPipeSeparated = false;
545
									// No type message necessary, we have a list of values.
546
									$type = null;
547
									break;
548
549 View Code Duplication
								case 'namespace':
550
									$namespaces = MWNamespace::getValidNamespaces();
551
									$count = count( $namespaces );
552
									$info[] = $context->msg( 'api-help-param-list' )
553
										->params( $multi ? 2 : 1 )
554
										->params( $context->getLanguage()->commaList( $namespaces ) )
555
										->parse();
556
									$hintPipeSeparated = false;
557
									// No type message necessary, we have a list of values.
558
									$type = null;
559
									break;
560
561 View Code Duplication
								case 'tags':
562
									$tags = ChangeTags::listExplicitlyDefinedTags();
563
									$count = count( $tags );
564
									$info[] = $context->msg( 'api-help-param-list' )
565
										->params( $multi ? 2 : 1 )
566
										->params( $context->getLanguage()->commaList( $tags ) )
567
										->parse();
568
									$hintPipeSeparated = false;
569
									$type = null;
570
									break;
571
572
								case 'limit':
573
									if ( isset( $settings[ApiBase::PARAM_MAX2] ) ) {
574
										$info[] = $context->msg( 'api-help-param-limit2' )
575
											->numParams( $settings[ApiBase::PARAM_MAX] )
576
											->numParams( $settings[ApiBase::PARAM_MAX2] )
577
											->parse();
578
									} else {
579
										$info[] = $context->msg( 'api-help-param-limit' )
580
											->numParams( $settings[ApiBase::PARAM_MAX] )
581
											->parse();
582
									}
583
									break;
584
585
								case 'integer':
586
									// Possible messages:
587
									// api-help-param-integer-min,
588
									// api-help-param-integer-max,
589
									// api-help-param-integer-minmax
590
									$suffix = '';
591
									$min = $max = 0;
592
									if ( isset( $settings[ApiBase::PARAM_MIN] ) ) {
593
										$suffix .= 'min';
594
										$min = $settings[ApiBase::PARAM_MIN];
595
									}
596
									if ( isset( $settings[ApiBase::PARAM_MAX] ) ) {
597
										$suffix .= 'max';
598
										$max = $settings[ApiBase::PARAM_MAX];
599
									}
600
									if ( $suffix !== '' ) {
601
										$info[] =
602
											$context->msg( "api-help-param-integer-$suffix" )
603
												->params( $multi ? 2 : 1 )
604
												->numParams( $min, $max )
605
												->parse();
606
									}
607
									break;
608
609
								case 'upload':
610
									$info[] = $context->msg( 'api-help-param-upload' )
611
										->parse();
612
									// No type message necessary, api-help-param-upload should handle it.
613
									$type = null;
614
									break;
615
616
								case 'string':
617
								case 'text':
618
									// Displaying a type message here would be useless.
619
									$type = null;
620
									break;
621
							}
622
						}
623
624
						// Add type. Messages for grep: api-help-param-type-limit
625
						// api-help-param-type-integer api-help-param-type-boolean
626
						// api-help-param-type-timestamp api-help-param-type-user
627
						// api-help-param-type-password
628
						if ( is_string( $type ) ) {
629
							$msg = $context->msg( "api-help-param-type-$type" );
630
							if ( !$msg->isDisabled() ) {
631
								$info[] = $msg->params( $multi ? 2 : 1 )->parse();
632
							}
633
						}
634
635
						if ( $multi ) {
636
							$extra = [];
637
							if ( $hintPipeSeparated ) {
638
								$extra[] = $context->msg( 'api-help-param-multi-separate' )->parse();
639
							}
640
							if ( $count > ApiBase::LIMIT_SML1 ) {
641
								$extra[] = $context->msg( 'api-help-param-multi-max' )
642
									->numParams( ApiBase::LIMIT_SML1, ApiBase::LIMIT_SML2 )
643
									->parse();
644
							}
645
							if ( $extra ) {
646
								$info[] = implode( ' ', $extra );
647
							}
648
						}
649
					}
650
651
					// Add default
652
					$default = isset( $settings[ApiBase::PARAM_DFLT] )
653
						? $settings[ApiBase::PARAM_DFLT]
654
						: null;
655
					if ( $default === '' ) {
656
						$info[] = $context->msg( 'api-help-param-default-empty' )
657
							->parse();
658
					} elseif ( $default !== null && $default !== false ) {
659
						// We can't know whether this contains LTR or RTL text.
660
						$info[] = $context->msg( 'api-help-param-default' )
661
							->params( Html::element( 'span', [ 'dir' => 'auto' ], $default ) )
662
							->parse();
663
					}
664
665
					if ( !array_filter( $description ) ) {
666
						$description = [ self::wrap(
667
							$context->msg( 'api-help-param-no-description' ),
668
							'apihelp-empty'
669
						) ];
670
					}
671
672
					// Add "deprecated" flag
673
					if ( !empty( $settings[ApiBase::PARAM_DEPRECATED] ) ) {
674
						$help['parameters'] .= Html::openElement( 'dd',
675
							[ 'class' => 'info' ] );
676
						$help['parameters'] .= self::wrap(
677
							$context->msg( 'api-help-param-deprecated' ),
678
							'apihelp-deprecated', 'strong'
679
						);
680
						$help['parameters'] .= Html::closeElement( 'dd' );
681
					}
682
683
					if ( $description ) {
684
						$description = implode( '', $description );
685
						$description = preg_replace( '!\s*</([oud]l)>\s*<\1>\s*!', "\n", $description );
686
						$help['parameters'] .= Html::rawElement( 'dd',
687
							[ 'class' => 'description' ], $description );
688
					}
689
690
					foreach ( $info as $i ) {
691
						$help['parameters'] .= Html::rawElement( 'dd', [ 'class' => 'info' ], $i );
692
					}
693
				}
694
695
				if ( $dynamicParams !== null ) {
696
					$dynamicParams = ApiBase::makeMessage( $dynamicParams, $context, [
697
						$module->getModulePrefix(),
698
						$module->getModuleName(),
699
						$module->getModulePath()
700
					] );
701
					$help['parameters'] .= Html::element( 'dt', null, '*' );
702
					$help['parameters'] .= Html::rawElement( 'dd',
703
						[ 'class' => 'description' ], $dynamicParams->parse() );
704
				}
705
706
				$help['parameters'] .= Html::closeElement( 'dl' );
707
				$help['parameters'] .= Html::closeElement( 'div' );
708
			}
709
710
			$examples = $module->getExamplesMessages();
711
			if ( $examples ) {
712
				$help['examples'] .= Html::openElement( 'div',
713
					[ 'class' => 'apihelp-block apihelp-examples' ] );
714
				$msg = $context->msg( 'api-help-examples' );
715
				if ( !$msg->isDisabled() ) {
716
					$help['examples'] .= self::wrap(
717
						$msg->numParams( count( $examples ) ), 'apihelp-block-head', 'div'
718
					);
719
				}
720
721
				$help['examples'] .= Html::openElement( 'dl' );
722
				foreach ( $examples as $qs => $msg ) {
723
					$msg = ApiBase::makeMessage( $msg, $context, [
724
						$module->getModulePrefix(),
725
						$module->getModuleName(),
726
						$module->getModulePath()
727
					] );
728
729
					$link = wfAppendQuery( wfScript( 'api' ), $qs );
730
					$sandbox = SpecialPage::getTitleFor( 'ApiSandbox' )->getLocalURL() . '#' . $qs;
731
					$help['examples'] .= Html::rawElement( 'dt', null, $msg->parse() );
732
					$help['examples'] .= Html::rawElement( 'dd', null,
733
						Html::element( 'a', [ 'href' => $link, 'dir' => 'ltr' ], "api.php?$qs" ) . ' ' .
734
						Html::rawElement( 'a', [ 'href' => $sandbox ],
735
							$context->msg( 'api-help-open-in-apisandbox' )->parse() )
736
					);
737
				}
738
				$help['examples'] .= Html::closeElement( 'dl' );
739
				$help['examples'] .= Html::closeElement( 'div' );
740
			}
741
742
			$subtocnumber = $tocnumber;
743
			$subtocnumber[$level + 1] = 0;
744
			$suboptions = [
745
				'submodules' => $options['recursivesubmodules'],
746
				'headerlevel' => $level + 1,
747
				'tocnumber' => &$subtocnumber,
748
				'noheader' => false,
749
			] + $options;
750
751
			if ( $options['submodules'] && $module->getModuleManager() ) {
752
				$manager = $module->getModuleManager();
753
				$submodules = [];
754
				foreach ( $groups as $group ) {
755
					$names = $manager->getNames( $group );
756
					sort( $names );
757
					foreach ( $names as $name ) {
758
						$submodules[] = $manager->getModule( $name );
759
					}
760
				}
761
				$help['submodules'] .= self::getHelpInternal(
762
					$context,
763
					$submodules,
764
					$suboptions,
765
					$haveModules
766
				);
767
			}
768
769
			$module->modifyHelp( $help, $suboptions, $haveModules );
770
771
			Hooks::run( 'APIHelpModifyOutput', [ $module, &$help, $suboptions, &$haveModules ] );
772
773
			$out .= implode( "\n", $help );
774
		}
775
776
		return $out;
777
	}
778
779
	public function shouldCheckMaxlag() {
780
		return false;
781
	}
782
783
	public function isReadMode() {
784
		return false;
785
	}
786
787
	public function getCustomPrinter() {
788
		$params = $this->extractRequestParams();
789
		if ( $params['wrap'] ) {
790
			return null;
791
		}
792
793
		$main = $this->getMain();
794
		$errorPrinter = $main->createPrinterByName( $main->getParameter( 'format' ) );
0 ignored issues
show
Bug introduced by
The method getParameter() cannot be called from this context as it is declared protected in class ApiBase.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
795
		return new ApiFormatRaw( $main, $errorPrinter );
796
	}
797
798
	public function getAllowedParams() {
799
		return [
800
			'modules' => [
801
				ApiBase::PARAM_DFLT => 'main',
802
				ApiBase::PARAM_ISMULTI => true,
803
			],
804
			'submodules' => false,
805
			'recursivesubmodules' => false,
806
			'wrap' => false,
807
			'toc' => false,
808
		];
809
	}
810
811
	protected function getExamplesMessages() {
812
		return [
813
			'action=help'
814
				=> 'apihelp-help-example-main',
815
			'action=help&modules=query&submodules=1'
816
				=> 'apihelp-help-example-submodules',
817
			'action=help&recursivesubmodules=1'
818
				=> 'apihelp-help-example-recursive',
819
			'action=help&modules=help'
820
				=> 'apihelp-help-example-help',
821
			'action=help&modules=query+info|query+categorymembers'
822
				=> 'apihelp-help-example-query',
823
		];
824
	}
825
826
	public function getHelpUrls() {
827
		return [
828
			'https://www.mediawiki.org/wiki/API:Main_page',
829
			'https://www.mediawiki.org/wiki/API:FAQ',
830
			'https://www.mediawiki.org/wiki/API:Quick_start_guide',
831
		];
832
	}
833
}
834