ApiBase::modifyHelp()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 3
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 *
4
 *
5
 * Created on Sep 5, 2006
6
 *
7
 * Copyright © 2006, 2010 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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
/**
28
 * This abstract class implements many basic API functions, and is the base of
29
 * all API classes.
30
 * The class functions are divided into several areas of functionality:
31
 *
32
 * Module parameters: Derived classes can define getAllowedParams() to specify
33
 *    which parameters to expect, how to parse and validate them.
34
 *
35
 * Self-documentation: code to allow the API to document its own state
36
 *
37
 * @ingroup API
38
 */
39
abstract class ApiBase extends ContextSource {
40
41
	/**
42
	 * @name Constants for ::getAllowedParams() arrays
43
	 * These constants are keys in the arrays returned by ::getAllowedParams()
44
	 * and accepted by ::getParameterFromSettings() that define how the
45
	 * parameters coming in from the request are to be interpreted.
46
	 * @{
47
	 */
48
49
	/** (null|boolean|integer|string) Default value of the parameter. */
50
	const PARAM_DFLT = 0;
51
52
	/** (boolean) Accept multiple pipe-separated values for this parameter (e.g. titles)? */
53
	const PARAM_ISMULTI = 1;
54
55
	/**
56
	 * (string|string[]) Either an array of allowed value strings, or a string
57
	 * type as described below. If not specified, will be determined from the
58
	 * type of PARAM_DFLT.
59
	 *
60
	 * Supported string types are:
61
	 * - boolean: A boolean parameter, returned as false if the parameter is
62
	 *   omitted and true if present (even with a falsey value, i.e. it works
63
	 *   like HTML checkboxes). PARAM_DFLT must be boolean false, if specified.
64
	 *   Cannot be used with PARAM_ISMULTI.
65
	 * - integer: An integer value. See also PARAM_MIN, PARAM_MAX, and
66
	 *   PARAM_RANGE_ENFORCE.
67
	 * - limit: An integer or the string 'max'. Default lower limit is 0 (but
68
	 *   see PARAM_MIN), and requires that PARAM_MAX and PARAM_MAX2 be
69
	 *   specified. Cannot be used with PARAM_ISMULTI.
70
	 * - namespace: An integer representing a MediaWiki namespace.
71
	 * - NULL: Any string.
72
	 * - password: Any non-empty string. Input value is private or sensitive.
73
	 *   <input type="password"> would be an appropriate HTML form field.
74
	 * - string: Any non-empty string, not expected to be very long or contain newlines.
75
	 *   <input type="text"> would be an appropriate HTML form field.
76
	 * - submodule: The name of a submodule of this module, see PARAM_SUBMODULE_MAP.
77
	 * - tags: A string naming an existing, explicitly-defined tag. Should usually be
78
	 *   used with PARAM_ISMULTI.
79
	 * - text: Any non-empty string, expected to be very long or contain newlines.
80
	 *   <textarea> would be an appropriate HTML form field.
81
	 * - timestamp: A timestamp in any format recognized by MWTimestamp, or the
82
	 *   string 'now' representing the current timestamp. Will be returned in
83
	 *   TS_MW format.
84
	 * - user: A MediaWiki username or IP. Will be returned normalized but not canonicalized.
85
	 * - upload: An uploaded file. Will be returned as a WebRequestUpload object.
86
	 *   Cannot be used with PARAM_ISMULTI.
87
	 */
88
	const PARAM_TYPE = 2;
89
90
	/** (integer) Max value allowed for the parameter, for PARAM_TYPE 'integer' and 'limit'. */
91
	const PARAM_MAX = 3;
92
93
	/**
94
	 * (integer) Max value allowed for the parameter for users with the
95
	 * apihighlimits right, for PARAM_TYPE 'limit'.
96
	 */
97
	const PARAM_MAX2 = 4;
98
99
	/** (integer) Lowest value allowed for the parameter, for PARAM_TYPE 'integer' and 'limit'. */
100
	const PARAM_MIN = 5;
101
102
	/** (boolean) Allow the same value to be set more than once when PARAM_ISMULTI is true? */
103
	const PARAM_ALLOW_DUPLICATES = 6;
104
105
	/** (boolean) Is the parameter deprecated (will show a warning)? */
106
	const PARAM_DEPRECATED = 7;
107
108
	/**
109
	 * (boolean) Is the parameter required?
110
	 * @since 1.17
111
	 */
112
	const PARAM_REQUIRED = 8;
113
114
	/**
115
	 * (boolean) For PARAM_TYPE 'integer', enforce PARAM_MIN and PARAM_MAX?
116
	 * @since 1.17
117
	 */
118
	const PARAM_RANGE_ENFORCE = 9;
119
120
	/**
121
	 * (string|array|Message) Specify an alternative i18n documentation message
122
	 * for this parameter. Default is apihelp-{$path}-param-{$param}.
123
	 * @since 1.25
124
	 */
125
	const PARAM_HELP_MSG = 10;
126
127
	/**
128
	 * ((string|array|Message)[]) Specify additional i18n messages to append to
129
	 * the normal message for this parameter.
130
	 * @since 1.25
131
	 */
132
	const PARAM_HELP_MSG_APPEND = 11;
133
134
	/**
135
	 * (array) Specify additional information tags for the parameter. Value is
136
	 * an array of arrays, with the first member being the 'tag' for the info
137
	 * and the remaining members being the values. In the help, this is
138
	 * formatted using apihelp-{$path}-paraminfo-{$tag}, which is passed
139
	 * $1 = count, $2 = comma-joined list of values, $3 = module prefix.
140
	 * @since 1.25
141
	 */
142
	const PARAM_HELP_MSG_INFO = 12;
143
144
	/**
145
	 * (string[]) When PARAM_TYPE is an array, this may be an array mapping
146
	 * those values to page titles which will be linked in the help.
147
	 * @since 1.25
148
	 */
149
	const PARAM_VALUE_LINKS = 13;
150
151
	/**
152
	 * ((string|array|Message)[]) When PARAM_TYPE is an array, this is an array
153
	 * mapping those values to $msg for ApiBase::makeMessage(). Any value not
154
	 * having a mapping will use apihelp-{$path}-paramvalue-{$param}-{$value}.
155
	 * @since 1.25
156
	 */
157
	const PARAM_HELP_MSG_PER_VALUE = 14;
158
159
	/**
160
	 * (string[]) When PARAM_TYPE is 'submodule', map parameter values to
161
	 * submodule paths. Default is to use all modules in
162
	 * $this->getModuleManager() in the group matching the parameter name.
163
	 * @since 1.26
164
	 */
165
	const PARAM_SUBMODULE_MAP = 15;
166
167
	/**
168
	 * (string) When PARAM_TYPE is 'submodule', used to indicate the 'g' prefix
169
	 * added by ApiQueryGeneratorBase (and similar if anything else ever does that).
170
	 * @since 1.26
171
	 */
172
	const PARAM_SUBMODULE_PARAM_PREFIX = 16;
173
174
	/**@}*/
175
176
	/** Fast query, standard limit. */
177
	const LIMIT_BIG1 = 500;
178
	/** Fast query, apihighlimits limit. */
179
	const LIMIT_BIG2 = 5000;
180
	/** Slow query, standard limit. */
181
	const LIMIT_SML1 = 50;
182
	/** Slow query, apihighlimits limit. */
183
	const LIMIT_SML2 = 500;
184
185
	/**
186
	 * getAllowedParams() flag: When set, the result could take longer to generate,
187
	 * but should be more thorough. E.g. get the list of generators for ApiSandBox extension
188
	 * @since 1.21
189
	 */
190
	const GET_VALUES_FOR_HELP = 1;
191
192
	/** @var array Maps extension paths to info arrays */
193
	private static $extensionInfo = null;
194
195
	/** @var ApiMain */
196
	private $mMainModule;
197
	/** @var string */
198
	private $mModuleName, $mModulePrefix;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
199
	private $mSlaveDB = null;
200
	private $mParamCache = [];
201
	/** @var array|null|bool */
202
	private $mModuleSource = false;
203
204
	/**
205
	 * @param ApiMain $mainModule
206
	 * @param string $moduleName Name of this module
207
	 * @param string $modulePrefix Prefix to use for parameter names
208
	 */
209
	public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
210
		$this->mMainModule = $mainModule;
211
		$this->mModuleName = $moduleName;
212
		$this->mModulePrefix = $modulePrefix;
213
214
		if ( !$this->isMain() ) {
215
			$this->setContext( $mainModule->getContext() );
216
		}
217
	}
218
219
	/************************************************************************//**
220
	 * @name   Methods to implement
221
	 * @{
222
	 */
223
224
	/**
225
	 * Evaluates the parameters, performs the requested query, and sets up
226
	 * the result. Concrete implementations of ApiBase must override this
227
	 * method to provide whatever functionality their module offers.
228
	 * Implementations must not produce any output on their own and are not
229
	 * expected to handle any errors.
230
	 *
231
	 * The execute() method will be invoked directly by ApiMain immediately
232
	 * before the result of the module is output. Aside from the
233
	 * constructor, implementations should assume that no other methods
234
	 * will be called externally on the module before the result is
235
	 * processed.
236
	 *
237
	 * The result data should be stored in the ApiResult object available
238
	 * through getResult().
239
	 */
240
	abstract public function execute();
241
242
	/**
243
	 * Get the module manager, or null if this module has no sub-modules
244
	 * @since 1.21
245
	 * @return ApiModuleManager
246
	 */
247
	public function getModuleManager() {
248
		return null;
249
	}
250
251
	/**
252
	 * If the module may only be used with a certain format module,
253
	 * it should override this method to return an instance of that formatter.
254
	 * A value of null means the default format will be used.
255
	 * @note Do not use this just because you don't want to support non-json
256
	 * formats. This should be used only when there is a fundamental
257
	 * requirement for a specific format.
258
	 * @return mixed Instance of a derived class of ApiFormatBase, or null
259
	 */
260
	public function getCustomPrinter() {
261
		return null;
262
	}
263
264
	/**
265
	 * Returns usage examples for this module.
266
	 *
267
	 * Return value has query strings as keys, with values being either strings
268
	 * (message key), arrays (message key + parameter), or Message objects.
269
	 *
270
	 * Do not call this base class implementation when overriding this method.
271
	 *
272
	 * @since 1.25
273
	 * @return array
274
	 */
275
	protected function getExamplesMessages() {
276
		// Fall back to old non-localised method
277
		$ret = [];
278
279
		$examples = $this->getExamples();
280
		if ( $examples ) {
281
			if ( !is_array( $examples ) ) {
282
				$examples = [ $examples ];
283
			} elseif ( $examples && ( count( $examples ) & 1 ) == 0 &&
284
				array_keys( $examples ) === range( 0, count( $examples ) - 1 ) &&
285
				!preg_match( '/^\s*api\.php\?/', $examples[0] )
286
			) {
287
				// Fix up the ugly "even numbered elements are description, odd
288
				// numbered elemts are the link" format (see doc for self::getExamples)
289
				$tmp = [];
290
				$examplesCount = count( $examples );
291
				for ( $i = 0; $i < $examplesCount; $i += 2 ) {
292
					$tmp[$examples[$i + 1]] = $examples[$i];
293
				}
294
				$examples = $tmp;
295
			}
296
297
			foreach ( $examples as $k => $v ) {
298
				if ( is_numeric( $k ) ) {
299
					$qs = $v;
300
					$msg = '';
301
				} else {
302
					$qs = $k;
303
					$msg = self::escapeWikiText( $v );
304
					if ( is_array( $msg ) ) {
305
						$msg = implode( ' ', $msg );
306
					}
307
				}
308
309
				$qs = preg_replace( '/^\s*api\.php\?/', '', $qs );
310
				$ret[$qs] = $this->msg( 'api-help-fallback-example', [ $msg ] );
311
			}
312
		}
313
314
		return $ret;
315
	}
316
317
	/**
318
	 * Return links to more detailed help pages about the module.
319
	 * @since 1.25, returning boolean false is deprecated
320
	 * @return string|array
321
	 */
322
	public function getHelpUrls() {
323
		return [];
324
	}
325
326
	/**
327
	 * Returns an array of allowed parameters (parameter name) => (default
328
	 * value) or (parameter name) => (array with PARAM_* constants as keys)
329
	 * Don't call this function directly: use getFinalParams() to allow
330
	 * hooks to modify parameters as needed.
331
	 *
332
	 * Some derived classes may choose to handle an integer $flags parameter
333
	 * in the overriding methods. Callers of this method can pass zero or
334
	 * more OR-ed flags like GET_VALUES_FOR_HELP.
335
	 *
336
	 * @return array
337
	 */
338
	protected function getAllowedParams( /* $flags = 0 */ ) {
339
		// int $flags is not declared because it causes "Strict standards"
340
		// warning. Most derived classes do not implement it.
341
		return [];
342
	}
343
344
	/**
345
	 * Indicates if this module needs maxlag to be checked
346
	 * @return bool
347
	 */
348
	public function shouldCheckMaxlag() {
349
		return true;
350
	}
351
352
	/**
353
	 * Indicates whether this module requires read rights
354
	 * @return bool
355
	 */
356
	public function isReadMode() {
357
		return true;
358
	}
359
360
	/**
361
	 * Indicates whether this module requires write mode
362
	 * @return bool
363
	 */
364
	public function isWriteMode() {
365
		return false;
366
	}
367
368
	/**
369
	 * Indicates whether this module must be called with a POST request
370
	 * @return bool
371
	 */
372
	public function mustBePosted() {
373
		return $this->needsToken() !== false;
374
	}
375
376
	/**
377
	 * Indicates whether this module is deprecated
378
	 * @since 1.25
379
	 * @return bool
380
	 */
381
	public function isDeprecated() {
382
		return false;
383
	}
384
385
	/**
386
	 * Indicates whether this module is "internal"
387
	 * Internal API modules are not (yet) intended for 3rd party use and may be unstable.
388
	 * @since 1.25
389
	 * @return bool
390
	 */
391
	public function isInternal() {
392
		return false;
393
	}
394
395
	/**
396
	 * Returns the token type this module requires in order to execute.
397
	 *
398
	 * Modules are strongly encouraged to use the core 'csrf' type unless they
399
	 * have specialized security needs. If the token type is not one of the
400
	 * core types, you must use the ApiQueryTokensRegisterTypes hook to
401
	 * register it.
402
	 *
403
	 * Returning a non-falsey value here will force the addition of an
404
	 * appropriate 'token' parameter in self::getFinalParams(). Also,
405
	 * self::mustBePosted() must return true when tokens are used.
406
	 *
407
	 * In previous versions of MediaWiki, true was a valid return value.
408
	 * Returning true will generate errors indicating that the API module needs
409
	 * updating.
410
	 *
411
	 * @return string|false
412
	 */
413
	public function needsToken() {
414
		return false;
415
	}
416
417
	/**
418
	 * Fetch the salt used in the Web UI corresponding to this module.
419
	 *
420
	 * Only override this if the Web UI uses a token with a non-constant salt.
421
	 *
422
	 * @since 1.24
423
	 * @param array $params All supplied parameters for the module
424
	 * @return string|array|null
425
	 */
426
	protected function getWebUITokenSalt( array $params ) {
427
		return null;
428
	}
429
430
	/**
431
	 * Returns data for HTTP conditional request mechanisms.
432
	 *
433
	 * @since 1.26
434
	 * @param string $condition Condition being queried:
435
	 *  - last-modified: Return a timestamp representing the maximum of the
436
	 *    last-modified dates for all resources involved in the request. See
437
	 *    RFC 7232 § 2.2 for semantics.
438
	 *  - etag: Return an entity-tag representing the state of all resources involved
439
	 *    in the request. Quotes must be included. See RFC 7232 § 2.3 for semantics.
440
	 * @return string|bool|null As described above, or null if no value is available.
441
	 */
442
	public function getConditionalRequestData( $condition ) {
443
		return null;
444
	}
445
446
	/**@}*/
447
448
	/************************************************************************//**
449
	 * @name   Data access methods
450
	 * @{
451
	 */
452
453
	/**
454
	 * Get the name of the module being executed by this instance
455
	 * @return string
456
	 */
457
	public function getModuleName() {
458
		return $this->mModuleName;
459
	}
460
461
	/**
462
	 * Get parameter prefix (usually two letters or an empty string).
463
	 * @return string
464
	 */
465
	public function getModulePrefix() {
466
		return $this->mModulePrefix;
467
	}
468
469
	/**
470
	 * Get the main module
471
	 * @return ApiMain
472
	 */
473
	public function getMain() {
474
		return $this->mMainModule;
475
	}
476
477
	/**
478
	 * Returns true if this module is the main module ($this === $this->mMainModule),
479
	 * false otherwise.
480
	 * @return bool
481
	 */
482
	public function isMain() {
483
		return $this === $this->mMainModule;
484
	}
485
486
	/**
487
	 * Get the parent of this module
488
	 * @since 1.25
489
	 * @return ApiBase|null
490
	 */
491
	public function getParent() {
492
		return $this->isMain() ? null : $this->getMain();
493
	}
494
495
	/**
496
	 * Returns true if the current request breaks the same-origin policy.
497
	 *
498
	 * For example, json with callbacks.
499
	 *
500
	 * https://en.wikipedia.org/wiki/Same-origin_policy
501
	 *
502
	 * @since 1.25
503
	 * @return bool
504
	 */
505
	public function lacksSameOriginSecurity() {
506
		// Main module has this method overridden
507
		// Safety - avoid infinite loop:
508
		if ( $this->isMain() ) {
509
			ApiBase::dieDebug( __METHOD__, 'base method was called on main module.' );
510
		}
511
512
		return $this->getMain()->lacksSameOriginSecurity();
513
	}
514
515
	/**
516
	 * Get the path to this module
517
	 *
518
	 * @since 1.25
519
	 * @return string
520
	 */
521
	public function getModulePath() {
522
		if ( $this->isMain() ) {
523
			return 'main';
524
		} elseif ( $this->getParent()->isMain() ) {
525
			return $this->getModuleName();
526
		} else {
527
			return $this->getParent()->getModulePath() . '+' . $this->getModuleName();
528
		}
529
	}
530
531
	/**
532
	 * Get a module from its module path
533
	 *
534
	 * @since 1.25
535
	 * @param string $path
536
	 * @return ApiBase|null
537
	 * @throws UsageException
538
	 */
539
	public function getModuleFromPath( $path ) {
540
		$module = $this->getMain();
541
		if ( $path === 'main' ) {
542
			return $module;
543
		}
544
545
		$parts = explode( '+', $path );
546
		if ( count( $parts ) === 1 ) {
547
			// In case the '+' was typed into URL, it resolves as a space
548
			$parts = explode( ' ', $path );
549
		}
550
551
		$count = count( $parts );
552
		for ( $i = 0; $i < $count; $i++ ) {
553
			$parent = $module;
554
			$manager = $parent->getModuleManager();
555
			if ( $manager === null ) {
556
				$errorPath = implode( '+', array_slice( $parts, 0, $i ) );
557
				$this->dieUsage( "The module \"$errorPath\" has no submodules", 'badmodule' );
558
			}
559
			$module = $manager->getModule( $parts[$i] );
560
561
			if ( $module === null ) {
562
				$errorPath = $i ? implode( '+', array_slice( $parts, 0, $i ) ) : $parent->getModuleName();
563
				$this->dieUsage(
564
					"The module \"$errorPath\" does not have a submodule \"{$parts[$i]}\"",
565
					'badmodule'
566
				);
567
			}
568
		}
569
570
		return $module;
571
	}
572
573
	/**
574
	 * Get the result object
575
	 * @return ApiResult
576
	 */
577
	public function getResult() {
578
		// Main module has getResult() method overridden
579
		// Safety - avoid infinite loop:
580
		if ( $this->isMain() ) {
581
			ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' );
582
		}
583
584
		return $this->getMain()->getResult();
585
	}
586
587
	/**
588
	 * Get the error formatter
589
	 * @return ApiErrorFormatter
590
	 */
591
	public function getErrorFormatter() {
592
		// Main module has getErrorFormatter() method overridden
593
		// Safety - avoid infinite loop:
594
		if ( $this->isMain() ) {
595
			ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' );
596
		}
597
598
		return $this->getMain()->getErrorFormatter();
599
	}
600
601
	/**
602
	 * Gets a default replica DB connection object
603
	 * @return Database
604
	 */
605
	protected function getDB() {
606
		if ( !isset( $this->mSlaveDB ) ) {
607
			$this->mSlaveDB = wfGetDB( DB_REPLICA, 'api' );
608
		}
609
610
		return $this->mSlaveDB;
611
	}
612
613
	/**
614
	 * Get the continuation manager
615
	 * @return ApiContinuationManager|null
616
	 */
617
	public function getContinuationManager() {
618
		// Main module has getContinuationManager() method overridden
619
		// Safety - avoid infinite loop:
620
		if ( $this->isMain() ) {
621
			ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' );
622
		}
623
624
		return $this->getMain()->getContinuationManager();
625
	}
626
627
	/**
628
	 * Set the continuation manager
629
	 * @param ApiContinuationManager|null
630
	 */
631
	public function setContinuationManager( $manager ) {
632
		// Main module has setContinuationManager() method overridden
633
		// Safety - avoid infinite loop:
634
		if ( $this->isMain() ) {
635
			ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' );
636
		}
637
638
		$this->getMain()->setContinuationManager( $manager );
639
	}
640
641
	/**@}*/
642
643
	/************************************************************************//**
644
	 * @name   Parameter handling
645
	 * @{
646
	 */
647
648
	/**
649
	 * Indicate if the module supports dynamically-determined parameters that
650
	 * cannot be included in self::getAllowedParams().
651
	 * @return string|array|Message|null Return null if the module does not
652
	 *  support additional dynamic parameters, otherwise return a message
653
	 *  describing them.
654
	 */
655
	public function dynamicParameterDocumentation() {
656
		return null;
657
	}
658
659
	/**
660
	 * This method mangles parameter name based on the prefix supplied to the constructor.
661
	 * Override this method to change parameter name during runtime
662
	 * @param string $paramName Parameter name
663
	 * @return string Prefixed parameter name
664
	 */
665
	public function encodeParamName( $paramName ) {
666
		return $this->mModulePrefix . $paramName;
667
	}
668
669
	/**
670
	 * Using getAllowedParams(), this function makes an array of the values
671
	 * provided by the user, with key being the name of the variable, and
672
	 * value - validated value from user or default. limits will not be
673
	 * parsed if $parseLimit is set to false; use this when the max
674
	 * limit is not definitive yet, e.g. when getting revisions.
675
	 * @param bool $parseLimit True by default
676
	 * @return array
677
	 */
678
	public function extractRequestParams( $parseLimit = true ) {
679
		// Cache parameters, for performance and to avoid bug 24564.
680
		if ( !isset( $this->mParamCache[$parseLimit] ) ) {
681
			$params = $this->getFinalParams();
682
			$results = [];
683
684
			if ( $params ) { // getFinalParams() can return false
685
				foreach ( $params as $paramName => $paramSettings ) {
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...
686
					$results[$paramName] = $this->getParameterFromSettings(
687
						$paramName, $paramSettings, $parseLimit );
688
				}
689
			}
690
			$this->mParamCache[$parseLimit] = $results;
691
		}
692
693
		return $this->mParamCache[$parseLimit];
694
	}
695
696
	/**
697
	 * Get a value for the given parameter
698
	 * @param string $paramName Parameter name
699
	 * @param bool $parseLimit See extractRequestParams()
700
	 * @return mixed Parameter value
701
	 */
702
	protected function getParameter( $paramName, $parseLimit = true ) {
703
		$paramSettings = $this->getFinalParams()[$paramName];
704
705
		return $this->getParameterFromSettings( $paramName, $paramSettings, $parseLimit );
706
	}
707
708
	/**
709
	 * Die if none or more than one of a certain set of parameters is set and not false.
710
	 *
711
	 * @param array $params User provided set of parameters, as from $this->extractRequestParams()
712
	 * @param string $required,... Names of parameters of which exactly one must be set
0 ignored issues
show
Documentation introduced by
There is no parameter named $required,.... Did you maybe mean $required?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
713
	 */
714
	public function requireOnlyOneParameter( $params, $required /*...*/ ) {
715
		$required = func_get_args();
716
		array_shift( $required );
717
		$p = $this->getModulePrefix();
718
719
		$intersection = array_intersect( array_keys( array_filter( $params,
720
			[ $this, 'parameterNotEmpty' ] ) ), $required );
721
722
		if ( count( $intersection ) > 1 ) {
723
			$this->dieUsage(
724
				"The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together',
725
				'invalidparammix' );
726
		} elseif ( count( $intersection ) == 0 ) {
727
			$this->dieUsage(
728
				"One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required',
729
				'missingparam'
730
			);
731
		}
732
	}
733
734
	/**
735
	 * Die if more than one of a certain set of parameters is set and not false.
736
	 *
737
	 * @param array $params User provided set of parameters, as from $this->extractRequestParams()
738
	 * @param string $required,... Names of parameters of which at most one must be set
0 ignored issues
show
Documentation introduced by
There is no parameter named $required,.... Did you maybe mean $required?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
739
	 */
740 View Code Duplication
	public function requireMaxOneParameter( $params, $required /*...*/ ) {
741
		$required = func_get_args();
742
		array_shift( $required );
743
		$p = $this->getModulePrefix();
744
745
		$intersection = array_intersect( array_keys( array_filter( $params,
746
			[ $this, 'parameterNotEmpty' ] ) ), $required );
747
748
		if ( count( $intersection ) > 1 ) {
749
			$this->dieUsage(
750
				"The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together',
751
				'invalidparammix'
752
			);
753
		}
754
	}
755
756
	/**
757
	 * Die if none of a certain set of parameters is set and not false.
758
	 *
759
	 * @since 1.23
760
	 * @param array $params User provided set of parameters, as from $this->extractRequestParams()
761
	 * @param string $required,... Names of parameters of which at least one must be set
0 ignored issues
show
Documentation introduced by
There is no parameter named $required,.... Did you maybe mean $required?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
762
	 */
763 View Code Duplication
	public function requireAtLeastOneParameter( $params, $required /*...*/ ) {
764
		$required = func_get_args();
765
		array_shift( $required );
766
		$p = $this->getModulePrefix();
767
768
		$intersection = array_intersect(
769
			array_keys( array_filter( $params, [ $this, 'parameterNotEmpty' ] ) ),
770
			$required
771
		);
772
773
		if ( count( $intersection ) == 0 ) {
774
			$this->dieUsage( "At least one of the parameters {$p}" .
775
				implode( ", {$p}", $required ) . ' is required', "{$p}missingparam" );
776
		}
777
	}
778
779
	/**
780
	 * Die if any of the specified parameters were found in the query part of
781
	 * the URL rather than the post body.
782
	 * @since 1.28
783
	 * @param string[] $params Parameters to check
784
	 * @param string $prefix Set to 'noprefix' to skip calling $this->encodeParamName()
785
	 */
786
	public function requirePostedParameters( $params, $prefix = 'prefix' ) {
787
		// Skip if $wgDebugAPI is set or we're in internal mode
788
		if ( $this->getConfig()->get( 'DebugAPI' ) || $this->getMain()->isInternalMode() ) {
789
			return;
790
		}
791
792
		$queryValues = $this->getRequest()->getQueryValues();
793
		$badParams = [];
794
		foreach ( $params as $param ) {
795
			if ( $prefix !== 'noprefix' ) {
796
				$param = $this->encodeParamName( $param );
797
			}
798
			if ( array_key_exists( $param, $queryValues ) ) {
799
				$badParams[] = $param;
800
			}
801
		}
802
803
		if ( $badParams ) {
804
			$this->dieUsage(
805
				'The following parameters were found in the query string, but must be in the POST body: '
806
					. join( ', ', $badParams ),
807
				'mustpostparams'
808
			);
809
		}
810
	}
811
812
	/**
813
	 * Callback function used in requireOnlyOneParameter to check whether required parameters are set
814
	 *
815
	 * @param object $x Parameter to check is not null/false
816
	 * @return bool
817
	 */
818
	private function parameterNotEmpty( $x ) {
819
		return !is_null( $x ) && $x !== false;
820
	}
821
822
	/**
823
	 * Get a WikiPage object from a title or pageid param, if possible.
824
	 * Can die, if no param is set or if the title or page id is not valid.
825
	 *
826
	 * @param array $params
827
	 * @param bool|string $load Whether load the object's state from the database:
828
	 *        - false: don't load (if the pageid is given, it will still be loaded)
829
	 *        - 'fromdb': load from a replica DB
830
	 *        - 'fromdbmaster': load from the master database
831
	 * @return WikiPage
832
	 */
833
	public function getTitleOrPageId( $params, $load = false ) {
834
		$this->requireOnlyOneParameter( $params, 'title', 'pageid' );
835
836
		$pageObj = null;
837
		if ( isset( $params['title'] ) ) {
838
			$titleObj = Title::newFromText( $params['title'] );
839
			if ( !$titleObj || $titleObj->isExternal() ) {
840
				$this->dieUsageMsg( [ 'invalidtitle', $params['title'] ] );
841
			}
842
			if ( !$titleObj->canExist() ) {
843
				$this->dieUsage( "Namespace doesn't allow actual pages", 'pagecannotexist' );
844
			}
845
			$pageObj = WikiPage::factory( $titleObj );
0 ignored issues
show
Bug introduced by
It seems like $titleObj defined by \Title::newFromText($params['title']) on line 838 can be null; however, WikiPage::factory() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
846
			if ( $load !== false ) {
847
				$pageObj->loadPageData( $load );
0 ignored issues
show
Bug introduced by
It seems like $load defined by parameter $load on line 833 can also be of type boolean; however, WikiPage::loadPageData() does only seem to accept string|object|integer, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
848
			}
849
		} elseif ( isset( $params['pageid'] ) ) {
850
			if ( $load === false ) {
851
				$load = 'fromdb';
852
			}
853
			$pageObj = WikiPage::newFromID( $params['pageid'], $load );
0 ignored issues
show
Bug introduced by
It seems like $load defined by parameter $load on line 833 can also be of type boolean; however, WikiPage::newFromID() does only seem to accept string|integer, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
854
			if ( !$pageObj ) {
855
				$this->dieUsageMsg( [ 'nosuchpageid', $params['pageid'] ] );
856
			}
857
		}
858
859
		return $pageObj;
860
	}
861
862
	/**
863
	 * Return true if we're to watch the page, false if not, null if no change.
864
	 * @param string $watchlist Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
865
	 * @param Title $titleObj The page under consideration
866
	 * @param string $userOption The user option to consider when $watchlist=preferences.
867
	 *    If not set will use watchdefault always and watchcreations if $titleObj doesn't exist.
868
	 * @return bool
869
	 */
870
	protected function getWatchlistValue( $watchlist, $titleObj, $userOption = null ) {
871
872
		$userWatching = $this->getUser()->isWatched( $titleObj, User::IGNORE_USER_RIGHTS );
873
874
		switch ( $watchlist ) {
875
			case 'watch':
876
				return true;
877
878
			case 'unwatch':
879
				return false;
880
881
			case 'preferences':
882
				# If the user is already watching, don't bother checking
883
				if ( $userWatching ) {
884
					return true;
885
				}
886
				# If no user option was passed, use watchdefault and watchcreations
887
				if ( is_null( $userOption ) ) {
888
					return $this->getUser()->getBoolOption( 'watchdefault' ) ||
889
						$this->getUser()->getBoolOption( 'watchcreations' ) && !$titleObj->exists();
890
				}
891
892
				# Watch the article based on the user preference
893
				return $this->getUser()->getBoolOption( $userOption );
894
895
			case 'nochange':
896
				return $userWatching;
897
898
			default:
899
				return $userWatching;
900
		}
901
	}
902
903
	/**
904
	 * Using the settings determine the value for the given parameter
905
	 *
906
	 * @param string $paramName Parameter name
907
	 * @param array|mixed $paramSettings Default value or an array of settings
908
	 *  using PARAM_* constants.
909
	 * @param bool $parseLimit Parse limit?
910
	 * @return mixed Parameter value
911
	 */
912
	protected function getParameterFromSettings( $paramName, $paramSettings, $parseLimit ) {
913
		// Some classes may decide to change parameter names
914
		$encParamName = $this->encodeParamName( $paramName );
915
916
		if ( !is_array( $paramSettings ) ) {
917
			$default = $paramSettings;
918
			$multi = false;
919
			$type = gettype( $paramSettings );
920
			$dupes = false;
921
			$deprecated = false;
922
			$required = false;
923
		} else {
924
			$default = isset( $paramSettings[self::PARAM_DFLT] )
925
				? $paramSettings[self::PARAM_DFLT]
926
				: null;
927
			$multi = isset( $paramSettings[self::PARAM_ISMULTI] )
928
				? $paramSettings[self::PARAM_ISMULTI]
929
				: false;
930
			$type = isset( $paramSettings[self::PARAM_TYPE] )
931
				? $paramSettings[self::PARAM_TYPE]
932
				: null;
933
			$dupes = isset( $paramSettings[self::PARAM_ALLOW_DUPLICATES] )
934
				? $paramSettings[self::PARAM_ALLOW_DUPLICATES]
935
				: false;
936
			$deprecated = isset( $paramSettings[self::PARAM_DEPRECATED] )
937
				? $paramSettings[self::PARAM_DEPRECATED]
938
				: false;
939
			$required = isset( $paramSettings[self::PARAM_REQUIRED] )
940
				? $paramSettings[self::PARAM_REQUIRED]
941
				: false;
942
943
			// When type is not given, and no choices, the type is the same as $default
944
			if ( !isset( $type ) ) {
945
				if ( isset( $default ) ) {
946
					$type = gettype( $default );
947
				} else {
948
					$type = 'NULL'; // allow everything
949
				}
950
			}
951
		}
952
953
		if ( $type == 'boolean' ) {
954
			if ( isset( $default ) && $default !== false ) {
955
				// Having a default value of anything other than 'false' is not allowed
956
				ApiBase::dieDebug(
957
					__METHOD__,
958
					"Boolean param $encParamName's default is set to '$default'. " .
959
						'Boolean parameters must default to false.'
960
				);
961
			}
962
963
			$value = $this->getMain()->getCheck( $encParamName );
964
		} elseif ( $type == 'upload' ) {
965
			if ( isset( $default ) ) {
966
				// Having a default value is not allowed
967
				ApiBase::dieDebug(
968
					__METHOD__,
969
					"File upload param $encParamName's default is set to " .
970
						"'$default'. File upload parameters may not have a default." );
971
			}
972
			if ( $multi ) {
973
				ApiBase::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
974
			}
975
			$value = $this->getMain()->getUpload( $encParamName );
976
			if ( !$value->exists() ) {
977
				// This will get the value without trying to normalize it
978
				// (because trying to normalize a large binary file
979
				// accidentally uploaded as a field fails spectacularly)
980
				$value = $this->getMain()->getRequest()->unsetVal( $encParamName );
981
				if ( $value !== null ) {
982
					$this->dieUsage(
983
						"File upload param $encParamName is not a file upload; " .
984
							'be sure to use multipart/form-data for your POST and include ' .
985
							'a filename in the Content-Disposition header.',
986
						"badupload_{$encParamName}"
987
					);
988
				}
989
			}
990
		} else {
991
			$value = $this->getMain()->getVal( $encParamName, $default );
992
993
			if ( isset( $value ) && $type == 'namespace' ) {
994
				$type = MWNamespace::getValidNamespaces();
995
			}
996
			if ( isset( $value ) && $type == 'submodule' ) {
997
				if ( isset( $paramSettings[self::PARAM_SUBMODULE_MAP] ) ) {
998
					$type = array_keys( $paramSettings[self::PARAM_SUBMODULE_MAP] );
999
				} else {
1000
					$type = $this->getModuleManager()->getNames( $paramName );
1001
				}
1002
			}
1003
1004
			$request = $this->getMain()->getRequest();
1005
			$rawValue = $request->getRawVal( $encParamName );
1006
			if ( $rawValue === null ) {
1007
				$rawValue = $default;
1008
			}
1009
1010
			// Preserve U+001F for self::parseMultiValue(), or error out if that won't be called
1011
			if ( isset( $value ) && substr( $rawValue, 0, 1 ) === "\x1f" ) {
1012
				if ( $multi ) {
1013
					// This loses the potential $wgContLang->checkTitleEncoding() transformation
1014
					// done by WebRequest for $_GET. Let's call that a feature.
1015
					$value = join( "\x1f", $request->normalizeUnicode( explode( "\x1f", $rawValue ) ) );
1016
				} else {
1017
					$this->dieUsage(
1018
						"U+001F multi-value separation may only be used for multi-valued parameters.",
1019
						'badvalue_notmultivalue'
1020
					);
1021
				}
1022
			}
1023
1024
			// Check for NFC normalization, and warn
1025
			if ( $rawValue !== $value ) {
1026
				$this->handleParamNormalization( $paramName, $value, $rawValue );
1027
			}
1028
		}
1029
1030
		if ( isset( $value ) && ( $multi || is_array( $type ) ) ) {
1031
			$value = $this->parseMultiValue(
1032
				$encParamName,
1033
				$value,
1034
				$multi,
1035
				is_array( $type ) ? $type : null
0 ignored issues
show
Bug introduced by
It seems like is_array($type) ? $type : null can also be of type array; however, ApiBase::parseMultiValue() does only seem to accept array<integer,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...
1036
			);
1037
		}
1038
1039
		// More validation only when choices were not given
1040
		// choices were validated in parseMultiValue()
1041
		if ( isset( $value ) ) {
1042
			if ( !is_array( $type ) ) {
1043
				switch ( $type ) {
1044
					case 'NULL': // nothing to do
1045
						break;
1046
					case 'string':
1047
					case 'text':
1048
					case 'password':
1049
						if ( $required && $value === '' ) {
1050
							$this->dieUsageMsg( [ 'missingparam', $paramName ] );
1051
						}
1052
						break;
1053
					case 'integer': // Force everything using intval() and optionally validate limits
1054
						$min = isset( $paramSettings[self::PARAM_MIN] ) ? $paramSettings[self::PARAM_MIN] : null;
1055
						$max = isset( $paramSettings[self::PARAM_MAX] ) ? $paramSettings[self::PARAM_MAX] : null;
1056
						$enforceLimits = isset( $paramSettings[self::PARAM_RANGE_ENFORCE] )
1057
							? $paramSettings[self::PARAM_RANGE_ENFORCE] : false;
1058
1059
						if ( is_array( $value ) ) {
1060
							$value = array_map( 'intval', $value );
1061 View Code Duplication
							if ( !is_null( $min ) || !is_null( $max ) ) {
1062
								foreach ( $value as &$v ) {
1063
									$this->validateLimit( $paramName, $v, $min, $max, null, $enforceLimits );
1064
								}
1065
							}
1066 View Code Duplication
						} else {
1067
							$value = intval( $value );
1068
							if ( !is_null( $min ) || !is_null( $max ) ) {
1069
								$this->validateLimit( $paramName, $value, $min, $max, null, $enforceLimits );
1070
							}
1071
						}
1072
						break;
1073
					case 'limit':
1074
						if ( !$parseLimit ) {
1075
							// Don't do any validation whatsoever
1076
							break;
1077
						}
1078
						if ( !isset( $paramSettings[self::PARAM_MAX] )
1079
							|| !isset( $paramSettings[self::PARAM_MAX2] )
1080
						) {
1081
							ApiBase::dieDebug(
1082
								__METHOD__,
1083
								"MAX1 or MAX2 are not defined for the limit $encParamName"
1084
							);
1085
						}
1086
						if ( $multi ) {
1087
							ApiBase::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
1088
						}
1089
						$min = isset( $paramSettings[self::PARAM_MIN] ) ? $paramSettings[self::PARAM_MIN] : 0;
1090
						if ( $value == 'max' ) {
1091
							$value = $this->getMain()->canApiHighLimits()
1092
								? $paramSettings[self::PARAM_MAX2]
1093
								: $paramSettings[self::PARAM_MAX];
1094
							$this->getResult()->addParsedLimit( $this->getModuleName(), $value );
1095
						} else {
1096
							$value = intval( $value );
1097
							$this->validateLimit(
1098
								$paramName,
1099
								$value,
1100
								$min,
1101
								$paramSettings[self::PARAM_MAX],
1102
								$paramSettings[self::PARAM_MAX2]
1103
							);
1104
						}
1105
						break;
1106
					case 'boolean':
1107
						if ( $multi ) {
1108
							ApiBase::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
1109
						}
1110
						break;
1111 View Code Duplication
					case 'timestamp':
1112
						if ( is_array( $value ) ) {
1113
							foreach ( $value as $key => $val ) {
1114
								$value[$key] = $this->validateTimestamp( $val, $encParamName );
1115
							}
1116
						} else {
1117
							$value = $this->validateTimestamp( $value, $encParamName );
1118
						}
1119
						break;
1120 View Code Duplication
					case 'user':
1121
						if ( is_array( $value ) ) {
1122
							foreach ( $value as $key => $val ) {
1123
								$value[$key] = $this->validateUser( $val, $encParamName );
1124
							}
1125
						} else {
1126
							$value = $this->validateUser( $value, $encParamName );
1127
						}
1128
						break;
1129
					case 'upload': // nothing to do
1130
						break;
1131
					case 'tags':
1132
						// If change tagging was requested, check that the tags are valid.
1133
						if ( !is_array( $value ) && !$multi ) {
1134
							$value = [ $value ];
1135
						}
1136
						$tagsStatus = ChangeTags::canAddTagsAccompanyingChange( $value );
1137
						if ( !$tagsStatus->isGood() ) {
1138
							$this->dieStatus( $tagsStatus );
1139
						}
1140
						break;
1141
					default:
1142
						ApiBase::dieDebug( __METHOD__, "Param $encParamName's type is unknown - $type" );
1143
				}
1144
			}
1145
1146
			// Throw out duplicates if requested
1147
			if ( !$dupes && is_array( $value ) ) {
1148
				$value = array_unique( $value );
1149
			}
1150
1151
			// Set a warning if a deprecated parameter has been passed
1152
			if ( $deprecated && $value !== false ) {
1153
				$this->setWarning( "The $encParamName parameter has been deprecated." );
1154
1155
				$feature = $encParamName;
1156
				$m = $this;
1157
				while ( !$m->isMain() ) {
1158
					$p = $m->getParent();
1159
					$name = $m->getModuleName();
1160
					$param = $p->encodeParamName( $p->getModuleManager()->getModuleGroup( $name ) );
1161
					$feature = "{$param}={$name}&{$feature}";
1162
					$m = $p;
1163
				}
1164
				$this->logFeatureUsage( $feature );
1165
			}
1166
		} elseif ( $required ) {
1167
			$this->dieUsageMsg( [ 'missingparam', $paramName ] );
1168
		}
1169
1170
		return $value;
1171
	}
1172
1173
	/**
1174
	 * Handle when a parameter was Unicode-normalized
1175
	 * @since 1.28
1176
	 * @param string $paramName Unprefixed parameter name
1177
	 * @param string $value Input that will be used.
1178
	 * @param string $rawValue Input before normalization.
1179
	 */
1180
	protected function handleParamNormalization( $paramName, $value, $rawValue ) {
1181
		$encParamName = $this->encodeParamName( $paramName );
1182
		$this->setWarning(
1183
			"The value passed for '$encParamName' contains invalid or non-normalized data. "
1184
			. 'Textual data should be valid, NFC-normalized Unicode without '
1185
			. 'C0 control characters other than HT (\\t), LF (\\n), and CR (\\r).'
1186
		);
1187
	}
1188
1189
	/**
1190
	 * Split a multi-valued parameter string, like explode()
1191
	 * @since 1.28
1192
	 * @param string $value
1193
	 * @param int $limit
1194
	 * @return string[]
1195
	 */
1196
	protected function explodeMultiValue( $value, $limit ) {
1197
		if ( substr( $value, 0, 1 ) === "\x1f" ) {
1198
			$sep = "\x1f";
1199
			$value = substr( $value, 1 );
1200
		} else {
1201
			$sep = '|';
1202
		}
1203
1204
		return explode( $sep, $value, $limit );
1205
	}
1206
1207
	/**
1208
	 * Return an array of values that were given in a 'a|b|c' notation,
1209
	 * after it optionally validates them against the list allowed values.
1210
	 *
1211
	 * @param string $valueName The name of the parameter (for error
1212
	 *  reporting)
1213
	 * @param mixed $value The value being parsed
1214
	 * @param bool $allowMultiple Can $value contain more than one value
1215
	 *  separated by '|'?
1216
	 * @param string[]|null $allowedValues An array of values to check against. If
1217
	 *  null, all values are accepted.
1218
	 * @return string|string[] (allowMultiple ? an_array_of_values : a_single_value)
1219
	 */
1220
	protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues ) {
1221
		if ( ( trim( $value ) === '' || trim( $value ) === "\x1f" ) && $allowMultiple ) {
1222
			return [];
1223
		}
1224
1225
		// This is a bit awkward, but we want to avoid calling canApiHighLimits()
1226
		// because it unstubs $wgUser
1227
		$valuesList = $this->explodeMultiValue( $value, self::LIMIT_SML2 + 1 );
1228
		$sizeLimit = count( $valuesList ) > self::LIMIT_SML1 && $this->mMainModule->canApiHighLimits()
1229
			? self::LIMIT_SML2
1230
			: self::LIMIT_SML1;
1231
1232
		if ( self::truncateArray( $valuesList, $sizeLimit ) ) {
1233
			$this->logFeatureUsage( "too-many-$valueName-for-{$this->getModulePath()}" );
1234
			$this->setWarning( "Too many values supplied for parameter '$valueName': " .
1235
				"the limit is $sizeLimit" );
1236
		}
1237
1238
		if ( !$allowMultiple && count( $valuesList ) != 1 ) {
1239
			// Bug 33482 - Allow entries with | in them for non-multiple values
1240
			if ( in_array( $value, $allowedValues, true ) ) {
1241
				return $value;
1242
			}
1243
1244
			$possibleValues = is_array( $allowedValues )
1245
				? "of '" . implode( "', '", $allowedValues ) . "'"
1246
				: '';
1247
			$this->dieUsage(
1248
				"Only one $possibleValues is allowed for parameter '$valueName'",
1249
				"multival_$valueName"
1250
			);
1251
		}
1252
1253
		if ( is_array( $allowedValues ) ) {
1254
			// Check for unknown values
1255
			$unknown = array_diff( $valuesList, $allowedValues );
1256
			if ( count( $unknown ) ) {
1257
				if ( $allowMultiple ) {
1258
					$s = count( $unknown ) > 1 ? 's' : '';
1259
					$vals = implode( ', ', $unknown );
1260
					$this->setWarning( "Unrecognized value$s for parameter '$valueName': $vals" );
1261
				} else {
1262
					$this->dieUsage(
1263
						"Unrecognized value for parameter '$valueName': {$valuesList[0]}",
1264
						"unknown_$valueName"
1265
					);
1266
				}
1267
			}
1268
			// Now throw them out
1269
			$valuesList = array_intersect( $valuesList, $allowedValues );
1270
		}
1271
1272
		return $allowMultiple ? $valuesList : $valuesList[0];
1273
	}
1274
1275
	/**
1276
	 * Validate the value against the minimum and user/bot maximum limits.
1277
	 * Prints usage info on failure.
1278
	 * @param string $paramName Parameter name
1279
	 * @param int $value Parameter value
1280
	 * @param int|null $min Minimum value
1281
	 * @param int|null $max Maximum value for users
1282
	 * @param int $botMax Maximum value for sysops/bots
1283
	 * @param bool $enforceLimits Whether to enforce (die) if value is outside limits
1284
	 */
1285
	protected function validateLimit( $paramName, &$value, $min, $max, $botMax = null,
1286
		$enforceLimits = false
1287
	) {
1288
		if ( !is_null( $min ) && $value < $min ) {
1289
			$msg = $this->encodeParamName( $paramName ) . " may not be less than $min (set to $value)";
1290
			$this->warnOrDie( $msg, $enforceLimits );
1291
			$value = $min;
1292
		}
1293
1294
		// Minimum is always validated, whereas maximum is checked only if not
1295
		// running in internal call mode
1296
		if ( $this->getMain()->isInternalMode() ) {
1297
			return;
1298
		}
1299
1300
		// Optimization: do not check user's bot status unless really needed -- skips db query
1301
		// assumes $botMax >= $max
1302
		if ( !is_null( $max ) && $value > $max ) {
1303
			if ( !is_null( $botMax ) && $this->getMain()->canApiHighLimits() ) {
1304 View Code Duplication
				if ( $value > $botMax ) {
1305
					$msg = $this->encodeParamName( $paramName ) .
1306
						" may not be over $botMax (set to $value) for bots or sysops";
1307
					$this->warnOrDie( $msg, $enforceLimits );
1308
					$value = $botMax;
1309
				}
1310 View Code Duplication
			} else {
1311
				$msg = $this->encodeParamName( $paramName ) . " may not be over $max (set to $value) for users";
1312
				$this->warnOrDie( $msg, $enforceLimits );
1313
				$value = $max;
1314
			}
1315
		}
1316
	}
1317
1318
	/**
1319
	 * Validate and normalize of parameters of type 'timestamp'
1320
	 * @param string $value Parameter value
1321
	 * @param string $encParamName Parameter name
1322
	 * @return string Validated and normalized parameter
1323
	 */
1324
	protected function validateTimestamp( $value, $encParamName ) {
1325
		// Confusing synonyms for the current time accepted by wfTimestamp()
1326
		// (wfTimestamp() also accepts various non-strings and the string of 14
1327
		// ASCII NUL bytes, but those can't get here)
1328
		if ( !$value ) {
1329
			$this->logFeatureUsage( 'unclear-"now"-timestamp' );
1330
			$this->setWarning(
1331
				"Passing '$value' for timestamp parameter $encParamName has been deprecated." .
1332
					' If for some reason you need to explicitly specify the current time without' .
1333
					' calculating it client-side, use "now".'
1334
			);
1335
			return wfTimestamp( TS_MW );
1336
		}
1337
1338
		// Explicit synonym for the current time
1339
		if ( $value === 'now' ) {
1340
			return wfTimestamp( TS_MW );
1341
		}
1342
1343
		$unixTimestamp = wfTimestamp( TS_UNIX, $value );
1344
		if ( $unixTimestamp === false ) {
1345
			$this->dieUsage(
1346
				"Invalid value '$value' for timestamp parameter $encParamName",
1347
				"badtimestamp_{$encParamName}"
1348
			);
1349
		}
1350
1351
		return wfTimestamp( TS_MW, $unixTimestamp );
1352
	}
1353
1354
	/**
1355
	 * Validate the supplied token.
1356
	 *
1357
	 * @since 1.24
1358
	 * @param string $token Supplied token
1359
	 * @param array $params All supplied parameters for the module
1360
	 * @return bool
1361
	 * @throws MWException
1362
	 */
1363
	final public function validateToken( $token, array $params ) {
1364
		$tokenType = $this->needsToken();
1365
		$salts = ApiQueryTokens::getTokenTypeSalts();
1366
		if ( !isset( $salts[$tokenType] ) ) {
1367
			throw new MWException(
1368
				"Module '{$this->getModuleName()}' tried to use token type '$tokenType' " .
1369
					'without registering it'
1370
			);
1371
		}
1372
1373
		$tokenObj = ApiQueryTokens::getToken(
1374
			$this->getUser(), $this->getRequest()->getSession(), $salts[$tokenType]
1375
		);
1376
		if ( $tokenObj->match( $token ) ) {
1377
			return true;
1378
		}
1379
1380
		$webUiSalt = $this->getWebUITokenSalt( $params );
1381
		if ( $webUiSalt !== null && $this->getUser()->matchEditToken(
1382
			$token,
1383
			$webUiSalt,
0 ignored issues
show
Bug introduced by
It seems like $webUiSalt defined by $this->getWebUITokenSalt($params) on line 1380 can also be of type array; however, User::matchEditToken() does only seem to accept string, 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...
1384
			$this->getRequest()
1385
		) ) {
1386
			return true;
1387
		}
1388
1389
		return false;
1390
	}
1391
1392
	/**
1393
	 * Validate and normalize of parameters of type 'user'
1394
	 * @param string $value Parameter value
1395
	 * @param string $encParamName Parameter name
1396
	 * @return string Validated and normalized parameter
1397
	 */
1398
	private function validateUser( $value, $encParamName ) {
1399
		$title = Title::makeTitleSafe( NS_USER, $value );
1400
		if ( $title === null || $title->hasFragment() ) {
1401
			$this->dieUsage(
1402
				"Invalid value '$value' for user parameter $encParamName",
1403
				"baduser_{$encParamName}"
1404
			);
1405
		}
1406
1407
		return $title->getText();
1408
	}
1409
1410
	/**@}*/
1411
1412
	/************************************************************************//**
1413
	 * @name   Utility methods
1414
	 * @{
1415
	 */
1416
1417
	/**
1418
	 * Set a watch (or unwatch) based the based on a watchlist parameter.
1419
	 * @param string $watch Valid values: 'watch', 'unwatch', 'preferences', 'nochange'
1420
	 * @param Title $titleObj The article's title to change
1421
	 * @param string $userOption The user option to consider when $watch=preferences
1422
	 */
1423
	protected function setWatch( $watch, $titleObj, $userOption = null ) {
1424
		$value = $this->getWatchlistValue( $watch, $titleObj, $userOption );
1425
		if ( $value === null ) {
1426
			return;
1427
		}
1428
1429
		WatchAction::doWatchOrUnwatch( $value, $titleObj, $this->getUser() );
1430
	}
1431
1432
	/**
1433
	 * Truncate an array to a certain length.
1434
	 * @param array $arr Array to truncate
1435
	 * @param int $limit Maximum length
1436
	 * @return bool True if the array was truncated, false otherwise
1437
	 */
1438
	public static function truncateArray( &$arr, $limit ) {
1439
		$modified = false;
1440
		while ( count( $arr ) > $limit ) {
1441
			array_pop( $arr );
1442
			$modified = true;
1443
		}
1444
1445
		return $modified;
1446
	}
1447
1448
	/**
1449
	 * Gets the user for whom to get the watchlist
1450
	 *
1451
	 * @param array $params
1452
	 * @return User
1453
	 */
1454
	public function getWatchlistUser( $params ) {
1455
		if ( !is_null( $params['owner'] ) && !is_null( $params['token'] ) ) {
1456
			$user = User::newFromName( $params['owner'], false );
1457
			if ( !( $user && $user->getId() ) ) {
1458
				$this->dieUsage( 'Specified user does not exist', 'bad_wlowner' );
1459
			}
1460
			$token = $user->getOption( 'watchlisttoken' );
1461
			if ( $token == '' || !hash_equals( $token, $params['token'] ) ) {
1462
				$this->dieUsage(
1463
					'Incorrect watchlist token provided -- please set a correct token in Special:Preferences',
1464
					'bad_wltoken'
1465
				);
1466
			}
1467
		} else {
1468
			if ( !$this->getUser()->isLoggedIn() ) {
1469
				$this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' );
1470
			}
1471
			if ( !$this->getUser()->isAllowed( 'viewmywatchlist' ) ) {
1472
				$this->dieUsage( 'You don\'t have permission to view your watchlist', 'permissiondenied' );
1473
			}
1474
			$user = $this->getUser();
1475
		}
1476
1477
		return $user;
1478
	}
1479
1480
	/**
1481
	 * A subset of wfEscapeWikiText for BC texts
1482
	 *
1483
	 * @since 1.25
1484
	 * @param string|array $v
1485
	 * @return string|array
1486
	 */
1487 View Code Duplication
	private static function escapeWikiText( $v ) {
1488
		if ( is_array( $v ) ) {
1489
			return array_map( 'self::escapeWikiText', $v );
1490
		} else {
1491
			return strtr( $v, [
1492
				'__' => '_&#95;', '{' => '&#123;', '}' => '&#125;',
1493
				'[[Category:' => '[[:Category:',
1494
				'[[File:' => '[[:File:', '[[Image:' => '[[:Image:',
1495
			] );
1496
		}
1497
	}
1498
1499
	/**
1500
	 * Create a Message from a string or array
1501
	 *
1502
	 * A string is used as a message key. An array has the message key as the
1503
	 * first value and message parameters as subsequent values.
1504
	 *
1505
	 * @since 1.25
1506
	 * @param string|array|Message $msg
1507
	 * @param IContextSource $context
1508
	 * @param array $params
1509
	 * @return Message|null
1510
	 */
1511
	public static function makeMessage( $msg, IContextSource $context, array $params = null ) {
1512
		if ( is_string( $msg ) ) {
1513
			$msg = wfMessage( $msg );
1514
		} elseif ( is_array( $msg ) ) {
1515
			$msg = call_user_func_array( 'wfMessage', $msg );
1516
		}
1517
		if ( !$msg instanceof Message ) {
1518
			return null;
1519
		}
1520
1521
		$msg->setContext( $context );
1522
		if ( $params ) {
1523
			$msg->params( $params );
1524
		}
1525
1526
		return $msg;
1527
	}
1528
1529
	/**@}*/
1530
1531
	/************************************************************************//**
1532
	 * @name   Warning and error reporting
1533
	 * @{
1534
	 */
1535
1536
	/**
1537
	 * Set warning section for this module. Users should monitor this
1538
	 * section to notice any changes in API. Multiple calls to this
1539
	 * function will result in the warning messages being separated by
1540
	 * newlines
1541
	 * @param string $warning Warning message
1542
	 */
1543
	public function setWarning( $warning ) {
1544
		$msg = new ApiRawMessage( $warning, 'warning' );
1545
		$this->getErrorFormatter()->addWarning( $this->getModuleName(), $msg );
1546
	}
1547
1548
	/**
1549
	 * Adds a warning to the output, else dies
1550
	 *
1551
	 * @param string $msg Message to show as a warning, or error message if dying
1552
	 * @param bool $enforceLimits Whether this is an enforce (die)
1553
	 */
1554
	private function warnOrDie( $msg, $enforceLimits = false ) {
1555
		if ( $enforceLimits ) {
1556
			$this->dieUsage( $msg, 'integeroutofrange' );
1557
		}
1558
1559
		$this->setWarning( $msg );
1560
	}
1561
1562
	/**
1563
	 * Throw a UsageException, which will (if uncaught) call the main module's
1564
	 * error handler and die with an error message.
1565
	 *
1566
	 * @param string $description One-line human-readable description of the
1567
	 *   error condition, e.g., "The API requires a valid action parameter"
1568
	 * @param string $errorCode Brief, arbitrary, stable string to allow easy
1569
	 *   automated identification of the error, e.g., 'unknown_action'
1570
	 * @param int $httpRespCode HTTP response code
1571
	 * @param array|null $extradata Data to add to the "<error>" element; array in ApiResult format
1572
	 * @throws UsageException always
1573
	 */
1574
	public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) {
1575
		throw new UsageException(
1576
			$description,
1577
			$this->encodeParamName( $errorCode ),
1578
			$httpRespCode,
1579
			$extradata
1580
		);
1581
	}
1582
1583
	/**
1584
	 * Throw a UsageException, which will (if uncaught) call the main module's
1585
	 * error handler and die with an error message including block info.
1586
	 *
1587
	 * @since 1.27
1588
	 * @param Block $block The block used to generate the UsageException
1589
	 * @throws UsageException always
1590
	 */
1591
	public function dieBlocked( Block $block ) {
1592
		// Die using the appropriate message depending on block type
1593
		if ( $block->getType() == Block::TYPE_AUTO ) {
1594
			$this->dieUsage(
1595
				'Your IP address has been blocked automatically, because it was used by a blocked user',
1596
				'autoblocked',
1597
				0,
1598
				[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
1599
			);
1600
		} else {
1601
			$this->dieUsage(
1602
				'You have been blocked from editing',
1603
				'blocked',
1604
				0,
1605
				[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
1606
			);
1607
		}
1608
	}
1609
1610
	/**
1611
	 * Get error (as code, string) from a Status object.
1612
	 *
1613
	 * @since 1.23
1614
	 * @param Status $status
1615
	 * @param array|null &$extraData Set if extra data from IApiMessage is available (since 1.27)
1616
	 * @return array Array of code and error string
1617
	 * @throws MWException
1618
	 */
1619
	public function getErrorFromStatus( $status, &$extraData = null ) {
1620
		if ( $status->isGood() ) {
1621
			throw new MWException( 'Successful status passed to ApiBase::dieStatus' );
1622
		}
1623
1624
		$errors = $status->getErrorsByType( 'error' );
1625
		if ( !$errors ) {
1626
			// No errors? Assume the warnings should be treated as errors
1627
			$errors = $status->getErrorsByType( 'warning' );
1628
		}
1629
		if ( !$errors ) {
1630
			// Still no errors? Punt
1631
			$errors = [ [ 'message' => 'unknownerror-nocode', 'params' => [] ] ];
1632
		}
1633
1634
		// Cannot use dieUsageMsg() because extensions might return custom
1635
		// error messages.
1636
		if ( $errors[0]['message'] instanceof Message ) {
1637
			$msg = $errors[0]['message'];
1638
			if ( $msg instanceof IApiMessage ) {
1639
				$extraData = $msg->getApiData();
1640
				$code = $msg->getApiCode();
1641
			} else {
1642
				$code = $msg->getKey();
1643
			}
1644
		} else {
1645
			$code = $errors[0]['message'];
1646
			$msg = wfMessage( $code, $errors[0]['params'] );
1647
		}
1648
		if ( isset( ApiBase::$messageMap[$code] ) ) {
1649
			// Translate message to code, for backwards compatibility
1650
			$code = ApiBase::$messageMap[$code]['code'];
1651
		}
1652
1653
		return [ $code, $msg->inLanguage( 'en' )->useDatabase( false )->plain() ];
1654
	}
1655
1656
	/**
1657
	 * Throw a UsageException based on the errors in the Status object.
1658
	 *
1659
	 * @since 1.22
1660
	 * @param Status $status
1661
	 * @throws UsageException always
1662
	 */
1663
	public function dieStatus( $status ) {
1664
		$extraData = null;
1665
		list( $code, $msg ) = $this->getErrorFromStatus( $status, $extraData );
1666
		$this->dieUsage( $msg, $code, 0, $extraData );
1667
	}
1668
1669
	// @codingStandardsIgnoreStart Allow long lines. Cannot split these.
1670
	/**
1671
	 * Array that maps message keys to error messages. $1 and friends are replaced.
1672
	 */
1673
	public static $messageMap = [
1674
		// This one MUST be present, or dieUsageMsg() will recurse infinitely
1675
		'unknownerror' => [ 'code' => 'unknownerror', 'info' => "Unknown error: \"\$1\"" ],
1676
		'unknownerror-nocode' => [ 'code' => 'unknownerror', 'info' => 'Unknown error' ],
1677
1678
		// Messages from Title::getUserPermissionsErrors()
1679
		'ns-specialprotected' => [
1680
			'code' => 'unsupportednamespace',
1681
			'info' => "Pages in the Special namespace can't be edited"
1682
		],
1683
		'protectedinterface' => [
1684
			'code' => 'protectednamespace-interface',
1685
			'info' => "You're not allowed to edit interface messages"
1686
		],
1687
		'namespaceprotected' => [
1688
			'code' => 'protectednamespace',
1689
			'info' => "You're not allowed to edit pages in the \"\$1\" namespace"
1690
		],
1691
		'customcssprotected' => [
1692
			'code' => 'customcssprotected',
1693
			'info' => "You're not allowed to edit custom CSS pages"
1694
		],
1695
		'customjsprotected' => [
1696
			'code' => 'customjsprotected',
1697
			'info' => "You're not allowed to edit custom JavaScript pages"
1698
		],
1699
		'cascadeprotected' => [
1700
			'code' => 'cascadeprotected',
1701
			'info' => "The page you're trying to edit is protected because it's included in a cascade-protected page"
1702
		],
1703
		'protectedpagetext' => [
1704
			'code' => 'protectedpage',
1705
			'info' => "The \"\$1\" right is required to edit this page"
1706
		],
1707
		'protect-cantedit' => [
1708
			'code' => 'cantedit',
1709
			'info' => "You can't protect this page because you can't edit it"
1710
		],
1711
		'deleteprotected' => [
1712
			'code' => 'cantedit',
1713
			'info' => "You can't delete this page because it has been protected"
1714
		],
1715
		'badaccess-group0' => [
1716
			'code' => 'permissiondenied',
1717
			'info' => 'Permission denied'
1718
		], // Generic permission denied message
1719
		'badaccess-groups' => [
1720
			'code' => 'permissiondenied',
1721
			'info' => 'Permission denied'
1722
		],
1723
		'titleprotected' => [
1724
			'code' => 'protectedtitle',
1725
			'info' => 'This title has been protected from creation'
1726
		],
1727
		'nocreate-loggedin' => [
1728
			'code' => 'cantcreate',
1729
			'info' => "You don't have permission to create new pages"
1730
		],
1731
		'nocreatetext' => [
1732
			'code' => 'cantcreate-anon',
1733
			'info' => "Anonymous users can't create new pages"
1734
		],
1735
		'movenologintext' => [
1736
			'code' => 'cantmove-anon',
1737
			'info' => "Anonymous users can't move pages"
1738
		],
1739
		'movenotallowed' => [
1740
			'code' => 'cantmove',
1741
			'info' => "You don't have permission to move pages"
1742
		],
1743
		'confirmedittext' => [
1744
			'code' => 'confirmemail',
1745
			'info' => 'You must confirm your email address before you can edit'
1746
		],
1747
		'blockedtext' => [
1748
			'code' => 'blocked',
1749
			'info' => 'You have been blocked from editing'
1750
		],
1751
		'autoblockedtext' => [
1752
			'code' => 'autoblocked',
1753
			'info' => 'Your IP address has been blocked automatically, because it was used by a blocked user'
1754
		],
1755
1756
		// Miscellaneous interface messages
1757
		'actionthrottledtext' => [
1758
			'code' => 'ratelimited',
1759
			'info' => "You've exceeded your rate limit. Please wait some time and try again"
1760
		],
1761
		'alreadyrolled' => [
1762
			'code' => 'alreadyrolled',
1763
			'info' => 'The page you tried to rollback was already rolled back'
1764
		],
1765
		'cantrollback' => [
1766
			'code' => 'onlyauthor',
1767
			'info' => 'The page you tried to rollback only has one author'
1768
		],
1769
		'readonlytext' => [
1770
			'code' => 'readonly',
1771
			'info' => 'The wiki is currently in read-only mode'
1772
		],
1773
		'sessionfailure' => [
1774
			'code' => 'badtoken',
1775
			'info' => 'Invalid token' ],
1776
		'cannotdelete' => [
1777
			'code' => 'cantdelete',
1778
			'info' => "Couldn't delete \"\$1\". Maybe it was deleted already by someone else"
1779
		],
1780
		'notanarticle' => [
1781
			'code' => 'missingtitle',
1782
			'info' => "The page you requested doesn't exist"
1783
		],
1784
		'selfmove' => [ 'code' => 'selfmove', 'info' => "Can't move a page to itself"
1785
		],
1786
		'immobile_namespace' => [
1787
			'code' => 'immobilenamespace',
1788
			'info' => 'You tried to move pages from or to a namespace that is protected from moving'
1789
		],
1790
		'articleexists' => [
1791
			'code' => 'articleexists',
1792
			'info' => 'The destination article already exists and is not a redirect to the source article'
1793
		],
1794
		'protectedpage' => [
1795
			'code' => 'protectedpage',
1796
			'info' => "You don't have permission to perform this move"
1797
		],
1798
		'hookaborted' => [
1799
			'code' => 'hookaborted',
1800
			'info' => 'The modification you tried to make was aborted by an extension hook'
1801
		],
1802
		'cantmove-titleprotected' => [
1803
			'code' => 'protectedtitle',
1804
			'info' => 'The destination article has been protected from creation'
1805
		],
1806
		'imagenocrossnamespace' => [
1807
			'code' => 'nonfilenamespace',
1808
			'info' => "Can't move a file to a non-file namespace"
1809
		],
1810
		'imagetypemismatch' => [
1811
			'code' => 'filetypemismatch',
1812
			'info' => "The new file extension doesn't match its type"
1813
		],
1814
		// 'badarticleerror' => shouldn't happen
1815
		// 'badtitletext' => shouldn't happen
1816
		'ip_range_invalid' => [ 'code' => 'invalidrange', 'info' => 'Invalid IP range' ],
1817
		'range_block_disabled' => [
1818
			'code' => 'rangedisabled',
1819
			'info' => 'Blocking IP ranges has been disabled'
1820
		],
1821
		'nosuchusershort' => [
1822
			'code' => 'nosuchuser',
1823
			'info' => "The user you specified doesn't exist"
1824
		],
1825
		'badipaddress' => [ 'code' => 'invalidip', 'info' => 'Invalid IP address specified' ],
1826
		'ipb_expiry_invalid' => [ 'code' => 'invalidexpiry', 'info' => 'Invalid expiry time' ],
1827
		'ipb_already_blocked' => [
1828
			'code' => 'alreadyblocked',
1829
			'info' => 'The user you tried to block was already blocked'
1830
		],
1831
		'ipb_blocked_as_range' => [
1832
			'code' => 'blockedasrange',
1833
			'info' => "IP address \"\$1\" was blocked as part of range \"\$2\". You can't unblock the IP individually, but you can unblock the range as a whole."
1834
		],
1835
		'ipb_cant_unblock' => [
1836
			'code' => 'cantunblock',
1837
			'info' => 'The block you specified was not found. It may have been unblocked already'
1838
		],
1839
		'mailnologin' => [
1840
			'code' => 'cantsend',
1841
			'info' => 'You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email'
1842
		],
1843
		'ipbblocked' => [
1844
			'code' => 'ipbblocked',
1845
			'info' => 'You cannot block or unblock users while you are yourself blocked'
1846
		],
1847
		'ipbnounblockself' => [
1848
			'code' => 'ipbnounblockself',
1849
			'info' => 'You are not allowed to unblock yourself'
1850
		],
1851
		'usermaildisabled' => [
1852
			'code' => 'usermaildisabled',
1853
			'info' => 'User email has been disabled'
1854
		],
1855
		'blockedemailuser' => [
1856
			'code' => 'blockedfrommail',
1857
			'info' => 'You have been blocked from sending email'
1858
		],
1859
		'notarget' => [
1860
			'code' => 'notarget',
1861
			'info' => 'You have not specified a valid target for this action'
1862
		],
1863
		'noemail' => [
1864
			'code' => 'noemail',
1865
			'info' => 'The user has not specified a valid email address, or has chosen not to receive email from other users'
1866
		],
1867
		'rcpatroldisabled' => [
1868
			'code' => 'patroldisabled',
1869
			'info' => 'Patrolling is disabled on this wiki'
1870
		],
1871
		'markedaspatrollederror-noautopatrol' => [
1872
			'code' => 'noautopatrol',
1873
			'info' => "You don't have permission to patrol your own changes"
1874
		],
1875
		'delete-toobig' => [
1876
			'code' => 'bigdelete',
1877
			'info' => "You can't delete this page because it has more than \$1 revisions"
1878
		],
1879
		'movenotallowedfile' => [
1880
			'code' => 'cantmovefile',
1881
			'info' => "You don't have permission to move files"
1882
		],
1883
		'userrights-no-interwiki' => [
1884
			'code' => 'nointerwikiuserrights',
1885
			'info' => "You don't have permission to change user rights on other wikis"
1886
		],
1887
		'userrights-nodatabase' => [
1888
			'code' => 'nosuchdatabase',
1889
			'info' => "Database \"\$1\" does not exist or is not local"
1890
		],
1891
		'nouserspecified' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ],
1892
		'noname' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ],
1893
		'summaryrequired' => [ 'code' => 'summaryrequired', 'info' => 'Summary required' ],
1894
		'import-rootpage-invalid' => [
1895
			'code' => 'import-rootpage-invalid',
1896
			'info' => 'Root page is an invalid title'
1897
		],
1898
		'import-rootpage-nosubpage' => [
1899
			'code' => 'import-rootpage-nosubpage',
1900
			'info' => 'Namespace "$1" of the root page does not allow subpages'
1901
		],
1902
1903
		// API-specific messages
1904
		'readrequired' => [
1905
			'code' => 'readapidenied',
1906
			'info' => 'You need read permission to use this module'
1907
		],
1908
		'writedisabled' => [
1909
			'code' => 'noapiwrite',
1910
			'info' => "Editing of this wiki through the API is disabled. Make sure the \$wgEnableWriteAPI=true; statement is included in the wiki's LocalSettings.php file"
1911
		],
1912
		'writerequired' => [
1913
			'code' => 'writeapidenied',
1914
			'info' => "You're not allowed to edit this wiki through the API"
1915
		],
1916
		'missingparam' => [ 'code' => 'no$1', 'info' => "The \$1 parameter must be set" ],
1917
		'invalidtitle' => [ 'code' => 'invalidtitle', 'info' => "Bad title \"\$1\"" ],
1918
		'nosuchpageid' => [ 'code' => 'nosuchpageid', 'info' => "There is no page with ID \$1" ],
1919
		'nosuchrevid' => [ 'code' => 'nosuchrevid', 'info' => "There is no revision with ID \$1" ],
1920
		'nosuchuser' => [ 'code' => 'nosuchuser', 'info' => "User \"\$1\" doesn't exist" ],
1921
		'invaliduser' => [ 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ],
1922
		'invalidexpiry' => [ 'code' => 'invalidexpiry', 'info' => "Invalid expiry time \"\$1\"" ],
1923
		'pastexpiry' => [ 'code' => 'pastexpiry', 'info' => "Expiry time \"\$1\" is in the past" ],
1924
		'create-titleexists' => [
1925
			'code' => 'create-titleexists',
1926
			'info' => "Existing titles can't be protected with 'create'"
1927
		],
1928
		'missingtitle-createonly' => [
1929
			'code' => 'missingtitle-createonly',
1930
			'info' => "Missing titles can only be protected with 'create'"
1931
		],
1932
		'cantblock' => [ 'code' => 'cantblock',
1933
			'info' => "You don't have permission to block users"
1934
		],
1935
		'canthide' => [
1936
			'code' => 'canthide',
1937
			'info' => "You don't have permission to hide user names from the block log"
1938
		],
1939
		'cantblock-email' => [
1940
			'code' => 'cantblock-email',
1941
			'info' => "You don't have permission to block users from sending email through the wiki"
1942
		],
1943
		'unblock-notarget' => [
1944
			'code' => 'notarget',
1945
			'info' => 'Either the id or the user parameter must be set'
1946
		],
1947
		'unblock-idanduser' => [
1948
			'code' => 'idanduser',
1949
			'info' => "The id and user parameters can't be used together"
1950
		],
1951
		'cantunblock' => [
1952
			'code' => 'permissiondenied',
1953
			'info' => "You don't have permission to unblock users"
1954
		],
1955
		'cannotundelete' => [
1956
			'code' => 'cantundelete',
1957
			'info' => "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already"
1958
		],
1959
		'permdenied-undelete' => [
1960
			'code' => 'permissiondenied',
1961
			'info' => "You don't have permission to restore deleted revisions"
1962
		],
1963
		'createonly-exists' => [
1964
			'code' => 'articleexists',
1965
			'info' => 'The article you tried to create has been created already'
1966
		],
1967
		'nocreate-missing' => [
1968
			'code' => 'missingtitle',
1969
			'info' => "The article you tried to edit doesn't exist"
1970
		],
1971
		'cantchangecontentmodel' => [
1972
			'code' => 'cantchangecontentmodel',
1973
			'info' => "You don't have permission to change the content model of a page"
1974
		],
1975
		'nosuchrcid' => [
1976
			'code' => 'nosuchrcid',
1977
			'info' => "There is no change with rcid \"\$1\""
1978
		],
1979
		'nosuchlogid' => [
1980
			'code' => 'nosuchlogid',
1981
			'info' => "There is no log entry with ID \"\$1\""
1982
		],
1983
		'protect-invalidaction' => [
1984
			'code' => 'protect-invalidaction',
1985
			'info' => "Invalid protection type \"\$1\""
1986
		],
1987
		'protect-invalidlevel' => [
1988
			'code' => 'protect-invalidlevel',
1989
			'info' => "Invalid protection level \"\$1\""
1990
		],
1991
		'toofewexpiries' => [
1992
			'code' => 'toofewexpiries',
1993
			'info' => "\$1 expiry timestamps were provided where \$2 were needed"
1994
		],
1995
		'cantimport' => [
1996
			'code' => 'cantimport',
1997
			'info' => "You don't have permission to import pages"
1998
		],
1999
		'cantimport-upload' => [
2000
			'code' => 'cantimport-upload',
2001
			'info' => "You don't have permission to import uploaded pages"
2002
		],
2003
		'importnofile' => [ 'code' => 'nofile', 'info' => "You didn't upload a file" ],
2004
		'importuploaderrorsize' => [
2005
			'code' => 'filetoobig',
2006
			'info' => 'The file you uploaded is bigger than the maximum upload size'
2007
		],
2008
		'importuploaderrorpartial' => [
2009
			'code' => 'partialupload',
2010
			'info' => 'The file was only partially uploaded'
2011
		],
2012
		'importuploaderrortemp' => [
2013
			'code' => 'notempdir',
2014
			'info' => 'The temporary upload directory is missing'
2015
		],
2016
		'importcantopen' => [
2017
			'code' => 'cantopenfile',
2018
			'info' => "Couldn't open the uploaded file"
2019
		],
2020
		'import-noarticle' => [
2021
			'code' => 'badinterwiki',
2022
			'info' => 'Invalid interwiki title specified'
2023
		],
2024
		'importbadinterwiki' => [
2025
			'code' => 'badinterwiki',
2026
			'info' => 'Invalid interwiki title specified'
2027
		],
2028
		'import-unknownerror' => [
2029
			'code' => 'import-unknownerror',
2030
			'info' => "Unknown error on import: \"\$1\""
2031
		],
2032
		'cantoverwrite-sharedfile' => [
2033
			'code' => 'cantoverwrite-sharedfile',
2034
			'info' => 'The target file exists on a shared repository and you do not have permission to override it'
2035
		],
2036
		'sharedfile-exists' => [
2037
			'code' => 'fileexists-sharedrepo-perm',
2038
			'info' => 'The target file exists on a shared repository. Use the ignorewarnings parameter to override it.'
2039
		],
2040
		'mustbeposted' => [
2041
			'code' => 'mustbeposted',
2042
			'info' => "The \$1 module requires a POST request"
2043
		],
2044
		'show' => [
2045
			'code' => 'show',
2046
			'info' => 'Incorrect parameter - mutually exclusive values may not be supplied'
2047
		],
2048
		'specialpage-cantexecute' => [
2049
			'code' => 'specialpage-cantexecute',
2050
			'info' => "You don't have permission to view the results of this special page"
2051
		],
2052
		'invalidoldimage' => [
2053
			'code' => 'invalidoldimage',
2054
			'info' => 'The oldimage parameter has invalid format'
2055
		],
2056
		'nodeleteablefile' => [
2057
			'code' => 'nodeleteablefile',
2058
			'info' => 'No such old version of the file'
2059
		],
2060
		'fileexists-forbidden' => [
2061
			'code' => 'fileexists-forbidden',
2062
			'info' => 'A file with name "$1" already exists, and cannot be overwritten.'
2063
		],
2064
		'fileexists-shared-forbidden' => [
2065
			'code' => 'fileexists-shared-forbidden',
2066
			'info' => 'A file with name "$1" already exists in the shared file repository, and cannot be overwritten.'
2067
		],
2068
		'filerevert-badversion' => [
2069
			'code' => 'filerevert-badversion',
2070
			'info' => 'There is no previous local version of this file with the provided timestamp.'
2071
		],
2072
2073
		// ApiEditPage messages
2074
		'noimageredirect-anon' => [
2075
			'code' => 'noimageredirect-anon',
2076
			'info' => "Anonymous users can't create image redirects"
2077
		],
2078
		'noimageredirect-logged' => [
2079
			'code' => 'noimageredirect',
2080
			'info' => "You don't have permission to create image redirects"
2081
		],
2082
		'spamdetected' => [
2083
			'code' => 'spamdetected',
2084
			'info' => "Your edit was refused because it contained a spam fragment: \"\$1\""
2085
		],
2086
		'contenttoobig' => [
2087
			'code' => 'contenttoobig',
2088
			'info' => "The content you supplied exceeds the article size limit of \$1 kilobytes"
2089
		],
2090
		'noedit-anon' => [ 'code' => 'noedit-anon', 'info' => "Anonymous users can't edit pages" ],
2091
		'noedit' => [ 'code' => 'noedit', 'info' => "You don't have permission to edit pages" ],
2092
		'wasdeleted' => [
2093
			'code' => 'pagedeleted',
2094
			'info' => 'The page has been deleted since you fetched its timestamp'
2095
		],
2096
		'blankpage' => [
2097
			'code' => 'emptypage',
2098
			'info' => 'Creating new, empty pages is not allowed'
2099
		],
2100
		'editconflict' => [ 'code' => 'editconflict', 'info' => 'Edit conflict detected' ],
2101
		'hashcheckfailed' => [ 'code' => 'badmd5', 'info' => 'The supplied MD5 hash was incorrect' ],
2102
		'missingtext' => [
2103
			'code' => 'notext',
2104
			'info' => 'One of the text, appendtext, prependtext and undo parameters must be set'
2105
		],
2106
		'emptynewsection' => [
2107
			'code' => 'emptynewsection',
2108
			'info' => 'Creating empty new sections is not possible.'
2109
		],
2110
		'revwrongpage' => [
2111
			'code' => 'revwrongpage',
2112
			'info' => "r\$1 is not a revision of \"\$2\""
2113
		],
2114
		'undo-failure' => [
2115
			'code' => 'undofailure',
2116
			'info' => 'Undo failed due to conflicting intermediate edits'
2117
		],
2118
		'content-not-allowed-here' => [
2119
			'code' => 'contentnotallowedhere',
2120
			'info' => 'Content model "$1" is not allowed at title "$2"'
2121
		],
2122
2123
		// Messages from WikiPage::doEit(]
2124
		'edit-hook-aborted' => [
2125
			'code' => 'edit-hook-aborted',
2126
			'info' => 'Your edit was aborted by an ArticleSave hook'
2127
		],
2128
		'edit-gone-missing' => [
2129
			'code' => 'edit-gone-missing',
2130
			'info' => "The page you tried to edit doesn't seem to exist anymore"
2131
		],
2132
		'edit-conflict' => [ 'code' => 'editconflict', 'info' => 'Edit conflict detected' ],
2133
		'edit-already-exists' => [
2134
			'code' => 'edit-already-exists',
2135
			'info' => 'It seems the page you tried to create already exist'
2136
		],
2137
2138
		// uploadMsgs
2139
		'invalid-file-key' => [ 'code' => 'invalid-file-key', 'info' => 'Not a valid file key' ],
2140
		'nouploadmodule' => [ 'code' => 'nouploadmodule', 'info' => 'No upload module set' ],
2141
		'uploaddisabled' => [
2142
			'code' => 'uploaddisabled',
2143
			'info' => 'Uploads are not enabled. Make sure $wgEnableUploads is set to true in LocalSettings.php and the PHP ini setting file_uploads is true'
2144
		],
2145
		'copyuploaddisabled' => [
2146
			'code' => 'copyuploaddisabled',
2147
			'info' => 'Uploads by URL is not enabled. Make sure $wgAllowCopyUploads is set to true in LocalSettings.php.'
2148
		],
2149
		'copyuploadbaddomain' => [
2150
			'code' => 'copyuploadbaddomain',
2151
			'info' => 'Uploads by URL are not allowed from this domain.'
2152
		],
2153
		'copyuploadbadurl' => [
2154
			'code' => 'copyuploadbadurl',
2155
			'info' => 'Upload not allowed from this URL.'
2156
		],
2157
2158
		'filename-tooshort' => [
2159
			'code' => 'filename-tooshort',
2160
			'info' => 'The filename is too short'
2161
		],
2162
		'filename-toolong' => [ 'code' => 'filename-toolong', 'info' => 'The filename is too long' ],
2163
		'illegal-filename' => [
2164
			'code' => 'illegal-filename',
2165
			'info' => 'The filename is not allowed'
2166
		],
2167
		'filetype-missing' => [
2168
			'code' => 'filetype-missing',
2169
			'info' => 'The file is missing an extension'
2170
		],
2171
2172
		'mustbeloggedin' => [ 'code' => 'mustbeloggedin', 'info' => 'You must be logged in to $1.' ]
2173
	];
2174
	// @codingStandardsIgnoreEnd
2175
2176
	/**
2177
	 * Helper function for readonly errors
2178
	 *
2179
	 * @throws UsageException always
2180
	 */
2181
	public function dieReadOnly() {
2182
		$parsed = $this->parseMsg( [ 'readonlytext' ] );
2183
		$this->dieUsage( $parsed['info'], $parsed['code'], /* http error */ 0,
2184
			[ 'readonlyreason' => wfReadOnlyReason() ] );
2185
	}
2186
2187
	/**
2188
	 * Output the error message related to a certain array
2189
	 * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
2190
	 * @throws UsageException always
2191
	 */
2192
	public function dieUsageMsg( $error ) {
2193
		# most of the time we send a 1 element, so we might as well send it as
2194
		# a string and make this an array here.
2195
		if ( is_string( $error ) ) {
2196
			$error = [ $error ];
2197
		}
2198
		$parsed = $this->parseMsg( $error );
2199
		$extraData = isset( $parsed['data'] ) ? $parsed['data'] : null;
2200
		$this->dieUsage( $parsed['info'], $parsed['code'], 0, $extraData );
2201
	}
2202
2203
	/**
2204
	 * Will only set a warning instead of failing if the global $wgDebugAPI
2205
	 * is set to true. Otherwise behaves exactly as dieUsageMsg().
2206
	 * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
2207
	 * @throws UsageException
2208
	 * @since 1.21
2209
	 */
2210
	public function dieUsageMsgOrDebug( $error ) {
2211
		if ( $this->getConfig()->get( 'DebugAPI' ) !== true ) {
2212
			$this->dieUsageMsg( $error );
2213
		}
2214
2215
		if ( is_string( $error ) ) {
2216
			$error = [ $error ];
2217
		}
2218
		$parsed = $this->parseMsg( $error );
2219
		$this->setWarning( '$wgDebugAPI: ' . $parsed['code'] . ' - ' . $parsed['info'] );
2220
	}
2221
2222
	/**
2223
	 * Die with the $prefix.'badcontinue' error. This call is common enough to
2224
	 * make it into the base method.
2225
	 * @param bool $condition Will only die if this value is true
2226
	 * @throws UsageException
2227
	 * @since 1.21
2228
	 */
2229
	protected function dieContinueUsageIf( $condition ) {
2230
		if ( $condition ) {
2231
			$this->dieUsage(
2232
				'Invalid continue param. You should pass the original value returned by the previous query',
2233
				'badcontinue' );
2234
		}
2235
	}
2236
2237
	/**
2238
	 * Return the error message related to a certain array
2239
	 * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
2240
	 * @return [ 'code' => code, 'info' => info ]
0 ignored issues
show
Documentation introduced by
The doc-type ">[ could not be parsed: Unknown type name "[" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
2241
	 */
2242
	public function parseMsg( $error ) {
2243
		// Check whether someone passed the whole array, instead of one element as
2244
		// documented. This breaks if it's actually an array of fallback keys, but
2245
		// that's long-standing misbehavior introduced in r87627 to incorrectly
2246
		// fix T30797.
2247
		if ( is_array( $error ) ) {
2248
			$first = reset( $error );
2249
			if ( is_array( $first ) ) {
2250
				wfDebug( __METHOD__ . ' was passed an array of arrays. ' . wfGetAllCallers( 5 ) );
2251
				$error = $first;
2252
			}
2253
		}
2254
2255
		$msg = Message::newFromSpecifier( $error );
2256
2257
		if ( $msg instanceof IApiMessage ) {
2258
			return [
2259
				'code' => $msg->getApiCode(),
2260
				'info' => $msg->inLanguage( 'en' )->useDatabase( false )->text(),
2261
				'data' => $msg->getApiData()
2262
			];
2263
		}
2264
2265
		$key = $msg->getKey();
2266
		if ( isset( self::$messageMap[$key] ) ) {
2267
			$params = $msg->getParams();
2268
			return [
2269
				'code' => wfMsgReplaceArgs( self::$messageMap[$key]['code'], $params ),
2270
				'info' => wfMsgReplaceArgs( self::$messageMap[$key]['info'], $params )
2271
			];
2272
		}
2273
2274
		// If the key isn't present, throw an "unknown error"
2275
		return $this->parseMsg( [ 'unknownerror', $key ] );
2276
	}
2277
2278
	/**
2279
	 * Internal code errors should be reported with this method
2280
	 * @param string $method Method or function name
2281
	 * @param string $message Error message
2282
	 * @throws MWException always
2283
	 */
2284
	protected static function dieDebug( $method, $message ) {
2285
		throw new MWException( "Internal error in $method: $message" );
2286
	}
2287
2288
	/**
2289
	 * Write logging information for API features to a debug log, for usage
2290
	 * analysis.
2291
	 * @param string $feature Feature being used.
2292
	 */
2293
	public function logFeatureUsage( $feature ) {
2294
		$request = $this->getRequest();
2295
		$s = '"' . addslashes( $feature ) . '"' .
2296
			' "' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . '"' .
2297
			' "' . $request->getIP() . '"' .
2298
			' "' . addslashes( $request->getHeader( 'Referer' ) ) . '"' .
2299
			' "' . addslashes( $this->getMain()->getUserAgent() ) . '"';
2300
		wfDebugLog( 'api-feature-usage', $s, 'private' );
2301
	}
2302
2303
	/**@}*/
2304
2305
	/************************************************************************//**
2306
	 * @name   Help message generation
2307
	 * @{
2308
	 */
2309
2310
	/**
2311
	 * Return the description message.
2312
	 *
2313
	 * @return string|array|Message
2314
	 */
2315
	protected function getDescriptionMessage() {
2316
		return "apihelp-{$this->getModulePath()}-description";
2317
	}
2318
2319
	/**
2320
	 * Get final module description, after hooks have had a chance to tweak it as
2321
	 * needed.
2322
	 *
2323
	 * @since 1.25, returns Message[] rather than string[]
2324
	 * @return Message[]
2325
	 */
2326
	public function getFinalDescription() {
2327
		$desc = $this->getDescription();
2328
		Hooks::run( 'APIGetDescription', [ &$this, &$desc ] );
2329
		$desc = self::escapeWikiText( $desc );
2330
		if ( is_array( $desc ) ) {
2331
			$desc = implode( "\n", $desc );
2332
		} else {
2333
			$desc = (string)$desc;
2334
		}
2335
2336
		$msg = ApiBase::makeMessage( $this->getDescriptionMessage(), $this->getContext(), [
2337
			$this->getModulePrefix(),
2338
			$this->getModuleName(),
2339
			$this->getModulePath(),
2340
		] );
2341
		if ( !$msg->exists() ) {
2342
			$msg = $this->msg( 'api-help-fallback-description', $desc );
2343
		}
2344
		$msgs = [ $msg ];
2345
2346
		Hooks::run( 'APIGetDescriptionMessages', [ $this, &$msgs ] );
2347
2348
		return $msgs;
2349
	}
2350
2351
	/**
2352
	 * Get final list of parameters, after hooks have had a chance to
2353
	 * tweak it as needed.
2354
	 *
2355
	 * @param int $flags Zero or more flags like GET_VALUES_FOR_HELP
2356
	 * @return array|bool False on no parameters
2357
	 * @since 1.21 $flags param added
2358
	 */
2359
	public function getFinalParams( $flags = 0 ) {
2360
		$params = $this->getAllowedParams( $flags );
2361
		if ( !$params ) {
2362
			$params = [];
2363
		}
2364
2365
		if ( $this->needsToken() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->needsToken() of type string|false 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...
2366
			$params['token'] = [
2367
				ApiBase::PARAM_TYPE => 'string',
2368
				ApiBase::PARAM_REQUIRED => true,
2369
				ApiBase::PARAM_HELP_MSG => [
2370
					'api-help-param-token',
2371
					$this->needsToken(),
2372
				],
2373
			] + ( isset( $params['token'] ) ? $params['token'] : [] );
2374
		}
2375
2376
		Hooks::run( 'APIGetAllowedParams', [ &$this, &$params, $flags ] );
2377
2378
		return $params;
2379
	}
2380
2381
	/**
2382
	 * Get final parameter descriptions, after hooks have had a chance to tweak it as
2383
	 * needed.
2384
	 *
2385
	 * @since 1.25, returns array of Message[] rather than array of string[]
2386
	 * @return array Keys are parameter names, values are arrays of Message objects
2387
	 */
2388
	public function getFinalParamDescription() {
2389
		$prefix = $this->getModulePrefix();
2390
		$name = $this->getModuleName();
2391
		$path = $this->getModulePath();
2392
2393
		$desc = $this->getParamDescription();
2394
		Hooks::run( 'APIGetParamDescription', [ &$this, &$desc ] );
2395
2396
		if ( !$desc ) {
2397
			$desc = [];
2398
		}
2399
		$desc = self::escapeWikiText( $desc );
0 ignored issues
show
Bug introduced by
It seems like $desc can also be of type boolean; however, ApiBase::escapeWikiText() does only seem to accept string|array, 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...
2400
2401
		$params = $this->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
2402
		$msgs = [];
2403
		foreach ( $params as $param => $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...
2404
			if ( !is_array( $settings ) ) {
2405
				$settings = [];
2406
			}
2407
2408
			$d = isset( $desc[$param] ) ? $desc[$param] : '';
2409
			if ( is_array( $d ) ) {
2410
				// Special handling for prop parameters
2411
				$d = array_map( function ( $line ) {
2412
					if ( preg_match( '/^\s+(\S+)\s+-\s+(.+)$/', $line, $m ) ) {
2413
						$line = "\n;{$m[1]}:{$m[2]}";
2414
					}
2415
					return $line;
2416
				}, $d );
2417
				$d = implode( ' ', $d );
2418
			}
2419
2420
			if ( isset( $settings[ApiBase::PARAM_HELP_MSG] ) ) {
2421
				$msg = $settings[ApiBase::PARAM_HELP_MSG];
2422
			} else {
2423
				$msg = $this->msg( "apihelp-{$path}-param-{$param}" );
2424
				if ( !$msg->exists() ) {
2425
					$msg = $this->msg( 'api-help-fallback-parameter', $d );
2426
				}
2427
			}
2428
			$msg = ApiBase::makeMessage( $msg, $this->getContext(),
2429
				[ $prefix, $param, $name, $path ] );
2430
			if ( !$msg ) {
2431
				self::dieDebug( __METHOD__,
2432
					'Value in ApiBase::PARAM_HELP_MSG is not valid' );
2433
			}
2434
			$msgs[$param] = [ $msg ];
2435
2436
			if ( isset( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) {
2437
				if ( !is_array( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) {
2438
					self::dieDebug( __METHOD__,
2439
						'ApiBase::PARAM_HELP_MSG_PER_VALUE is not valid' );
2440
				}
2441
				if ( !is_array( $settings[ApiBase::PARAM_TYPE] ) ) {
2442
					self::dieDebug( __METHOD__,
2443
						'ApiBase::PARAM_HELP_MSG_PER_VALUE may only be used when ' .
2444
						'ApiBase::PARAM_TYPE is an array' );
2445
				}
2446
2447
				$valueMsgs = $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE];
2448
				foreach ( $settings[ApiBase::PARAM_TYPE] as $value ) {
2449
					if ( isset( $valueMsgs[$value] ) ) {
2450
						$msg = $valueMsgs[$value];
2451
					} else {
2452
						$msg = "apihelp-{$path}-paramvalue-{$param}-{$value}";
2453
					}
2454
					$m = ApiBase::makeMessage( $msg, $this->getContext(),
2455
						[ $prefix, $param, $name, $path, $value ] );
2456
					if ( $m ) {
2457
						$m = new ApiHelpParamValueMessage(
2458
							$value,
2459
							[ $m->getKey(), 'api-help-param-no-description' ],
2460
							$m->getParams()
2461
						);
2462
						$msgs[$param][] = $m->setContext( $this->getContext() );
2463
					} else {
2464
						self::dieDebug( __METHOD__,
2465
							"Value in ApiBase::PARAM_HELP_MSG_PER_VALUE for $value is not valid" );
2466
					}
2467
				}
2468
			}
2469
2470
			if ( isset( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) {
2471
				if ( !is_array( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) {
2472
					self::dieDebug( __METHOD__,
2473
						'Value for ApiBase::PARAM_HELP_MSG_APPEND is not an array' );
2474
				}
2475
				foreach ( $settings[ApiBase::PARAM_HELP_MSG_APPEND] as $m ) {
2476
					$m = ApiBase::makeMessage( $m, $this->getContext(),
2477
						[ $prefix, $param, $name, $path ] );
2478
					if ( $m ) {
2479
						$msgs[$param][] = $m;
2480
					} else {
2481
						self::dieDebug( __METHOD__,
2482
							'Value in ApiBase::PARAM_HELP_MSG_APPEND is not valid' );
2483
					}
2484
				}
2485
			}
2486
		}
2487
2488
		Hooks::run( 'APIGetParamDescriptionMessages', [ $this, &$msgs ] );
2489
2490
		return $msgs;
2491
	}
2492
2493
	/**
2494
	 * Generates the list of flags for the help screen and for action=paraminfo
2495
	 *
2496
	 * Corresponding messages: api-help-flag-deprecated,
2497
	 * api-help-flag-internal, api-help-flag-readrights,
2498
	 * api-help-flag-writerights, api-help-flag-mustbeposted
2499
	 *
2500
	 * @return string[]
2501
	 */
2502
	protected function getHelpFlags() {
2503
		$flags = [];
2504
2505
		if ( $this->isDeprecated() ) {
2506
			$flags[] = 'deprecated';
2507
		}
2508
		if ( $this->isInternal() ) {
2509
			$flags[] = 'internal';
2510
		}
2511
		if ( $this->isReadMode() ) {
2512
			$flags[] = 'readrights';
2513
		}
2514
		if ( $this->isWriteMode() ) {
2515
			$flags[] = 'writerights';
2516
		}
2517
		if ( $this->mustBePosted() ) {
2518
			$flags[] = 'mustbeposted';
2519
		}
2520
2521
		return $flags;
2522
	}
2523
2524
	/**
2525
	 * Returns information about the source of this module, if known
2526
	 *
2527
	 * Returned array is an array with the following keys:
2528
	 * - path: Install path
2529
	 * - name: Extension name, or "MediaWiki" for core
2530
	 * - namemsg: (optional) i18n message key for a display name
2531
	 * - license-name: (optional) Name of license
2532
	 *
2533
	 * @return array|null
2534
	 */
2535
	protected function getModuleSourceInfo() {
2536
		global $IP;
2537
2538
		if ( $this->mModuleSource !== false ) {
2539
			return $this->mModuleSource;
2540
		}
2541
2542
		// First, try to find where the module comes from...
2543
		$rClass = new ReflectionClass( $this );
2544
		$path = $rClass->getFileName();
2545
		if ( !$path ) {
2546
			// No path known?
2547
			$this->mModuleSource = null;
2548
			return null;
2549
		}
2550
		$path = realpath( $path ) ?: $path;
2551
2552
		// Build map of extension directories to extension info
2553
		if ( self::$extensionInfo === null ) {
2554
			$extDir = $this->getConfig()->get( 'ExtensionDirectory' );
2555
			self::$extensionInfo = [
2556
				realpath( __DIR__ ) ?: __DIR__ => [
2557
					'path' => $IP,
2558
					'name' => 'MediaWiki',
2559
					'license-name' => 'GPL-2.0+',
2560
				],
2561
				realpath( "$IP/extensions" ) ?: "$IP/extensions" => null,
2562
				realpath( $extDir ) ?: $extDir => null,
2563
			];
2564
			$keep = [
2565
				'path' => null,
2566
				'name' => null,
2567
				'namemsg' => null,
2568
				'license-name' => null,
2569
			];
2570
			foreach ( $this->getConfig()->get( 'ExtensionCredits' ) as $group ) {
2571
				foreach ( $group as $ext ) {
2572
					if ( !isset( $ext['path'] ) || !isset( $ext['name'] ) ) {
2573
						// This shouldn't happen, but does anyway.
2574
						continue;
2575
					}
2576
2577
					$extpath = $ext['path'];
2578
					if ( !is_dir( $extpath ) ) {
2579
						$extpath = dirname( $extpath );
2580
					}
2581
					self::$extensionInfo[realpath( $extpath ) ?: $extpath] =
2582
						array_intersect_key( $ext, $keep );
2583
				}
2584
			}
2585
			foreach ( ExtensionRegistry::getInstance()->getAllThings() as $ext ) {
2586
				$extpath = $ext['path'];
2587
				if ( !is_dir( $extpath ) ) {
2588
					$extpath = dirname( $extpath );
2589
				}
2590
				self::$extensionInfo[realpath( $extpath ) ?: $extpath] =
2591
					array_intersect_key( $ext, $keep );
2592
			}
2593
		}
2594
2595
		// Now traverse parent directories until we find a match or run out of
2596
		// parents.
2597
		do {
2598
			if ( array_key_exists( $path, self::$extensionInfo ) ) {
2599
				// Found it!
2600
				$this->mModuleSource = self::$extensionInfo[$path];
2601
				return $this->mModuleSource;
2602
			}
2603
2604
			$oldpath = $path;
2605
			$path = dirname( $path );
2606
		} while ( $path !== $oldpath );
2607
2608
		// No idea what extension this might be.
2609
		$this->mModuleSource = null;
2610
		return null;
2611
	}
2612
2613
	/**
2614
	 * Called from ApiHelp before the pieces are joined together and returned.
2615
	 *
2616
	 * This exists mainly for ApiMain to add the Permissions and Credits
2617
	 * sections. Other modules probably don't need it.
2618
	 *
2619
	 * @param string[] &$help Array of help data
2620
	 * @param array $options Options passed to ApiHelp::getHelp
2621
	 * @param array &$tocData If a TOC is being generated, this array has keys
2622
	 *   as anchors in the page and values as for Linker::generateTOC().
2623
	 */
2624
	public function modifyHelp( array &$help, array $options, array &$tocData ) {
2625
	}
2626
2627
	/**@}*/
2628
2629
	/************************************************************************//**
2630
	 * @name   Deprecated
2631
	 * @{
2632
	 */
2633
2634
	/**
2635
	 * Returns the description string for this module
2636
	 *
2637
	 * Ignored if an i18n message exists for
2638
	 * "apihelp-{$this->getModulePath()}-description".
2639
	 *
2640
	 * @deprecated since 1.25
2641
	 * @return Message|string|array
2642
	 */
2643
	protected function getDescription() {
2644
		return false;
2645
	}
2646
2647
	/**
2648
	 * Returns an array of parameter descriptions.
2649
	 *
2650
	 * For each parameter, ignored if an i18n message exists for the parameter.
2651
	 * By default that message is
2652
	 * "apihelp-{$this->getModulePath()}-param-{$param}", but it may be
2653
	 * overridden using ApiBase::PARAM_HELP_MSG in the data returned by
2654
	 * self::getFinalParams().
2655
	 *
2656
	 * @deprecated since 1.25
2657
	 * @return array|bool False on no parameter descriptions
2658
	 */
2659
	protected function getParamDescription() {
2660
		return [];
2661
	}
2662
2663
	/**
2664
	 * Returns usage examples for this module.
2665
	 *
2666
	 * Return value as an array is either:
2667
	 *  - numeric keys with partial URLs ("api.php?" plus a query string) as
2668
	 *    values
2669
	 *  - sequential numeric keys with even-numbered keys being display-text
2670
	 *    and odd-numbered keys being partial urls
2671
	 *  - partial URLs as keys with display-text (string or array-to-be-joined)
2672
	 *    as values
2673
	 * Return value as a string is the same as an array with a numeric key and
2674
	 * that value, and boolean false means "no examples".
2675
	 *
2676
	 * @deprecated since 1.25, use getExamplesMessages() instead
2677
	 * @return bool|string|array
2678
	 */
2679
	protected function getExamples() {
2680
		return false;
2681
	}
2682
2683
	/**
2684
	 * @deprecated since 1.25, always returns empty string
2685
	 * @param IDatabase|bool $db
2686
	 * @return string
2687
	 */
2688
	public function getModuleProfileName( $db = false ) {
2689
		wfDeprecated( __METHOD__, '1.25' );
2690
		return '';
2691
	}
2692
2693
	/**
2694
	 * @deprecated since 1.25
2695
	 */
2696
	public function profileIn() {
2697
		// No wfDeprecated() yet because extensions call this and might need to
2698
		// keep doing so for BC.
2699
	}
2700
2701
	/**
2702
	 * @deprecated since 1.25
2703
	 */
2704
	public function profileOut() {
2705
		// No wfDeprecated() yet because extensions call this and might need to
2706
		// keep doing so for BC.
2707
	}
2708
2709
	/**
2710
	 * @deprecated since 1.25
2711
	 */
2712
	public function safeProfileOut() {
2713
		wfDeprecated( __METHOD__, '1.25' );
2714
	}
2715
2716
	/**
2717
	 * @deprecated since 1.25, always returns 0
2718
	 * @return float
2719
	 */
2720
	public function getProfileTime() {
2721
		wfDeprecated( __METHOD__, '1.25' );
2722
		return 0;
2723
	}
2724
2725
	/**
2726
	 * @deprecated since 1.25
2727
	 */
2728
	public function profileDBIn() {
2729
		wfDeprecated( __METHOD__, '1.25' );
2730
	}
2731
2732
	/**
2733
	 * @deprecated since 1.25
2734
	 */
2735
	public function profileDBOut() {
2736
		wfDeprecated( __METHOD__, '1.25' );
2737
	}
2738
2739
	/**
2740
	 * @deprecated since 1.25, always returns 0
2741
	 * @return float
2742
	 */
2743
	public function getProfileDBTime() {
2744
		wfDeprecated( __METHOD__, '1.25' );
2745
		return 0;
2746
	}
2747
2748
	/**
2749
	 * Call wfTransactionalTimeLimit() if this request was POSTed
2750
	 * @since 1.26
2751
	 */
2752
	protected function useTransactionalTimeLimit() {
2753
		if ( $this->getRequest()->wasPosted() ) {
2754
			wfTransactionalTimeLimit();
2755
		}
2756
	}
2757
2758
	/**@}*/
2759
}
2760
2761
/**
2762
 * For really cool vim folding this needs to be at the end:
2763
 * vim: foldmarker=@{,@} foldmethod=marker
2764
 */
2765