Completed
Branch master (e2eefa)
by
unknown
25:58
created

ApiMain::executeActionWithErrorHandling()   C

Complexity

Conditions 7
Paths 16

Size

Total Lines 47
Code Lines 24

Duplication

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

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1088
			if ( $ifNoneMatch === [ '*' ] ) {
1089
				// API responses always "exist"
1090
				$etag = '*';
1091
			} else {
1092
				$etag = $module->getConditionalRequestData( 'etag' );
1093
			}
1094
		}
1095
		if ( $ifNoneMatch && $etag !== null ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ifNoneMatch of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

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