Completed
Branch master (13ece3)
by
unknown
22:07
created

ApiMain::lacksSameOriginSecurity()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 4
eloc 12
c 1
b 0
f 1
nc 4
nop 0
dl 0
loc 24
rs 8.6845
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
/**
29
 * This is the main API class, used for both external and internal processing.
30
 * When executed, it will create the requested formatter object,
31
 * instantiate and execute an object associated with the needed action,
32
 * and use formatter to print results.
33
 * In case of an exception, an error message will be printed using the same formatter.
34
 *
35
 * To use API from another application, run it using FauxRequest object, in which
36
 * case any internal exceptions will not be handled but passed up to the caller.
37
 * After successful execution, use getResult() for the resulting data.
38
 *
39
 * @ingroup API
40
 */
41
class ApiMain extends ApiBase {
42
	/**
43
	 * When no format parameter is given, this format will be used
44
	 */
45
	const API_DEFAULT_FORMAT = 'jsonfm';
46
47
	/**
48
	 * List of available modules: action name => module class
49
	 */
50
	private static $Modules = [
51
		'login' => 'ApiLogin',
52
		'clientlogin' => 'ApiClientLogin',
53
		'logout' => 'ApiLogout',
54
		'createaccount' => 'ApiAMCreateAccount',
55
		'linkaccount' => 'ApiLinkAccount',
56
		'unlinkaccount' => 'ApiRemoveAuthenticationData',
57
		'changeauthenticationdata' => 'ApiChangeAuthenticationData',
58
		'removeauthenticationdata' => 'ApiRemoveAuthenticationData',
59
		'resetpassword' => 'ApiResetPassword',
60
		'query' => 'ApiQuery',
61
		'expandtemplates' => 'ApiExpandTemplates',
62
		'parse' => 'ApiParse',
63
		'stashedit' => 'ApiStashEdit',
64
		'opensearch' => 'ApiOpenSearch',
65
		'feedcontributions' => 'ApiFeedContributions',
66
		'feedrecentchanges' => 'ApiFeedRecentChanges',
67
		'feedwatchlist' => 'ApiFeedWatchlist',
68
		'help' => 'ApiHelp',
69
		'paraminfo' => 'ApiParamInfo',
70
		'rsd' => 'ApiRsd',
71
		'compare' => 'ApiComparePages',
72
		'tokens' => 'ApiTokens',
73
		'checktoken' => 'ApiCheckToken',
74
75
		// Write modules
76
		'purge' => 'ApiPurge',
77
		'setnotificationtimestamp' => 'ApiSetNotificationTimestamp',
78
		'rollback' => 'ApiRollback',
79
		'delete' => 'ApiDelete',
80
		'undelete' => 'ApiUndelete',
81
		'protect' => 'ApiProtect',
82
		'block' => 'ApiBlock',
83
		'unblock' => 'ApiUnblock',
84
		'move' => 'ApiMove',
85
		'edit' => 'ApiEditPage',
86
		'upload' => 'ApiUpload',
87
		'filerevert' => 'ApiFileRevert',
88
		'emailuser' => 'ApiEmailUser',
89
		'watch' => 'ApiWatch',
90
		'patrol' => 'ApiPatrol',
91
		'import' => 'ApiImport',
92
		'clearhasmsg' => 'ApiClearHasMsg',
93
		'userrights' => 'ApiUserrights',
94
		'options' => 'ApiOptions',
95
		'imagerotate' => 'ApiImageRotate',
96
		'revisiondelete' => 'ApiRevisionDelete',
97
		'managetags' => 'ApiManageTags',
98
		'tag' => 'ApiTag',
99
		'mergehistory' => 'ApiMergeHistory',
100
	];
101
102
	/**
103
	 * List of available formats: format name => format class
104
	 */
105
	private static $Formats = [
106
		'json' => 'ApiFormatJson',
107
		'jsonfm' => 'ApiFormatJson',
108
		'php' => 'ApiFormatPhp',
109
		'phpfm' => 'ApiFormatPhp',
110
		'xml' => 'ApiFormatXml',
111
		'xmlfm' => 'ApiFormatXml',
112
		'rawfm' => 'ApiFormatJson',
113
		'none' => 'ApiFormatNone',
114
	];
115
116
	// @codingStandardsIgnoreStart String contenation on "msg" not allowed to break long line
117
	/**
118
	 * List of user roles that are specifically relevant to the API.
119
	 * array( 'right' => array ( 'msg'    => 'Some message with a $1',
120
	 *                           'params' => array ( $someVarToSubst ) ),
121
	 *                          );
122
	 */
123
	private static $mRights = [
124
		'writeapi' => [
125
			'msg' => 'right-writeapi',
126
			'params' => []
127
		],
128
		'apihighlimits' => [
129
			'msg' => 'api-help-right-apihighlimits',
130
			'params' => [ ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ]
131
		]
132
	];
133
	// @codingStandardsIgnoreEnd
134
135
	/**
136
	 * @var ApiFormatBase
137
	 */
138
	private $mPrinter;
139
140
	private $mModuleMgr, $mResult, $mErrorFormatter, $mContinuationManager;
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...
141
	private $mAction;
142
	private $mEnableWrite;
143
	private $mInternalMode, $mSquidMaxage;
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...
144
	/** @var ApiBase */
145
	private $mModule;
146
147
	private $mCacheMode = 'private';
148
	private $mCacheControl = [];
149
	private $mParamsUsed = [];
150
151
	/** @var bool|null Cached return value from self::lacksSameOriginSecurity() */
152
	private $lacksSameOriginSecurity = null;
153
154
	/**
155
	 * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
156
	 *
157
	 * @param IContextSource|WebRequest $context If this is an instance of
158
	 *    FauxRequest, errors are thrown and no printing occurs
159
	 * @param bool $enableWrite Should be set to true if the api may modify data
160
	 */
161
	public function __construct( $context = null, $enableWrite = false ) {
162
		if ( $context === null ) {
163
			$context = RequestContext::getMain();
164
		} elseif ( $context instanceof WebRequest ) {
165
			// BC for pre-1.19
166
			$request = $context;
167
			$context = RequestContext::getMain();
168
		}
169
		// We set a derivative context so we can change stuff later
170
		$this->setContext( new DerivativeContext( $context ) );
171
172
		if ( isset( $request ) ) {
173
			$this->getContext()->setRequest( $request );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setRequest() does only exist in the following implementations of said interface: DerivativeContext, RequestContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
174
		}
175
176
		$this->mInternalMode = ( $this->getRequest() instanceof FauxRequest );
177
178
		// Special handling for the main module: $parent === $this
179
		parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
180
181
		if ( !$this->mInternalMode ) {
182
			// Impose module restrictions.
183
			// If the current user cannot read,
184
			// Remove all modules other than login
185
			global $wgUser;
186
187
			if ( $this->lacksSameOriginSecurity() ) {
188
				// If we're in a mode that breaks the same-origin policy, strip
189
				// user credentials for security.
190
				wfDebug( "API: stripping user credentials when the same-origin policy is not applied\n" );
191
				$wgUser = new User();
192
				$this->getContext()->setUser( $wgUser );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setUser() does only exist in the following implementations of said interface: DerivativeContext, RequestContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
193
			}
194
		}
195
196
		$uselang = $this->getParameter( 'uselang' );
197
		if ( $uselang === 'user' ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
198
			// Assume the parent context is going to return the user language
199
			// for uselang=user (see T85635).
200
		} else {
201
			if ( $uselang === 'content' ) {
202
				global $wgContLang;
203
				$uselang = $wgContLang->getCode();
204
			}
205
			$code = RequestContext::sanitizeLangCode( $uselang );
206
			$this->getContext()->setLanguage( $code );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setLanguage() does only exist in the following implementations of said interface: DerivativeContext, RequestContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
207
			if ( !$this->mInternalMode ) {
208
				global $wgLang;
209
				$wgLang = $this->getContext()->getLanguage();
210
				RequestContext::getMain()->setLanguage( $wgLang );
211
			}
212
		}
213
214
		$config = $this->getConfig();
215
		$this->mModuleMgr = new ApiModuleManager( $this );
216
		$this->mModuleMgr->addModules( self::$Modules, 'action' );
217
		$this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
218
		$this->mModuleMgr->addModules( self::$Formats, 'format' );
219
		$this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
220
221
		Hooks::run( 'ApiMain::moduleManager', [ $this->mModuleMgr ] );
222
223
		$this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
224
		$this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
225
		$this->mResult->setErrorFormatter( $this->mErrorFormatter );
226
		$this->mResult->setMainForContinuation( $this );
0 ignored issues
show
Deprecated Code introduced by
The method ApiResult::setMainForContinuation() has been deprecated with message: for backwards compatibility only, do not use

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
227
		$this->mContinuationManager = null;
228
		$this->mEnableWrite = $enableWrite;
229
230
		$this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling()
231
		$this->mCommit = false;
0 ignored issues
show
Bug introduced by
The property mCommit does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
232
	}
233
234
	/**
235
	 * Return true if the API was started by other PHP code using FauxRequest
236
	 * @return bool
237
	 */
238
	public function isInternalMode() {
239
		return $this->mInternalMode;
240
	}
241
242
	/**
243
	 * Get the ApiResult object associated with current request
244
	 *
245
	 * @return ApiResult
246
	 */
247
	public function getResult() {
248
		return $this->mResult;
249
	}
250
251
	/**
252
	 * Get the security flag for the current request
253
	 * @return bool
254
	 */
255
	public function lacksSameOriginSecurity() {
256
		if ( $this->lacksSameOriginSecurity !== null ) {
257
			return $this->lacksSameOriginSecurity;
258
		}
259
260
		$request = $this->getRequest();
261
262
		// JSONP mode
263
		if ( $request->getVal( 'callback' ) !== null ) {
264
			$this->lacksSameOriginSecurity = true;
265
			return true;
266
		}
267
268
		// Header to be used from XMLHTTPRequest when the request might
269
		// otherwise be used for XSS.
270
		if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) {
271
			$this->lacksSameOriginSecurity = true;
272
			return true;
273
		}
274
275
		// Allow extensions to override.
276
		$this->lacksSameOriginSecurity = !Hooks::run( 'RequestHasSameOriginSecurity', array( $request ) );
277
		return $this->lacksSameOriginSecurity;
278
	}
279
280
281
	/**
282
	 * Get the ApiErrorFormatter object associated with current request
283
	 * @return ApiErrorFormatter
284
	 */
285
	public function getErrorFormatter() {
286
		return $this->mErrorFormatter;
287
	}
288
289
	/**
290
	 * Get the continuation manager
291
	 * @return ApiContinuationManager|null
292
	 */
293
	public function getContinuationManager() {
294
		return $this->mContinuationManager;
295
	}
296
297
	/**
298
	 * Set the continuation manager
299
	 * @param ApiContinuationManager|null
300
	 */
301
	public function setContinuationManager( $manager ) {
302
		if ( $manager !== null ) {
303
			if ( !$manager instanceof ApiContinuationManager ) {
304
				throw new InvalidArgumentException( __METHOD__ . ': Was passed ' .
305
					is_object( $manager ) ? get_class( $manager ) : gettype( $manager )
306
				);
307
			}
308
			if ( $this->mContinuationManager !== null ) {
309
				throw new UnexpectedValueException(
310
					__METHOD__ . ': tried to set manager from ' . $manager->getSource() .
311
					' when a manager is already set from ' . $this->mContinuationManager->getSource()
0 ignored issues
show
Bug introduced by
The method getSource cannot be called on $this->mContinuationManager (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
312
				);
313
			}
314
		}
315
		$this->mContinuationManager = $manager;
316
	}
317
318
	/**
319
	 * Get the API module object. Only works after executeAction()
320
	 *
321
	 * @return ApiBase
322
	 */
323
	public function getModule() {
324
		return $this->mModule;
325
	}
326
327
	/**
328
	 * Get the result formatter object. Only works after setupExecuteAction()
329
	 *
330
	 * @return ApiFormatBase
331
	 */
332
	public function getPrinter() {
333
		return $this->mPrinter;
334
	}
335
336
	/**
337
	 * Set how long the response should be cached.
338
	 *
339
	 * @param int $maxage
340
	 */
341
	public function setCacheMaxAge( $maxage ) {
342
		$this->setCacheControl( [
343
			'max-age' => $maxage,
344
			's-maxage' => $maxage
345
		] );
346
	}
347
348
	/**
349
	 * Set the type of caching headers which will be sent.
350
	 *
351
	 * @param string $mode One of:
352
	 *    - 'public':     Cache this object in public caches, if the maxage or smaxage
353
	 *         parameter is set, or if setCacheMaxAge() was called. If a maximum age is
354
	 *         not provided by any of these means, the object will be private.
355
	 *    - 'private':    Cache this object only in private client-side caches.
356
	 *    - 'anon-public-user-private': Make this object cacheable for logged-out
357
	 *         users, but private for logged-in users. IMPORTANT: If this is set, it must be
358
	 *         set consistently for a given URL, it cannot be set differently depending on
359
	 *         things like the contents of the database, or whether the user is logged in.
360
	 *
361
	 *  If the wiki does not allow anonymous users to read it, the mode set here
362
	 *  will be ignored, and private caching headers will always be sent. In other words,
363
	 *  the "public" mode is equivalent to saying that the data sent is as public as a page
364
	 *  view.
365
	 *
366
	 *  For user-dependent data, the private mode should generally be used. The
367
	 *  anon-public-user-private mode should only be used where there is a particularly
368
	 *  good performance reason for caching the anonymous response, but where the
369
	 *  response to logged-in users may differ, or may contain private data.
370
	 *
371
	 *  If this function is never called, then the default will be the private mode.
372
	 */
373
	public function setCacheMode( $mode ) {
374
		if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
375
			wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
376
377
			// Ignore for forwards-compatibility
378
			return;
379
		}
380
381
		if ( !User::isEveryoneAllowed( 'read' ) ) {
382
			// Private wiki, only private headers
383
			if ( $mode !== 'private' ) {
384
				wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
385
386
				return;
387
			}
388
		}
389
390
		if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
391
			// User language is used for i18n, so we don't want to publicly
392
			// cache. Anons are ok, because if they have non-default language
393
			// then there's an appropriate Vary header set by whatever set
394
			// their non-default language.
395
			wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
396
				"'anon-public-user-private' due to uselang=user\n" );
397
			$mode = 'anon-public-user-private';
398
		}
399
400
		wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
401
		$this->mCacheMode = $mode;
402
	}
403
404
	/**
405
	 * Set directives (key/value pairs) for the Cache-Control header.
406
	 * Boolean values will be formatted as such, by including or omitting
407
	 * without an equals sign.
408
	 *
409
	 * Cache control values set here will only be used if the cache mode is not
410
	 * private, see setCacheMode().
411
	 *
412
	 * @param array $directives
413
	 */
414
	public function setCacheControl( $directives ) {
415
		$this->mCacheControl = $directives + $this->mCacheControl;
416
	}
417
418
	/**
419
	 * Create an instance of an output formatter by its name
420
	 *
421
	 * @param string $format
422
	 *
423
	 * @return ApiFormatBase
424
	 */
425
	public function createPrinterByName( $format ) {
426
		$printer = $this->mModuleMgr->getModule( $format, 'format' );
427
		if ( $printer === null ) {
428
			$this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
429
		}
430
431
		return $printer;
432
	}
433
434
	/**
435
	 * Execute api request. Any errors will be handled if the API was called by the remote client.
436
	 */
437
	public function execute() {
438
		if ( $this->mInternalMode ) {
439
			$this->executeAction();
440
		} else {
441
			$this->executeActionWithErrorHandling();
442
		}
443
	}
444
445
	/**
446
	 * Execute an action, and in case of an error, erase whatever partial results
447
	 * have been accumulated, and replace it with an error message and a help screen.
448
	 */
449
	protected function executeActionWithErrorHandling() {
450
		// Verify the CORS header before executing the action
451
		if ( !$this->handleCORS() ) {
452
			// handleCORS() has sent a 403, abort
453
			return;
454
		}
455
456
		// Exit here if the request method was OPTIONS
457
		// (assume there will be a followup GET or POST)
458
		if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
459
			return;
460
		}
461
462
		// In case an error occurs during data output,
463
		// clear the output buffer and print just the error information
464
		$obLevel = ob_get_level();
465
		ob_start();
466
467
		$t = microtime( true );
468
		$isError = false;
469
		try {
470
			$this->executeAction();
471
			$runTime = microtime( true ) - $t;
472
			$this->logRequest( $runTime );
473
			if ( $this->mModule->isWriteMode() && $this->getRequest()->wasPosted() ) {
474
				$this->getStats()->timing(
0 ignored issues
show
Deprecated Code introduced by
The method ContextSource::getStats() has been deprecated with message: since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
475
					'api.' . $this->getModuleName() . '.executeTiming', 1000 * $runTime );
476
			}
477
		} catch ( Exception $e ) {
478
			$this->handleException( $e );
479
			$this->logRequest( microtime( true ) - $t, $e );
480
			$isError = true;
481
		}
482
483
		// Commit DBs and send any related cookies and headers
484
		MediaWiki::preOutputCommit( $this->getContext() );
485
486
		// Send cache headers after any code which might generate an error, to
487
		// avoid sending public cache headers for errors.
488
		$this->sendCacheHeaders( $isError );
489
490
		// Executing the action might have already messed with the output
491
		// buffers.
492
		while ( ob_get_level() > $obLevel ) {
493
			ob_end_flush();
494
		}
495
	}
496
497
	/**
498
	 * Handle an exception as an API response
499
	 *
500
	 * @since 1.23
501
	 * @param Exception $e
502
	 */
503
	protected function handleException( Exception $e ) {
504
		// Bug 63145: Rollback any open database transactions
505
		if ( !( $e instanceof UsageException ) ) {
506
			// UsageExceptions are intentional, so don't rollback if that's the case
507
			try {
508
				MWExceptionHandler::rollbackMasterChangesAndLog( $e );
509
			} catch ( DBError $e2 ) {
510
				// Rollback threw an exception too. Log it, but don't interrupt
511
				// our regularly scheduled exception handling.
512
				MWExceptionHandler::logException( $e2 );
513
			}
514
		}
515
516
		// Allow extra cleanup and logging
517
		Hooks::run( 'ApiMain::onException', [ $this, $e ] );
518
519
		// Log it
520
		if ( !( $e instanceof UsageException ) ) {
521
			MWExceptionHandler::logException( $e );
522
		}
523
524
		// Handle any kind of exception by outputting properly formatted error message.
525
		// If this fails, an unhandled exception should be thrown so that global error
526
		// handler will process and log it.
527
528
		$errCode = $this->substituteResultWithError( $e );
529
530
		// Error results should not be cached
531
		$this->setCacheMode( 'private' );
532
533
		$response = $this->getRequest()->response();
534
		$headerStr = 'MediaWiki-API-Error: ' . $errCode;
535
		if ( $e->getCode() === 0 ) {
536
			$response->header( $headerStr );
537
		} else {
538
			$response->header( $headerStr, true, $e->getCode() );
539
		}
540
541
		// Reset and print just the error message
542
		ob_clean();
543
544
		// Printer may not be initialized if the extractRequestParams() fails for the main module
545
		$this->createErrorPrinter();
546
547
		try {
548
			$this->printResult( true );
549
		} catch ( UsageException $ex ) {
550
			// The error printer itself is failing. Try suppressing its request
551
			// parameters and redo.
552
			$this->setWarning(
553
				'Error printer failed (will retry without params): ' . $ex->getMessage()
554
			);
555
			$this->mPrinter = null;
556
			$this->createErrorPrinter();
557
			$this->mPrinter->forceDefaultParams();
0 ignored issues
show
Bug introduced by
The method forceDefaultParams cannot be called on $this->mPrinter (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
558
			$this->printResult( true );
559
		}
560
	}
561
562
	/**
563
	 * Handle an exception from the ApiBeforeMain hook.
564
	 *
565
	 * This tries to print the exception as an API response, to be more
566
	 * friendly to clients. If it fails, it will rethrow the exception.
567
	 *
568
	 * @since 1.23
569
	 * @param Exception $e
570
	 * @throws Exception
571
	 */
572
	public static function handleApiBeforeMainException( Exception $e ) {
573
		ob_start();
574
575
		try {
576
			$main = new self( RequestContext::getMain(), false );
577
			$main->handleException( $e );
578
			$main->logRequest( 0, $e );
579
		} catch ( Exception $e2 ) {
580
			// Nope, even that didn't work. Punt.
581
			throw $e;
582
		}
583
584
		// Reset cache headers
585
		$main->sendCacheHeaders( true );
586
587
		ob_end_flush();
588
	}
589
590
	/**
591
	 * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
592
	 *
593
	 * If no origin parameter is present, nothing happens.
594
	 * If an origin parameter is present but doesn't match the Origin header, a 403 status code
595
	 * is set and false is returned.
596
	 * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
597
	 * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
598
	 * headers are set.
599
	 * http://www.w3.org/TR/cors/#resource-requests
600
	 * http://www.w3.org/TR/cors/#resource-preflight-requests
601
	 *
602
	 * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
603
	 */
604
	protected function handleCORS() {
605
		$originParam = $this->getParameter( 'origin' ); // defaults to null
606
		if ( $originParam === null ) {
607
			// No origin parameter, nothing to do
608
			return true;
609
		}
610
611
		$request = $this->getRequest();
612
		$response = $request->response();
613
614
		// Origin: header is a space-separated list of origins, check all of them
615
		$originHeader = $request->getHeader( 'Origin' );
616
		if ( $originHeader === false ) {
617
			$origins = [];
618
		} else {
619
			$originHeader = trim( $originHeader );
620
			$origins = preg_split( '/\s+/', $originHeader );
621
		}
622
623
		if ( !in_array( $originParam, $origins ) ) {
624
			// origin parameter set but incorrect
625
			// Send a 403 response
626
			$response->statusHeader( 403 );
627
			$response->header( 'Cache-Control: no-cache' );
628
			echo "'origin' parameter does not match Origin header\n";
629
630
			return false;
631
		}
632
633
		$config = $this->getConfig();
634
		$matchOrigin = count( $origins ) === 1 && self::matchOrigin(
635
			$originParam,
636
			$config->get( 'CrossSiteAJAXdomains' ),
637
			$config->get( 'CrossSiteAJAXdomainExceptions' )
638
		);
639
640
		if ( $matchOrigin ) {
641
			$requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
642
			$preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
643
			if ( $preflight ) {
644
				// This is a CORS preflight request
645
				if ( $requestedMethod !== 'POST' && $requestedMethod !== 'GET' ) {
646
					// If method is not a case-sensitive match, do not set any additional headers and terminate.
647
					return true;
648
				}
649
				// We allow the actual request to send the following headers
650
				$requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
651
				if ( $requestedHeaders !== false ) {
652
					if ( !self::matchRequestedHeaders( $requestedHeaders ) ) {
653
						return true;
654
					}
655
					$response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
656
				}
657
658
				// We only allow the actual request to be GET or POST
659
				$response->header( 'Access-Control-Allow-Methods: POST, GET' );
660
			}
661
662
			$response->header( "Access-Control-Allow-Origin: $originHeader" );
663
			$response->header( 'Access-Control-Allow-Credentials: true' );
664
			// http://www.w3.org/TR/resource-timing/#timing-allow-origin
665
			$response->header( "Timing-Allow-Origin: $originHeader" );
666
667
			if ( !$preflight ) {
668
				$response->header(
669
					'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag'
670
				);
671
			}
672
		}
673
674
		$this->getOutput()->addVaryHeader( 'Origin' );
675
		return true;
676
	}
677
678
	/**
679
	 * Attempt to match an Origin header against a set of rules and a set of exceptions
680
	 * @param string $value Origin header
681
	 * @param array $rules Set of wildcard rules
682
	 * @param array $exceptions Set of wildcard rules
683
	 * @return bool True if $value matches a rule in $rules and doesn't match
684
	 *    any rules in $exceptions, false otherwise
685
	 */
686
	protected static function matchOrigin( $value, $rules, $exceptions ) {
687
		foreach ( $rules as $rule ) {
688
			if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
689
				// Rule matches, check exceptions
690
				foreach ( $exceptions as $exc ) {
691
					if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
692
						return false;
693
					}
694
				}
695
696
				return true;
697
			}
698
		}
699
700
		return false;
701
	}
702
703
	/**
704
	 * Attempt to validate the value of Access-Control-Request-Headers against a list
705
	 * of headers that we allow the follow up request to send.
706
	 *
707
	 * @param string $requestedHeaders Comma seperated list of HTTP headers
708
	 * @return bool True if all requested headers are in the list of allowed headers
709
	 */
710
	protected static function matchRequestedHeaders( $requestedHeaders ) {
711
		if ( trim( $requestedHeaders ) === '' ) {
712
			return true;
713
		}
714
		$requestedHeaders = explode( ',', $requestedHeaders );
715
		$allowedAuthorHeaders = array_flip( [
716
			/* simple headers (see spec) */
717
			'accept',
718
			'accept-language',
719
			'content-language',
720
			'content-type',
721
			/* non-authorable headers in XHR, which are however requested by some UAs */
722
			'accept-encoding',
723
			'dnt',
724
			'origin',
725
			/* MediaWiki whitelist */
726
			'api-user-agent',
727
		] );
728
		foreach ( $requestedHeaders as $rHeader ) {
729
			$rHeader = strtolower( trim( $rHeader ) );
730
			if ( !isset( $allowedAuthorHeaders[$rHeader] ) ) {
731
				wfDebugLog( 'api', 'CORS preflight failed on requested header: ' . $rHeader );
732
				return false;
733
			}
734
		}
735
		return true;
736
	}
737
738
	/**
739
	 * Helper function to convert wildcard string into a regex
740
	 * '*' => '.*?'
741
	 * '?' => '.'
742
	 *
743
	 * @param string $wildcard String with wildcards
744
	 * @return string Regular expression
745
	 */
746
	protected static function wildcardToRegex( $wildcard ) {
747
		$wildcard = preg_quote( $wildcard, '/' );
748
		$wildcard = str_replace(
749
			[ '\*', '\?' ],
750
			[ '.*?', '.' ],
751
			$wildcard
752
		);
753
754
		return "/^https?:\/\/$wildcard$/";
755
	}
756
757
	/**
758
	 * Send caching headers
759
	 * @param bool $isError Whether an error response is being output
760
	 * @since 1.26 added $isError parameter
761
	 */
762
	protected function sendCacheHeaders( $isError ) {
763
		$response = $this->getRequest()->response();
764
		$out = $this->getOutput();
765
766
		$out->addVaryHeader( 'Treat-as-Untrusted' );
767
768
		$config = $this->getConfig();
769
770
		if ( $config->get( 'VaryOnXFP' ) ) {
771
			$out->addVaryHeader( 'X-Forwarded-Proto' );
772
		}
773
774
		if ( !$isError && $this->mModule &&
775
			( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
776
		) {
777
			$etag = $this->mModule->getConditionalRequestData( 'etag' );
778
			if ( $etag !== null ) {
779
				$response->header( "ETag: $etag" );
780
			}
781
			$lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
782
			if ( $lastMod !== null ) {
783
				$response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
784
			}
785
		}
786
787
		// The logic should be:
788
		// $this->mCacheControl['max-age'] is set?
789
		//    Use it, the module knows better than our guess.
790
		// !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
791
		//    Use 0 because we can guess caching is probably the wrong thing to do.
792
		// Use $this->getParameter( 'maxage' ), which already defaults to 0.
793
		$maxage = 0;
794
		if ( isset( $this->mCacheControl['max-age'] ) ) {
795
			$maxage = $this->mCacheControl['max-age'];
796
		} elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
797
			$this->mCacheMode !== 'private'
798
		) {
799
			$maxage = $this->getParameter( 'maxage' );
800
		}
801
		$privateCache = 'private, must-revalidate, max-age=' . $maxage;
802
803
		if ( $this->mCacheMode == 'private' ) {
804
			$response->header( "Cache-Control: $privateCache" );
805
			return;
806
		}
807
808
		$useKeyHeader = $config->get( 'UseKeyHeader' );
809
		if ( $this->mCacheMode == 'anon-public-user-private' ) {
810
			$out->addVaryHeader( 'Cookie' );
811
			$response->header( $out->getVaryHeader() );
812
			if ( $useKeyHeader ) {
813
				$response->header( $out->getKeyHeader() );
814
				if ( $out->haveCacheVaryCookies() ) {
815
					// Logged in, mark this request private
816
					$response->header( "Cache-Control: $privateCache" );
817
					return;
818
				}
819
				// Logged out, send normal public headers below
820
			} elseif ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
821
				// Logged in or otherwise has session (e.g. anonymous users who have edited)
822
				// Mark request private
823
				$response->header( "Cache-Control: $privateCache" );
824
825
				return;
826
			} // else no Key and anonymous, send public headers below
827
		}
828
829
		// Send public headers
830
		$response->header( $out->getVaryHeader() );
831
		if ( $useKeyHeader ) {
832
			$response->header( $out->getKeyHeader() );
833
		}
834
835
		// If nobody called setCacheMaxAge(), use the (s)maxage parameters
836
		if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
837
			$this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
838
		}
839
		if ( !isset( $this->mCacheControl['max-age'] ) ) {
840
			$this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
841
		}
842
843
		if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
844
			// Public cache not requested
845
			// Sending a Vary header in this case is harmless, and protects us
846
			// against conditional calls of setCacheMaxAge().
847
			$response->header( "Cache-Control: $privateCache" );
848
849
			return;
850
		}
851
852
		$this->mCacheControl['public'] = true;
853
854
		// Send an Expires header
855
		$maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
856
		$expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
857
		$response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
858
859
		// Construct the Cache-Control header
860
		$ccHeader = '';
861
		$separator = '';
862
		foreach ( $this->mCacheControl as $name => $value ) {
863
			if ( is_bool( $value ) ) {
864
				if ( $value ) {
865
					$ccHeader .= $separator . $name;
866
					$separator = ', ';
867
				}
868
			} else {
869
				$ccHeader .= $separator . "$name=$value";
870
				$separator = ', ';
871
			}
872
		}
873
874
		$response->header( "Cache-Control: $ccHeader" );
875
	}
876
877
	/**
878
	 * Create the printer for error output
879
	 */
880
	private function createErrorPrinter() {
881
		if ( !isset( $this->mPrinter ) ) {
882
			$value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
883
			if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
884
				$value = self::API_DEFAULT_FORMAT;
885
			}
886
			$this->mPrinter = $this->createPrinterByName( $value );
887
		}
888
889
		// Printer may not be able to handle errors. This is particularly
890
		// likely if the module returns something for getCustomPrinter().
891
		if ( !$this->mPrinter->canPrintErrors() ) {
892
			$this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
893
		}
894
	}
895
896
	/**
897
	 * Create an error message for the given exception.
898
	 *
899
	 * If the exception is a UsageException then
900
	 * UsageException::getMessageArray() will be called to create the message.
901
	 *
902
	 * @param Exception $e
903
	 * @return array ['code' => 'some string', 'info' => 'some other string']
904
	 * @since 1.27
905
	 */
906
	protected function errorMessageFromException( $e ) {
907
		if ( $e instanceof UsageException ) {
908
			// User entered incorrect parameters - generate error response
909
			$errMessage = $e->getMessageArray();
910
		} else {
911
			$config = $this->getConfig();
912
			// Something is seriously wrong
913
			if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) {
914
				$info = 'Database query error';
915
			} else {
916
				$info = "Exception Caught: {$e->getMessage()}";
917
			}
918
919
			$errMessage = [
920
				'code' => 'internal_api_error_' . get_class( $e ),
921
				'info' => '[' . WebRequest::getRequestId() . '] ' . $info,
922
			];
923
		}
924
		return $errMessage;
925
	}
926
927
	/**
928
	 * Replace the result data with the information about an exception.
929
	 * Returns the error code
930
	 * @param Exception $e
931
	 * @return string
932
	 */
933
	protected function substituteResultWithError( $e ) {
934
		$result = $this->getResult();
935
		$config = $this->getConfig();
936
937
		$errMessage = $this->errorMessageFromException( $e );
938
		if ( $e instanceof UsageException ) {
939
			// User entered incorrect parameters - generate error response
940
			$link = wfExpandUrl( wfScript( 'api' ) );
941
			ApiResult::setContentValue( $errMessage, 'docref', "See $link for API usage" );
942
		} else {
943
			// Something is seriously wrong
944
			if ( $config->get( 'ShowExceptionDetails' ) ) {
945
				ApiResult::setContentValue(
946
					$errMessage,
947
					'trace',
948
					MWExceptionHandler::getRedactedTraceAsString( $e )
949
				);
950
			}
951
		}
952
953
		// Remember all the warnings to re-add them later
954
		$warnings = $result->getResultData( [ 'warnings' ] );
955
956
		$result->reset();
957
		// Re-add the id
958
		$requestid = $this->getParameter( 'requestid' );
959
		if ( !is_null( $requestid ) ) {
960
			$result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
961
		}
962
		if ( $config->get( 'ShowHostnames' ) ) {
963
			// servedby is especially useful when debugging errors
964
			$result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
965
		}
966
		if ( $warnings !== null ) {
967
			$result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
968
		}
969
970
		$result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK );
971
972
		return $errMessage['code'];
973
	}
974
975
	/**
976
	 * Set up for the execution.
977
	 * @return array
978
	 */
979
	protected function setupExecuteAction() {
980
		// First add the id to the top element
981
		$result = $this->getResult();
982
		$requestid = $this->getParameter( 'requestid' );
983
		if ( !is_null( $requestid ) ) {
984
			$result->addValue( null, 'requestid', $requestid );
985
		}
986
987
		if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
988
			$servedby = $this->getParameter( 'servedby' );
989
			if ( $servedby ) {
990
				$result->addValue( null, 'servedby', wfHostname() );
991
			}
992
		}
993
994
		if ( $this->getParameter( 'curtimestamp' ) ) {
995
			$result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ),
996
				ApiResult::NO_SIZE_CHECK );
997
		}
998
999
		$params = $this->extractRequestParams();
1000
1001
		$this->mAction = $params['action'];
1002
1003
		if ( !is_string( $this->mAction ) ) {
1004
			$this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
1005
		}
1006
1007
		return $params;
1008
	}
1009
1010
	/**
1011
	 * Set up the module for response
1012
	 * @return ApiBase The module that will handle this action
1013
	 * @throws MWException
1014
	 * @throws UsageException
1015
	 */
1016
	protected function setupModule() {
1017
		// Instantiate the module requested by the user
1018
		$module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
1019
		if ( $module === null ) {
1020
			$this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
1021
		}
1022
		$moduleParams = $module->extractRequestParams();
1023
1024
		// Check token, if necessary
1025
		if ( $module->needsToken() === true ) {
1026
			throw new MWException(
1027
				"Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1028
				'See documentation for ApiBase::needsToken for details.'
1029
			);
1030
		}
1031
		if ( $module->needsToken() ) {
1032
			if ( !$module->mustBePosted() ) {
1033
				throw new MWException(
1034
					"Module '{$module->getModuleName()}' must require POST to use tokens."
1035
				);
1036
			}
1037
1038
			if ( !isset( $moduleParams['token'] ) ) {
1039
				$this->dieUsageMsg( [ 'missingparam', 'token' ] );
1040
			}
1041
1042
			if ( !$this->getConfig()->get( 'DebugAPI' ) &&
1043
				array_key_exists(
1044
					$module->encodeParamName( 'token' ),
1045
					$this->getRequest()->getQueryValues()
1046
				)
1047
			) {
1048
				$this->dieUsage(
1049
					"The '{$module->encodeParamName( 'token' )}' parameter was " .
1050
						'found in the query string, but must be in the POST body',
1051
					'mustposttoken'
1052
				);
1053
			}
1054
1055
			if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
1056
				$this->dieUsageMsg( 'sessionfailure' );
1057
			}
1058
		}
1059
1060
		return $module;
1061
	}
1062
1063
	/**
1064
	 * Check the max lag if necessary
1065
	 * @param ApiBase $module Api module being used
1066
	 * @param array $params Array an array containing the request parameters.
1067
	 * @return bool True on success, false should exit immediately
1068
	 */
1069
	protected function checkMaxLag( $module, $params ) {
1070
		if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
1071
			$maxLag = $params['maxlag'];
1072
			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...
1073
			if ( $lag > $maxLag ) {
1074
				$response = $this->getRequest()->response();
1075
1076
				$response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
1077
				$response->header( 'X-Database-Lag: ' . intval( $lag ) );
1078
1079
				if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1080
					$this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
1081
				}
1082
1083
				$this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
1084
			}
1085
		}
1086
1087
		return true;
1088
	}
1089
1090
	/**
1091
	 * Check selected RFC 7232 precondition headers
1092
	 *
1093
	 * RFC 7232 envisions a particular model where you send your request to "a
1094
	 * resource", and for write requests that you can read "the resource" by
1095
	 * changing the method to GET. When the API receives a GET request, it
1096
	 * works out even though "the resource" from RFC 7232's perspective might
1097
	 * be many resources from MediaWiki's perspective. But it totally fails for
1098
	 * a POST, since what HTTP sees as "the resource" is probably just
1099
	 * "/api.php" with all the interesting bits in the body.
1100
	 *
1101
	 * Therefore, we only support RFC 7232 precondition headers for GET (and
1102
	 * HEAD). That means we don't need to bother with If-Match and
1103
	 * If-Unmodified-Since since they only apply to modification requests.
1104
	 *
1105
	 * And since we don't support Range, If-Range is ignored too.
1106
	 *
1107
	 * @since 1.26
1108
	 * @param ApiBase $module Api module being used
1109
	 * @return bool True on success, false should exit immediately
1110
	 */
1111
	protected function checkConditionalRequestHeaders( $module ) {
1112
		if ( $this->mInternalMode ) {
1113
			// No headers to check in internal mode
1114
			return true;
1115
		}
1116
1117
		if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
1118
			// Don't check POSTs
1119
			return true;
1120
		}
1121
1122
		$return304 = false;
1123
1124
		$ifNoneMatch = array_diff(
1125
			$this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1126
			[ '' ]
1127
		);
1128
		if ( $ifNoneMatch ) {
1129
			if ( $ifNoneMatch === [ '*' ] ) {
1130
				// API responses always "exist"
1131
				$etag = '*';
1132
			} else {
1133
				$etag = $module->getConditionalRequestData( 'etag' );
1134
			}
1135
		}
1136
		if ( $ifNoneMatch && $etag !== null ) {
1137
			$test = substr( $etag, 0, 2 ) === 'W/' ? substr( $etag, 2 ) : $etag;
0 ignored issues
show
Bug introduced by
The variable $etag does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
1782
	}
1783
1784
	/**
1785
	 * Override the parent to generate help messages for all available modules.
1786
	 *
1787
	 * @deprecated since 1.25
1788
	 * @return string
1789
	 */
1790
	public function makeHelpMsg() {
1791
		wfDeprecated( __METHOD__, '1.25' );
1792
1793
		$this->setHelp();
0 ignored issues
show
Deprecated Code introduced by
The method ApiMain::setHelp() has been deprecated with message: since 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
1794
		$cacheHelpTimeout = $this->getConfig()->get( 'APICacheHelpTimeout' );
1795
1796
		return ObjectCache::getMainWANInstance()->getWithSetCallback(
1797
			wfMemcKey(
1798
				'apihelp',
1799
				$this->getModuleName(),
1800
				str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) )
1801
			),
1802
			$cacheHelpTimeout > 0 ? $cacheHelpTimeout : WANObjectCache::TTL_UNCACHEABLE,
1803
			[ $this, 'reallyMakeHelpMsg' ]
1804
		);
1805
	}
1806
1807
	/**
1808
	 * @deprecated since 1.25
1809
	 * @return mixed|string
1810
	 */
1811
	public function reallyMakeHelpMsg() {
1812
		wfDeprecated( __METHOD__, '1.25' );
1813
		$this->setHelp();
0 ignored issues
show
Deprecated Code introduced by
The method ApiMain::setHelp() has been deprecated with message: since 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
1814
1815
		// Use parent to make default message for the main module
1816
		$msg = parent::makeHelpMsg();
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (makeHelpMsg() instead of reallyMakeHelpMsg()). Are you sure this is correct? If so, you might want to change this to $this->makeHelpMsg().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
Deprecated Code introduced by
The method ApiBase::makeHelpMsg() has been deprecated with message: since 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
1817
1818
		$asterisks = str_repeat( '*** ', 14 );
1819
		$msg .= "\n\n$asterisks Modules  $asterisks\n\n";
1820
1821 View Code Duplication
		foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) {
1822
			$module = $this->mModuleMgr->getModule( $name );
1823
			$msg .= self::makeHelpMsgHeader( $module, 'action' );
0 ignored issues
show
Bug introduced by
It seems like $module defined by $this->mModuleMgr->getModule($name) on line 1822 can be null; however, ApiMain::makeHelpMsgHeader() 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...
Deprecated Code introduced by
The method ApiMain::makeHelpMsgHeader() has been deprecated with message: since 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
1824
1825
			$msg2 = $module->makeHelpMsg();
1826
			if ( $msg2 !== false ) {
1827
				$msg .= $msg2;
1828
			}
1829
			$msg .= "\n";
1830
		}
1831
1832
		$msg .= "\n$asterisks Permissions $asterisks\n\n";
1833
		foreach ( self::$mRights as $right => $rightMsg ) {
1834
			$rightsMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )
1835
				->useDatabase( false )
1836
				->inLanguage( 'en' )
1837
				->text();
1838
			$groups = User::getGroupsWithPermission( $right );
1839
			$msg .= '* ' . $right . " *\n  $rightsMsg" .
1840
				"\nGranted to:\n  " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n";
1841
		}
1842
1843
		$msg .= "\n$asterisks Formats  $asterisks\n\n";
1844 View Code Duplication
		foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) {
1845
			$module = $this->mModuleMgr->getModule( $name );
1846
			$msg .= self::makeHelpMsgHeader( $module, 'format' );
0 ignored issues
show
Bug introduced by
It seems like $module defined by $this->mModuleMgr->getModule($name) on line 1845 can be null; however, ApiMain::makeHelpMsgHeader() 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...
Deprecated Code introduced by
The method ApiMain::makeHelpMsgHeader() has been deprecated with message: since 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
1847
			$msg2 = $module->makeHelpMsg();
1848
			if ( $msg2 !== false ) {
1849
				$msg .= $msg2;
1850
			}
1851
			$msg .= "\n";
1852
		}
1853
1854
		$credits = $this->msg( 'api-credits' )->useDatabase( 'false' )->inLanguage( 'en' )->text();
1855
		$credits = str_replace( "\n", "\n   ", $credits );
1856
		$msg .= "\n*** Credits: ***\n   $credits\n";
1857
1858
		return $msg;
1859
	}
1860
1861
	/**
1862
	 * @deprecated since 1.25
1863
	 * @param ApiBase $module
1864
	 * @param string $paramName What type of request is this? e.g. action,
1865
	 *    query, list, prop, meta, format
1866
	 * @return string
1867
	 */
1868
	public static function makeHelpMsgHeader( $module, $paramName ) {
1869
		wfDeprecated( __METHOD__, '1.25' );
1870
		$modulePrefix = $module->getModulePrefix();
1871
		if ( strval( $modulePrefix ) !== '' ) {
1872
			$modulePrefix = "($modulePrefix) ";
1873
		}
1874
1875
		return "* $paramName={$module->getModuleName()} $modulePrefix*";
1876
	}
1877
1878
	/**@}*/
1879
1880
}
1881
1882
/**
1883
 * This exception will be thrown when dieUsage is called to stop module execution.
1884
 *
1885
 * @ingroup API
1886
 */
1887
class UsageException extends MWException {
1888
1889
	private $mCodestr;
1890
1891
	/**
1892
	 * @var null|array
1893
	 */
1894
	private $mExtraData;
1895
1896
	/**
1897
	 * @param string $message
1898
	 * @param string $codestr
1899
	 * @param int $code
1900
	 * @param array|null $extradata
1901
	 */
1902
	public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
1903
		parent::__construct( $message, $code );
1904
		$this->mCodestr = $codestr;
1905
		$this->mExtraData = $extradata;
1906
	}
1907
1908
	/**
1909
	 * @return string
1910
	 */
1911
	public function getCodeString() {
1912
		return $this->mCodestr;
1913
	}
1914
1915
	/**
1916
	 * @return array
1917
	 */
1918
	public function getMessageArray() {
1919
		$result = [
1920
			'code' => $this->mCodestr,
1921
			'info' => $this->getMessage()
1922
		];
1923
		if ( is_array( $this->mExtraData ) ) {
1924
			$result = array_merge( $result, $this->mExtraData );
1925
		}
1926
1927
		return $result;
1928
	}
1929
1930
	/**
1931
	 * @return string
1932
	 */
1933
	public function __toString() {
1934
		return "{$this->getCodeString()}: {$this->getMessage()}";
1935
	}
1936
}
1937
1938
/**
1939
 * For really cool vim folding this needs to be at the end:
1940
 * vim: foldmarker=@{,@} foldmethod=marker
1941
 */
1942