ApiQuery::outputGeneralPageInfo()   F
last analyzed

Complexity

Conditions 24
Paths > 20000

Size

Total Lines 101
Code Lines 62

Duplication

Lines 18
Ratio 17.82 %

Importance

Changes 0
Metric Value
cc 24
eloc 62
nc 489888
nop 0
dl 18
loc 101
rs 2
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 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 );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ApiBase as the method requestExtraData() does only exist in the following sub-classes of ApiBase: 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...
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
Bug introduced by
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
Unused Code introduced by
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