Issues (4122)

Security Analysis    not enabled

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

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

includes/api/ApiQuery.php (2 issues)

Upgrade to new PHP Analysis Engine

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

1
<?php
2
/**
3
 *
4
 *
5
 * Created on Sep 7, 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
 */
26
27
/**
28
 * This is the main query class. It behaves similar to ApiMain: based on the
29
 * parameters given, it will create a list of titles to work on (an ApiPageSet
30
 * object), instantiate and execute various property/list/meta modules, and
31
 * assemble all resulting data into a single ApiResult object.
32
 *
33
 * In generator mode, a generator will be executed first to populate a second
34
 * ApiPageSet object, and that object will be used for all subsequent modules.
35
 *
36
 * @ingroup API
37
 */
38
class ApiQuery extends ApiBase {
39
40
	/**
41
	 * List of Api Query prop modules
42
	 * @var array
43
	 */
44
	private static $QueryPropModules = [
45
		'categories' => 'ApiQueryCategories',
46
		'categoryinfo' => 'ApiQueryCategoryInfo',
47
		'contributors' => 'ApiQueryContributors',
48
		'deletedrevisions' => 'ApiQueryDeletedRevisions',
49
		'duplicatefiles' => 'ApiQueryDuplicateFiles',
50
		'extlinks' => 'ApiQueryExternalLinks',
51
		'fileusage' => 'ApiQueryBacklinksprop',
52
		'images' => 'ApiQueryImages',
53
		'imageinfo' => 'ApiQueryImageInfo',
54
		'info' => 'ApiQueryInfo',
55
		'links' => 'ApiQueryLinks',
56
		'linkshere' => 'ApiQueryBacklinksprop',
57
		'iwlinks' => 'ApiQueryIWLinks',
58
		'langlinks' => 'ApiQueryLangLinks',
59
		'pageprops' => 'ApiQueryPageProps',
60
		'redirects' => 'ApiQueryBacklinksprop',
61
		'revisions' => 'ApiQueryRevisions',
62
		'stashimageinfo' => 'ApiQueryStashImageInfo',
63
		'templates' => 'ApiQueryLinks',
64
		'transcludedin' => 'ApiQueryBacklinksprop',
65
	];
66
67
	/**
68
	 * List of Api Query list modules
69
	 * @var array
70
	 */
71
	private static $QueryListModules = [
72
		'allcategories' => 'ApiQueryAllCategories',
73
		'alldeletedrevisions' => 'ApiQueryAllDeletedRevisions',
74
		'allfileusages' => 'ApiQueryAllLinks',
75
		'allimages' => 'ApiQueryAllImages',
76
		'alllinks' => 'ApiQueryAllLinks',
77
		'allpages' => 'ApiQueryAllPages',
78
		'allredirects' => 'ApiQueryAllLinks',
79
		'allrevisions' => 'ApiQueryAllRevisions',
80
		'mystashedfiles' => 'ApiQueryMyStashedFiles',
81
		'alltransclusions' => 'ApiQueryAllLinks',
82
		'allusers' => 'ApiQueryAllUsers',
83
		'backlinks' => 'ApiQueryBacklinks',
84
		'blocks' => 'ApiQueryBlocks',
85
		'categorymembers' => 'ApiQueryCategoryMembers',
86
		'deletedrevs' => 'ApiQueryDeletedrevs',
87
		'embeddedin' => 'ApiQueryBacklinks',
88
		'exturlusage' => 'ApiQueryExtLinksUsage',
89
		'filearchive' => 'ApiQueryFilearchive',
90
		'imageusage' => 'ApiQueryBacklinks',
91
		'iwbacklinks' => 'ApiQueryIWBacklinks',
92
		'langbacklinks' => 'ApiQueryLangBacklinks',
93
		'logevents' => 'ApiQueryLogEvents',
94
		'pageswithprop' => 'ApiQueryPagesWithProp',
95
		'pagepropnames' => 'ApiQueryPagePropNames',
96
		'prefixsearch' => 'ApiQueryPrefixSearch',
97
		'protectedtitles' => 'ApiQueryProtectedTitles',
98
		'querypage' => 'ApiQueryQueryPage',
99
		'random' => 'ApiQueryRandom',
100
		'recentchanges' => 'ApiQueryRecentChanges',
101
		'search' => 'ApiQuerySearch',
102
		'tags' => 'ApiQueryTags',
103
		'usercontribs' => 'ApiQueryContributions',
104
		'users' => 'ApiQueryUsers',
105
		'watchlist' => 'ApiQueryWatchlist',
106
		'watchlistraw' => 'ApiQueryWatchlistRaw',
107
	];
108
109
	/**
110
	 * List of Api Query meta modules
111
	 * @var array
112
	 */
113
	private static $QueryMetaModules = [
114
		'allmessages' => 'ApiQueryAllMessages',
115
		'authmanagerinfo' => 'ApiQueryAuthManagerInfo',
116
		'siteinfo' => 'ApiQuerySiteinfo',
117
		'userinfo' => 'ApiQueryUserInfo',
118
		'filerepoinfo' => 'ApiQueryFileRepoInfo',
119
		'tokens' => 'ApiQueryTokens',
120
	];
121
122
	/**
123
	 * @var ApiPageSet
124
	 */
125
	private $mPageSet;
126
127
	private $mParams;
128
	private $mNamedDB = [];
129
	private $mModuleMgr;
130
131
	/**
132
	 * @param ApiMain $main
133
	 * @param string $action
134
	 */
135
	public function __construct( ApiMain $main, $action ) {
136
		parent::__construct( $main, $action );
137
138
		$this->mModuleMgr = new ApiModuleManager( $this );
139
140
		// Allow custom modules to be added in LocalSettings.php
141
		$config = $this->getConfig();
142
		$this->mModuleMgr->addModules( self::$QueryPropModules, 'prop' );
143
		$this->mModuleMgr->addModules( $config->get( 'APIPropModules' ), 'prop' );
144
		$this->mModuleMgr->addModules( self::$QueryListModules, 'list' );
145
		$this->mModuleMgr->addModules( $config->get( 'APIListModules' ), 'list' );
146
		$this->mModuleMgr->addModules( self::$QueryMetaModules, 'meta' );
147
		$this->mModuleMgr->addModules( $config->get( 'APIMetaModules' ), 'meta' );
148
149
		Hooks::run( 'ApiQuery::moduleManager', [ $this->mModuleMgr ] );
150
151
		// Create PageSet that will process titles/pageids/revids/generator
152
		$this->mPageSet = new ApiPageSet( $this );
153
	}
154
155
	/**
156
	 * Overrides to return this instance's module manager.
157
	 * @return ApiModuleManager
158
	 */
159
	public function getModuleManager() {
160
		return $this->mModuleMgr;
161
	}
162
163
	/**
164
	 * Get the query database connection with the given name.
165
	 * If no such connection has been requested before, it will be created.
166
	 * Subsequent calls with the same $name will return the same connection
167
	 * as the first, regardless of the values of $db and $groups
168
	 * @param string $name Name to assign to the database connection
169
	 * @param int $db One of the DB_* constants
170
	 * @param array $groups Query groups
171
	 * @return Database
172
	 */
173
	public function getNamedDB( $name, $db, $groups ) {
174
		if ( !array_key_exists( $name, $this->mNamedDB ) ) {
175
			$this->mNamedDB[$name] = wfGetDB( $db, $groups );
176
		}
177
178
		return $this->mNamedDB[$name];
179
	}
180
181
	/**
182
	 * Gets the set of pages the user has requested (or generated)
183
	 * @return ApiPageSet
184
	 */
185
	public function getPageSet() {
186
		return $this->mPageSet;
187
	}
188
189
	/**
190
	 * @return ApiFormatRaw|null
191
	 */
192
	public function getCustomPrinter() {
193
		// If &exportnowrap is set, use the raw formatter
194
		if ( $this->getParameter( 'export' ) &&
195
			$this->getParameter( 'exportnowrap' )
196
		) {
197
			return new ApiFormatRaw( $this->getMain(),
198
				$this->getMain()->createPrinterByName( 'xml' ) );
199
		} else {
200
			return null;
201
		}
202
	}
203
204
	/**
205
	 * Query execution happens in the following steps:
206
	 * #1 Create a PageSet object with any pages requested by the user
207
	 * #2 If using a generator, execute it to get a new ApiPageSet object
208
	 * #3 Instantiate all requested modules.
209
	 *    This way the PageSet object will know what shared data is required,
210
	 *    and minimize DB calls.
211
	 * #4 Output all normalization and redirect resolution information
212
	 * #5 Execute all requested modules
213
	 */
214
	public function execute() {
215
		$this->mParams = $this->extractRequestParams();
216
217
		// Instantiate requested modules
218
		$allModules = [];
219
		$this->instantiateModules( $allModules, 'prop' );
220
		$propModules = array_keys( $allModules );
221
		$this->instantiateModules( $allModules, 'list' );
222
		$this->instantiateModules( $allModules, 'meta' );
223
224
		// Filter modules based on continue parameter
225
		$continuationManager = new ApiContinuationManager( $this, $allModules, $propModules );
226
		$this->setContinuationManager( $continuationManager );
227
		$modules = $continuationManager->getRunModules();
228
229
		if ( !$continuationManager->isGeneratorDone() ) {
230
			// Query modules may optimize data requests through the $this->getPageSet()
231
			// object by adding extra fields from the page table.
232
			foreach ( $modules as $module ) {
233
				$module->requestExtraData( $this->mPageSet );
234
			}
235
			// Populate page/revision information
236
			$this->mPageSet->execute();
237
			// Record page information (title, namespace, if exists, etc)
238
			$this->outputGeneralPageInfo();
239
		} else {
240
			$this->mPageSet->executeDryRun();
241
		}
242
243
		$cacheMode = $this->mPageSet->getCacheMode();
244
245
		// Execute all unfinished modules
246
		/** @var $module ApiQueryBase */
247
		foreach ( $modules as $module ) {
248
			$params = $module->extractRequestParams();
249
			$cacheMode = $this->mergeCacheMode(
250
				$cacheMode, $module->getCacheMode( $params ) );
0 ignored issues
show
It seems like you code against a specific sub-type and not the parent class ApiBase as the method getCacheMode() does only exist in the following sub-classes of ApiBase: ApiPageSet, ApiQueryAllCategories, ApiQueryAllDeletedRevisions, ApiQueryAllImages, ApiQueryAllLinks, ApiQueryAllMessages, ApiQueryAllPages, ApiQueryAllRevisions, ApiQueryAllUsers, ApiQueryAuthManagerInfo, ApiQueryBacklinks, ApiQueryBacklinksprop, ApiQueryBase, ApiQueryBlocks, ApiQueryCategories, ApiQueryCategoryInfo, ApiQueryCategoryMembers, ApiQueryContributions, ApiQueryContributors, ApiQueryDeletedRevisions, ApiQueryDeletedrevs, ApiQueryDisabled, ApiQueryDuplicateFiles, ApiQueryExtLinksUsage, ApiQueryExternalLinks, ApiQueryFileRepoInfo, ApiQueryFilearchive, ApiQueryGeneratorBase, ApiQueryIWBacklinks, ApiQueryIWLinks, ApiQueryImageInfo, ApiQueryImages, ApiQueryInfo, ApiQueryLangBacklinks, ApiQueryLangLinks, ApiQueryLinks, ApiQueryLogEvents, ApiQueryMyStashedFiles, ApiQueryPagePropNames, ApiQueryPageProps, ApiQueryPagesWithProp, ApiQueryPrefixSearch, ApiQueryProtectedTitles, ApiQueryQueryPage, ApiQueryRandom, ApiQueryRecentChanges, ApiQueryRevisions, ApiQueryRevisionsBase, ApiQuerySearch, ApiQuerySiteinfo, ApiQueryStashImageInfo, ApiQueryTags, ApiQueryTokens, ApiQueryUserInfo, ApiQueryUsers, ApiQueryWatchlist, ApiQueryWatchlistRaw. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
251
			$module->execute();
252
			Hooks::run( 'APIQueryAfterExecute', [ &$module ] );
253
		}
254
255
		// Set the cache mode
256
		$this->getMain()->setCacheMode( $cacheMode );
257
258
		// Write the continuation data into the result
259
		$this->setContinuationManager( null );
260
		if ( $this->mParams['rawcontinue'] ) {
261
			$data = $continuationManager->getRawNonContinuation();
262
			if ( $data ) {
263
				$this->getResult()->addValue( null, 'query-noncontinue', $data,
264
					ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
265
			}
266
			$data = $continuationManager->getRawContinuation();
267
			if ( $data ) {
268
				$this->getResult()->addValue( null, 'query-continue', $data,
269
					ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK );
270
			}
271
		} else {
272
			$continuationManager->setContinuationIntoResult( $this->getResult() );
273
		}
274
	}
275
276
	/**
277
	 * Update a cache mode string, applying the cache mode of a new module to it.
278
	 * The cache mode may increase in the level of privacy, but public modules
279
	 * added to private data do not decrease the level of privacy.
280
	 *
281
	 * @param string $cacheMode
282
	 * @param string $modCacheMode
283
	 * @return string
284
	 */
285
	protected function mergeCacheMode( $cacheMode, $modCacheMode ) {
286
		if ( $modCacheMode === 'anon-public-user-private' ) {
287
			if ( $cacheMode !== 'private' ) {
288
				$cacheMode = 'anon-public-user-private';
289
			}
290
		} elseif ( $modCacheMode === 'public' ) {
0 ignored issues
show
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif 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 elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
291
			// do nothing, if it's public already it will stay public
292
		} else { // private
293
			$cacheMode = 'private';
294
		}
295
296
		return $cacheMode;
297
	}
298
299
	/**
300
	 * Create instances of all modules requested by the client
301
	 * @param array $modules To append instantiated modules to
302
	 * @param string $param Parameter name to read modules from
303
	 */
304
	private function instantiateModules( &$modules, $param ) {
305
		$wasPosted = $this->getRequest()->wasPosted();
306
		if ( isset( $this->mParams[$param] ) ) {
307
			foreach ( $this->mParams[$param] as $moduleName ) {
308
				$instance = $this->mModuleMgr->getModule( $moduleName, $param );
309
				if ( $instance === null ) {
310
					ApiBase::dieDebug( __METHOD__, 'Error instantiating module' );
311
				}
312
				if ( !$wasPosted && $instance->mustBePosted() ) {
313
					$this->dieUsageMsgOrDebug( [ 'mustbeposted', $moduleName ] );
314
				}
315
				// Ignore duplicates. TODO 2.0: die()?
316
				if ( !array_key_exists( $moduleName, $modules ) ) {
317
					$modules[$moduleName] = $instance;
318
				}
319
			}
320
		}
321
	}
322
323
	/**
324
	 * Appends an element for each page in the current pageSet with the
325
	 * most general information (id, title), plus any title normalizations
326
	 * and missing or invalid title/pageids/revids.
327
	 */
328
	private function outputGeneralPageInfo() {
329
		$pageSet = $this->getPageSet();
330
		$result = $this->getResult();
331
332
		// We can't really handle max-result-size failure here, but we need to
333
		// check anyway in case someone set the limit stupidly low.
334
		$fit = true;
335
336
		$values = $pageSet->getNormalizedTitlesAsResult( $result );
337
		if ( $values ) {
338
			$fit = $fit && $result->addValue( 'query', 'normalized', $values );
339
		}
340
		$values = $pageSet->getConvertedTitlesAsResult( $result );
341
		if ( $values ) {
342
			$fit = $fit && $result->addValue( 'query', 'converted', $values );
343
		}
344
		$values = $pageSet->getInterwikiTitlesAsResult( $result, $this->mParams['iwurl'] );
345
		if ( $values ) {
346
			$fit = $fit && $result->addValue( 'query', 'interwiki', $values );
347
		}
348
		$values = $pageSet->getRedirectTitlesAsResult( $result );
349
		if ( $values ) {
350
			$fit = $fit && $result->addValue( 'query', 'redirects', $values );
351
		}
352
		$values = $pageSet->getMissingRevisionIDsAsResult( $result );
353
		if ( $values ) {
354
			$fit = $fit && $result->addValue( 'query', 'badrevids', $values );
355
		}
356
357
		// Page elements
358
		$pages = [];
359
360
		// Report any missing titles
361 View Code Duplication
		foreach ( $pageSet->getMissingTitles() as $fakeId => $title ) {
362
			$vals = [];
363
			ApiQueryBase::addTitleInfo( $vals, $title );
364
			$vals['missing'] = true;
365
			if ( $title->isKnown() ) {
366
				$vals['known'] = true;
367
			}
368
			$pages[$fakeId] = $vals;
369
		}
370
		// Report any invalid titles
371
		foreach ( $pageSet->getInvalidTitlesAndReasons() as $fakeId => $data ) {
372
			$pages[$fakeId] = $data + [ 'invalid' => true ];
373
		}
374
		// Report any missing page ids
375
		foreach ( $pageSet->getMissingPageIDs() as $pageid ) {
376
			$pages[$pageid] = [
377
				'pageid' => $pageid,
378
				'missing' => true,
379
			];
380
		}
381
		// Report special pages
382
		/** @var $title Title */
383 View Code Duplication
		foreach ( $pageSet->getSpecialTitles() as $fakeId => $title ) {
384
			$vals = [];
385
			ApiQueryBase::addTitleInfo( $vals, $title );
386
			$vals['special'] = true;
387
			if ( !$title->isKnown() ) {
388
				$vals['missing'] = true;
389
			}
390
			$pages[$fakeId] = $vals;
391
		}
392
393
		// Output general page information for found titles
394
		foreach ( $pageSet->getGoodTitles() as $pageid => $title ) {
395
			$vals = [];
396
			$vals['pageid'] = $pageid;
397
			ApiQueryBase::addTitleInfo( $vals, $title );
398
			$pages[$pageid] = $vals;
399
		}
400
401
		if ( count( $pages ) ) {
402
			$pageSet->populateGeneratorData( $pages );
403
			ApiResult::setArrayType( $pages, 'BCarray' );
404
405
			if ( $this->mParams['indexpageids'] ) {
406
				$pageIDs = array_keys( ApiResult::stripMetadataNonRecursive( $pages ) );
407
				// json treats all map keys as strings - converting to match
408
				$pageIDs = array_map( 'strval', $pageIDs );
409
				ApiResult::setIndexedTagName( $pageIDs, 'id' );
410
				$fit = $fit && $result->addValue( 'query', 'pageids', $pageIDs );
411
			}
412
413
			ApiResult::setIndexedTagName( $pages, 'page' );
414
			$fit = $fit && $result->addValue( 'query', 'pages', $pages );
415
		}
416
417
		if ( !$fit ) {
418
			$this->dieUsage(
419
				'The value of $wgAPIMaxResultSize on this wiki is ' .
420
					'too small to hold basic result information',
421
				'badconfig'
422
			);
423
		}
424
425
		if ( $this->mParams['export'] ) {
426
			$this->doExport( $pageSet, $result );
427
		}
428
	}
429
430
	/**
431
	 * @param ApiPageSet $pageSet Pages to be exported
432
	 * @param ApiResult $result Result to output to
433
	 */
434
	private function doExport( $pageSet, $result ) {
435
		$exportTitles = [];
436
		$titles = $pageSet->getGoodTitles();
437
		if ( count( $titles ) ) {
438
			$user = $this->getUser();
439
			/** @var $title Title */
440
			foreach ( $titles as $title ) {
441
				if ( $title->userCan( 'read', $user ) ) {
442
					$exportTitles[] = $title;
443
				}
444
			}
445
		}
446
447
		$exporter = new WikiExporter( $this->getDB() );
448
		$sink = new DumpStringOutput;
449
		$exporter->setOutputSink( $sink );
450
		$exporter->openStream();
451
		foreach ( $exportTitles as $title ) {
452
			$exporter->pageByTitle( $title );
453
		}
454
		$exporter->closeStream();
455
456
		// Don't check the size of exported stuff
457
		// It's not continuable, so it would cause more
458
		// problems than it'd solve
459
		if ( $this->mParams['exportnowrap'] ) {
460
			$result->reset();
461
			// Raw formatter will handle this
462
			$result->addValue( null, 'text', $sink, ApiResult::NO_SIZE_CHECK );
463
			$result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK );
464
		} else {
465
			$result->addValue( 'query', 'export', $sink, ApiResult::NO_SIZE_CHECK );
466
			$result->addValue( 'query', ApiResult::META_BC_SUBELEMENTS, [ 'export' ] );
467
		}
468
	}
469
470
	public function getAllowedParams( $flags = 0 ) {
471
		$result = [
472
			'prop' => [
473
				ApiBase::PARAM_ISMULTI => true,
474
				ApiBase::PARAM_TYPE => 'submodule',
475
			],
476
			'list' => [
477
				ApiBase::PARAM_ISMULTI => true,
478
				ApiBase::PARAM_TYPE => 'submodule',
479
			],
480
			'meta' => [
481
				ApiBase::PARAM_ISMULTI => true,
482
				ApiBase::PARAM_TYPE => 'submodule',
483
			],
484
			'indexpageids' => false,
485
			'export' => false,
486
			'exportnowrap' => false,
487
			'iwurl' => false,
488
			'continue' => [
489
				ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
490
			],
491
			'rawcontinue' => false,
492
		];
493
		if ( $flags ) {
494
			$result += $this->getPageSet()->getFinalParams( $flags );
495
		}
496
497
		return $result;
498
	}
499
500
	public function isReadMode() {
501
		// We need to make an exception for certain meta modules that should be
502
		// accessible even without the 'read' right. Restrict the exception as
503
		// much as possible: no other modules allowed, and no pageset
504
		// parameters either. We do allow the 'rawcontinue' and 'indexpageids'
505
		// parameters since frameworks might add these unconditionally and they
506
		// can't expose anything here.
507
		$this->mParams = $this->extractRequestParams();
508
		$params = array_filter(
509
			array_diff_key(
510
				$this->mParams + $this->getPageSet()->extractRequestParams(),
511
				[ 'rawcontinue' => 1, 'indexpageids' => 1 ]
512
			)
513
		);
514
		if ( array_keys( $params ) !== [ 'meta' ] ) {
515
			return true;
516
		}
517
518
		// Ask each module if it requires read mode. Any true => this returns
519
		// true.
520
		$modules = [];
521
		$this->instantiateModules( $modules, 'meta' );
522
		foreach ( $modules as $module ) {
523
			if ( $module->isReadMode() ) {
524
				return true;
525
			}
526
		}
527
528
		return false;
529
	}
530
531
	protected function getExamplesMessages() {
532
		return [
533
			'action=query&prop=revisions&meta=siteinfo&' .
534
				'titles=Main%20Page&rvprop=user|comment&continue='
535
				=> 'apihelp-query-example-revisions',
536
			'action=query&generator=allpages&gapprefix=API/&prop=revisions&continue='
537
				=> 'apihelp-query-example-allpages',
538
		];
539
	}
540
541
	public function getHelpUrls() {
542
		return [
543
			'https://www.mediawiki.org/wiki/API:Query',
544
			'https://www.mediawiki.org/wiki/API:Meta',
545
			'https://www.mediawiki.org/wiki/API:Properties',
546
			'https://www.mediawiki.org/wiki/API:Lists',
547
		];
548
	}
549
}
550