Issues (4122)

Security Analysis    not enabled

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

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

includes/api/ApiMain.php (3 issues)

Upgrade to new PHP Analysis Engine

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

1
<?php
2
/**
3
 *
4
 *
5
 * Created on Sep 4, 2006
6
 *
7
 * Copyright © 2006 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
 * @defgroup API API
26
 */
27
28
use MediaWiki\Logger\LoggerFactory;
29
30
/**
31
 * This is the main API class, used for both external and internal processing.
32
 * When executed, it will create the requested formatter object,
33
 * instantiate and execute an object associated with the needed action,
34
 * and use formatter to print results.
35
 * In case of an exception, an error message will be printed using the same formatter.
36
 *
37
 * To use API from another application, run it using FauxRequest object, in which
38
 * case any internal exceptions will not be handled but passed up to the caller.
39
 * After successful execution, use getResult() for the resulting data.
40
 *
41
 * @ingroup API
42
 */
43
class ApiMain extends ApiBase {
44
	/**
45
	 * When no format parameter is given, this format will be used
46
	 */
47
	const API_DEFAULT_FORMAT = 'jsonfm';
48
49
	/**
50
	 * List of available modules: action name => module class
51
	 */
52
	private static $Modules = [
53
		'login' => 'ApiLogin',
54
		'clientlogin' => 'ApiClientLogin',
55
		'logout' => 'ApiLogout',
56
		'createaccount' => 'ApiAMCreateAccount',
57
		'linkaccount' => 'ApiLinkAccount',
58
		'unlinkaccount' => 'ApiRemoveAuthenticationData',
59
		'changeauthenticationdata' => 'ApiChangeAuthenticationData',
60
		'removeauthenticationdata' => 'ApiRemoveAuthenticationData',
61
		'resetpassword' => 'ApiResetPassword',
62
		'query' => 'ApiQuery',
63
		'expandtemplates' => 'ApiExpandTemplates',
64
		'parse' => 'ApiParse',
65
		'stashedit' => 'ApiStashEdit',
66
		'opensearch' => 'ApiOpenSearch',
67
		'feedcontributions' => 'ApiFeedContributions',
68
		'feedrecentchanges' => 'ApiFeedRecentChanges',
69
		'feedwatchlist' => 'ApiFeedWatchlist',
70
		'help' => 'ApiHelp',
71
		'paraminfo' => 'ApiParamInfo',
72
		'rsd' => 'ApiRsd',
73
		'compare' => 'ApiComparePages',
74
		'tokens' => 'ApiTokens',
75
		'checktoken' => 'ApiCheckToken',
76
		'cspreport' => 'ApiCSPReport',
77
78
		// Write modules
79
		'purge' => 'ApiPurge',
80
		'setnotificationtimestamp' => 'ApiSetNotificationTimestamp',
81
		'rollback' => 'ApiRollback',
82
		'delete' => 'ApiDelete',
83
		'undelete' => 'ApiUndelete',
84
		'protect' => 'ApiProtect',
85
		'block' => 'ApiBlock',
86
		'unblock' => 'ApiUnblock',
87
		'move' => 'ApiMove',
88
		'edit' => 'ApiEditPage',
89
		'upload' => 'ApiUpload',
90
		'filerevert' => 'ApiFileRevert',
91
		'emailuser' => 'ApiEmailUser',
92
		'watch' => 'ApiWatch',
93
		'patrol' => 'ApiPatrol',
94
		'import' => 'ApiImport',
95
		'clearhasmsg' => 'ApiClearHasMsg',
96
		'userrights' => 'ApiUserrights',
97
		'options' => 'ApiOptions',
98
		'imagerotate' => 'ApiImageRotate',
99
		'revisiondelete' => 'ApiRevisionDelete',
100
		'managetags' => 'ApiManageTags',
101
		'tag' => 'ApiTag',
102
		'mergehistory' => 'ApiMergeHistory',
103
	];
104
105
	/**
106
	 * List of available formats: format name => format class
107
	 */
108
	private static $Formats = [
109
		'json' => 'ApiFormatJson',
110
		'jsonfm' => 'ApiFormatJson',
111
		'php' => 'ApiFormatPhp',
112
		'phpfm' => 'ApiFormatPhp',
113
		'xml' => 'ApiFormatXml',
114
		'xmlfm' => 'ApiFormatXml',
115
		'rawfm' => 'ApiFormatJson',
116
		'none' => 'ApiFormatNone',
117
	];
118
119
	// @codingStandardsIgnoreStart String contenation on "msg" not allowed to break long line
120
	/**
121
	 * List of user roles that are specifically relevant to the API.
122
	 * [ 'right' => [ 'msg'    => 'Some message with a $1',
123
	 *                'params' => [ $someVarToSubst ] ],
124
	 * ];
125
	 */
126
	private static $mRights = [
127
		'writeapi' => [
128
			'msg' => 'right-writeapi',
129
			'params' => []
130
		],
131
		'apihighlimits' => [
132
			'msg' => 'api-help-right-apihighlimits',
133
			'params' => [ ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ]
134
		]
135
	];
136
	// @codingStandardsIgnoreEnd
137
138
	/**
139
	 * @var ApiFormatBase
140
	 */
141
	private $mPrinter;
142
143
	private $mModuleMgr, $mResult, $mErrorFormatter;
144
	/** @var ApiContinuationManager|null */
145
	private $mContinuationManager;
146
	private $mAction;
147
	private $mEnableWrite;
148
	private $mInternalMode, $mSquidMaxage;
0 ignored issues
show
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...
149
	/** @var ApiBase */
150
	private $mModule;
151
152
	private $mCacheMode = 'private';
153
	private $mCacheControl = [];
154
	private $mParamsUsed = [];
155
156
	/** @var bool|null Cached return value from self::lacksSameOriginSecurity() */
157
	private $lacksSameOriginSecurity = null;
158
159
	/**
160
	 * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
161
	 *
162
	 * @param IContextSource|WebRequest $context If this is an instance of
163
	 *    FauxRequest, errors are thrown and no printing occurs
164
	 * @param bool $enableWrite Should be set to true if the api may modify data
165
	 */
166
	public function __construct( $context = null, $enableWrite = false ) {
167
		if ( $context === null ) {
168
			$context = RequestContext::getMain();
169
		} elseif ( $context instanceof WebRequest ) {
170
			// BC for pre-1.19
171
			$request = $context;
172
			$context = RequestContext::getMain();
173
		}
174
		// We set a derivative context so we can change stuff later
175
		$this->setContext( new DerivativeContext( $context ) );
176
177
		if ( isset( $request ) ) {
178
			$this->getContext()->setRequest( $request );
179
		} else {
180
			$request = $this->getRequest();
181
		}
182
183
		$this->mInternalMode = ( $request instanceof FauxRequest );
184
185
		// Special handling for the main module: $parent === $this
186
		parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
187
188
		$config = $this->getConfig();
189
190
		if ( !$this->mInternalMode ) {
191
			// Log if a request with a non-whitelisted Origin header is seen
192
			// with session cookies.
193
			$originHeader = $request->getHeader( 'Origin' );
194 View Code Duplication
			if ( $originHeader === false ) {
195
				$origins = [];
196
			} else {
197
				$originHeader = trim( $originHeader );
198
				$origins = preg_split( '/\s+/', $originHeader );
199
			}
200
			$sessionCookies = array_intersect(
201
				array_keys( $_COOKIE ),
202
				MediaWiki\Session\SessionManager::singleton()->getVaryCookies()
203
			);
204
			if ( $origins && $sessionCookies && (
205
				count( $origins ) !== 1 || !self::matchOrigin(
206
					$origins[0],
207
					$config->get( 'CrossSiteAJAXdomains' ),
208
					$config->get( 'CrossSiteAJAXdomainExceptions' )
209
				)
210
			) ) {
211
				LoggerFactory::getInstance( 'cors' )->warning(
212
					'Non-whitelisted CORS request with session cookies', [
213
						'origin' => $originHeader,
214
						'cookies' => $sessionCookies,
215
						'ip' => $request->getIP(),
216
						'userAgent' => $this->getUserAgent(),
217
						'wiki' => wfWikiID(),
218
					]
219
				);
220
			}
221
222
			// If we're in a mode that breaks the same-origin policy, strip
223
			// user credentials for security.
224
			if ( $this->lacksSameOriginSecurity() ) {
225
				global $wgUser;
226
				wfDebug( "API: stripping user credentials when the same-origin policy is not applied\n" );
227
				$wgUser = new User();
228
				$this->getContext()->setUser( $wgUser );
229
			}
230
		}
231
232
		$uselang = $this->getParameter( 'uselang' );
233
		if ( $uselang === 'user' ) {
234
			// Assume the parent context is going to return the user language
235
			// for uselang=user (see T85635).
236
		} else {
237
			if ( $uselang === 'content' ) {
238
				global $wgContLang;
239
				$uselang = $wgContLang->getCode();
240
			}
241
			$code = RequestContext::sanitizeLangCode( $uselang );
242
			$this->getContext()->setLanguage( $code );
243
			if ( !$this->mInternalMode ) {
244
				global $wgLang;
245
				$wgLang = $this->getContext()->getLanguage();
246
				RequestContext::getMain()->setLanguage( $wgLang );
247
			}
248
		}
249
250
		$this->mModuleMgr = new ApiModuleManager( $this );
251
		$this->mModuleMgr->addModules( self::$Modules, 'action' );
252
		$this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
253
		$this->mModuleMgr->addModules( self::$Formats, 'format' );
254
		$this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
255
256
		Hooks::run( 'ApiMain::moduleManager', [ $this->mModuleMgr ] );
257
258
		$this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
259
		$this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
260
		$this->mResult->setErrorFormatter( $this->mErrorFormatter );
261
		$this->mContinuationManager = null;
262
		$this->mEnableWrite = $enableWrite;
263
264
		$this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling()
265
		$this->mCommit = false;
266
	}
267
268
	/**
269
	 * Return true if the API was started by other PHP code using FauxRequest
270
	 * @return bool
271
	 */
272
	public function isInternalMode() {
273
		return $this->mInternalMode;
274
	}
275
276
	/**
277
	 * Get the ApiResult object associated with current request
278
	 *
279
	 * @return ApiResult
280
	 */
281
	public function getResult() {
282
		return $this->mResult;
283
	}
284
285
	/**
286
	 * Get the security flag for the current request
287
	 * @return bool
288
	 */
289
	public function lacksSameOriginSecurity() {
290
		if ( $this->lacksSameOriginSecurity !== null ) {
291
			return $this->lacksSameOriginSecurity;
292
		}
293
294
		$request = $this->getRequest();
295
296
		// JSONP mode
297
		if ( $request->getVal( 'callback' ) !== null ) {
298
			$this->lacksSameOriginSecurity = true;
299
			return true;
300
		}
301
302
		// Anonymous CORS
303
		if ( $request->getVal( 'origin' ) === '*' ) {
304
			$this->lacksSameOriginSecurity = true;
305
			return true;
306
		}
307
308
		// Header to be used from XMLHTTPRequest when the request might
309
		// otherwise be used for XSS.
310
		if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) {
311
			$this->lacksSameOriginSecurity = true;
312
			return true;
313
		}
314
315
		// Allow extensions to override.
316
		$this->lacksSameOriginSecurity = !Hooks::run( 'RequestHasSameOriginSecurity', [ $request ] );
317
		return $this->lacksSameOriginSecurity;
318
	}
319
320
	/**
321
	 * Get the ApiErrorFormatter object associated with current request
322
	 * @return ApiErrorFormatter
323
	 */
324
	public function getErrorFormatter() {
325
		return $this->mErrorFormatter;
326
	}
327
328
	/**
329
	 * Get the continuation manager
330
	 * @return ApiContinuationManager|null
331
	 */
332
	public function getContinuationManager() {
333
		return $this->mContinuationManager;
334
	}
335
336
	/**
337
	 * Set the continuation manager
338
	 * @param ApiContinuationManager|null
339
	 */
340
	public function setContinuationManager( $manager ) {
341
		if ( $manager !== null ) {
342
			if ( !$manager instanceof ApiContinuationManager ) {
343
				throw new InvalidArgumentException( __METHOD__ . ': Was passed ' .
344
					is_object( $manager ) ? get_class( $manager ) : gettype( $manager )
345
				);
346
			}
347
			if ( $this->mContinuationManager !== null ) {
348
				throw new UnexpectedValueException(
349
					__METHOD__ . ': tried to set manager from ' . $manager->getSource() .
350
					' when a manager is already set from ' . $this->mContinuationManager->getSource()
351
				);
352
			}
353
		}
354
		$this->mContinuationManager = $manager;
355
	}
356
357
	/**
358
	 * Get the API module object. Only works after executeAction()
359
	 *
360
	 * @return ApiBase
361
	 */
362
	public function getModule() {
363
		return $this->mModule;
364
	}
365
366
	/**
367
	 * Get the result formatter object. Only works after setupExecuteAction()
368
	 *
369
	 * @return ApiFormatBase
370
	 */
371
	public function getPrinter() {
372
		return $this->mPrinter;
373
	}
374
375
	/**
376
	 * Set how long the response should be cached.
377
	 *
378
	 * @param int $maxage
379
	 */
380
	public function setCacheMaxAge( $maxage ) {
381
		$this->setCacheControl( [
382
			'max-age' => $maxage,
383
			's-maxage' => $maxage
384
		] );
385
	}
386
387
	/**
388
	 * Set the type of caching headers which will be sent.
389
	 *
390
	 * @param string $mode One of:
391
	 *    - 'public':     Cache this object in public caches, if the maxage or smaxage
392
	 *         parameter is set, or if setCacheMaxAge() was called. If a maximum age is
393
	 *         not provided by any of these means, the object will be private.
394
	 *    - 'private':    Cache this object only in private client-side caches.
395
	 *    - 'anon-public-user-private': Make this object cacheable for logged-out
396
	 *         users, but private for logged-in users. IMPORTANT: If this is set, it must be
397
	 *         set consistently for a given URL, it cannot be set differently depending on
398
	 *         things like the contents of the database, or whether the user is logged in.
399
	 *
400
	 *  If the wiki does not allow anonymous users to read it, the mode set here
401
	 *  will be ignored, and private caching headers will always be sent. In other words,
402
	 *  the "public" mode is equivalent to saying that the data sent is as public as a page
403
	 *  view.
404
	 *
405
	 *  For user-dependent data, the private mode should generally be used. The
406
	 *  anon-public-user-private mode should only be used where there is a particularly
407
	 *  good performance reason for caching the anonymous response, but where the
408
	 *  response to logged-in users may differ, or may contain private data.
409
	 *
410
	 *  If this function is never called, then the default will be the private mode.
411
	 */
412
	public function setCacheMode( $mode ) {
413
		if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
414
			wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
415
416
			// Ignore for forwards-compatibility
417
			return;
418
		}
419
420
		if ( !User::isEveryoneAllowed( 'read' ) ) {
421
			// Private wiki, only private headers
422
			if ( $mode !== 'private' ) {
423
				wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
424
425
				return;
426
			}
427
		}
428
429
		if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
430
			// User language is used for i18n, so we don't want to publicly
431
			// cache. Anons are ok, because if they have non-default language
432
			// then there's an appropriate Vary header set by whatever set
433
			// their non-default language.
434
			wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
435
				"'anon-public-user-private' due to uselang=user\n" );
436
			$mode = 'anon-public-user-private';
437
		}
438
439
		wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
440
		$this->mCacheMode = $mode;
441
	}
442
443
	/**
444
	 * Set directives (key/value pairs) for the Cache-Control header.
445
	 * Boolean values will be formatted as such, by including or omitting
446
	 * without an equals sign.
447
	 *
448
	 * Cache control values set here will only be used if the cache mode is not
449
	 * private, see setCacheMode().
450
	 *
451
	 * @param array $directives
452
	 */
453
	public function setCacheControl( $directives ) {
454
		$this->mCacheControl = $directives + $this->mCacheControl;
455
	}
456
457
	/**
458
	 * Create an instance of an output formatter by its name
459
	 *
460
	 * @param string $format
461
	 *
462
	 * @return ApiFormatBase
463
	 */
464
	public function createPrinterByName( $format ) {
465
		$printer = $this->mModuleMgr->getModule( $format, 'format' );
466
		if ( $printer === null ) {
467
			$this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
468
		}
469
470
		return $printer;
471
	}
472
473
	/**
474
	 * Execute api request. Any errors will be handled if the API was called by the remote client.
475
	 */
476
	public function execute() {
477
		if ( $this->mInternalMode ) {
478
			$this->executeAction();
479
		} else {
480
			$this->executeActionWithErrorHandling();
481
		}
482
	}
483
484
	/**
485
	 * Execute an action, and in case of an error, erase whatever partial results
486
	 * have been accumulated, and replace it with an error message and a help screen.
487
	 */
488
	protected function executeActionWithErrorHandling() {
489
		// Verify the CORS header before executing the action
490
		if ( !$this->handleCORS() ) {
491
			// handleCORS() has sent a 403, abort
492
			return;
493
		}
494
495
		// Exit here if the request method was OPTIONS
496
		// (assume there will be a followup GET or POST)
497
		if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
498
			return;
499
		}
500
501
		// In case an error occurs during data output,
502
		// clear the output buffer and print just the error information
503
		$obLevel = ob_get_level();
504
		ob_start();
505
506
		$t = microtime( true );
507
		$isError = false;
508
		try {
509
			$this->executeAction();
510
			$runTime = microtime( true ) - $t;
511
			$this->logRequest( $runTime );
512
			if ( $this->mModule->isWriteMode() && $this->getRequest()->wasPosted() ) {
513
				$this->getStats()->timing(
514
					'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime
515
				);
516
			}
517
		} catch ( Exception $e ) {
518
			$this->handleException( $e );
519
			$this->logRequest( microtime( true ) - $t, $e );
520
			$isError = true;
521
		}
522
523
		// Commit DBs and send any related cookies and headers
524
		MediaWiki::preOutputCommit( $this->getContext() );
525
526
		// Send cache headers after any code which might generate an error, to
527
		// avoid sending public cache headers for errors.
528
		$this->sendCacheHeaders( $isError );
529
530
		// Executing the action might have already messed with the output
531
		// buffers.
532
		while ( ob_get_level() > $obLevel ) {
533
			ob_end_flush();
534
		}
535
	}
536
537
	/**
538
	 * Handle an exception as an API response
539
	 *
540
	 * @since 1.23
541
	 * @param Exception $e
542
	 */
543
	protected function handleException( Exception $e ) {
544
		// Bug 63145: Rollback any open database transactions
545
		if ( !( $e instanceof UsageException ) ) {
546
			// UsageExceptions are intentional, so don't rollback if that's the case
547
			try {
548
				MWExceptionHandler::rollbackMasterChangesAndLog( $e );
549
			} catch ( DBError $e2 ) {
550
				// Rollback threw an exception too. Log it, but don't interrupt
551
				// our regularly scheduled exception handling.
552
				MWExceptionHandler::logException( $e2 );
553
			}
554
		}
555
556
		// Allow extra cleanup and logging
557
		Hooks::run( 'ApiMain::onException', [ $this, $e ] );
558
559
		// Log it
560
		if ( !( $e instanceof UsageException ) ) {
561
			MWExceptionHandler::logException( $e );
562
		}
563
564
		// Handle any kind of exception by outputting properly formatted error message.
565
		// If this fails, an unhandled exception should be thrown so that global error
566
		// handler will process and log it.
567
568
		$errCode = $this->substituteResultWithError( $e );
569
570
		// Error results should not be cached
571
		$this->setCacheMode( 'private' );
572
573
		$response = $this->getRequest()->response();
574
		$headerStr = 'MediaWiki-API-Error: ' . $errCode;
575
		$response->header( $headerStr );
576
577
		// Reset and print just the error message
578
		ob_clean();
579
580
		// Printer may not be initialized if the extractRequestParams() fails for the main module
581
		$this->createErrorPrinter();
582
583
		try {
584
			$this->printResult( $e->getCode() );
585
		} catch ( UsageException $ex ) {
586
			// The error printer itself is failing. Try suppressing its request
587
			// parameters and redo.
588
			$this->setWarning(
589
				'Error printer failed (will retry without params): ' . $ex->getMessage()
590
			);
591
			$this->mPrinter = null;
592
			$this->createErrorPrinter();
593
			$this->mPrinter->forceDefaultParams();
594
			if ( $e->getCode() ) {
595
				$response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
596
			}
597
			$this->printResult( $e->getCode() );
598
		}
599
	}
600
601
	/**
602
	 * Handle an exception from the ApiBeforeMain hook.
603
	 *
604
	 * This tries to print the exception as an API response, to be more
605
	 * friendly to clients. If it fails, it will rethrow the exception.
606
	 *
607
	 * @since 1.23
608
	 * @param Exception $e
609
	 * @throws Exception
610
	 */
611
	public static function handleApiBeforeMainException( Exception $e ) {
612
		ob_start();
613
614
		try {
615
			$main = new self( RequestContext::getMain(), false );
616
			$main->handleException( $e );
617
			$main->logRequest( 0, $e );
618
		} catch ( Exception $e2 ) {
619
			// Nope, even that didn't work. Punt.
620
			throw $e;
621
		}
622
623
		// Reset cache headers
624
		$main->sendCacheHeaders( true );
625
626
		ob_end_flush();
627
	}
628
629
	/**
630
	 * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
631
	 *
632
	 * If no origin parameter is present, nothing happens.
633
	 * If an origin parameter is present but doesn't match the Origin header, a 403 status code
634
	 * is set and false is returned.
635
	 * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
636
	 * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
637
	 * headers are set.
638
	 * https://www.w3.org/TR/cors/#resource-requests
639
	 * https://www.w3.org/TR/cors/#resource-preflight-requests
640
	 *
641
	 * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
642
	 */
643
	protected function handleCORS() {
644
		$originParam = $this->getParameter( 'origin' ); // defaults to null
645
		if ( $originParam === null ) {
646
			// No origin parameter, nothing to do
647
			return true;
648
		}
649
650
		$request = $this->getRequest();
651
		$response = $request->response();
652
653
		$matchOrigin = false;
654
		$allowTiming = false;
655
		$varyOrigin = true;
656
657
		if ( $originParam === '*' ) {
658
			// Request for anonymous CORS
659
			$matchOrigin = true;
660
			$allowOrigin = '*';
661
			$allowCredentials = 'false';
662
			$varyOrigin = false; // No need to vary
663
		} else {
664
			// Non-anonymous CORS, check we allow the domain
665
666
			// Origin: header is a space-separated list of origins, check all of them
667
			$originHeader = $request->getHeader( 'Origin' );
668 View Code Duplication
			if ( $originHeader === false ) {
669
				$origins = [];
670
			} else {
671
				$originHeader = trim( $originHeader );
672
				$origins = preg_split( '/\s+/', $originHeader );
673
			}
674
675
			if ( !in_array( $originParam, $origins ) ) {
676
				// origin parameter set but incorrect
677
				// Send a 403 response
678
				$response->statusHeader( 403 );
679
				$response->header( 'Cache-Control: no-cache' );
680
				echo "'origin' parameter does not match Origin header\n";
681
682
				return false;
683
			}
684
685
			$config = $this->getConfig();
686
			$matchOrigin = count( $origins ) === 1 && self::matchOrigin(
687
				$originParam,
688
				$config->get( 'CrossSiteAJAXdomains' ),
689
				$config->get( 'CrossSiteAJAXdomainExceptions' )
690
			);
691
692
			$allowOrigin = $originHeader;
693
			$allowCredentials = 'true';
694
			$allowTiming = $originHeader;
695
		}
696
697
		if ( $matchOrigin ) {
698
			$requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
699
			$preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
700
			if ( $preflight ) {
701
				// This is a CORS preflight request
702
				if ( $requestedMethod !== 'POST' && $requestedMethod !== 'GET' ) {
703
					// If method is not a case-sensitive match, do not set any additional headers and terminate.
704
					return true;
705
				}
706
				// We allow the actual request to send the following headers
707
				$requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
708
				if ( $requestedHeaders !== false ) {
709
					if ( !self::matchRequestedHeaders( $requestedHeaders ) ) {
710
						return true;
711
					}
712
					$response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
713
				}
714
715
				// We only allow the actual request to be GET or POST
716
				$response->header( 'Access-Control-Allow-Methods: POST, GET' );
717
			}
718
719
			$response->header( "Access-Control-Allow-Origin: $allowOrigin" );
720
			$response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
721
			// https://www.w3.org/TR/resource-timing/#timing-allow-origin
722
			if ( $allowTiming !== false ) {
723
				$response->header( "Timing-Allow-Origin: $allowTiming" );
724
			}
725
726
			if ( !$preflight ) {
727
				$response->header(
728
					'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag'
729
				);
730
			}
731
		}
732
733
		if ( $varyOrigin ) {
734
			$this->getOutput()->addVaryHeader( 'Origin' );
735
		}
736
737
		return true;
738
	}
739
740
	/**
741
	 * Attempt to match an Origin header against a set of rules and a set of exceptions
742
	 * @param string $value Origin header
743
	 * @param array $rules Set of wildcard rules
744
	 * @param array $exceptions Set of wildcard rules
745
	 * @return bool True if $value matches a rule in $rules and doesn't match
746
	 *    any rules in $exceptions, false otherwise
747
	 */
748
	protected static function matchOrigin( $value, $rules, $exceptions ) {
749
		foreach ( $rules as $rule ) {
750
			if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
751
				// Rule matches, check exceptions
752
				foreach ( $exceptions as $exc ) {
753
					if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
754
						return false;
755
					}
756
				}
757
758
				return true;
759
			}
760
		}
761
762
		return false;
763
	}
764
765
	/**
766
	 * Attempt to validate the value of Access-Control-Request-Headers against a list
767
	 * of headers that we allow the follow up request to send.
768
	 *
769
	 * @param string $requestedHeaders Comma seperated list of HTTP headers
770
	 * @return bool True if all requested headers are in the list of allowed headers
771
	 */
772
	protected static function matchRequestedHeaders( $requestedHeaders ) {
773
		if ( trim( $requestedHeaders ) === '' ) {
774
			return true;
775
		}
776
		$requestedHeaders = explode( ',', $requestedHeaders );
777
		$allowedAuthorHeaders = array_flip( [
778
			/* simple headers (see spec) */
779
			'accept',
780
			'accept-language',
781
			'content-language',
782
			'content-type',
783
			/* non-authorable headers in XHR, which are however requested by some UAs */
784
			'accept-encoding',
785
			'dnt',
786
			'origin',
787
			/* MediaWiki whitelist */
788
			'api-user-agent',
789
		] );
790
		foreach ( $requestedHeaders as $rHeader ) {
791
			$rHeader = strtolower( trim( $rHeader ) );
792
			if ( !isset( $allowedAuthorHeaders[$rHeader] ) ) {
793
				wfDebugLog( 'api', 'CORS preflight failed on requested header: ' . $rHeader );
794
				return false;
795
			}
796
		}
797
		return true;
798
	}
799
800
	/**
801
	 * Helper function to convert wildcard string into a regex
802
	 * '*' => '.*?'
803
	 * '?' => '.'
804
	 *
805
	 * @param string $wildcard String with wildcards
806
	 * @return string Regular expression
807
	 */
808
	protected static function wildcardToRegex( $wildcard ) {
809
		$wildcard = preg_quote( $wildcard, '/' );
810
		$wildcard = str_replace(
811
			[ '\*', '\?' ],
812
			[ '.*?', '.' ],
813
			$wildcard
814
		);
815
816
		return "/^https?:\/\/$wildcard$/";
817
	}
818
819
	/**
820
	 * Send caching headers
821
	 * @param bool $isError Whether an error response is being output
822
	 * @since 1.26 added $isError parameter
823
	 */
824
	protected function sendCacheHeaders( $isError ) {
825
		$response = $this->getRequest()->response();
826
		$out = $this->getOutput();
827
828
		$out->addVaryHeader( 'Treat-as-Untrusted' );
829
830
		$config = $this->getConfig();
831
832
		if ( $config->get( 'VaryOnXFP' ) ) {
833
			$out->addVaryHeader( 'X-Forwarded-Proto' );
834
		}
835
836
		if ( !$isError && $this->mModule &&
837
			( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
838
		) {
839
			$etag = $this->mModule->getConditionalRequestData( 'etag' );
840
			if ( $etag !== null ) {
841
				$response->header( "ETag: $etag" );
842
			}
843
			$lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
844
			if ( $lastMod !== null ) {
845
				$response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
846
			}
847
		}
848
849
		// The logic should be:
850
		// $this->mCacheControl['max-age'] is set?
851
		//    Use it, the module knows better than our guess.
852
		// !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
853
		//    Use 0 because we can guess caching is probably the wrong thing to do.
854
		// Use $this->getParameter( 'maxage' ), which already defaults to 0.
855
		$maxage = 0;
856
		if ( isset( $this->mCacheControl['max-age'] ) ) {
857
			$maxage = $this->mCacheControl['max-age'];
858
		} elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
859
			$this->mCacheMode !== 'private'
860
		) {
861
			$maxage = $this->getParameter( 'maxage' );
862
		}
863
		$privateCache = 'private, must-revalidate, max-age=' . $maxage;
864
865
		if ( $this->mCacheMode == 'private' ) {
866
			$response->header( "Cache-Control: $privateCache" );
867
			return;
868
		}
869
870
		$useKeyHeader = $config->get( 'UseKeyHeader' );
871
		if ( $this->mCacheMode == 'anon-public-user-private' ) {
872
			$out->addVaryHeader( 'Cookie' );
873
			$response->header( $out->getVaryHeader() );
874
			if ( $useKeyHeader ) {
875
				$response->header( $out->getKeyHeader() );
876
				if ( $out->haveCacheVaryCookies() ) {
877
					// Logged in, mark this request private
878
					$response->header( "Cache-Control: $privateCache" );
879
					return;
880
				}
881
				// Logged out, send normal public headers below
882
			} elseif ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
883
				// Logged in or otherwise has session (e.g. anonymous users who have edited)
884
				// Mark request private
885
				$response->header( "Cache-Control: $privateCache" );
886
887
				return;
888
			} // else no Key and anonymous, send public headers below
889
		}
890
891
		// Send public headers
892
		$response->header( $out->getVaryHeader() );
893
		if ( $useKeyHeader ) {
894
			$response->header( $out->getKeyHeader() );
895
		}
896
897
		// If nobody called setCacheMaxAge(), use the (s)maxage parameters
898
		if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
899
			$this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
900
		}
901
		if ( !isset( $this->mCacheControl['max-age'] ) ) {
902
			$this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
903
		}
904
905
		if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
906
			// Public cache not requested
907
			// Sending a Vary header in this case is harmless, and protects us
908
			// against conditional calls of setCacheMaxAge().
909
			$response->header( "Cache-Control: $privateCache" );
910
911
			return;
912
		}
913
914
		$this->mCacheControl['public'] = true;
915
916
		// Send an Expires header
917
		$maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
918
		$expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
919
		$response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
920
921
		// Construct the Cache-Control header
922
		$ccHeader = '';
923
		$separator = '';
924
		foreach ( $this->mCacheControl as $name => $value ) {
925
			if ( is_bool( $value ) ) {
926
				if ( $value ) {
927
					$ccHeader .= $separator . $name;
928
					$separator = ', ';
929
				}
930
			} else {
931
				$ccHeader .= $separator . "$name=$value";
932
				$separator = ', ';
933
			}
934
		}
935
936
		$response->header( "Cache-Control: $ccHeader" );
937
	}
938
939
	/**
940
	 * Create the printer for error output
941
	 */
942
	private function createErrorPrinter() {
943
		if ( !isset( $this->mPrinter ) ) {
944
			$value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
945
			if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
946
				$value = self::API_DEFAULT_FORMAT;
947
			}
948
			$this->mPrinter = $this->createPrinterByName( $value );
949
		}
950
951
		// Printer may not be able to handle errors. This is particularly
952
		// likely if the module returns something for getCustomPrinter().
953
		if ( !$this->mPrinter->canPrintErrors() ) {
954
			$this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
955
		}
956
	}
957
958
	/**
959
	 * Create an error message for the given exception.
960
	 *
961
	 * If the exception is a UsageException then
962
	 * UsageException::getMessageArray() will be called to create the message.
963
	 *
964
	 * @param Exception $e
965
	 * @return array ['code' => 'some string', 'info' => 'some other string']
966
	 * @since 1.27
967
	 */
968
	protected function errorMessageFromException( $e ) {
969
		if ( $e instanceof UsageException ) {
970
			// User entered incorrect parameters - generate error response
971
			$errMessage = $e->getMessageArray();
972
		} else {
973
			$config = $this->getConfig();
974
			// Something is seriously wrong
975
			if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) {
976
				$info = 'Database query error';
977
			} else {
978
				$info = "Exception Caught: {$e->getMessage()}";
979
			}
980
981
			$errMessage = [
982
				'code' => 'internal_api_error_' . get_class( $e ),
983
				'info' => '[' . WebRequest::getRequestId() . '] ' . $info,
984
			];
985
		}
986
		return $errMessage;
987
	}
988
989
	/**
990
	 * Replace the result data with the information about an exception.
991
	 * Returns the error code
992
	 * @param Exception $e
993
	 * @return string
994
	 */
995
	protected function substituteResultWithError( $e ) {
996
		$result = $this->getResult();
997
		$config = $this->getConfig();
998
999
		$errMessage = $this->errorMessageFromException( $e );
1000
		if ( $e instanceof UsageException ) {
1001
			// User entered incorrect parameters - generate error response
1002
			$link = wfExpandUrl( wfScript( 'api' ) );
1003
			ApiResult::setContentValue( $errMessage, 'docref', "See $link for API usage" );
1004
		} else {
1005
			// Something is seriously wrong
1006
			if ( $config->get( 'ShowExceptionDetails' ) ) {
1007
				ApiResult::setContentValue(
1008
					$errMessage,
1009
					'trace',
1010
					MWExceptionHandler::getRedactedTraceAsString( $e )
1011
				);
1012
			}
1013
		}
1014
1015
		// Remember all the warnings to re-add them later
1016
		$warnings = $result->getResultData( [ 'warnings' ] );
1017
1018
		$result->reset();
1019
		// Re-add the id
1020
		$requestid = $this->getParameter( 'requestid' );
1021
		if ( !is_null( $requestid ) ) {
1022
			$result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
1023
		}
1024
		if ( $config->get( 'ShowHostnames' ) ) {
1025
			// servedby is especially useful when debugging errors
1026
			$result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
1027
		}
1028
		if ( $warnings !== null ) {
1029
			$result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
1030
		}
1031
1032
		$result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK );
1033
1034
		return $errMessage['code'];
1035
	}
1036
1037
	/**
1038
	 * Set up for the execution.
1039
	 * @return array
1040
	 */
1041
	protected function setupExecuteAction() {
1042
		// First add the id to the top element
1043
		$result = $this->getResult();
1044
		$requestid = $this->getParameter( 'requestid' );
1045
		if ( !is_null( $requestid ) ) {
1046
			$result->addValue( null, 'requestid', $requestid );
1047
		}
1048
1049
		if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1050
			$servedby = $this->getParameter( 'servedby' );
1051
			if ( $servedby ) {
1052
				$result->addValue( null, 'servedby', wfHostname() );
1053
			}
1054
		}
1055
1056
		if ( $this->getParameter( 'curtimestamp' ) ) {
1057
			$result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ),
1058
				ApiResult::NO_SIZE_CHECK );
1059
		}
1060
1061
		$params = $this->extractRequestParams();
1062
1063
		$this->mAction = $params['action'];
1064
1065
		if ( !is_string( $this->mAction ) ) {
1066
			$this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
1067
		}
1068
1069
		return $params;
1070
	}
1071
1072
	/**
1073
	 * Set up the module for response
1074
	 * @return ApiBase The module that will handle this action
1075
	 * @throws MWException
1076
	 * @throws UsageException
1077
	 */
1078
	protected function setupModule() {
1079
		// Instantiate the module requested by the user
1080
		$module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
1081
		if ( $module === null ) {
1082
			$this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
1083
		}
1084
		$moduleParams = $module->extractRequestParams();
1085
1086
		// Check token, if necessary
1087
		if ( $module->needsToken() === true ) {
1088
			throw new MWException(
1089
				"Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1090
				'See documentation for ApiBase::needsToken for details.'
1091
			);
1092
		}
1093
		if ( $module->needsToken() ) {
1094
			if ( !$module->mustBePosted() ) {
1095
				throw new MWException(
1096
					"Module '{$module->getModuleName()}' must require POST to use tokens."
1097
				);
1098
			}
1099
1100
			if ( !isset( $moduleParams['token'] ) ) {
1101
				$this->dieUsageMsg( [ 'missingparam', 'token' ] );
1102
			}
1103
1104
			$module->requirePostedParameters( [ 'token' ] );
1105
1106
			if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
1107
				$this->dieUsageMsg( 'sessionfailure' );
1108
			}
1109
		}
1110
1111
		return $module;
1112
	}
1113
1114
	/**
1115
	 * Check the max lag if necessary
1116
	 * @param ApiBase $module Api module being used
1117
	 * @param array $params Array an array containing the request parameters.
1118
	 * @return bool True on success, false should exit immediately
1119
	 */
1120
	protected function checkMaxLag( $module, $params ) {
1121
		if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
1122
			$maxLag = $params['maxlag'];
1123
			list( $host, $lag ) = wfGetLB()->getMaxLag();
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
1124
			if ( $lag > $maxLag ) {
1125
				$response = $this->getRequest()->response();
1126
1127
				$response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
1128
				$response->header( 'X-Database-Lag: ' . intval( $lag ) );
1129
1130
				if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1131
					$this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
1132
				}
1133
1134
				$this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
1135
			}
1136
		}
1137
1138
		return true;
1139
	}
1140
1141
	/**
1142
	 * Check selected RFC 7232 precondition headers
1143
	 *
1144
	 * RFC 7232 envisions a particular model where you send your request to "a
1145
	 * resource", and for write requests that you can read "the resource" by
1146
	 * changing the method to GET. When the API receives a GET request, it
1147
	 * works out even though "the resource" from RFC 7232's perspective might
1148
	 * be many resources from MediaWiki's perspective. But it totally fails for
1149
	 * a POST, since what HTTP sees as "the resource" is probably just
1150
	 * "/api.php" with all the interesting bits in the body.
1151
	 *
1152
	 * Therefore, we only support RFC 7232 precondition headers for GET (and
1153
	 * HEAD). That means we don't need to bother with If-Match and
1154
	 * If-Unmodified-Since since they only apply to modification requests.
1155
	 *
1156
	 * And since we don't support Range, If-Range is ignored too.
1157
	 *
1158
	 * @since 1.26
1159
	 * @param ApiBase $module Api module being used
1160
	 * @return bool True on success, false should exit immediately
1161
	 */
1162
	protected function checkConditionalRequestHeaders( $module ) {
1163
		if ( $this->mInternalMode ) {
1164
			// No headers to check in internal mode
1165
			return true;
1166
		}
1167
1168
		if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
1169
			// Don't check POSTs
1170
			return true;
1171
		}
1172
1173
		$return304 = false;
1174
1175
		$ifNoneMatch = array_diff(
1176
			$this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1177
			[ '' ]
1178
		);
1179
		if ( $ifNoneMatch ) {
1180
			if ( $ifNoneMatch === [ '*' ] ) {
1181
				// API responses always "exist"
1182
				$etag = '*';
1183
			} else {
1184
				$etag = $module->getConditionalRequestData( 'etag' );
1185
			}
1186
		}
1187
		if ( $ifNoneMatch && $etag !== null ) {
1188
			$test = substr( $etag, 0, 2 ) === 'W/' ? substr( $etag, 2 ) : $etag;
1189
			$match = array_map( function ( $s ) {
1190
				return substr( $s, 0, 2 ) === 'W/' ? substr( $s, 2 ) : $s;
1191
			}, $ifNoneMatch );
1192
			$return304 = in_array( $test, $match, true );
1193
		} else {
1194
			$value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) );
1195
1196
			// Some old browsers sends sizes after the date, like this:
1197
			//  Wed, 20 Aug 2003 06:51:19 GMT; length=5202
1198
			// Ignore that.
1199
			$i = strpos( $value, ';' );
1200
			if ( $i !== false ) {
1201
				$value = trim( substr( $value, 0, $i ) );
1202
			}
1203
1204
			if ( $value !== '' ) {
1205
				try {
1206
					$ts = new MWTimestamp( $value );
1207
					if (
1208
						// RFC 7231 IMF-fixdate
1209
						$ts->getTimestamp( TS_RFC2822 ) === $value ||
1210
						// RFC 850
1211
						$ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value ||
1212
						// asctime (with and without space-padded day)
1213
						$ts->format( 'D M j H:i:s Y' ) === $value ||
1214
						$ts->format( 'D M  j H:i:s Y' ) === $value
1215
					) {
1216
						$lastMod = $module->getConditionalRequestData( 'last-modified' );
1217
						if ( $lastMod !== null ) {
1218
							// Mix in some MediaWiki modification times
1219
							$modifiedTimes = [
1220
								'page' => $lastMod,
1221
								'user' => $this->getUser()->getTouched(),
1222
								'epoch' => $this->getConfig()->get( 'CacheEpoch' ),
1223
							];
1224
							if ( $this->getConfig()->get( 'UseSquid' ) ) {
1225
								// T46570: the core page itself may not change, but resources might
1226
								$modifiedTimes['sepoch'] = wfTimestamp(
1227
									TS_MW, time() - $this->getConfig()->get( 'SquidMaxage' )
1228
								);
1229
							}
1230
							Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this->getOutput() ] );
1231
							$lastMod = max( $modifiedTimes );
1232
							$return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
1233
						}
1234
					}
1235
				} catch ( TimestampException $e ) {
1236
					// Invalid timestamp, ignore it
1237
				}
1238
			}
1239
		}
1240
1241
		if ( $return304 ) {
1242
			$this->getRequest()->response()->statusHeader( 304 );
1243
1244
			// Avoid outputting the compressed representation of a zero-length body
1245
			MediaWiki\suppressWarnings();
1246
			ini_set( 'zlib.output_compression', 0 );
1247
			MediaWiki\restoreWarnings();
1248
			wfClearOutputBuffers();
1249
1250
			return false;
1251
		}
1252
1253
		return true;
1254
	}
1255
1256
	/**
1257
	 * Check for sufficient permissions to execute
1258
	 * @param ApiBase $module An Api module
1259
	 */
1260
	protected function checkExecutePermissions( $module ) {
1261
		$user = $this->getUser();
1262
		if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
1263
			!$user->isAllowed( 'read' )
1264
		) {
1265
			$this->dieUsageMsg( 'readrequired' );
1266
		}
1267
1268
		if ( $module->isWriteMode() ) {
1269
			if ( !$this->mEnableWrite ) {
1270
				$this->dieUsageMsg( 'writedisabled' );
1271
			} elseif ( !$user->isAllowed( 'writeapi' ) ) {
1272
				$this->dieUsageMsg( 'writerequired' );
1273
			} elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
1274
				$this->dieUsage(
1275
					'Promise-Non-Write-API-Action HTTP header cannot be sent to write API modules',
1276
					'promised-nonwrite-api'
1277
				);
1278
			}
1279
1280
			$this->checkReadOnly( $module );
1281
		}
1282
1283
		// Allow extensions to stop execution for arbitrary reasons.
1284
		$message = false;
1285
		if ( !Hooks::run( 'ApiCheckCanExecute', [ $module, $user, &$message ] ) ) {
1286
			$this->dieUsageMsg( $message );
1287
		}
1288
	}
1289
1290
	/**
1291
	 * Check if the DB is read-only for this user
1292
	 * @param ApiBase $module An Api module
1293
	 */
1294
	protected function checkReadOnly( $module ) {
1295
		if ( wfReadOnly() ) {
1296
			$this->dieReadOnly();
1297
		}
1298
1299
		if ( $module->isWriteMode()
1300
			&& $this->getUser()->isBot()
1301
			&& wfGetLB()->getServerCount() > 1
1302
		) {
1303
			$this->checkBotReadOnly();
1304
		}
1305
	}
1306
1307
	/**
1308
	 * Check whether we are readonly for bots
1309
	 */
1310
	private function checkBotReadOnly() {
1311
		// Figure out how many servers have passed the lag threshold
1312
		$numLagged = 0;
1313
		$lagLimit = $this->getConfig()->get( 'APIMaxLagThreshold' );
1314
		$laggedServers = [];
1315
		$loadBalancer = wfGetLB();
1316
		foreach ( $loadBalancer->getLagTimes() as $serverIndex => $lag ) {
1317
			if ( $lag > $lagLimit ) {
1318
				++$numLagged;
1319
				$laggedServers[] = $loadBalancer->getServerName( $serverIndex ) . " ({$lag}s)";
1320
			}
1321
		}
1322
1323
		// If a majority of replica DBs are too lagged then disallow writes
1324
		$replicaCount = wfGetLB()->getServerCount() - 1;
1325
		if ( $numLagged >= ceil( $replicaCount / 2 ) ) {
1326
			$laggedServers = implode( ', ', $laggedServers );
1327
			wfDebugLog(
1328
				'api-readonly',
1329
				"Api request failed as read only because the following DBs are lagged: $laggedServers"
1330
			);
1331
1332
			$parsed = $this->parseMsg( [ 'readonlytext' ] );
1333
			$this->dieUsage(
1334
				$parsed['info'],
1335
				$parsed['code'],
1336
				/* http error */
1337
				0,
1338
				[ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
1339
			);
1340
		}
1341
	}
1342
1343
	/**
1344
	 * Check asserts of the user's rights
1345
	 * @param array $params
1346
	 */
1347
	protected function checkAsserts( $params ) {
1348
		if ( isset( $params['assert'] ) ) {
1349
			$user = $this->getUser();
1350
			switch ( $params['assert'] ) {
1351
				case 'user':
1352
					if ( $user->isAnon() ) {
1353
						$this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' );
1354
					}
1355
					break;
1356
				case 'bot':
1357
					if ( !$user->isAllowed( 'bot' ) ) {
1358
						$this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' );
1359
					}
1360
					break;
1361
			}
1362
		}
1363
		if ( isset( $params['assertuser'] ) ) {
1364
			$assertUser = User::newFromName( $params['assertuser'], false );
1365
			if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
1366
				$this->dieUsage(
1367
					'Assertion that the user is "' . $params['assertuser'] . '" failed',
1368
					'assertnameduserfailed'
1369
				);
1370
			}
1371
		}
1372
	}
1373
1374
	/**
1375
	 * Check POST for external response and setup result printer
1376
	 * @param ApiBase $module An Api module
1377
	 * @param array $params An array with the request parameters
1378
	 */
1379
	protected function setupExternalResponse( $module, $params ) {
1380
		$request = $this->getRequest();
1381
		if ( !$request->wasPosted() && $module->mustBePosted() ) {
1382
			// Module requires POST. GET request might still be allowed
1383
			// if $wgDebugApi is true, otherwise fail.
1384
			$this->dieUsageMsgOrDebug( [ 'mustbeposted', $this->mAction ] );
1385
		}
1386
1387
		// See if custom printer is used
1388
		$this->mPrinter = $module->getCustomPrinter();
1389
		if ( is_null( $this->mPrinter ) ) {
1390
			// Create an appropriate printer
1391
			$this->mPrinter = $this->createPrinterByName( $params['format'] );
1392
		}
1393
1394
		if ( $request->getProtocol() === 'http' && (
1395
			$request->getSession()->shouldForceHTTPS() ||
1396
			( $this->getUser()->isLoggedIn() &&
1397
				$this->getUser()->requiresHTTPS() )
1398
		) ) {
1399
			$this->logFeatureUsage( 'https-expected' );
1400
			$this->setWarning( 'HTTP used when HTTPS was expected' );
1401
		}
1402
	}
1403
1404
	/**
1405
	 * Execute the actual module, without any error handling
1406
	 */
1407
	protected function executeAction() {
1408
		$params = $this->setupExecuteAction();
1409
		$module = $this->setupModule();
1410
		$this->mModule = $module;
1411
1412
		if ( !$this->mInternalMode ) {
1413
			$this->setRequestExpectations( $module );
1414
		}
1415
1416
		$this->checkExecutePermissions( $module );
1417
1418
		if ( !$this->checkMaxLag( $module, $params ) ) {
1419
			return;
1420
		}
1421
1422
		if ( !$this->checkConditionalRequestHeaders( $module ) ) {
1423
			return;
1424
		}
1425
1426
		if ( !$this->mInternalMode ) {
1427
			$this->setupExternalResponse( $module, $params );
1428
		}
1429
1430
		$this->checkAsserts( $params );
1431
1432
		// Execute
1433
		$module->execute();
1434
		Hooks::run( 'APIAfterExecute', [ &$module ] );
1435
1436
		$this->reportUnusedParams();
1437
1438
		if ( !$this->mInternalMode ) {
1439
			// append Debug information
1440
			MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
1441
1442
			// Print result data
1443
			$this->printResult();
1444
		}
1445
	}
1446
1447
	/**
1448
	 * Set database connection, query, and write expectations given this module request
1449
	 * @param ApiBase $module
1450
	 */
1451
	protected function setRequestExpectations( ApiBase $module ) {
1452
		$limits = $this->getConfig()->get( 'TrxProfilerLimits' );
1453
		$trxProfiler = Profiler::instance()->getTransactionProfiler();
1454
		$trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
1455
		if ( $this->getRequest()->hasSafeMethod() ) {
1456
			$trxProfiler->setExpectations( $limits['GET'], __METHOD__ );
1457
		} elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) {
1458
			$trxProfiler->setExpectations( $limits['POST-nonwrite'], __METHOD__ );
1459
			$this->getRequest()->markAsSafeRequest();
1460
		} else {
1461
			$trxProfiler->setExpectations( $limits['POST'], __METHOD__ );
1462
		}
1463
	}
1464
1465
	/**
1466
	 * Log the preceding request
1467
	 * @param float $time Time in seconds
1468
	 * @param Exception $e Exception caught while processing the request
1469
	 */
1470
	protected function logRequest( $time, $e = null ) {
1471
		$request = $this->getRequest();
1472
		$logCtx = [
1473
			'ts' => time(),
1474
			'ip' => $request->getIP(),
1475
			'userAgent' => $this->getUserAgent(),
1476
			'wiki' => wfWikiID(),
1477
			'timeSpentBackend' => (int)round( $time * 1000 ),
1478
			'hadError' => $e !== null,
1479
			'errorCodes' => [],
1480
			'params' => [],
1481
		];
1482
1483
		if ( $e ) {
1484
			$logCtx['errorCodes'][] = $this->errorMessageFromException( $e )['code'];
1485
		}
1486
1487
		// Construct space separated message for 'api' log channel
1488
		$msg = "API {$request->getMethod()} " .
1489
			wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
1490
			" {$logCtx['ip']} " .
1491
			"T={$logCtx['timeSpentBackend']}ms";
1492
1493
		foreach ( $this->getParamsUsed() as $name ) {
1494
			$value = $request->getVal( $name );
1495
			if ( $value === null ) {
1496
				continue;
1497
			}
1498
1499
			if ( strlen( $value ) > 256 ) {
1500
				$value = substr( $value, 0, 256 );
1501
				$encValue = $this->encodeRequestLogValue( $value ) . '[...]';
1502
			} else {
1503
				$encValue = $this->encodeRequestLogValue( $value );
1504
			}
1505
1506
			$logCtx['params'][$name] = $value;
1507
			$msg .= " {$name}={$encValue}";
1508
		}
1509
1510
		wfDebugLog( 'api', $msg, 'private' );
1511
		// ApiAction channel is for structured data consumers
1512
		wfDebugLog( 'ApiAction', '', 'private', $logCtx );
1513
	}
1514
1515
	/**
1516
	 * Encode a value in a format suitable for a space-separated log line.
1517
	 * @param string $s
1518
	 * @return string
1519
	 */
1520
	protected function encodeRequestLogValue( $s ) {
1521
		static $table;
1522
		if ( !$table ) {
1523
			$chars = ';@$!*(),/:';
1524
			$numChars = strlen( $chars );
1525
			for ( $i = 0; $i < $numChars; $i++ ) {
1526
				$table[rawurlencode( $chars[$i] )] = $chars[$i];
1527
			}
1528
		}
1529
1530
		return strtr( rawurlencode( $s ), $table );
1531
	}
1532
1533
	/**
1534
	 * Get the request parameters used in the course of the preceding execute() request
1535
	 * @return array
1536
	 */
1537
	protected function getParamsUsed() {
1538
		return array_keys( $this->mParamsUsed );
1539
	}
1540
1541
	/**
1542
	 * Mark parameters as used
1543
	 * @param string|string[] $params
1544
	 */
1545
	public function markParamsUsed( $params ) {
1546
		$this->mParamsUsed += array_fill_keys( (array)$params, true );
1547
	}
1548
1549
	/**
1550
	 * Get a request value, and register the fact that it was used, for logging.
1551
	 * @param string $name
1552
	 * @param mixed $default
1553
	 * @return mixed
1554
	 */
1555
	public function getVal( $name, $default = null ) {
1556
		$this->mParamsUsed[$name] = true;
1557
1558
		$ret = $this->getRequest()->getVal( $name );
1559
		if ( $ret === null ) {
1560
			if ( $this->getRequest()->getArray( $name ) !== null ) {
1561
				// See bug 10262 for why we don't just implode( '|', ... ) the
1562
				// array.
1563
				$this->setWarning(
1564
					"Parameter '$name' uses unsupported PHP array syntax"
1565
				);
1566
			}
1567
			$ret = $default;
1568
		}
1569
		return $ret;
1570
	}
1571
1572
	/**
1573
	 * Get a boolean request value, and register the fact that the parameter
1574
	 * was used, for logging.
1575
	 * @param string $name
1576
	 * @return bool
1577
	 */
1578
	public function getCheck( $name ) {
1579
		return $this->getVal( $name, null ) !== null;
1580
	}
1581
1582
	/**
1583
	 * Get a request upload, and register the fact that it was used, for logging.
1584
	 *
1585
	 * @since 1.21
1586
	 * @param string $name Parameter name
1587
	 * @return WebRequestUpload
1588
	 */
1589
	public function getUpload( $name ) {
1590
		$this->mParamsUsed[$name] = true;
1591
1592
		return $this->getRequest()->getUpload( $name );
1593
	}
1594
1595
	/**
1596
	 * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,
1597
	 * for example in case of spelling mistakes or a missing 'g' prefix for generators.
1598
	 */
1599
	protected function reportUnusedParams() {
1600
		$paramsUsed = $this->getParamsUsed();
1601
		$allParams = $this->getRequest()->getValueNames();
1602
1603
		if ( !$this->mInternalMode ) {
1604
			// Printer has not yet executed; don't warn that its parameters are unused
1605
			$printerParams = array_map(
1606
				[ $this->mPrinter, 'encodeParamName' ],
1607
				array_keys( $this->mPrinter->getFinalParams() ?: [] )
1608
			);
1609
			$unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1610
		} else {
1611
			$unusedParams = array_diff( $allParams, $paramsUsed );
1612
		}
1613
1614
		if ( count( $unusedParams ) ) {
1615
			$s = count( $unusedParams ) > 1 ? 's' : '';
1616
			$this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
1617
		}
1618
	}
1619
1620
	/**
1621
	 * Print results using the current printer
1622
	 *
1623
	 * @param int $httpCode HTTP status code, or 0 to not change
1624
	 */
1625
	protected function printResult( $httpCode = 0 ) {
1626
		if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
1627
			$this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
1628
		}
1629
1630
		$printer = $this->mPrinter;
1631
		$printer->initPrinter( false );
1632
		if ( $httpCode ) {
1633
			$printer->setHttpStatus( $httpCode );
1634
		}
1635
		$printer->execute();
1636
		$printer->closePrinter();
1637
	}
1638
1639
	/**
1640
	 * @return bool
1641
	 */
1642
	public function isReadMode() {
1643
		return false;
1644
	}
1645
1646
	/**
1647
	 * See ApiBase for description.
1648
	 *
1649
	 * @return array
1650
	 */
1651
	public function getAllowedParams() {
1652
		return [
1653
			'action' => [
1654
				ApiBase::PARAM_DFLT => 'help',
1655
				ApiBase::PARAM_TYPE => 'submodule',
1656
			],
1657
			'format' => [
1658
				ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT,
1659
				ApiBase::PARAM_TYPE => 'submodule',
1660
			],
1661
			'maxlag' => [
1662
				ApiBase::PARAM_TYPE => 'integer'
1663
			],
1664
			'smaxage' => [
1665
				ApiBase::PARAM_TYPE => 'integer',
1666
				ApiBase::PARAM_DFLT => 0
1667
			],
1668
			'maxage' => [
1669
				ApiBase::PARAM_TYPE => 'integer',
1670
				ApiBase::PARAM_DFLT => 0
1671
			],
1672
			'assert' => [
1673
				ApiBase::PARAM_TYPE => [ 'user', 'bot' ]
1674
			],
1675
			'assertuser' => [
1676
				ApiBase::PARAM_TYPE => 'user',
1677
			],
1678
			'requestid' => null,
1679
			'servedby' => false,
1680
			'curtimestamp' => false,
1681
			'origin' => null,
1682
			'uselang' => [
1683
				ApiBase::PARAM_DFLT => 'user',
1684
			],
1685
		];
1686
	}
1687
1688
	/** @see ApiBase::getExamplesMessages() */
1689
	protected function getExamplesMessages() {
1690
		return [
1691
			'action=help'
1692
				=> 'apihelp-help-example-main',
1693
			'action=help&recursivesubmodules=1'
1694
				=> 'apihelp-help-example-recursive',
1695
		];
1696
	}
1697
1698
	public function modifyHelp( array &$help, array $options, array &$tocData ) {
1699
		// Wish PHP had an "array_insert_before". Instead, we have to manually
1700
		// reindex the array to get 'permissions' in the right place.
1701
		$oldHelp = $help;
1702
		$help = [];
1703
		foreach ( $oldHelp as $k => $v ) {
1704
			if ( $k === 'submodules' ) {
1705
				$help['permissions'] = '';
1706
			}
1707
			$help[$k] = $v;
1708
		}
1709
		$help['datatypes'] = '';
1710
		$help['credits'] = '';
1711
1712
		// Fill 'permissions'
1713
		$help['permissions'] .= Html::openElement( 'div',
1714
			[ 'class' => 'apihelp-block apihelp-permissions' ] );
1715
		$m = $this->msg( 'api-help-permissions' );
1716
		if ( !$m->isDisabled() ) {
1717
			$help['permissions'] .= Html::rawElement( 'div', [ 'class' => 'apihelp-block-head' ],
1718
				$m->numParams( count( self::$mRights ) )->parse()
1719
			);
1720
		}
1721
		$help['permissions'] .= Html::openElement( 'dl' );
1722
		foreach ( self::$mRights as $right => $rightMsg ) {
1723
			$help['permissions'] .= Html::element( 'dt', null, $right );
1724
1725
			$rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
1726
			$help['permissions'] .= Html::rawElement( 'dd', null, $rightMsg );
1727
1728
			$groups = array_map( function ( $group ) {
1729
				return $group == '*' ? 'all' : $group;
1730
			}, User::getGroupsWithPermission( $right ) );
1731
1732
			$help['permissions'] .= Html::rawElement( 'dd', null,
1733
				$this->msg( 'api-help-permissions-granted-to' )
1734
					->numParams( count( $groups ) )
1735
					->params( $this->getLanguage()->commaList( $groups ) )
1736
					->parse()
1737
			);
1738
		}
1739
		$help['permissions'] .= Html::closeElement( 'dl' );
1740
		$help['permissions'] .= Html::closeElement( 'div' );
1741
1742
		// Fill 'datatypes' and 'credits', if applicable
1743
		if ( empty( $options['nolead'] ) ) {
1744
			$level = $options['headerlevel'];
1745
			$tocnumber = &$options['tocnumber'];
1746
1747
			$header = $this->msg( 'api-help-datatypes-header' )->parse();
1748
1749
			// Add an additional span with sanitized ID
1750 View Code Duplication
			if ( !$this->getConfig()->get( 'ExperimentalHtmlIds' ) ) {
1751
				$header = Html::element( 'span', [ 'id' => Sanitizer::escapeId( 'main/datatypes' ) ] ) .
1752
					$header;
1753
			}
1754
			$help['datatypes'] .= Html::rawElement( 'h' . min( 6, $level ),
1755
				[ 'id' => 'main/datatypes', 'class' => 'apihelp-header' ],
1756
				$header
1757
			);
1758
			$help['datatypes'] .= $this->msg( 'api-help-datatypes' )->parseAsBlock();
1759 View Code Duplication
			if ( !isset( $tocData['main/datatypes'] ) ) {
1760
				$tocnumber[$level]++;
1761
				$tocData['main/datatypes'] = [
1762
					'toclevel' => count( $tocnumber ),
1763
					'level' => $level,
1764
					'anchor' => 'main/datatypes',
1765
					'line' => $header,
1766
					'number' => implode( '.', $tocnumber ),
1767
					'index' => false,
1768
				];
1769
			}
1770
1771
			// Add an additional span with sanitized ID
1772 View Code Duplication
			if ( !$this->getConfig()->get( 'ExperimentalHtmlIds' ) ) {
1773
				$header = Html::element( 'span', [ 'id' => Sanitizer::escapeId( 'main/credits' ) ] ) .
0 ignored issues
show
$header is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1774
					$header;
1775
			}
1776
			$header = $this->msg( 'api-credits-header' )->parse();
1777
			$help['credits'] .= Html::rawElement( 'h' . min( 6, $level ),
1778
				[ 'id' => 'main/credits', 'class' => 'apihelp-header' ],
1779
				$header
1780
			);
1781
			$help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
1782 View Code Duplication
			if ( !isset( $tocData['main/credits'] ) ) {
1783
				$tocnumber[$level]++;
1784
				$tocData['main/credits'] = [
1785
					'toclevel' => count( $tocnumber ),
1786
					'level' => $level,
1787
					'anchor' => 'main/credits',
1788
					'line' => $header,
1789
					'number' => implode( '.', $tocnumber ),
1790
					'index' => false,
1791
				];
1792
			}
1793
		}
1794
	}
1795
1796
	private $mCanApiHighLimits = null;
1797
1798
	/**
1799
	 * Check whether the current user is allowed to use high limits
1800
	 * @return bool
1801
	 */
1802
	public function canApiHighLimits() {
1803
		if ( !isset( $this->mCanApiHighLimits ) ) {
1804
			$this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' );
1805
		}
1806
1807
		return $this->mCanApiHighLimits;
1808
	}
1809
1810
	/**
1811
	 * Overrides to return this instance's module manager.
1812
	 * @return ApiModuleManager
1813
	 */
1814
	public function getModuleManager() {
1815
		return $this->mModuleMgr;
1816
	}
1817
1818
	/**
1819
	 * Fetches the user agent used for this request
1820
	 *
1821
	 * The value will be the combination of the 'Api-User-Agent' header (if
1822
	 * any) and the standard User-Agent header (if any).
1823
	 *
1824
	 * @return string
1825
	 */
1826
	public function getUserAgent() {
1827
		return trim(
1828
			$this->getRequest()->getHeader( 'Api-user-agent' ) . ' ' .
1829
			$this->getRequest()->getHeader( 'User-agent' )
1830
		);
1831
	}
1832
}
1833
1834
/**
1835
 * This exception will be thrown when dieUsage is called to stop module execution.
1836
 *
1837
 * @ingroup API
1838
 */
1839
class UsageException extends MWException {
1840
1841
	private $mCodestr;
1842
1843
	/**
1844
	 * @var null|array
1845
	 */
1846
	private $mExtraData;
1847
1848
	/**
1849
	 * @param string $message
1850
	 * @param string $codestr
1851
	 * @param int $code
1852
	 * @param array|null $extradata
1853
	 */
1854
	public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
1855
		parent::__construct( $message, $code );
1856
		$this->mCodestr = $codestr;
1857
		$this->mExtraData = $extradata;
1858
1859
		// This should never happen, so throw an exception about it that will
1860
		// hopefully get logged with a backtrace (T138585)
1861
		if ( !is_string( $codestr ) || $codestr === '' ) {
1862
			throw new InvalidArgumentException( 'Invalid $codestr, was ' .
1863
				( $codestr === '' ? 'empty string' : gettype( $codestr ) )
1864
			);
1865
		}
1866
	}
1867
1868
	/**
1869
	 * @return string
1870
	 */
1871
	public function getCodeString() {
1872
		return $this->mCodestr;
1873
	}
1874
1875
	/**
1876
	 * @return array
1877
	 */
1878
	public function getMessageArray() {
1879
		$result = [
1880
			'code' => $this->mCodestr,
1881
			'info' => $this->getMessage()
1882
		];
1883
		if ( is_array( $this->mExtraData ) ) {
1884
			$result = array_merge( $result, $this->mExtraData );
1885
		}
1886
1887
		return $result;
1888
	}
1889
1890
	/**
1891
	 * @return string
1892
	 */
1893
	public function __toString() {
1894
		return "{$this->getCodeString()}: {$this->getMessage()}";
1895
	}
1896
}
1897
1898
/**
1899
 * For really cool vim folding this needs to be at the end:
1900
 * vim: foldmarker=@{,@} foldmethod=marker
1901
 */
1902