Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/api/ApiHelp.php (6 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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
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
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
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
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