Completed
Branch master (af7ffa)
by
unknown
24:08
created

ApiMain::handleCORS()   F

Complexity

Conditions 16
Paths 263

Size

Total Lines 96
Code Lines 56

Duplication

Lines 6
Ratio 6.25 %

Importance

Changes 0
Metric Value
cc 16
eloc 56
nc 263
nop 0
dl 6
loc 96
rs 3.7109
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
		'cspreport' => 'ApiCSPReport',
75
76
		// Write modules
77
		'purge' => 'ApiPurge',
78
		'setnotificationtimestamp' => 'ApiSetNotificationTimestamp',
79
		'rollback' => 'ApiRollback',
80
		'delete' => 'ApiDelete',
81
		'undelete' => 'ApiUndelete',
82
		'protect' => 'ApiProtect',
83
		'block' => 'ApiBlock',
84
		'unblock' => 'ApiUnblock',
85
		'move' => 'ApiMove',
86
		'edit' => 'ApiEditPage',
87
		'upload' => 'ApiUpload',
88
		'filerevert' => 'ApiFileRevert',
89
		'emailuser' => 'ApiEmailUser',
90
		'watch' => 'ApiWatch',
91
		'patrol' => 'ApiPatrol',
92
		'import' => 'ApiImport',
93
		'clearhasmsg' => 'ApiClearHasMsg',
94
		'userrights' => 'ApiUserrights',
95
		'options' => 'ApiOptions',
96
		'imagerotate' => 'ApiImageRotate',
97
		'revisiondelete' => 'ApiRevisionDelete',
98
		'managetags' => 'ApiManageTags',
99
		'tag' => 'ApiTag',
100
		'mergehistory' => 'ApiMergeHistory',
101
	];
102
103
	/**
104
	 * List of available formats: format name => format class
105
	 */
106
	private static $Formats = [
107
		'json' => 'ApiFormatJson',
108
		'jsonfm' => 'ApiFormatJson',
109
		'php' => 'ApiFormatPhp',
110
		'phpfm' => 'ApiFormatPhp',
111
		'xml' => 'ApiFormatXml',
112
		'xmlfm' => 'ApiFormatXml',
113
		'rawfm' => 'ApiFormatJson',
114
		'none' => 'ApiFormatNone',
115
	];
116
117
	// @codingStandardsIgnoreStart String contenation on "msg" not allowed to break long line
118
	/**
119
	 * List of user roles that are specifically relevant to the API.
120
	 * array( 'right' => array ( 'msg'    => 'Some message with a $1',
121
	 *                           'params' => array ( $someVarToSubst ) ),
122
	 *                          );
123
	 */
124
	private static $mRights = [
125
		'writeapi' => [
126
			'msg' => 'right-writeapi',
127
			'params' => []
128
		],
129
		'apihighlimits' => [
130
			'msg' => 'api-help-right-apihighlimits',
131
			'params' => [ ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ]
132
		]
133
	];
134
	// @codingStandardsIgnoreEnd
135
136
	/**
137
	 * @var ApiFormatBase
138
	 */
139
	private $mPrinter;
140
141
	private $mModuleMgr, $mResult, $mErrorFormatter;
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...
142
	/** @var ApiContinuationManager|null */
143
	private $mContinuationManager;
144
	private $mAction;
145
	private $mEnableWrite;
146
	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...
147
	/** @var ApiBase */
148
	private $mModule;
149
150
	private $mCacheMode = 'private';
151
	private $mCacheControl = [];
152
	private $mParamsUsed = [];
153
154
	/** @var bool|null Cached return value from self::lacksSameOriginSecurity() */
155
	private $lacksSameOriginSecurity = null;
156
157
	/**
158
	 * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
159
	 *
160
	 * @param IContextSource|WebRequest $context If this is an instance of
161
	 *    FauxRequest, errors are thrown and no printing occurs
162
	 * @param bool $enableWrite Should be set to true if the api may modify data
163
	 */
164
	public function __construct( $context = null, $enableWrite = false ) {
0 ignored issues
show
Coding Style introduced by
__construct uses the super-global variable $_COOKIE which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
165
		if ( $context === null ) {
166
			$context = RequestContext::getMain();
167
		} elseif ( $context instanceof WebRequest ) {
168
			// BC for pre-1.19
169
			$request = $context;
170
			$context = RequestContext::getMain();
171
		}
172
		// We set a derivative context so we can change stuff later
173
		$this->setContext( new DerivativeContext( $context ) );
174
175
		if ( isset( $request ) ) {
176
			$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...
177
		} else {
178
			$request = $this->getRequest();
179
		}
180
181
		$this->mInternalMode = ( $request instanceof FauxRequest );
182
183
		// Special handling for the main module: $parent === $this
184
		parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
185
186
		$config = $this->getConfig();
187
188
		if ( !$this->mInternalMode ) {
189
			// Log if a request with a non-whitelisted Origin header is seen
190
			// with session cookies.
191
			$originHeader = $request->getHeader( 'Origin' );
192 View Code Duplication
			if ( $originHeader === false ) {
193
				$origins = [];
194
			} else {
195
				$originHeader = trim( $originHeader );
196
				$origins = preg_split( '/\s+/', $originHeader );
197
			}
198
			$sessionCookies = array_intersect(
199
				array_keys( $_COOKIE ),
200
				MediaWiki\Session\SessionManager::singleton()->getVaryCookies()
201
			);
202
			if ( $origins && $sessionCookies && (
203
				count( $origins ) !== 1 || !self::matchOrigin(
204
					$origins[0],
205
					$config->get( 'CrossSiteAJAXdomains' ),
206
					$config->get( 'CrossSiteAJAXdomainExceptions' )
207
				)
208
			) ) {
209
				MediaWiki\Logger\LoggerFactory::getInstance( 'cors' )->warning(
210
					'Non-whitelisted CORS request with session cookies', [
211
						'origin' => $originHeader,
212
						'cookies' => $sessionCookies,
213
						'ip' => $request->getIP(),
214
						'userAgent' => $this->getUserAgent(),
215
						'wiki' => wfWikiID(),
216
					]
217
				);
218
			}
219
220
			// If we're in a mode that breaks the same-origin policy, strip
221
			// user credentials for security.
222
			if ( $this->lacksSameOriginSecurity() ) {
223
				global $wgUser;
224
				wfDebug( "API: stripping user credentials when the same-origin policy is not applied\n" );
225
				$wgUser = new User();
226
				$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...
227
			}
228
		}
229
230
		$uselang = $this->getParameter( 'uselang' );
231
		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...
232
			// Assume the parent context is going to return the user language
233
			// for uselang=user (see T85635).
234
		} else {
235
			if ( $uselang === 'content' ) {
236
				global $wgContLang;
237
				$uselang = $wgContLang->getCode();
238
			}
239
			$code = RequestContext::sanitizeLangCode( $uselang );
240
			$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...
241
			if ( !$this->mInternalMode ) {
242
				global $wgLang;
243
				$wgLang = $this->getContext()->getLanguage();
244
				RequestContext::getMain()->setLanguage( $wgLang );
245
			}
246
		}
247
248
		$this->mModuleMgr = new ApiModuleManager( $this );
249
		$this->mModuleMgr->addModules( self::$Modules, 'action' );
250
		$this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
251
		$this->mModuleMgr->addModules( self::$Formats, 'format' );
252
		$this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
253
254
		Hooks::run( 'ApiMain::moduleManager', [ $this->mModuleMgr ] );
255
256
		$this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
257
		$this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
258
		$this->mResult->setErrorFormatter( $this->mErrorFormatter );
259
		$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...
260
		$this->mContinuationManager = null;
261
		$this->mEnableWrite = $enableWrite;
262
263
		$this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling()
264
		$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...
265
	}
266
267
	/**
268
	 * Return true if the API was started by other PHP code using FauxRequest
269
	 * @return bool
270
	 */
271
	public function isInternalMode() {
272
		return $this->mInternalMode;
273
	}
274
275
	/**
276
	 * Get the ApiResult object associated with current request
277
	 *
278
	 * @return ApiResult
279
	 */
280
	public function getResult() {
281
		return $this->mResult;
282
	}
283
284
	/**
285
	 * Get the security flag for the current request
286
	 * @return bool
287
	 */
288
	public function lacksSameOriginSecurity() {
289
		if ( $this->lacksSameOriginSecurity !== null ) {
290
			return $this->lacksSameOriginSecurity;
291
		}
292
293
		$request = $this->getRequest();
294
295
		// JSONP mode
296
		if ( $request->getVal( 'callback' ) !== null ) {
297
			$this->lacksSameOriginSecurity = true;
298
			return true;
299
		}
300
301
		// Anonymous CORS
302
		if ( $request->getVal( 'origin' ) === '*' ) {
303
			$this->lacksSameOriginSecurity = true;
304
			return true;
305
		}
306
307
		// Header to be used from XMLHTTPRequest when the request might
308
		// otherwise be used for XSS.
309
		if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) {
310
			$this->lacksSameOriginSecurity = true;
311
			return true;
312
		}
313
314
		// Allow extensions to override.
315
		$this->lacksSameOriginSecurity = !Hooks::run( 'RequestHasSameOriginSecurity', [ $request ] );
316
		return $this->lacksSameOriginSecurity;
317
	}
318
319
	/**
320
	 * Get the ApiErrorFormatter object associated with current request
321
	 * @return ApiErrorFormatter
322
	 */
323
	public function getErrorFormatter() {
324
		return $this->mErrorFormatter;
325
	}
326
327
	/**
328
	 * Get the continuation manager
329
	 * @return ApiContinuationManager|null
330
	 */
331
	public function getContinuationManager() {
332
		return $this->mContinuationManager;
333
	}
334
335
	/**
336
	 * Set the continuation manager
337
	 * @param ApiContinuationManager|null
338
	 */
339
	public function setContinuationManager( $manager ) {
340
		if ( $manager !== null ) {
341
			if ( !$manager instanceof ApiContinuationManager ) {
342
				throw new InvalidArgumentException( __METHOD__ . ': Was passed ' .
343
					is_object( $manager ) ? get_class( $manager ) : gettype( $manager )
344
				);
345
			}
346
			if ( $this->mContinuationManager !== null ) {
347
				throw new UnexpectedValueException(
348
					__METHOD__ . ': tried to set manager from ' . $manager->getSource() .
349
					' when a manager is already set from ' . $this->mContinuationManager->getSource()
350
				);
351
			}
352
		}
353
		$this->mContinuationManager = $manager;
354
	}
355
356
	/**
357
	 * Get the API module object. Only works after executeAction()
358
	 *
359
	 * @return ApiBase
360
	 */
361
	public function getModule() {
362
		return $this->mModule;
363
	}
364
365
	/**
366
	 * Get the result formatter object. Only works after setupExecuteAction()
367
	 *
368
	 * @return ApiFormatBase
369
	 */
370
	public function getPrinter() {
371
		return $this->mPrinter;
372
	}
373
374
	/**
375
	 * Set how long the response should be cached.
376
	 *
377
	 * @param int $maxage
378
	 */
379
	public function setCacheMaxAge( $maxage ) {
380
		$this->setCacheControl( [
381
			'max-age' => $maxage,
382
			's-maxage' => $maxage
383
		] );
384
	}
385
386
	/**
387
	 * Set the type of caching headers which will be sent.
388
	 *
389
	 * @param string $mode One of:
390
	 *    - 'public':     Cache this object in public caches, if the maxage or smaxage
391
	 *         parameter is set, or if setCacheMaxAge() was called. If a maximum age is
392
	 *         not provided by any of these means, the object will be private.
393
	 *    - 'private':    Cache this object only in private client-side caches.
394
	 *    - 'anon-public-user-private': Make this object cacheable for logged-out
395
	 *         users, but private for logged-in users. IMPORTANT: If this is set, it must be
396
	 *         set consistently for a given URL, it cannot be set differently depending on
397
	 *         things like the contents of the database, or whether the user is logged in.
398
	 *
399
	 *  If the wiki does not allow anonymous users to read it, the mode set here
400
	 *  will be ignored, and private caching headers will always be sent. In other words,
401
	 *  the "public" mode is equivalent to saying that the data sent is as public as a page
402
	 *  view.
403
	 *
404
	 *  For user-dependent data, the private mode should generally be used. The
405
	 *  anon-public-user-private mode should only be used where there is a particularly
406
	 *  good performance reason for caching the anonymous response, but where the
407
	 *  response to logged-in users may differ, or may contain private data.
408
	 *
409
	 *  If this function is never called, then the default will be the private mode.
410
	 */
411
	public function setCacheMode( $mode ) {
412
		if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
413
			wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" );
414
415
			// Ignore for forwards-compatibility
416
			return;
417
		}
418
419
		if ( !User::isEveryoneAllowed( 'read' ) ) {
420
			// Private wiki, only private headers
421
			if ( $mode !== 'private' ) {
422
				wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
423
424
				return;
425
			}
426
		}
427
428
		if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
429
			// User language is used for i18n, so we don't want to publicly
430
			// cache. Anons are ok, because if they have non-default language
431
			// then there's an appropriate Vary header set by whatever set
432
			// their non-default language.
433
			wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
434
				"'anon-public-user-private' due to uselang=user\n" );
435
			$mode = 'anon-public-user-private';
436
		}
437
438
		wfDebug( __METHOD__ . ": setting cache mode $mode\n" );
439
		$this->mCacheMode = $mode;
440
	}
441
442
	/**
443
	 * Set directives (key/value pairs) for the Cache-Control header.
444
	 * Boolean values will be formatted as such, by including or omitting
445
	 * without an equals sign.
446
	 *
447
	 * Cache control values set here will only be used if the cache mode is not
448
	 * private, see setCacheMode().
449
	 *
450
	 * @param array $directives
451
	 */
452
	public function setCacheControl( $directives ) {
453
		$this->mCacheControl = $directives + $this->mCacheControl;
454
	}
455
456
	/**
457
	 * Create an instance of an output formatter by its name
458
	 *
459
	 * @param string $format
460
	 *
461
	 * @return ApiFormatBase
462
	 */
463
	public function createPrinterByName( $format ) {
464
		$printer = $this->mModuleMgr->getModule( $format, 'format' );
465
		if ( $printer === null ) {
466
			$this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
467
		}
468
469
		return $printer;
470
	}
471
472
	/**
473
	 * Execute api request. Any errors will be handled if the API was called by the remote client.
474
	 */
475
	public function execute() {
476
		if ( $this->mInternalMode ) {
477
			$this->executeAction();
478
		} else {
479
			$this->executeActionWithErrorHandling();
480
		}
481
	}
482
483
	/**
484
	 * Execute an action, and in case of an error, erase whatever partial results
485
	 * have been accumulated, and replace it with an error message and a help screen.
486
	 */
487
	protected function executeActionWithErrorHandling() {
488
		// Verify the CORS header before executing the action
489
		if ( !$this->handleCORS() ) {
490
			// handleCORS() has sent a 403, abort
491
			return;
492
		}
493
494
		// Exit here if the request method was OPTIONS
495
		// (assume there will be a followup GET or POST)
496
		if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
497
			return;
498
		}
499
500
		// In case an error occurs during data output,
501
		// clear the output buffer and print just the error information
502
		$obLevel = ob_get_level();
503
		ob_start();
504
505
		$t = microtime( true );
506
		$isError = false;
507
		try {
508
			$this->executeAction();
509
			$runTime = microtime( true ) - $t;
510
			$this->logRequest( $runTime );
511
			if ( $this->mModule->isWriteMode() && $this->getRequest()->wasPosted() ) {
512
				$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...
513
					'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime
514
				);
515
			}
516
		} catch ( Exception $e ) {
517
			$this->handleException( $e );
518
			$this->logRequest( microtime( true ) - $t, $e );
519
			$isError = true;
520
		}
521
522
		// Commit DBs and send any related cookies and headers
523
		MediaWiki::preOutputCommit( $this->getContext() );
524
525
		// Send cache headers after any code which might generate an error, to
526
		// avoid sending public cache headers for errors.
527
		$this->sendCacheHeaders( $isError );
528
529
		// Executing the action might have already messed with the output
530
		// buffers.
531
		while ( ob_get_level() > $obLevel ) {
532
			ob_end_flush();
533
		}
534
	}
535
536
	/**
537
	 * Handle an exception as an API response
538
	 *
539
	 * @since 1.23
540
	 * @param Exception $e
541
	 */
542
	protected function handleException( Exception $e ) {
543
		// Bug 63145: Rollback any open database transactions
544
		if ( !( $e instanceof UsageException ) ) {
545
			// UsageExceptions are intentional, so don't rollback if that's the case
546
			try {
547
				MWExceptionHandler::rollbackMasterChangesAndLog( $e );
548
			} catch ( DBError $e2 ) {
549
				// Rollback threw an exception too. Log it, but don't interrupt
550
				// our regularly scheduled exception handling.
551
				MWExceptionHandler::logException( $e2 );
552
			}
553
		}
554
555
		// Allow extra cleanup and logging
556
		Hooks::run( 'ApiMain::onException', [ $this, $e ] );
557
558
		// Log it
559
		if ( !( $e instanceof UsageException ) ) {
560
			MWExceptionHandler::logException( $e );
561
		}
562
563
		// Handle any kind of exception by outputting properly formatted error message.
564
		// If this fails, an unhandled exception should be thrown so that global error
565
		// handler will process and log it.
566
567
		$errCode = $this->substituteResultWithError( $e );
568
569
		// Error results should not be cached
570
		$this->setCacheMode( 'private' );
571
572
		$response = $this->getRequest()->response();
573
		$headerStr = 'MediaWiki-API-Error: ' . $errCode;
574
		if ( $e->getCode() === 0 ) {
575
			$response->header( $headerStr );
576
		} else {
577
			$response->header( $headerStr, true, $e->getCode() );
578
		}
579
580
		// Reset and print just the error message
581
		ob_clean();
582
583
		// Printer may not be initialized if the extractRequestParams() fails for the main module
584
		$this->createErrorPrinter();
585
586
		try {
587
			$this->printResult( true );
588
		} catch ( UsageException $ex ) {
589
			// The error printer itself is failing. Try suppressing its request
590
			// parameters and redo.
591
			$this->setWarning(
592
				'Error printer failed (will retry without params): ' . $ex->getMessage()
593
			);
594
			$this->mPrinter = null;
595
			$this->createErrorPrinter();
596
			$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...
597
			$this->printResult( true );
598
		}
599
	}
600
601
	/**
602
	 * Handle an exception from the ApiBeforeMain hook.
603
	 *
604
	 * This tries to print the exception as an API response, to be more
605
	 * friendly to clients. If it fails, it will rethrow the exception.
606
	 *
607
	 * @since 1.23
608
	 * @param Exception $e
609
	 * @throws Exception
610
	 */
611
	public static function handleApiBeforeMainException( Exception $e ) {
612
		ob_start();
613
614
		try {
615
			$main = new self( RequestContext::getMain(), false );
616
			$main->handleException( $e );
617
			$main->logRequest( 0, $e );
618
		} catch ( Exception $e2 ) {
619
			// Nope, even that didn't work. Punt.
620
			throw $e;
621
		}
622
623
		// Reset cache headers
624
		$main->sendCacheHeaders( true );
625
626
		ob_end_flush();
627
	}
628
629
	/**
630
	 * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
631
	 *
632
	 * If no origin parameter is present, nothing happens.
633
	 * If an origin parameter is present but doesn't match the Origin header, a 403 status code
634
	 * is set and false is returned.
635
	 * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
636
	 * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
637
	 * headers are set.
638
	 * http://www.w3.org/TR/cors/#resource-requests
639
	 * http://www.w3.org/TR/cors/#resource-preflight-requests
640
	 *
641
	 * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
642
	 */
643
	protected function handleCORS() {
644
		$originParam = $this->getParameter( 'origin' ); // defaults to null
645
		if ( $originParam === null ) {
646
			// No origin parameter, nothing to do
647
			return true;
648
		}
649
650
		$request = $this->getRequest();
651
		$response = $request->response();
652
653
		$matchOrigin = false;
0 ignored issues
show
Unused Code introduced by
$matchOrigin 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...
654
		$allowTiming = false;
655
		$varyOrigin = true;
656
657
		if ( $originParam === '*' ) {
658
			// Request for anonymous CORS
659
			$matchOrigin = true;
660
			$allowOrigin = '*';
661
			$allowCredentials = 'false';
662
			$varyOrigin = false; // No need to vary
663
		} else {
664
			// Non-anonymous CORS, check we allow the domain
665
666
			// Origin: header is a space-separated list of origins, check all of them
667
			$originHeader = $request->getHeader( 'Origin' );
668 View Code Duplication
			if ( $originHeader === false ) {
669
				$origins = [];
670
			} else {
671
				$originHeader = trim( $originHeader );
672
				$origins = preg_split( '/\s+/', $originHeader );
673
			}
674
675
			if ( !in_array( $originParam, $origins ) ) {
676
				// origin parameter set but incorrect
677
				// Send a 403 response
678
				$response->statusHeader( 403 );
679
				$response->header( 'Cache-Control: no-cache' );
680
				echo "'origin' parameter does not match Origin header\n";
681
682
				return false;
683
			}
684
685
			$config = $this->getConfig();
686
			$matchOrigin = count( $origins ) === 1 && self::matchOrigin(
687
				$originParam,
688
				$config->get( 'CrossSiteAJAXdomains' ),
689
				$config->get( 'CrossSiteAJAXdomainExceptions' )
690
			);
691
692
			$allowOrigin = $originHeader;
693
			$allowCredentials = 'true';
694
			$allowTiming = $originHeader;
695
		}
696
697
		if ( $matchOrigin ) {
698
			$requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
699
			$preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
700
			if ( $preflight ) {
701
				// This is a CORS preflight request
702
				if ( $requestedMethod !== 'POST' && $requestedMethod !== 'GET' ) {
703
					// If method is not a case-sensitive match, do not set any additional headers and terminate.
704
					return true;
705
				}
706
				// We allow the actual request to send the following headers
707
				$requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
708
				if ( $requestedHeaders !== false ) {
709
					if ( !self::matchRequestedHeaders( $requestedHeaders ) ) {
710
						return true;
711
					}
712
					$response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
713
				}
714
715
				// We only allow the actual request to be GET or POST
716
				$response->header( 'Access-Control-Allow-Methods: POST, GET' );
717
			}
718
719
			$response->header( "Access-Control-Allow-Origin: $allowOrigin" );
720
			$response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
721
			// http://www.w3.org/TR/resource-timing/#timing-allow-origin
722
			if ( $allowTiming !== false ) {
723
				$response->header( "Timing-Allow-Origin: $allowTiming" );
724
			}
725
726
			if ( !$preflight ) {
727
				$response->header(
728
					'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag'
729
				);
730
			}
731
		}
732
733
		if ( $varyOrigin ) {
734
			$this->getOutput()->addVaryHeader( 'Origin' );
735
		}
736
737
		return true;
738
	}
739
740
	/**
741
	 * Attempt to match an Origin header against a set of rules and a set of exceptions
742
	 * @param string $value Origin header
743
	 * @param array $rules Set of wildcard rules
744
	 * @param array $exceptions Set of wildcard rules
745
	 * @return bool True if $value matches a rule in $rules and doesn't match
746
	 *    any rules in $exceptions, false otherwise
747
	 */
748
	protected static function matchOrigin( $value, $rules, $exceptions ) {
749
		foreach ( $rules as $rule ) {
750
			if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
751
				// Rule matches, check exceptions
752
				foreach ( $exceptions as $exc ) {
753
					if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
754
						return false;
755
					}
756
				}
757
758
				return true;
759
			}
760
		}
761
762
		return false;
763
	}
764
765
	/**
766
	 * Attempt to validate the value of Access-Control-Request-Headers against a list
767
	 * of headers that we allow the follow up request to send.
768
	 *
769
	 * @param string $requestedHeaders Comma seperated list of HTTP headers
770
	 * @return bool True if all requested headers are in the list of allowed headers
771
	 */
772
	protected static function matchRequestedHeaders( $requestedHeaders ) {
773
		if ( trim( $requestedHeaders ) === '' ) {
774
			return true;
775
		}
776
		$requestedHeaders = explode( ',', $requestedHeaders );
777
		$allowedAuthorHeaders = array_flip( [
778
			/* simple headers (see spec) */
779
			'accept',
780
			'accept-language',
781
			'content-language',
782
			'content-type',
783
			/* non-authorable headers in XHR, which are however requested by some UAs */
784
			'accept-encoding',
785
			'dnt',
786
			'origin',
787
			/* MediaWiki whitelist */
788
			'api-user-agent',
789
		] );
790
		foreach ( $requestedHeaders as $rHeader ) {
791
			$rHeader = strtolower( trim( $rHeader ) );
792
			if ( !isset( $allowedAuthorHeaders[$rHeader] ) ) {
793
				wfDebugLog( 'api', 'CORS preflight failed on requested header: ' . $rHeader );
794
				return false;
795
			}
796
		}
797
		return true;
798
	}
799
800
	/**
801
	 * Helper function to convert wildcard string into a regex
802
	 * '*' => '.*?'
803
	 * '?' => '.'
804
	 *
805
	 * @param string $wildcard String with wildcards
806
	 * @return string Regular expression
807
	 */
808
	protected static function wildcardToRegex( $wildcard ) {
809
		$wildcard = preg_quote( $wildcard, '/' );
810
		$wildcard = str_replace(
811
			[ '\*', '\?' ],
812
			[ '.*?', '.' ],
813
			$wildcard
814
		);
815
816
		return "/^https?:\/\/$wildcard$/";
817
	}
818
819
	/**
820
	 * Send caching headers
821
	 * @param bool $isError Whether an error response is being output
822
	 * @since 1.26 added $isError parameter
823
	 */
824
	protected function sendCacheHeaders( $isError ) {
825
		$response = $this->getRequest()->response();
826
		$out = $this->getOutput();
827
828
		$out->addVaryHeader( 'Treat-as-Untrusted' );
829
830
		$config = $this->getConfig();
831
832
		if ( $config->get( 'VaryOnXFP' ) ) {
833
			$out->addVaryHeader( 'X-Forwarded-Proto' );
834
		}
835
836
		if ( !$isError && $this->mModule &&
837
			( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
838
		) {
839
			$etag = $this->mModule->getConditionalRequestData( 'etag' );
840
			if ( $etag !== null ) {
841
				$response->header( "ETag: $etag" );
842
			}
843
			$lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
844
			if ( $lastMod !== null ) {
845
				$response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
846
			}
847
		}
848
849
		// The logic should be:
850
		// $this->mCacheControl['max-age'] is set?
851
		//    Use it, the module knows better than our guess.
852
		// !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
853
		//    Use 0 because we can guess caching is probably the wrong thing to do.
854
		// Use $this->getParameter( 'maxage' ), which already defaults to 0.
855
		$maxage = 0;
856
		if ( isset( $this->mCacheControl['max-age'] ) ) {
857
			$maxage = $this->mCacheControl['max-age'];
858
		} elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
859
			$this->mCacheMode !== 'private'
860
		) {
861
			$maxage = $this->getParameter( 'maxage' );
862
		}
863
		$privateCache = 'private, must-revalidate, max-age=' . $maxage;
864
865
		if ( $this->mCacheMode == 'private' ) {
866
			$response->header( "Cache-Control: $privateCache" );
867
			return;
868
		}
869
870
		$useKeyHeader = $config->get( 'UseKeyHeader' );
871
		if ( $this->mCacheMode == 'anon-public-user-private' ) {
872
			$out->addVaryHeader( 'Cookie' );
873
			$response->header( $out->getVaryHeader() );
874
			if ( $useKeyHeader ) {
875
				$response->header( $out->getKeyHeader() );
876
				if ( $out->haveCacheVaryCookies() ) {
877
					// Logged in, mark this request private
878
					$response->header( "Cache-Control: $privateCache" );
879
					return;
880
				}
881
				// Logged out, send normal public headers below
882
			} elseif ( MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() ) {
883
				// Logged in or otherwise has session (e.g. anonymous users who have edited)
884
				// Mark request private
885
				$response->header( "Cache-Control: $privateCache" );
886
887
				return;
888
			} // else no Key and anonymous, send public headers below
889
		}
890
891
		// Send public headers
892
		$response->header( $out->getVaryHeader() );
893
		if ( $useKeyHeader ) {
894
			$response->header( $out->getKeyHeader() );
895
		}
896
897
		// If nobody called setCacheMaxAge(), use the (s)maxage parameters
898
		if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
899
			$this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
900
		}
901
		if ( !isset( $this->mCacheControl['max-age'] ) ) {
902
			$this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
903
		}
904
905
		if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
906
			// Public cache not requested
907
			// Sending a Vary header in this case is harmless, and protects us
908
			// against conditional calls of setCacheMaxAge().
909
			$response->header( "Cache-Control: $privateCache" );
910
911
			return;
912
		}
913
914
		$this->mCacheControl['public'] = true;
915
916
		// Send an Expires header
917
		$maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
918
		$expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
919
		$response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
920
921
		// Construct the Cache-Control header
922
		$ccHeader = '';
923
		$separator = '';
924
		foreach ( $this->mCacheControl as $name => $value ) {
925
			if ( is_bool( $value ) ) {
926
				if ( $value ) {
927
					$ccHeader .= $separator . $name;
928
					$separator = ', ';
929
				}
930
			} else {
931
				$ccHeader .= $separator . "$name=$value";
932
				$separator = ', ';
933
			}
934
		}
935
936
		$response->header( "Cache-Control: $ccHeader" );
937
	}
938
939
	/**
940
	 * Create the printer for error output
941
	 */
942
	private function createErrorPrinter() {
943
		if ( !isset( $this->mPrinter ) ) {
944
			$value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
945
			if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
946
				$value = self::API_DEFAULT_FORMAT;
947
			}
948
			$this->mPrinter = $this->createPrinterByName( $value );
949
		}
950
951
		// Printer may not be able to handle errors. This is particularly
952
		// likely if the module returns something for getCustomPrinter().
953
		if ( !$this->mPrinter->canPrintErrors() ) {
954
			$this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
955
		}
956
	}
957
958
	/**
959
	 * Create an error message for the given exception.
960
	 *
961
	 * If the exception is a UsageException then
962
	 * UsageException::getMessageArray() will be called to create the message.
963
	 *
964
	 * @param Exception $e
965
	 * @return array ['code' => 'some string', 'info' => 'some other string']
966
	 * @since 1.27
967
	 */
968
	protected function errorMessageFromException( $e ) {
969
		if ( $e instanceof UsageException ) {
970
			// User entered incorrect parameters - generate error response
971
			$errMessage = $e->getMessageArray();
972
		} else {
973
			$config = $this->getConfig();
974
			// Something is seriously wrong
975
			if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) {
976
				$info = 'Database query error';
977
			} else {
978
				$info = "Exception Caught: {$e->getMessage()}";
979
			}
980
981
			$errMessage = [
982
				'code' => 'internal_api_error_' . get_class( $e ),
983
				'info' => '[' . WebRequest::getRequestId() . '] ' . $info,
984
			];
985
		}
986
		return $errMessage;
987
	}
988
989
	/**
990
	 * Replace the result data with the information about an exception.
991
	 * Returns the error code
992
	 * @param Exception $e
993
	 * @return string
994
	 */
995
	protected function substituteResultWithError( $e ) {
996
		$result = $this->getResult();
997
		$config = $this->getConfig();
998
999
		$errMessage = $this->errorMessageFromException( $e );
1000
		if ( $e instanceof UsageException ) {
1001
			// User entered incorrect parameters - generate error response
1002
			$link = wfExpandUrl( wfScript( 'api' ) );
1003
			ApiResult::setContentValue( $errMessage, 'docref', "See $link for API usage" );
1004
		} else {
1005
			// Something is seriously wrong
1006
			if ( $config->get( 'ShowExceptionDetails' ) ) {
1007
				ApiResult::setContentValue(
1008
					$errMessage,
1009
					'trace',
1010
					MWExceptionHandler::getRedactedTraceAsString( $e )
1011
				);
1012
			}
1013
		}
1014
1015
		// Remember all the warnings to re-add them later
1016
		$warnings = $result->getResultData( [ 'warnings' ] );
1017
1018
		$result->reset();
1019
		// Re-add the id
1020
		$requestid = $this->getParameter( 'requestid' );
1021
		if ( !is_null( $requestid ) ) {
1022
			$result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
1023
		}
1024
		if ( $config->get( 'ShowHostnames' ) ) {
1025
			// servedby is especially useful when debugging errors
1026
			$result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
1027
		}
1028
		if ( $warnings !== null ) {
1029
			$result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
1030
		}
1031
1032
		$result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK );
1033
1034
		return $errMessage['code'];
1035
	}
1036
1037
	/**
1038
	 * Set up for the execution.
1039
	 * @return array
1040
	 */
1041
	protected function setupExecuteAction() {
1042
		// First add the id to the top element
1043
		$result = $this->getResult();
1044
		$requestid = $this->getParameter( 'requestid' );
1045
		if ( !is_null( $requestid ) ) {
1046
			$result->addValue( null, 'requestid', $requestid );
1047
		}
1048
1049
		if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1050
			$servedby = $this->getParameter( 'servedby' );
1051
			if ( $servedby ) {
1052
				$result->addValue( null, 'servedby', wfHostname() );
1053
			}
1054
		}
1055
1056
		if ( $this->getParameter( 'curtimestamp' ) ) {
1057
			$result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ),
1058
				ApiResult::NO_SIZE_CHECK );
1059
		}
1060
1061
		$params = $this->extractRequestParams();
1062
1063
		$this->mAction = $params['action'];
1064
1065
		if ( !is_string( $this->mAction ) ) {
1066
			$this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
1067
		}
1068
1069
		return $params;
1070
	}
1071
1072
	/**
1073
	 * Set up the module for response
1074
	 * @return ApiBase The module that will handle this action
1075
	 * @throws MWException
1076
	 * @throws UsageException
1077
	 */
1078
	protected function setupModule() {
1079
		// Instantiate the module requested by the user
1080
		$module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
1081
		if ( $module === null ) {
1082
			$this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
1083
		}
1084
		$moduleParams = $module->extractRequestParams();
1085
1086
		// Check token, if necessary
1087
		if ( $module->needsToken() === true ) {
1088
			throw new MWException(
1089
				"Module '{$module->getModuleName()}' must be updated for the new token handling. " .
1090
				'See documentation for ApiBase::needsToken for details.'
1091
			);
1092
		}
1093
		if ( $module->needsToken() ) {
1094
			if ( !$module->mustBePosted() ) {
1095
				throw new MWException(
1096
					"Module '{$module->getModuleName()}' must require POST to use tokens."
1097
				);
1098
			}
1099
1100
			if ( !isset( $moduleParams['token'] ) ) {
1101
				$this->dieUsageMsg( [ 'missingparam', 'token' ] );
1102
			}
1103
1104
			if ( !$this->getConfig()->get( 'DebugAPI' ) &&
1105
				array_key_exists(
1106
					$module->encodeParamName( 'token' ),
1107
					$this->getRequest()->getQueryValues()
1108
				)
1109
			) {
1110
				$this->dieUsage(
1111
					"The '{$module->encodeParamName( 'token' )}' parameter was " .
1112
						'found in the query string, but must be in the POST body',
1113
					'mustposttoken'
1114
				);
1115
			}
1116
1117
			if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
1118
				$this->dieUsageMsg( 'sessionfailure' );
1119
			}
1120
		}
1121
1122
		return $module;
1123
	}
1124
1125
	/**
1126
	 * Check the max lag if necessary
1127
	 * @param ApiBase $module Api module being used
1128
	 * @param array $params Array an array containing the request parameters.
1129
	 * @return bool True on success, false should exit immediately
1130
	 */
1131
	protected function checkMaxLag( $module, $params ) {
1132
		if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
1133
			$maxLag = $params['maxlag'];
1134
			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...
1135
			if ( $lag > $maxLag ) {
1136
				$response = $this->getRequest()->response();
1137
1138
				$response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
1139
				$response->header( 'X-Database-Lag: ' . intval( $lag ) );
1140
1141
				if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1142
					$this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
1143
				}
1144
1145
				$this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
1146
			}
1147
		}
1148
1149
		return true;
1150
	}
1151
1152
	/**
1153
	 * Check selected RFC 7232 precondition headers
1154
	 *
1155
	 * RFC 7232 envisions a particular model where you send your request to "a
1156
	 * resource", and for write requests that you can read "the resource" by
1157
	 * changing the method to GET. When the API receives a GET request, it
1158
	 * works out even though "the resource" from RFC 7232's perspective might
1159
	 * be many resources from MediaWiki's perspective. But it totally fails for
1160
	 * a POST, since what HTTP sees as "the resource" is probably just
1161
	 * "/api.php" with all the interesting bits in the body.
1162
	 *
1163
	 * Therefore, we only support RFC 7232 precondition headers for GET (and
1164
	 * HEAD). That means we don't need to bother with If-Match and
1165
	 * If-Unmodified-Since since they only apply to modification requests.
1166
	 *
1167
	 * And since we don't support Range, If-Range is ignored too.
1168
	 *
1169
	 * @since 1.26
1170
	 * @param ApiBase $module Api module being used
1171
	 * @return bool True on success, false should exit immediately
1172
	 */
1173
	protected function checkConditionalRequestHeaders( $module ) {
1174
		if ( $this->mInternalMode ) {
1175
			// No headers to check in internal mode
1176
			return true;
1177
		}
1178
1179
		if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
1180
			// Don't check POSTs
1181
			return true;
1182
		}
1183
1184
		$return304 = false;
1185
1186
		$ifNoneMatch = array_diff(
1187
			$this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: [],
1188
			[ '' ]
1189
		);
1190
		if ( $ifNoneMatch ) {
1191
			if ( $ifNoneMatch === [ '*' ] ) {
1192
				// API responses always "exist"
1193
				$etag = '*';
1194
			} else {
1195
				$etag = $module->getConditionalRequestData( 'etag' );
1196
			}
1197
		}
1198
		if ( $ifNoneMatch && $etag !== null ) {
1199
			$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...
1200
			$match = array_map( function ( $s ) {
1201
				return substr( $s, 0, 2 ) === 'W/' ? substr( $s, 2 ) : $s;
1202
			}, $ifNoneMatch );
1203
			$return304 = in_array( $test, $match, true );
1204
		} else {
1205
			$value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) );
1206
1207
			// Some old browsers sends sizes after the date, like this:
1208
			//  Wed, 20 Aug 2003 06:51:19 GMT; length=5202
1209
			// Ignore that.
1210
			$i = strpos( $value, ';' );
1211
			if ( $i !== false ) {
1212
				$value = trim( substr( $value, 0, $i ) );
1213
			}
1214
1215
			if ( $value !== '' ) {
1216
				try {
1217
					$ts = new MWTimestamp( $value );
1218
					if (
1219
						// RFC 7231 IMF-fixdate
1220
						$ts->getTimestamp( TS_RFC2822 ) === $value ||
1221
						// RFC 850
1222
						$ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value ||
1223
						// asctime (with and without space-padded day)
1224
						$ts->format( 'D M j H:i:s Y' ) === $value ||
1225
						$ts->format( 'D M  j H:i:s Y' ) === $value
1226
					) {
1227
						$lastMod = $module->getConditionalRequestData( 'last-modified' );
1228
						if ( $lastMod !== null ) {
1229
							// Mix in some MediaWiki modification times
1230
							$modifiedTimes = [
1231
								'page' => $lastMod,
1232
								'user' => $this->getUser()->getTouched(),
1233
								'epoch' => $this->getConfig()->get( 'CacheEpoch' ),
1234
							];
1235
							if ( $this->getConfig()->get( 'UseSquid' ) ) {
1236
								// T46570: the core page itself may not change, but resources might
1237
								$modifiedTimes['sepoch'] = wfTimestamp(
1238
									TS_MW, time() - $this->getConfig()->get( 'SquidMaxage' )
1239
								);
1240
							}
1241
							Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this->getOutput() ] );
1242
							$lastMod = max( $modifiedTimes );
1243
							$return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
1244
						}
1245
					}
1246
				} catch ( TimestampException $e ) {
1247
					// Invalid timestamp, ignore it
1248
				}
1249
			}
1250
		}
1251
1252
		if ( $return304 ) {
1253
			$this->getRequest()->response()->statusHeader( 304 );
1254
1255
			// Avoid outputting the compressed representation of a zero-length body
1256
			MediaWiki\suppressWarnings();
1257
			ini_set( 'zlib.output_compression', 0 );
1258
			MediaWiki\restoreWarnings();
1259
			wfClearOutputBuffers();
1260
1261
			return false;
1262
		}
1263
1264
		return true;
1265
	}
1266
1267
	/**
1268
	 * Check for sufficient permissions to execute
1269
	 * @param ApiBase $module An Api module
1270
	 */
1271
	protected function checkExecutePermissions( $module ) {
1272
		$user = $this->getUser();
1273
		if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
1274
			!$user->isAllowed( 'read' )
1275
		) {
1276
			$this->dieUsageMsg( 'readrequired' );
1277
		}
1278
1279
		if ( $module->isWriteMode() ) {
1280
			if ( !$this->mEnableWrite ) {
1281
				$this->dieUsageMsg( 'writedisabled' );
1282
			} elseif ( !$user->isAllowed( 'writeapi' ) ) {
1283
				$this->dieUsageMsg( 'writerequired' );
1284
			} elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
1285
				$this->dieUsage(
1286
					'Promise-Non-Write-API-Action HTTP header cannot be sent to write API modules',
1287
					'promised-nonwrite-api'
1288
				);
1289
			}
1290
1291
			$this->checkReadOnly( $module );
1292
		}
1293
1294
		// Allow extensions to stop execution for arbitrary reasons.
1295
		$message = false;
1296
		if ( !Hooks::run( 'ApiCheckCanExecute', [ $module, $user, &$message ] ) ) {
1297
			$this->dieUsageMsg( $message );
1298
		}
1299
	}
1300
1301
	/**
1302
	 * Check if the DB is read-only for this user
1303
	 * @param ApiBase $module An Api module
1304
	 */
1305
	protected function checkReadOnly( $module ) {
1306
		if ( wfReadOnly() ) {
1307
			$this->dieReadOnly();
1308
		}
1309
1310
		if ( $module->isWriteMode()
1311
			&& in_array( 'bot', $this->getUser()->getGroups() )
1312
			&& 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...
1313
		) {
1314
			$this->checkBotReadOnly();
1315
		}
1316
	}
1317
1318
	/**
1319
	 * Check whether we are readonly for bots
1320
	 */
1321
	private function checkBotReadOnly() {
1322
		// Figure out how many servers have passed the lag threshold
1323
		$numLagged = 0;
1324
		$lagLimit = $this->getConfig()->get( 'APIMaxLagThreshold' );
1325
		$laggedServers = [];
1326
		$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...
1327
		foreach ( $loadBalancer->getLagTimes() as $serverIndex => $lag ) {
1328
			if ( $lag > $lagLimit ) {
1329
				++$numLagged;
1330
				$laggedServers[] = $loadBalancer->getServerName( $serverIndex ) . " ({$lag}s)";
1331
			}
1332
		}
1333
1334
		// If a majority of slaves are too lagged then disallow writes
1335
		$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...
1336
		if ( $numLagged >= ceil( $slaveCount / 2 ) ) {
1337
			$laggedServers = implode( ', ', $laggedServers );
1338
			wfDebugLog(
1339
				'api-readonly',
1340
				"Api request failed as read only because the following DBs are lagged: $laggedServers"
1341
			);
1342
1343
			$parsed = $this->parseMsg( [ 'readonlytext' ] );
1344
			$this->dieUsage(
1345
				$parsed['info'],
1346
				$parsed['code'],
1347
				/* http error */
1348
				0,
1349
				[ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
1350
			);
1351
		}
1352
	}
1353
1354
	/**
1355
	 * Check asserts of the user's rights
1356
	 * @param array $params
1357
	 */
1358
	protected function checkAsserts( $params ) {
1359
		if ( isset( $params['assert'] ) ) {
1360
			$user = $this->getUser();
1361
			switch ( $params['assert'] ) {
1362
				case 'user':
1363
					if ( $user->isAnon() ) {
1364
						$this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' );
1365
					}
1366
					break;
1367
				case 'bot':
1368
					if ( !$user->isAllowed( 'bot' ) ) {
1369
						$this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' );
1370
					}
1371
					break;
1372
			}
1373
		}
1374
	}
1375
1376
	/**
1377
	 * Check POST for external response and setup result printer
1378
	 * @param ApiBase $module An Api module
1379
	 * @param array $params An array with the request parameters
1380
	 */
1381
	protected function setupExternalResponse( $module, $params ) {
1382
		$request = $this->getRequest();
1383
		if ( !$request->wasPosted() && $module->mustBePosted() ) {
1384
			// Module requires POST. GET request might still be allowed
1385
			// if $wgDebugApi is true, otherwise fail.
1386
			$this->dieUsageMsgOrDebug( [ 'mustbeposted', $this->mAction ] );
1387
		}
1388
1389
		// See if custom printer is used
1390
		$this->mPrinter = $module->getCustomPrinter();
1391
		if ( is_null( $this->mPrinter ) ) {
1392
			// Create an appropriate printer
1393
			$this->mPrinter = $this->createPrinterByName( $params['format'] );
1394
		}
1395
1396
		if ( $request->getProtocol() === 'http' && (
1397
			$request->getSession()->shouldForceHTTPS() ||
1398
			( $this->getUser()->isLoggedIn() &&
1399
				$this->getUser()->requiresHTTPS() )
1400
		) ) {
1401
			$this->logFeatureUsage( 'https-expected' );
1402
			$this->setWarning( 'HTTP used when HTTPS was expected' );
1403
		}
1404
	}
1405
1406
	/**
1407
	 * Execute the actual module, without any error handling
1408
	 */
1409
	protected function executeAction() {
1410
		$params = $this->setupExecuteAction();
1411
		$module = $this->setupModule();
1412
		$this->mModule = $module;
1413
1414
		if ( !$this->mInternalMode ) {
1415
			$this->setRequestExpectations( $module );
0 ignored issues
show
Bug introduced by
It seems like $module defined by $this->setupModule() on line 1411 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...
1416
		}
1417
1418
		$this->checkExecutePermissions( $module );
0 ignored issues
show
Bug introduced by
It seems like $module defined by $this->setupModule() on line 1411 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...
1419
1420
		if ( !$this->checkMaxLag( $module, $params ) ) {
0 ignored issues
show
Bug introduced by
It seems like $module defined by $this->setupModule() on line 1411 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...
1421
			return;
1422
		}
1423
1424
		if ( !$this->checkConditionalRequestHeaders( $module ) ) {
0 ignored issues
show
Bug introduced by
It seems like $module defined by $this->setupModule() on line 1411 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...
1425
			return;
1426
		}
1427
1428
		if ( !$this->mInternalMode ) {
1429
			$this->setupExternalResponse( $module, $params );
0 ignored issues
show
Bug introduced by
It seems like $module defined by $this->setupModule() on line 1411 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...
1430
		}
1431
1432
		$this->checkAsserts( $params );
1433
1434
		// Execute
1435
		$module->execute();
1436
		Hooks::run( 'APIAfterExecute', [ &$module ] );
1437
1438
		$this->reportUnusedParams();
1439
1440
		if ( !$this->mInternalMode ) {
1441
			// append Debug information
1442
			MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
1443
1444
			// Print result data
1445
			$this->printResult( false );
1446
		}
1447
	}
1448
1449
	/**
1450
	 * Set database connection, query, and write expectations given this module request
1451
	 * @param ApiBase $module
1452
	 */
1453
	protected function setRequestExpectations( ApiBase $module ) {
1454
		$limits = $this->getConfig()->get( 'TrxProfilerLimits' );
1455
		$trxProfiler = Profiler::instance()->getTransactionProfiler();
1456
		if ( $this->getRequest()->hasSafeMethod() ) {
1457
			$trxProfiler->setExpectations( $limits['GET'], __METHOD__ );
1458
		} elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) {
1459
			$trxProfiler->setExpectations( $limits['POST-nonwrite'], __METHOD__ );
1460
			$this->getRequest()->markAsSafeRequest();
1461
		} else {
1462
			$trxProfiler->setExpectations( $limits['POST'], __METHOD__ );
1463
		}
1464
	}
1465
1466
	/**
1467
	 * Log the preceding request
1468
	 * @param float $time Time in seconds
1469
	 * @param Exception $e Exception caught while processing the request
1470
	 */
1471
	protected function logRequest( $time, $e = null ) {
1472
		$request = $this->getRequest();
1473
		$logCtx = [
1474
			'ts' => time(),
1475
			'ip' => $request->getIP(),
1476
			'userAgent' => $this->getUserAgent(),
1477
			'wiki' => wfWikiID(),
1478
			'timeSpentBackend' => (int) round( $time * 1000 ),
1479
			'hadError' => $e !== null,
1480
			'errorCodes' => [],
1481
			'params' => [],
1482
		];
1483
1484
		if ( $e ) {
1485
			$logCtx['errorCodes'][] = $this->errorMessageFromException( $e )['code'];
1486
		}
1487
1488
		// Construct space separated message for 'api' log channel
1489
		$msg = "API {$request->getMethod()} " .
1490
			wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
1491
			" {$logCtx['ip']} " .
1492
			"T={$logCtx['timeSpentBackend']}ms";
1493
1494
		foreach ( $this->getParamsUsed() as $name ) {
1495
			$value = $request->getVal( $name );
1496
			if ( $value === null ) {
1497
				continue;
1498
			}
1499
1500
			if ( strlen( $value ) > 256 ) {
1501
				$value = substr( $value, 0, 256 );
1502
				$encValue = $this->encodeRequestLogValue( $value ) . '[...]';
1503
			} else {
1504
				$encValue = $this->encodeRequestLogValue( $value );
1505
			}
1506
1507
			$logCtx['params'][$name] = $value;
1508
			$msg .= " {$name}={$encValue}";
1509
		}
1510
1511
		wfDebugLog( 'api', $msg, 'private' );
1512
		// ApiAction channel is for structured data consumers
1513
		wfDebugLog( 'ApiAction', '', 'private', $logCtx );
1514
	}
1515
1516
	/**
1517
	 * Encode a value in a format suitable for a space-separated log line.
1518
	 * @param string $s
1519
	 * @return string
1520
	 */
1521
	protected function encodeRequestLogValue( $s ) {
1522
		static $table;
1523
		if ( !$table ) {
1524
			$chars = ';@$!*(),/:';
1525
			$numChars = strlen( $chars );
1526
			for ( $i = 0; $i < $numChars; $i++ ) {
1527
				$table[rawurlencode( $chars[$i] )] = $chars[$i];
1528
			}
1529
		}
1530
1531
		return strtr( rawurlencode( $s ), $table );
1532
	}
1533
1534
	/**
1535
	 * Get the request parameters used in the course of the preceding execute() request
1536
	 * @return array
1537
	 */
1538
	protected function getParamsUsed() {
1539
		return array_keys( $this->mParamsUsed );
1540
	}
1541
1542
	/**
1543
	 * Mark parameters as used
1544
	 * @param string|string[] $params
1545
	 */
1546
	public function markParamsUsed( $params ) {
1547
		$this->mParamsUsed += array_fill_keys( (array)$params, true );
1548
	}
1549
1550
	/**
1551
	 * Get a request value, and register the fact that it was used, for logging.
1552
	 * @param string $name
1553
	 * @param mixed $default
1554
	 * @return mixed
1555
	 */
1556
	public function getVal( $name, $default = null ) {
1557
		$this->mParamsUsed[$name] = true;
1558
1559
		$ret = $this->getRequest()->getVal( $name );
1560
		if ( $ret === null ) {
1561
			if ( $this->getRequest()->getArray( $name ) !== null ) {
1562
				// See bug 10262 for why we don't just implode( '|', ... ) the
1563
				// array.
1564
				$this->setWarning(
1565
					"Parameter '$name' uses unsupported PHP array syntax"
1566
				);
1567
			}
1568
			$ret = $default;
1569
		}
1570
		return $ret;
1571
	}
1572
1573
	/**
1574
	 * Get a boolean request value, and register the fact that the parameter
1575
	 * was used, for logging.
1576
	 * @param string $name
1577
	 * @return bool
1578
	 */
1579
	public function getCheck( $name ) {
1580
		return $this->getVal( $name, null ) !== null;
1581
	}
1582
1583
	/**
1584
	 * Get a request upload, and register the fact that it was used, for logging.
1585
	 *
1586
	 * @since 1.21
1587
	 * @param string $name Parameter name
1588
	 * @return WebRequestUpload
1589
	 */
1590
	public function getUpload( $name ) {
1591
		$this->mParamsUsed[$name] = true;
1592
1593
		return $this->getRequest()->getUpload( $name );
1594
	}
1595
1596
	/**
1597
	 * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,
1598
	 * for example in case of spelling mistakes or a missing 'g' prefix for generators.
1599
	 */
1600
	protected function reportUnusedParams() {
1601
		$paramsUsed = $this->getParamsUsed();
1602
		$allParams = $this->getRequest()->getValueNames();
1603
1604
		if ( !$this->mInternalMode ) {
1605
			// Printer has not yet executed; don't warn that its parameters are unused
1606
			$printerParams = array_map(
1607
				[ $this->mPrinter, 'encodeParamName' ],
1608
				array_keys( $this->mPrinter->getFinalParams() ?: [] )
1609
			);
1610
			$unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1611
		} else {
1612
			$unusedParams = array_diff( $allParams, $paramsUsed );
1613
		}
1614
1615
		if ( count( $unusedParams ) ) {
1616
			$s = count( $unusedParams ) > 1 ? 's' : '';
1617
			$this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
1618
		}
1619
	}
1620
1621
	/**
1622
	 * Print results using the current printer
1623
	 *
1624
	 * @param bool $isError
1625
	 */
1626
	protected function printResult( $isError ) {
1627
		if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
1628
			$this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
1629
		}
1630
1631
		$printer = $this->mPrinter;
1632
		$printer->initPrinter( false );
1633
		$printer->execute();
1634
		$printer->closePrinter();
1635
	}
1636
1637
	/**
1638
	 * @return bool
1639
	 */
1640
	public function isReadMode() {
1641
		return false;
1642
	}
1643
1644
	/**
1645
	 * See ApiBase for description.
1646
	 *
1647
	 * @return array
1648
	 */
1649
	public function getAllowedParams() {
1650
		return [
1651
			'action' => [
1652
				ApiBase::PARAM_DFLT => 'help',
1653
				ApiBase::PARAM_TYPE => 'submodule',
1654
			],
1655
			'format' => [
1656
				ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT,
1657
				ApiBase::PARAM_TYPE => 'submodule',
1658
			],
1659
			'maxlag' => [
1660
				ApiBase::PARAM_TYPE => 'integer'
1661
			],
1662
			'smaxage' => [
1663
				ApiBase::PARAM_TYPE => 'integer',
1664
				ApiBase::PARAM_DFLT => 0
1665
			],
1666
			'maxage' => [
1667
				ApiBase::PARAM_TYPE => 'integer',
1668
				ApiBase::PARAM_DFLT => 0
1669
			],
1670
			'assert' => [
1671
				ApiBase::PARAM_TYPE => [ 'user', 'bot' ]
1672
			],
1673
			'requestid' => null,
1674
			'servedby' => false,
1675
			'curtimestamp' => false,
1676
			'origin' => null,
1677
			'uselang' => [
1678
				ApiBase::PARAM_DFLT => 'user',
1679
			],
1680
		];
1681
	}
1682
1683
	/** @see ApiBase::getExamplesMessages() */
1684
	protected function getExamplesMessages() {
1685
		return [
1686
			'action=help'
1687
				=> 'apihelp-help-example-main',
1688
			'action=help&recursivesubmodules=1'
1689
				=> 'apihelp-help-example-recursive',
1690
		];
1691
	}
1692
1693
	public function modifyHelp( array &$help, array $options, array &$tocData ) {
1694
		// Wish PHP had an "array_insert_before". Instead, we have to manually
1695
		// reindex the array to get 'permissions' in the right place.
1696
		$oldHelp = $help;
1697
		$help = [];
1698
		foreach ( $oldHelp as $k => $v ) {
1699
			if ( $k === 'submodules' ) {
1700
				$help['permissions'] = '';
1701
			}
1702
			$help[$k] = $v;
1703
		}
1704
		$help['datatypes'] = '';
1705
		$help['credits'] = '';
1706
1707
		// Fill 'permissions'
1708
		$help['permissions'] .= Html::openElement( 'div',
1709
			[ 'class' => 'apihelp-block apihelp-permissions' ] );
1710
		$m = $this->msg( 'api-help-permissions' );
1711
		if ( !$m->isDisabled() ) {
1712
			$help['permissions'] .= Html::rawElement( 'div', [ 'class' => 'apihelp-block-head' ],
1713
				$m->numParams( count( self::$mRights ) )->parse()
1714
			);
1715
		}
1716
		$help['permissions'] .= Html::openElement( 'dl' );
1717
		foreach ( self::$mRights as $right => $rightMsg ) {
1718
			$help['permissions'] .= Html::element( 'dt', null, $right );
1719
1720
			$rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
1721
			$help['permissions'] .= Html::rawElement( 'dd', null, $rightMsg );
1722
1723
			$groups = array_map( function ( $group ) {
1724
				return $group == '*' ? 'all' : $group;
1725
			}, User::getGroupsWithPermission( $right ) );
1726
1727
			$help['permissions'] .= Html::rawElement( 'dd', null,
1728
				$this->msg( 'api-help-permissions-granted-to' )
1729
					->numParams( count( $groups ) )
1730
					->params( $this->getLanguage()->commaList( $groups ) )
1731
					->parse()
1732
			);
1733
		}
1734
		$help['permissions'] .= Html::closeElement( 'dl' );
1735
		$help['permissions'] .= Html::closeElement( 'div' );
1736
1737
		// Fill 'datatypes' and 'credits', if applicable
1738
		if ( empty( $options['nolead'] ) ) {
1739
			$level = $options['headerlevel'];
1740
			$tocnumber = &$options['tocnumber'];
1741
1742
			$header = $this->msg( 'api-help-datatypes-header' )->parse();
1743
1744
			// Add an additional span with sanitized ID
1745 View Code Duplication
			if ( !$this->getConfig()->get( 'ExperimentalHtmlIds' ) ) {
1746
				$header = Html::element( 'span', [ 'id' => Sanitizer::escapeId( 'main/datatypes' ) ] ) .
1747
					$header;
1748
			}
1749
			$help['datatypes'] .= Html::rawElement( 'h' . min( 6, $level ),
1750
				[ 'id' => 'main/datatypes', 'class' => 'apihelp-header' ],
1751
				$header
1752
			);
1753
			$help['datatypes'] .= $this->msg( 'api-help-datatypes' )->parseAsBlock();
1754 View Code Duplication
			if ( !isset( $tocData['main/datatypes'] ) ) {
1755
				$tocnumber[$level]++;
1756
				$tocData['main/datatypes'] = [
1757
					'toclevel' => count( $tocnumber ),
1758
					'level' => $level,
1759
					'anchor' => 'main/datatypes',
1760
					'line' => $header,
1761
					'number' => implode( '.', $tocnumber ),
1762
					'index' => false,
1763
				];
1764
			}
1765
1766
			// Add an additional span with sanitized ID
1767 View Code Duplication
			if ( !$this->getConfig()->get( 'ExperimentalHtmlIds' ) ) {
1768
				$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...
1769
					$header;
1770
			}
1771
			$header = $this->msg( 'api-credits-header' )->parse();
1772
			$help['credits'] .= Html::rawElement( 'h' . min( 6, $level ),
1773
				[ 'id' => 'main/credits', 'class' => 'apihelp-header' ],
1774
				$header
1775
			);
1776
			$help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
1777 View Code Duplication
			if ( !isset( $tocData['main/credits'] ) ) {
1778
				$tocnumber[$level]++;
1779
				$tocData['main/credits'] = [
1780
					'toclevel' => count( $tocnumber ),
1781
					'level' => $level,
1782
					'anchor' => 'main/credits',
1783
					'line' => $header,
1784
					'number' => implode( '.', $tocnumber ),
1785
					'index' => false,
1786
				];
1787
			}
1788
		}
1789
	}
1790
1791
	private $mCanApiHighLimits = null;
1792
1793
	/**
1794
	 * Check whether the current user is allowed to use high limits
1795
	 * @return bool
1796
	 */
1797
	public function canApiHighLimits() {
1798
		if ( !isset( $this->mCanApiHighLimits ) ) {
1799
			$this->mCanApiHighLimits = $this->getUser()->isAllowed( 'apihighlimits' );
1800
		}
1801
1802
		return $this->mCanApiHighLimits;
1803
	}
1804
1805
	/**
1806
	 * Overrides to return this instance's module manager.
1807
	 * @return ApiModuleManager
1808
	 */
1809
	public function getModuleManager() {
1810
		return $this->mModuleMgr;
1811
	}
1812
1813
	/**
1814
	 * Fetches the user agent used for this request
1815
	 *
1816
	 * The value will be the combination of the 'Api-User-Agent' header (if
1817
	 * any) and the standard User-Agent header (if any).
1818
	 *
1819
	 * @return string
1820
	 */
1821
	public function getUserAgent() {
1822
		return trim(
1823
			$this->getRequest()->getHeader( 'Api-user-agent' ) . ' ' .
1824
			$this->getRequest()->getHeader( 'User-agent' )
1825
		);
1826
	}
1827
1828
	/************************************************************************//**
1829
	 * @name   Deprecated
1830
	 * @{
1831
	 */
1832
1833
	/**
1834
	 * Sets whether the pretty-printer should format *bold* and $italics$
1835
	 *
1836
	 * @deprecated since 1.25
1837
	 * @param bool $help
1838
	 */
1839
	public function setHelp( $help = true ) {
1840
		wfDeprecated( __METHOD__, '1.25' );
1841
		$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...
1842
	}
1843
1844
	/**
1845
	 * Override the parent to generate help messages for all available modules.
1846
	 *
1847
	 * @deprecated since 1.25
1848
	 * @return string
1849
	 */
1850
	public function makeHelpMsg() {
1851
		wfDeprecated( __METHOD__, '1.25' );
1852
1853
		$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...
1854
		$cacheHelpTimeout = $this->getConfig()->get( 'APICacheHelpTimeout' );
1855
1856
		return ObjectCache::getMainWANInstance()->getWithSetCallback(
1857
			wfMemcKey(
1858
				'apihelp',
1859
				$this->getModuleName(),
1860
				str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) )
1861
			),
1862
			$cacheHelpTimeout > 0 ? $cacheHelpTimeout : WANObjectCache::TTL_UNCACHEABLE,
1863
			[ $this, 'reallyMakeHelpMsg' ]
1864
		);
1865
	}
1866
1867
	/**
1868
	 * @deprecated since 1.25
1869
	 * @return mixed|string
1870
	 */
1871
	public function reallyMakeHelpMsg() {
1872
		wfDeprecated( __METHOD__, '1.25' );
1873
		$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...
1874
1875
		// Use parent to make default message for the main module
1876
		$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...
1877
1878
		$asterisks = str_repeat( '*** ', 14 );
1879
		$msg .= "\n\n$asterisks Modules  $asterisks\n\n";
1880
1881 View Code Duplication
		foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) {
1882
			$module = $this->mModuleMgr->getModule( $name );
1883
			$msg .= self::makeHelpMsgHeader( $module, 'action' );
0 ignored issues
show
Bug introduced by
It seems like $module defined by $this->mModuleMgr->getModule($name) on line 1882 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...
1884
1885
			$msg2 = $module->makeHelpMsg();
1886
			if ( $msg2 !== false ) {
1887
				$msg .= $msg2;
1888
			}
1889
			$msg .= "\n";
1890
		}
1891
1892
		$msg .= "\n$asterisks Permissions $asterisks\n\n";
1893
		foreach ( self::$mRights as $right => $rightMsg ) {
1894
			$rightsMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )
1895
				->useDatabase( false )
1896
				->inLanguage( 'en' )
1897
				->text();
1898
			$groups = User::getGroupsWithPermission( $right );
1899
			$msg .= '* ' . $right . " *\n  $rightsMsg" .
1900
				"\nGranted to:\n  " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n";
1901
		}
1902
1903
		$msg .= "\n$asterisks Formats  $asterisks\n\n";
1904 View Code Duplication
		foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) {
1905
			$module = $this->mModuleMgr->getModule( $name );
1906
			$msg .= self::makeHelpMsgHeader( $module, 'format' );
0 ignored issues
show
Bug introduced by
It seems like $module defined by $this->mModuleMgr->getModule($name) on line 1905 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...
1907
			$msg2 = $module->makeHelpMsg();
1908
			if ( $msg2 !== false ) {
1909
				$msg .= $msg2;
1910
			}
1911
			$msg .= "\n";
1912
		}
1913
1914
		$credits = $this->msg( 'api-credits' )->useDatabase( 'false' )->inLanguage( 'en' )->text();
1915
		$credits = str_replace( "\n", "\n   ", $credits );
1916
		$msg .= "\n*** Credits: ***\n   $credits\n";
1917
1918
		return $msg;
1919
	}
1920
1921
	/**
1922
	 * @deprecated since 1.25
1923
	 * @param ApiBase $module
1924
	 * @param string $paramName What type of request is this? e.g. action,
1925
	 *    query, list, prop, meta, format
1926
	 * @return string
1927
	 */
1928
	public static function makeHelpMsgHeader( $module, $paramName ) {
1929
		wfDeprecated( __METHOD__, '1.25' );
1930
		$modulePrefix = $module->getModulePrefix();
1931
		if ( strval( $modulePrefix ) !== '' ) {
1932
			$modulePrefix = "($modulePrefix) ";
1933
		}
1934
1935
		return "* $paramName={$module->getModuleName()} $modulePrefix*";
1936
	}
1937
1938
	/**@}*/
1939
1940
}
1941
1942
/**
1943
 * This exception will be thrown when dieUsage is called to stop module execution.
1944
 *
1945
 * @ingroup API
1946
 */
1947
class UsageException extends MWException {
1948
1949
	private $mCodestr;
1950
1951
	/**
1952
	 * @var null|array
1953
	 */
1954
	private $mExtraData;
1955
1956
	/**
1957
	 * @param string $message
1958
	 * @param string $codestr
1959
	 * @param int $code
1960
	 * @param array|null $extradata
1961
	 */
1962
	public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
1963
		parent::__construct( $message, $code );
1964
		$this->mCodestr = $codestr;
1965
		$this->mExtraData = $extradata;
1966
1967
		// This should never happen, so throw an exception about it that will
1968
		// hopefully get logged with a backtrace (T138585)
1969
		if ( !is_string( $codestr ) || $codestr === '' ) {
1970
			throw new InvalidArgumentException( 'Invalid $codestr, was ' .
1971
				( $codestr === '' ? 'empty string' : gettype( $codestr ) )
1972
			);
1973
		}
1974
	}
1975
1976
	/**
1977
	 * @return string
1978
	 */
1979
	public function getCodeString() {
1980
		return $this->mCodestr;
1981
	}
1982
1983
	/**
1984
	 * @return array
1985
	 */
1986
	public function getMessageArray() {
1987
		$result = [
1988
			'code' => $this->mCodestr,
1989
			'info' => $this->getMessage()
1990
		];
1991
		if ( is_array( $this->mExtraData ) ) {
1992
			$result = array_merge( $result, $this->mExtraData );
1993
		}
1994
1995
		return $result;
1996
	}
1997
1998
	/**
1999
	 * @return string
2000
	 */
2001
	public function __toString() {
2002
		return "{$this->getCodeString()}: {$this->getMessage()}";
2003
	}
2004
}
2005
2006
/**
2007
 * For really cool vim folding this needs to be at the end:
2008
 * vim: foldmarker=@{,@} foldmethod=marker
2009
 */
2010