Completed
Branch master (62f6c6)
by
unknown
21:31
created

ApiPageSet   F

Complexity

Total Complexity 200

Size/Duplication

Total Lines 1394
Duplicated Lines 4.02 %

Coupling/Cohesion

Components 1
Dependencies 19

Importance

Changes 0
Metric Value
dl 56
loc 1394
rs 0.6314
c 0
b 0
f 0
wmc 200
lcom 1
cbo 19

59 Methods

Rating   Name   Duplication   Size   Complexity  
B addValues() 16 16 5
A __construct() 0 10 1
A executeDryRun() 0 3 1
A execute() 0 3 1
F executeInternal() 12 85 20
A isResolvingRedirects() 0 3 1
B getDataSource() 0 16 6
A requestField() 0 3 1
A getCustomField() 0 3 1
B getPageTableFields() 0 28 4
A getAllTitlesByNamespace() 0 3 1
A getTitles() 0 3 1
A getTitleCount() 0 3 1
A getGoodTitlesByNamespace() 0 3 1
A getGoodTitles() 0 3 1
A getGoodTitleCount() 0 3 1
A getMissingTitlesByNamespace() 0 3 1
A getMissingTitles() 0 3 1
A getGoodAndMissingTitlesByNamespace() 0 3 1
A getGoodAndMissingTitles() 0 3 1
A getInvalidTitles() 0 6 1
A getInvalidTitlesAndReasons() 0 3 1
A getMissingPageIDs() 0 3 1
A getRedirectTitles() 0 3 1
C getRedirectTitlesAsResult() 0 30 8
A getNormalizedTitles() 0 3 1
A getNormalizedTitlesAsResult() 14 14 4
A getConvertedTitles() 0 3 1
A getConvertedTitlesAsResult() 14 14 4
A getInterwikiTitles() 0 3 1
B getInterwikiTitlesAsResult() 0 19 5
C getInvalidTitlesAndRevisions() 0 25 7
A getRevisionIDs() 0 3 1
A getLiveRevisionIDs() 0 3 1
A getDeletedRevisionIDs() 0 3 1
A getMissingRevisionIDs() 0 3 1
A getMissingRevisionIDsAsResult() 0 13 4
A getSpecialTitles() 0 3 1
A getRevisionCount() 0 3 1
A populateFromTitles() 0 3 1
A populateFromPageIDs() 0 3 1
A populateFromQueryResult() 0 3 1
A populateFromRevisionIDs() 0 3 1
A processDbRow() 0 20 4
A initFromTitles() 0 20 2
B initFromPageIds() 0 27 3
C initFromQueryResult() 0 63 14
C initFromRevIDs() 0 77 11
B resolvePendingRedirects() 0 29 5
B getRedirectTargets() 0 54 7
A getCacheMode() 0 3 1
D processTitlesArray() 0 77 16
A setGeneratorData() 0 5 1
A setRedirectMergePolicy() 0 3 1
D populateGeneratorData() 0 85 22
A getDB() 0 3 1
A getPositiveIntegers() 0 12 3
B getAllowedParams() 0 51 4
B getGenerators() 0 22 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ApiPageSet often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApiPageSet, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 *
4
 *
5
 * Created on Sep 24, 2006
6
 *
7
 * Copyright © 2006, 2013 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 class contains a list of pages that the client has requested.
29
 * Initially, when the client passes in titles=, pageids=, or revisions=
30
 * parameter, an instance of the ApiPageSet class will normalize titles,
31
 * determine if the pages/revisions exist, and prefetch any additional page
32
 * data requested.
33
 *
34
 * When a generator is used, the result of the generator will become the input
35
 * for the second instance of this class, and all subsequent actions will use
36
 * the second instance for all their work.
37
 *
38
 * @ingroup API
39
 * @since 1.21 derives from ApiBase instead of ApiQueryBase
40
 */
41
class ApiPageSet extends ApiBase {
42
	/**
43
	 * Constructor flag: The new instance of ApiPageSet will ignore the 'generator=' parameter
44
	 * @since 1.21
45
	 */
46
	const DISABLE_GENERATORS = 1;
47
48
	private $mDbSource;
49
	private $mParams;
50
	private $mResolveRedirects;
51
	private $mConvertTitles;
52
	private $mAllowGenerator;
53
54
	private $mAllPages = []; // [ns][dbkey] => page_id or negative when missing
55
	private $mTitles = [];
56
	private $mGoodAndMissingPages = []; // [ns][dbkey] => page_id or negative when missing
57
	private $mGoodPages = []; // [ns][dbkey] => page_id
58
	private $mGoodTitles = [];
59
	private $mMissingPages = []; // [ns][dbkey] => fake page_id
60
	private $mMissingTitles = [];
61
	/** @var array [fake_page_id] => array( 'title' => $title, 'invalidreason' => $reason ) */
62
	private $mInvalidTitles = [];
63
	private $mMissingPageIDs = [];
64
	private $mRedirectTitles = [];
65
	private $mSpecialTitles = [];
66
	private $mNormalizedTitles = [];
67
	private $mInterwikiTitles = [];
68
	/** @var Title[] */
69
	private $mPendingRedirectIDs = [];
70
	private $mResolvedRedirectTitles = [];
71
	private $mConvertedTitles = [];
72
	private $mGoodRevIDs = [];
73
	private $mLiveRevIDs = [];
74
	private $mDeletedRevIDs = [];
75
	private $mMissingRevIDs = [];
76
	private $mGeneratorData = []; // [ns][dbkey] => data array
77
	private $mFakePageId = -1;
78
	private $mCacheMode = 'public';
79
	private $mRequestedPageFields = [];
80
	/** @var int */
81
	private $mDefaultNamespace = NS_MAIN;
82
	/** @var callable|null */
83
	private $mRedirectMergePolicy;
84
85
	/**
86
	 * Add all items from $values into the result
87
	 * @param array $result Output
88
	 * @param array $values Values to add
89
	 * @param string $flag The name of the boolean flag to mark this element
90
	 * @param string $name If given, name of the value
91
	 */
92 View Code Duplication
	private static function addValues( array &$result, $values, $flag = null, $name = null ) {
93
		foreach ( $values as $val ) {
94
			if ( $val instanceof Title ) {
95
				$v = [];
96
				ApiQueryBase::addTitleInfo( $v, $val );
97
			} elseif ( $name !== null ) {
98
				$v = [ $name => $val ];
99
			} else {
100
				$v = $val;
101
			}
102
			if ( $flag !== null ) {
103
				$v[$flag] = true;
104
			}
105
			$result[] = $v;
106
		}
107
	}
108
109
	/**
110
	 * @param ApiBase $dbSource Module implementing getDB().
111
	 *        Allows PageSet to reuse existing db connection from the shared state like ApiQuery.
112
	 * @param int $flags Zero or more flags like DISABLE_GENERATORS
113
	 * @param int $defaultNamespace The namespace to use if none is specified by a prefix.
114
	 * @since 1.21 accepts $flags instead of two boolean values
115
	 */
116
	public function __construct( ApiBase $dbSource, $flags = 0, $defaultNamespace = NS_MAIN ) {
117
		parent::__construct( $dbSource->getMain(), $dbSource->getModuleName() );
118
		$this->mDbSource = $dbSource;
119
		$this->mAllowGenerator = ( $flags & ApiPageSet::DISABLE_GENERATORS ) == 0;
120
		$this->mDefaultNamespace = $defaultNamespace;
121
122
		$this->mParams = $this->extractRequestParams();
123
		$this->mResolveRedirects = $this->mParams['redirects'];
124
		$this->mConvertTitles = $this->mParams['converttitles'];
125
	}
126
127
	/**
128
	 * In case execute() is not called, call this method to mark all relevant parameters as used
129
	 * This prevents unused parameters from being reported as warnings
130
	 */
131
	public function executeDryRun() {
132
		$this->executeInternal( true );
133
	}
134
135
	/**
136
	 * Populate the PageSet from the request parameters.
137
	 */
138
	public function execute() {
139
		$this->executeInternal( false );
140
	}
141
142
	/**
143
	 * Populate the PageSet from the request parameters.
144
	 * @param bool $isDryRun If true, instantiates generator, but only to mark
145
	 *    relevant parameters as used
146
	 */
147
	private function executeInternal( $isDryRun ) {
148
		$generatorName = $this->mAllowGenerator ? $this->mParams['generator'] : null;
149
		if ( isset( $generatorName ) ) {
150
			$dbSource = $this->mDbSource;
151
			if ( !$dbSource instanceof ApiQuery ) {
152
				// If the parent container of this pageset is not ApiQuery, we must create it to run generator
153
				$dbSource = $this->getMain()->getModuleManager()->getModule( 'query' );
154
			}
155
			$generator = $dbSource->getModuleManager()->getModule( $generatorName, null, true );
156
			if ( $generator === null ) {
157
				$this->dieUsage( 'Unknown generator=' . $generatorName, 'badgenerator' );
158
			}
159
			if ( !$generator instanceof ApiQueryGeneratorBase ) {
160
				$this->dieUsage( "Module $generatorName cannot be used as a generator", 'badgenerator' );
161
			}
162
			// Create a temporary pageset to store generator's output,
163
			// add any additional fields generator may need, and execute pageset to populate titles/pageids
164
			$tmpPageSet = new ApiPageSet( $dbSource, ApiPageSet::DISABLE_GENERATORS );
0 ignored issues
show
Bug introduced by
It seems like $dbSource defined by $this->getMain()->getMod...r()->getModule('query') on line 153 can be null; however, ApiPageSet::__construct() 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...
165
			$generator->setGeneratorMode( $tmpPageSet );
166
			$this->mCacheMode = $generator->getCacheMode( $generator->extractRequestParams() );
167
168
			if ( !$isDryRun ) {
169
				$generator->requestExtraData( $tmpPageSet );
170
			}
171
			$tmpPageSet->executeInternal( $isDryRun );
172
173
			// populate this pageset with the generator output
174
			if ( !$isDryRun ) {
175
				$generator->executeGenerator( $this );
176
				Hooks::run( 'APIQueryGeneratorAfterExecute', [ &$generator, &$this ] );
177
			} else {
178
				// Prevent warnings from being reported on these parameters
179
				$main = $this->getMain();
180
				foreach ( $generator->extractRequestParams() as $paramName => $param ) {
181
					$main->markParamsUsed( $generator->encodeParamName( $paramName ) );
182
				}
183
			}
184
185
			if ( !$isDryRun ) {
186
				$this->resolvePendingRedirects();
187
			}
188
		} else {
189
			// Only one of the titles/pageids/revids is allowed at the same time
190
			$dataSource = null;
191
			if ( isset( $this->mParams['titles'] ) ) {
192
				$dataSource = 'titles';
193
			}
194 View Code Duplication
			if ( isset( $this->mParams['pageids'] ) ) {
195
				if ( isset( $dataSource ) ) {
196
					$this->dieUsage( "Cannot use 'pageids' at the same time as '$dataSource'", 'multisource' );
197
				}
198
				$dataSource = 'pageids';
199
			}
200 View Code Duplication
			if ( isset( $this->mParams['revids'] ) ) {
201
				if ( isset( $dataSource ) ) {
202
					$this->dieUsage( "Cannot use 'revids' at the same time as '$dataSource'", 'multisource' );
203
				}
204
				$dataSource = 'revids';
205
			}
206
207
			if ( !$isDryRun ) {
208
				// Populate page information with the original user input
209
				switch ( $dataSource ) {
210
					case 'titles':
211
						$this->initFromTitles( $this->mParams['titles'] );
212
						break;
213
					case 'pageids':
214
						$this->initFromPageIds( $this->mParams['pageids'] );
215
						break;
216
					case 'revids':
217
						if ( $this->mResolveRedirects ) {
218
							$this->setWarning( 'Redirect resolution cannot be used ' .
219
								'together with the revids= parameter. Any redirects ' .
220
								'the revids= point to have not been resolved.' );
221
						}
222
						$this->mResolveRedirects = false;
223
						$this->initFromRevIDs( $this->mParams['revids'] );
224
						break;
225
					default:
226
						// Do nothing - some queries do not need any of the data sources.
227
						break;
228
				}
229
			}
230
		}
231
	}
232
233
	/**
234
	 * Check whether this PageSet is resolving redirects
235
	 * @return bool
236
	 */
237
	public function isResolvingRedirects() {
238
		return $this->mResolveRedirects;
239
	}
240
241
	/**
242
	 * Return the parameter name that is the source of data for this PageSet
243
	 *
244
	 * If multiple source parameters are specified (e.g. titles and pageids),
245
	 * one will be named arbitrarily.
246
	 *
247
	 * @return string|null
248
	 */
249
	public function getDataSource() {
250
		if ( $this->mAllowGenerator && isset( $this->mParams['generator'] ) ) {
251
			return 'generator';
252
		}
253
		if ( isset( $this->mParams['titles'] ) ) {
254
			return 'titles';
255
		}
256
		if ( isset( $this->mParams['pageids'] ) ) {
257
			return 'pageids';
258
		}
259
		if ( isset( $this->mParams['revids'] ) ) {
260
			return 'revids';
261
		}
262
263
		return null;
264
	}
265
266
	/**
267
	 * Request an additional field from the page table.
268
	 * Must be called before execute()
269
	 * @param string $fieldName Field name
270
	 */
271
	public function requestField( $fieldName ) {
272
		$this->mRequestedPageFields[$fieldName] = null;
273
	}
274
275
	/**
276
	 * Get the value of a custom field previously requested through
277
	 * requestField()
278
	 * @param string $fieldName Field name
279
	 * @return mixed Field value
280
	 */
281
	public function getCustomField( $fieldName ) {
282
		return $this->mRequestedPageFields[$fieldName];
283
	}
284
285
	/**
286
	 * Get the fields that have to be queried from the page table:
287
	 * the ones requested through requestField() and a few basic ones
288
	 * we always need
289
	 * @return array Array of field names
290
	 */
291
	public function getPageTableFields() {
292
		// Ensure we get minimum required fields
293
		// DON'T change this order
294
		$pageFlds = [
295
			'page_namespace' => null,
296
			'page_title' => null,
297
			'page_id' => null,
298
		];
299
300
		if ( $this->mResolveRedirects ) {
301
			$pageFlds['page_is_redirect'] = null;
302
		}
303
304
		if ( $this->getConfig()->get( 'ContentHandlerUseDB' ) ) {
305
			$pageFlds['page_content_model'] = null;
306
		}
307
308
		if ( $this->getConfig()->get( 'PageLanguageUseDB' ) ) {
309
			$pageFlds['page_lang'] = null;
310
		}
311
312
		// only store non-default fields
313
		$this->mRequestedPageFields = array_diff_key( $this->mRequestedPageFields, $pageFlds );
314
315
		$pageFlds = array_merge( $pageFlds, $this->mRequestedPageFields );
316
317
		return array_keys( $pageFlds );
318
	}
319
320
	/**
321
	 * Returns an array [ns][dbkey] => page_id for all requested titles.
322
	 * page_id is a unique negative number in case title was not found.
323
	 * Invalid titles will also have negative page IDs and will be in namespace 0
324
	 * @return array
325
	 */
326
	public function getAllTitlesByNamespace() {
327
		return $this->mAllPages;
328
	}
329
330
	/**
331
	 * All Title objects provided.
332
	 * @return Title[]
333
	 */
334
	public function getTitles() {
335
		return $this->mTitles;
336
	}
337
338
	/**
339
	 * Returns the number of unique pages (not revisions) in the set.
340
	 * @return int
341
	 */
342
	public function getTitleCount() {
343
		return count( $this->mTitles );
344
	}
345
346
	/**
347
	 * Returns an array [ns][dbkey] => page_id for all good titles.
348
	 * @return array
349
	 */
350
	public function getGoodTitlesByNamespace() {
351
		return $this->mGoodPages;
352
	}
353
354
	/**
355
	 * Title objects that were found in the database.
356
	 * @return Title[] Array page_id (int) => Title (obj)
357
	 */
358
	public function getGoodTitles() {
359
		return $this->mGoodTitles;
360
	}
361
362
	/**
363
	 * Returns the number of found unique pages (not revisions) in the set.
364
	 * @return int
365
	 */
366
	public function getGoodTitleCount() {
367
		return count( $this->mGoodTitles );
368
	}
369
370
	/**
371
	 * Returns an array [ns][dbkey] => fake_page_id for all missing titles.
372
	 * fake_page_id is a unique negative number.
373
	 * @return array
374
	 */
375
	public function getMissingTitlesByNamespace() {
376
		return $this->mMissingPages;
377
	}
378
379
	/**
380
	 * Title objects that were NOT found in the database.
381
	 * The array's index will be negative for each item
382
	 * @return Title[]
383
	 */
384
	public function getMissingTitles() {
385
		return $this->mMissingTitles;
386
	}
387
388
	/**
389
	 * Returns an array [ns][dbkey] => page_id for all good and missing titles.
390
	 * @return array
391
	 */
392
	public function getGoodAndMissingTitlesByNamespace() {
393
		return $this->mGoodAndMissingPages;
394
	}
395
396
	/**
397
	 * Title objects for good and missing titles.
398
	 * @return array
399
	 */
400
	public function getGoodAndMissingTitles() {
401
		return $this->mGoodTitles + $this->mMissingTitles;
402
	}
403
404
	/**
405
	 * Titles that were deemed invalid by Title::newFromText()
406
	 * The array's index will be unique and negative for each item
407
	 * @deprecated since 1.26, use self::getInvalidTitlesAndReasons()
408
	 * @return string[] Array of strings (not Title objects)
409
	 */
410
	public function getInvalidTitles() {
411
		wfDeprecated( __METHOD__, '1.26' );
412
		return array_map( function ( $t ) {
413
			return $t['title'];
414
		}, $this->mInvalidTitles );
415
	}
416
417
	/**
418
	 * Titles that were deemed invalid by Title::newFromText()
419
	 * The array's index will be unique and negative for each item
420
	 * @return array[] Array of arrays with 'title' and 'invalidreason' properties
421
	 */
422
	public function getInvalidTitlesAndReasons() {
423
		return $this->mInvalidTitles;
424
	}
425
426
	/**
427
	 * Page IDs that were not found in the database
428
	 * @return array Array of page IDs
429
	 */
430
	public function getMissingPageIDs() {
431
		return $this->mMissingPageIDs;
432
	}
433
434
	/**
435
	 * Get a list of redirect resolutions - maps a title to its redirect
436
	 * target, as an array of output-ready arrays
437
	 * @return Title[]
438
	 */
439
	public function getRedirectTitles() {
440
		return $this->mRedirectTitles;
441
	}
442
443
	/**
444
	 * Get a list of redirect resolutions - maps a title to its redirect
445
	 * target. Includes generator data for redirect source when available.
446
	 * @param ApiResult $result
447
	 * @return array Array of prefixed_title (string) => Title object
448
	 * @since 1.21
449
	 */
450
	public function getRedirectTitlesAsResult( $result = null ) {
451
		$values = [];
452
		foreach ( $this->getRedirectTitles() as $titleStrFrom => $titleTo ) {
453
			$r = [
454
				'from' => strval( $titleStrFrom ),
455
				'to' => $titleTo->getPrefixedText(),
456
			];
457
			if ( $titleTo->hasFragment() ) {
458
				$r['tofragment'] = $titleTo->getFragment();
459
			}
460
			if ( $titleTo->isExternal() ) {
461
				$r['tointerwiki'] = $titleTo->getInterwiki();
462
			}
463
			if ( isset( $this->mResolvedRedirectTitles[$titleStrFrom] ) ) {
464
				$titleFrom = $this->mResolvedRedirectTitles[$titleStrFrom];
465
				$ns = $titleFrom->getNamespace();
466
				$dbkey = $titleFrom->getDBkey();
467
				if ( isset( $this->mGeneratorData[$ns][$dbkey] ) ) {
468
					$r = array_merge( $this->mGeneratorData[$ns][$dbkey], $r );
469
				}
470
			}
471
472
			$values[] = $r;
473
		}
474
		if ( !empty( $values ) && $result ) {
475
			ApiResult::setIndexedTagName( $values, 'r' );
476
		}
477
478
		return $values;
479
	}
480
481
	/**
482
	 * Get a list of title normalizations - maps a title to its normalized
483
	 * version.
484
	 * @return array Array of raw_prefixed_title (string) => prefixed_title (string)
485
	 */
486
	public function getNormalizedTitles() {
487
		return $this->mNormalizedTitles;
488
	}
489
490
	/**
491
	 * Get a list of title normalizations - maps a title to its normalized
492
	 * version in the form of result array.
493
	 * @param ApiResult $result
494
	 * @return array Array of raw_prefixed_title (string) => prefixed_title (string)
495
	 * @since 1.21
496
	 */
497 View Code Duplication
	public function getNormalizedTitlesAsResult( $result = null ) {
498
		$values = [];
499
		foreach ( $this->getNormalizedTitles() as $rawTitleStr => $titleStr ) {
500
			$values[] = [
501
				'from' => $rawTitleStr,
502
				'to' => $titleStr
503
			];
504
		}
505
		if ( !empty( $values ) && $result ) {
506
			ApiResult::setIndexedTagName( $values, 'n' );
507
		}
508
509
		return $values;
510
	}
511
512
	/**
513
	 * Get a list of title conversions - maps a title to its converted
514
	 * version.
515
	 * @return array Array of raw_prefixed_title (string) => prefixed_title (string)
516
	 */
517
	public function getConvertedTitles() {
518
		return $this->mConvertedTitles;
519
	}
520
521
	/**
522
	 * Get a list of title conversions - maps a title to its converted
523
	 * version as a result array.
524
	 * @param ApiResult $result
525
	 * @return array Array of (from, to) strings
526
	 * @since 1.21
527
	 */
528 View Code Duplication
	public function getConvertedTitlesAsResult( $result = null ) {
529
		$values = [];
530
		foreach ( $this->getConvertedTitles() as $rawTitleStr => $titleStr ) {
531
			$values[] = [
532
				'from' => $rawTitleStr,
533
				'to' => $titleStr
534
			];
535
		}
536
		if ( !empty( $values ) && $result ) {
537
			ApiResult::setIndexedTagName( $values, 'c' );
538
		}
539
540
		return $values;
541
	}
542
543
	/**
544
	 * Get a list of interwiki titles - maps a title to its interwiki
545
	 * prefix.
546
	 * @return array Array of raw_prefixed_title (string) => interwiki_prefix (string)
547
	 */
548
	public function getInterwikiTitles() {
549
		return $this->mInterwikiTitles;
550
	}
551
552
	/**
553
	 * Get a list of interwiki titles - maps a title to its interwiki
554
	 * prefix as result.
555
	 * @param ApiResult $result
556
	 * @param bool $iwUrl
557
	 * @return array Array of raw_prefixed_title (string) => interwiki_prefix (string)
558
	 * @since 1.21
559
	 */
560
	public function getInterwikiTitlesAsResult( $result = null, $iwUrl = false ) {
561
		$values = [];
562
		foreach ( $this->getInterwikiTitles() as $rawTitleStr => $interwikiStr ) {
563
			$item = [
564
				'title' => $rawTitleStr,
565
				'iw' => $interwikiStr,
566
			];
567
			if ( $iwUrl ) {
568
				$title = Title::newFromText( $rawTitleStr );
569
				$item['url'] = $title->getFullURL( '', false, PROTO_CURRENT );
570
			}
571
			$values[] = $item;
572
		}
573
		if ( !empty( $values ) && $result ) {
574
			ApiResult::setIndexedTagName( $values, 'i' );
575
		}
576
577
		return $values;
578
	}
579
580
	/**
581
	 * Get an array of invalid/special/missing titles.
582
	 *
583
	 * @param array $invalidChecks List of types of invalid titles to include.
584
	 *   Recognized values are:
585
	 *   - invalidTitles: Titles and reasons from $this->getInvalidTitlesAndReasons()
586
	 *   - special: Titles from $this->getSpecialTitles()
587
	 *   - missingIds: ids from $this->getMissingPageIDs()
588
	 *   - missingRevIds: ids from $this->getMissingRevisionIDs()
589
	 *   - missingTitles: Titles from $this->getMissingTitles()
590
	 *   - interwikiTitles: Titles from $this->getInterwikiTitlesAsResult()
591
	 * @return array Array suitable for inclusion in the response
592
	 * @since 1.23
593
	 */
594
	public function getInvalidTitlesAndRevisions( $invalidChecks = [ 'invalidTitles',
595
		'special', 'missingIds', 'missingRevIds', 'missingTitles', 'interwikiTitles' ]
596
	) {
597
		$result = [];
598
		if ( in_array( 'invalidTitles', $invalidChecks ) ) {
599
			self::addValues( $result, $this->getInvalidTitlesAndReasons(), 'invalid' );
600
		}
601
		if ( in_array( 'special', $invalidChecks ) ) {
602
			self::addValues( $result, $this->getSpecialTitles(), 'special', 'title' );
603
		}
604
		if ( in_array( 'missingIds', $invalidChecks ) ) {
605
			self::addValues( $result, $this->getMissingPageIDs(), 'missing', 'pageid' );
606
		}
607
		if ( in_array( 'missingRevIds', $invalidChecks ) ) {
608
			self::addValues( $result, $this->getMissingRevisionIDs(), 'missing', 'revid' );
609
		}
610
		if ( in_array( 'missingTitles', $invalidChecks ) ) {
611
			self::addValues( $result, $this->getMissingTitles(), 'missing' );
612
		}
613
		if ( in_array( 'interwikiTitles', $invalidChecks ) ) {
614
			self::addValues( $result, $this->getInterwikiTitlesAsResult() );
615
		}
616
617
		return $result;
618
	}
619
620
	/**
621
	 * Get the list of valid revision IDs (requested with the revids= parameter)
622
	 * @return array Array of revID (int) => pageID (int)
623
	 */
624
	public function getRevisionIDs() {
625
		return $this->mGoodRevIDs;
626
	}
627
628
	/**
629
	 * Get the list of non-deleted revision IDs (requested with the revids= parameter)
630
	 * @return array Array of revID (int) => pageID (int)
631
	 */
632
	public function getLiveRevisionIDs() {
633
		return $this->mLiveRevIDs;
634
	}
635
636
	/**
637
	 * Get the list of revision IDs that were associated with deleted titles.
638
	 * @return array Array of revID (int) => pageID (int)
639
	 */
640
	public function getDeletedRevisionIDs() {
641
		return $this->mDeletedRevIDs;
642
	}
643
644
	/**
645
	 * Revision IDs that were not found in the database
646
	 * @return array Array of revision IDs
647
	 */
648
	public function getMissingRevisionIDs() {
649
		return $this->mMissingRevIDs;
650
	}
651
652
	/**
653
	 * Revision IDs that were not found in the database as result array.
654
	 * @param ApiResult $result
655
	 * @return array Array of revision IDs
656
	 * @since 1.21
657
	 */
658
	public function getMissingRevisionIDsAsResult( $result = null ) {
659
		$values = [];
660
		foreach ( $this->getMissingRevisionIDs() as $revid ) {
661
			$values[$revid] = [
662
				'revid' => $revid
663
			];
664
		}
665
		if ( !empty( $values ) && $result ) {
666
			ApiResult::setIndexedTagName( $values, 'rev' );
667
		}
668
669
		return $values;
670
	}
671
672
	/**
673
	 * Get the list of titles with negative namespace
674
	 * @return Title[]
675
	 */
676
	public function getSpecialTitles() {
677
		return $this->mSpecialTitles;
678
	}
679
680
	/**
681
	 * Returns the number of revisions (requested with revids= parameter).
682
	 * @return int Number of revisions.
683
	 */
684
	public function getRevisionCount() {
685
		return count( $this->getRevisionIDs() );
686
	}
687
688
	/**
689
	 * Populate this PageSet from a list of Titles
690
	 * @param array $titles Array of Title objects
691
	 */
692
	public function populateFromTitles( $titles ) {
693
		$this->initFromTitles( $titles );
694
	}
695
696
	/**
697
	 * Populate this PageSet from a list of page IDs
698
	 * @param array $pageIDs Array of page IDs
699
	 */
700
	public function populateFromPageIDs( $pageIDs ) {
701
		$this->initFromPageIds( $pageIDs );
702
	}
703
704
	/**
705
	 * Populate this PageSet from a rowset returned from the database
706
	 *
707
	 * Note that the query result must include the columns returned by
708
	 * $this->getPageTableFields().
709
	 *
710
	 * @param IDatabase $db
711
	 * @param ResultWrapper $queryResult Query result object
712
	 */
713
	public function populateFromQueryResult( $db, $queryResult ) {
714
		$this->initFromQueryResult( $queryResult );
715
	}
716
717
	/**
718
	 * Populate this PageSet from a list of revision IDs
719
	 * @param array $revIDs Array of revision IDs
720
	 */
721
	public function populateFromRevisionIDs( $revIDs ) {
722
		$this->initFromRevIDs( $revIDs );
723
	}
724
725
	/**
726
	 * Extract all requested fields from the row received from the database
727
	 * @param stdClass $row Result row
728
	 */
729
	public function processDbRow( $row ) {
730
		// Store Title object in various data structures
731
		$title = Title::newFromRow( $row );
732
733
		$pageId = intval( $row->page_id );
734
		$this->mAllPages[$row->page_namespace][$row->page_title] = $pageId;
735
		$this->mTitles[] = $title;
736
737
		if ( $this->mResolveRedirects && $row->page_is_redirect == '1' ) {
738
			$this->mPendingRedirectIDs[$pageId] = $title;
739
		} else {
740
			$this->mGoodPages[$row->page_namespace][$row->page_title] = $pageId;
741
			$this->mGoodAndMissingPages[$row->page_namespace][$row->page_title] = $pageId;
742
			$this->mGoodTitles[$pageId] = $title;
743
		}
744
745
		foreach ( $this->mRequestedPageFields as $fieldName => &$fieldValues ) {
746
			$fieldValues[$pageId] = $row->$fieldName;
747
		}
748
	}
749
750
	/**
751
	 * This method populates internal variables with page information
752
	 * based on the given array of title strings.
753
	 *
754
	 * Steps:
755
	 * #1 For each title, get data from `page` table
756
	 * #2 If page was not found in the DB, store it as missing
757
	 *
758
	 * Additionally, when resolving redirects:
759
	 * #3 If no more redirects left, stop.
760
	 * #4 For each redirect, get its target from the `redirect` table.
761
	 * #5 Substitute the original LinkBatch object with the new list
762
	 * #6 Repeat from step #1
763
	 *
764
	 * @param array $titles Array of Title objects or strings
765
	 */
766
	private function initFromTitles( $titles ) {
767
		// Get validated and normalized title objects
768
		$linkBatch = $this->processTitlesArray( $titles );
769
		if ( $linkBatch->isEmpty() ) {
770
			return;
771
		}
772
773
		$db = $this->getDB();
774
		$set = $linkBatch->constructSet( 'page', $db );
775
776
		// Get pageIDs data from the `page` table
777
		$res = $db->select( 'page', $this->getPageTableFields(), $set,
0 ignored issues
show
Bug introduced by
It seems like $set defined by $linkBatch->constructSet('page', $db) on line 774 can also be of type boolean; however, DatabaseBase::select() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
778
			__METHOD__ );
779
780
		// Hack: get the ns:titles stored in array(ns => array(titles)) format
781
		$this->initFromQueryResult( $res, $linkBatch->data, true ); // process Titles
0 ignored issues
show
Bug introduced by
It seems like $res defined by $db->select('page', $thi...ds(), $set, __METHOD__) on line 777 can also be of type boolean; however, ApiPageSet::initFromQueryResult() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
782
783
		// Resolve any found redirects
784
		$this->resolvePendingRedirects();
785
	}
786
787
	/**
788
	 * Does the same as initFromTitles(), but is based on page IDs instead
789
	 * @param array $pageids Array of page IDs
790
	 */
791
	private function initFromPageIds( $pageids ) {
792
		if ( !$pageids ) {
793
			return;
794
		}
795
796
		$pageids = array_map( 'intval', $pageids ); // paranoia
797
		$remaining = array_flip( $pageids );
798
799
		$pageids = self::getPositiveIntegers( $pageids );
800
801
		$res = null;
802
		if ( !empty( $pageids ) ) {
803
			$set = [
804
				'page_id' => $pageids
805
			];
806
			$db = $this->getDB();
807
808
			// Get pageIDs data from the `page` table
809
			$res = $db->select( 'page', $this->getPageTableFields(), $set,
810
				__METHOD__ );
811
		}
812
813
		$this->initFromQueryResult( $res, $remaining, false ); // process PageIDs
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type boolean or null; however, ApiPageSet::initFromQueryResult() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
814
815
		// Resolve any found redirects
816
		$this->resolvePendingRedirects();
817
	}
818
819
	/**
820
	 * Iterate through the result of the query on 'page' table,
821
	 * and for each row create and store title object and save any extra fields requested.
822
	 * @param ResultWrapper $res DB Query result
823
	 * @param array $remaining Array of either pageID or ns/title elements (optional).
824
	 *        If given, any missing items will go to $mMissingPageIDs and $mMissingTitles
825
	 * @param bool $processTitles Must be provided together with $remaining.
826
	 *        If true, treat $remaining as an array of [ns][title]
827
	 *        If false, treat it as an array of [pageIDs]
828
	 */
829
	private function initFromQueryResult( $res, &$remaining = null, $processTitles = null ) {
830
		if ( !is_null( $remaining ) && is_null( $processTitles ) ) {
831
			ApiBase::dieDebug( __METHOD__, 'Missing $processTitles parameter when $remaining is provided' );
832
		}
833
834
		$usernames = [];
835
		if ( $res ) {
836
			foreach ( $res as $row ) {
837
				$pageId = intval( $row->page_id );
838
839
				// Remove found page from the list of remaining items
840
				if ( isset( $remaining ) ) {
841
					if ( $processTitles ) {
842
						unset( $remaining[$row->page_namespace][$row->page_title] );
843
					} else {
844
						unset( $remaining[$pageId] );
845
					}
846
				}
847
848
				// Store any extra fields requested by modules
849
				$this->processDbRow( $row );
850
851
				// Need gender information
852
				if ( MWNamespace::hasGenderDistinction( $row->page_namespace ) ) {
853
					$usernames[] = $row->page_title;
854
				}
855
			}
856
		}
857
858
		if ( isset( $remaining ) ) {
859
			// Any items left in the $remaining list are added as missing
860
			if ( $processTitles ) {
861
				// The remaining titles in $remaining are non-existent pages
862
				foreach ( $remaining as $ns => $dbkeys ) {
863
					foreach ( array_keys( $dbkeys ) as $dbkey ) {
864
						$title = Title::makeTitle( $ns, $dbkey );
865
						$this->mAllPages[$ns][$dbkey] = $this->mFakePageId;
866
						$this->mMissingPages[$ns][$dbkey] = $this->mFakePageId;
867
						$this->mGoodAndMissingPages[$ns][$dbkey] = $this->mFakePageId;
868
						$this->mMissingTitles[$this->mFakePageId] = $title;
869
						$this->mFakePageId--;
870
						$this->mTitles[] = $title;
871
872
						// need gender information
873
						if ( MWNamespace::hasGenderDistinction( $ns ) ) {
874
							$usernames[] = $dbkey;
875
						}
876
					}
877
				}
878
			} else {
879
				// The remaining pageids do not exist
880
				if ( !$this->mMissingPageIDs ) {
881
					$this->mMissingPageIDs = array_keys( $remaining );
882
				} else {
883
					$this->mMissingPageIDs = array_merge( $this->mMissingPageIDs, array_keys( $remaining ) );
884
				}
885
			}
886
		}
887
888
		// Get gender information
889
		$genderCache = GenderCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method GenderCache::singleton() has been deprecated with message: in 1.28 see MediaWikiServices::getInstance()->getGenderCache()

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...
890
		$genderCache->doQuery( $usernames, __METHOD__ );
891
	}
892
893
	/**
894
	 * Does the same as initFromTitles(), but is based on revision IDs
895
	 * instead
896
	 * @param array $revids Array of revision IDs
897
	 */
898
	private function initFromRevIDs( $revids ) {
899
		if ( !$revids ) {
900
			return;
901
		}
902
903
		$revids = array_map( 'intval', $revids ); // paranoia
904
		$db = $this->getDB();
905
		$pageids = [];
906
		$remaining = array_flip( $revids );
907
908
		$revids = self::getPositiveIntegers( $revids );
909
910
		if ( !empty( $revids ) ) {
911
			$tables = [ 'revision', 'page' ];
912
			$fields = [ 'rev_id', 'rev_page' ];
913
			$where = [ 'rev_id' => $revids, 'rev_page = page_id' ];
914
915
			// Get pageIDs data from the `page` table
916
			$res = $db->select( $tables, $fields, $where, __METHOD__ );
917
			foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
918
				$revid = intval( $row->rev_id );
919
				$pageid = intval( $row->rev_page );
920
				$this->mGoodRevIDs[$revid] = $pageid;
921
				$this->mLiveRevIDs[$revid] = $pageid;
922
				$pageids[$pageid] = '';
923
				unset( $remaining[$revid] );
924
			}
925
		}
926
927
		$this->mMissingRevIDs = array_keys( $remaining );
928
929
		// Populate all the page information
930
		$this->initFromPageIds( array_keys( $pageids ) );
931
932
		// If the user can see deleted revisions, pull out the corresponding
933
		// titles from the archive table and include them too. We ignore
934
		// ar_page_id because deleted revisions are tied by title, not page_id.
935
		if ( !empty( $this->mMissingRevIDs ) && $this->getUser()->isAllowed( 'deletedhistory' ) ) {
936
			$remaining = array_flip( $this->mMissingRevIDs );
937
			$tables = [ 'archive' ];
938
			$fields = [ 'ar_rev_id', 'ar_namespace', 'ar_title' ];
939
			$where = [ 'ar_rev_id' => $this->mMissingRevIDs ];
940
941
			$res = $db->select( $tables, $fields, $where, __METHOD__ );
942
			$titles = [];
943
			foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
944
				$revid = intval( $row->ar_rev_id );
945
				$titles[$revid] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
946
				unset( $remaining[$revid] );
947
			}
948
949
			$this->initFromTitles( $titles );
950
951
			foreach ( $titles as $revid => $title ) {
952
				$ns = $title->getNamespace();
953
				$dbkey = $title->getDBkey();
954
955
				// Handle converted titles
956
				if ( !isset( $this->mAllPages[$ns][$dbkey] ) &&
957
					isset( $this->mConvertedTitles[$title->getPrefixedText()] )
958
				) {
959
					$title = Title::newFromText( $this->mConvertedTitles[$title->getPrefixedText()] );
960
					$ns = $title->getNamespace();
961
					$dbkey = $title->getDBkey();
962
				}
963
964
				if ( isset( $this->mAllPages[$ns][$dbkey] ) ) {
965
					$this->mGoodRevIDs[$revid] = $this->mAllPages[$ns][$dbkey];
966
					$this->mDeletedRevIDs[$revid] = $this->mAllPages[$ns][$dbkey];
967
				} else {
968
					$remaining[$revid] = true;
969
				}
970
			}
971
972
			$this->mMissingRevIDs = array_keys( $remaining );
973
		}
974
	}
975
976
	/**
977
	 * Resolve any redirects in the result if redirect resolution was
978
	 * requested. This function is called repeatedly until all redirects
979
	 * have been resolved.
980
	 */
981
	private function resolvePendingRedirects() {
982
		if ( $this->mResolveRedirects ) {
983
			$db = $this->getDB();
984
			$pageFlds = $this->getPageTableFields();
985
986
			// Repeat until all redirects have been resolved
987
			// The infinite loop is prevented by keeping all known pages in $this->mAllPages
988
			while ( $this->mPendingRedirectIDs ) {
989
				// Resolve redirects by querying the pagelinks table, and repeat the process
990
				// Create a new linkBatch object for the next pass
991
				$linkBatch = $this->getRedirectTargets();
992
993
				if ( $linkBatch->isEmpty() ) {
994
					break;
995
				}
996
997
				$set = $linkBatch->constructSet( 'page', $db );
998
				if ( $set === false ) {
999
					break;
1000
				}
1001
1002
				// Get pageIDs data from the `page` table
1003
				$res = $db->select( 'page', $pageFlds, $set, __METHOD__ );
0 ignored issues
show
Bug introduced by
It seems like $set defined by $linkBatch->constructSet('page', $db) on line 997 can also be of type boolean; however, DatabaseBase::select() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1004
1005
				// Hack: get the ns:titles stored in array(ns => array(titles)) format
1006
				$this->initFromQueryResult( $res, $linkBatch->data, true );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $db->select('page', $pageFlds, $set, __METHOD__) on line 1003 can also be of type boolean; however, ApiPageSet::initFromQueryResult() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1007
			}
1008
		}
1009
	}
1010
1011
	/**
1012
	 * Get the targets of the pending redirects from the database
1013
	 *
1014
	 * Also creates entries in the redirect table for redirects that don't
1015
	 * have one.
1016
	 * @return LinkBatch
1017
	 */
1018
	private function getRedirectTargets() {
1019
		$lb = new LinkBatch();
1020
		$db = $this->getDB();
1021
1022
		$res = $db->select(
1023
			'redirect',
1024
			[
1025
				'rd_from',
1026
				'rd_namespace',
1027
				'rd_fragment',
1028
				'rd_interwiki',
1029
				'rd_title'
1030
			], [ 'rd_from' => array_keys( $this->mPendingRedirectIDs ) ],
1031
			__METHOD__
1032
		);
1033
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1034
			$rdfrom = intval( $row->rd_from );
1035
			$from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText();
1036
			$to = Title::makeTitle(
1037
				$row->rd_namespace,
1038
				$row->rd_title,
1039
				$row->rd_fragment,
1040
				$row->rd_interwiki
1041
			);
1042
			$this->mResolvedRedirectTitles[$from] = $this->mPendingRedirectIDs[$rdfrom];
1043
			unset( $this->mPendingRedirectIDs[$rdfrom] );
1044
			if ( $to->isExternal() ) {
1045
				$this->mInterwikiTitles[$to->getPrefixedText()] = $to->getInterwiki();
1046
			} elseif ( !isset( $this->mAllPages[$row->rd_namespace][$row->rd_title] ) ) {
1047
				$lb->add( $row->rd_namespace, $row->rd_title );
1048
			}
1049
			$this->mRedirectTitles[$from] = $to;
1050
		}
1051
1052
		if ( $this->mPendingRedirectIDs ) {
1053
			// We found pages that aren't in the redirect table
1054
			// Add them
1055
			foreach ( $this->mPendingRedirectIDs as $id => $title ) {
1056
				$page = WikiPage::factory( $title );
1057
				$rt = $page->insertRedirect();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $rt is correct as $page->insertRedirect() (which targets WikiPage::insertRedirect()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1058
				if ( !$rt ) {
1059
					// What the hell. Let's just ignore this
1060
					continue;
1061
				}
1062
				$lb->addObj( $rt );
1063
				$from = $title->getPrefixedText();
1064
				$this->mResolvedRedirectTitles[$from] = $title;
1065
				$this->mRedirectTitles[$from] = $rt;
1066
				unset( $this->mPendingRedirectIDs[$id] );
1067
			}
1068
		}
1069
1070
		return $lb;
1071
	}
1072
1073
	/**
1074
	 * Get the cache mode for the data generated by this module.
1075
	 * All PageSet users should take into account whether this returns a more-restrictive
1076
	 * cache mode than the using module itself. For possible return values and other
1077
	 * details about cache modes, see ApiMain::setCacheMode()
1078
	 *
1079
	 * Public caching will only be allowed if *all* the modules that supply
1080
	 * data for a given request return a cache mode of public.
1081
	 *
1082
	 * @param array|null $params
1083
	 * @return string
1084
	 * @since 1.21
1085
	 */
1086
	public function getCacheMode( $params = null ) {
1087
		return $this->mCacheMode;
1088
	}
1089
1090
	/**
1091
	 * Given an array of title strings, convert them into Title objects.
1092
	 * Alternatively, an array of Title objects may be given.
1093
	 * This method validates access rights for the title,
1094
	 * and appends normalization values to the output.
1095
	 *
1096
	 * @param array $titles Array of Title objects or strings
1097
	 * @return LinkBatch
1098
	 */
1099
	private function processTitlesArray( $titles ) {
1100
		$usernames = [];
1101
		$linkBatch = new LinkBatch();
1102
1103
		foreach ( $titles as $title ) {
1104
			if ( is_string( $title ) ) {
1105
				try {
1106
					$titleObj = Title::newFromTextThrow( $title, $this->mDefaultNamespace );
1107
				} catch ( MalformedTitleException $ex ) {
1108
					// Handle invalid titles gracefully
1109
					$this->mAllPages[0][$title] = $this->mFakePageId;
1110
					$this->mInvalidTitles[$this->mFakePageId] = [
1111
						'title' => $title,
1112
						'invalidreason' => $ex->getMessage(),
1113
					];
1114
					$this->mFakePageId--;
1115
					continue; // There's nothing else we can do
1116
				}
1117
			} else {
1118
				$titleObj = $title;
1119
			}
1120
			$unconvertedTitle = $titleObj->getPrefixedText();
1121
			$titleWasConverted = false;
1122
			if ( $titleObj->isExternal() ) {
1123
				// This title is an interwiki link.
1124
				$this->mInterwikiTitles[$unconvertedTitle] = $titleObj->getInterwiki();
1125
			} else {
1126
				// Variants checking
1127
				global $wgContLang;
1128
				if ( $this->mConvertTitles &&
1129
					count( $wgContLang->getVariants() ) > 1 &&
1130
					!$titleObj->exists()
1131
				) {
1132
					// Language::findVariantLink will modify titleText and titleObj into
1133
					// the canonical variant if possible
1134
					$titleText = is_string( $title ) ? $title : $titleObj->getPrefixedText();
1135
					$wgContLang->findVariantLink( $titleText, $titleObj );
1136
					$titleWasConverted = $unconvertedTitle !== $titleObj->getPrefixedText();
1137
				}
1138
1139
				if ( $titleObj->getNamespace() < 0 ) {
1140
					// Handle Special and Media pages
1141
					$titleObj = $titleObj->fixSpecialName();
1142
					$this->mSpecialTitles[$this->mFakePageId] = $titleObj;
1143
					$this->mFakePageId--;
1144
				} else {
1145
					// Regular page
1146
					$linkBatch->addObj( $titleObj );
1147
				}
1148
			}
1149
1150
			// Make sure we remember the original title that was
1151
			// given to us. This way the caller can correlate new
1152
			// titles with the originally requested when e.g. the
1153
			// namespace is localized or the capitalization is
1154
			// different
1155
			if ( $titleWasConverted ) {
1156
				$this->mConvertedTitles[$unconvertedTitle] = $titleObj->getPrefixedText();
1157
				// In this case the page can't be Special.
1158
				if ( is_string( $title ) && $title !== $unconvertedTitle ) {
1159
					$this->mNormalizedTitles[$title] = $unconvertedTitle;
1160
				}
1161
			} elseif ( is_string( $title ) && $title !== $titleObj->getPrefixedText() ) {
1162
				$this->mNormalizedTitles[$title] = $titleObj->getPrefixedText();
1163
			}
1164
1165
			// Need gender information
1166
			if ( MWNamespace::hasGenderDistinction( $titleObj->getNamespace() ) ) {
1167
				$usernames[] = $titleObj->getText();
1168
			}
1169
		}
1170
		// Get gender information
1171
		$genderCache = GenderCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method GenderCache::singleton() has been deprecated with message: in 1.28 see MediaWikiServices::getInstance()->getGenderCache()

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...
1172
		$genderCache->doQuery( $usernames, __METHOD__ );
1173
1174
		return $linkBatch;
1175
	}
1176
1177
	/**
1178
	 * Set data for a title.
1179
	 *
1180
	 * This data may be extracted into an ApiResult using
1181
	 * self::populateGeneratorData. This should generally be limited to
1182
	 * data that is likely to be particularly useful to end users rather than
1183
	 * just being a dump of everything returned in non-generator mode.
1184
	 *
1185
	 * Redirects here will *not* be followed, even if 'redirects' was
1186
	 * specified, since in the case of multiple redirects we can't know which
1187
	 * source's data to use on the target.
1188
	 *
1189
	 * @param Title $title
1190
	 * @param array $data
1191
	 */
1192
	public function setGeneratorData( Title $title, array $data ) {
1193
		$ns = $title->getNamespace();
1194
		$dbkey = $title->getDBkey();
1195
		$this->mGeneratorData[$ns][$dbkey] = $data;
1196
	}
1197
1198
	/**
1199
	 * Controls how generator data about a redirect source is merged into
1200
	 * the generator data for the redirect target. When not set no data
1201
	 * is merged. Note that if multiple titles redirect to the same target
1202
	 * the order of operations is undefined.
1203
	 *
1204
	 * Example to include generated data from redirect in target, prefering
1205
	 * the data generated for the destination when there is a collision:
1206
	 * @code
1207
	 *   $pageSet->setRedirectMergePolicy( function( array $current, array $new ) {
1208
	 *       return $current + $new;
1209
	 *   } );
1210
	 * @endcode
1211
	 *
1212
	 * @param callable|null $callable Recieves two array arguments, first the
1213
	 *  generator data for the redirect target and second the generator data
1214
	 *  for the redirect source. Returns the resulting generator data to use
1215
	 *  for the redirect target.
1216
	 */
1217
	public function setRedirectMergePolicy( $callable ) {
1218
		$this->mRedirectMergePolicy = $callable;
1219
	}
1220
1221
	/**
1222
	 * Populate the generator data for all titles in the result
1223
	 *
1224
	 * The page data may be inserted into an ApiResult object or into an
1225
	 * associative array. The $path parameter specifies the path within the
1226
	 * ApiResult or array to find the "pages" node.
1227
	 *
1228
	 * The "pages" node itself must be an associative array mapping the page ID
1229
	 * or fake page ID values returned by this pageset (see
1230
	 * self::getAllTitlesByNamespace() and self::getSpecialTitles()) to
1231
	 * associative arrays of page data. Each of those subarrays will have the
1232
	 * data from self::setGeneratorData() merged in.
1233
	 *
1234
	 * Data that was set by self::setGeneratorData() for pages not in the
1235
	 * "pages" node will be ignored.
1236
	 *
1237
	 * @param ApiResult|array &$result
1238
	 * @param array $path
1239
	 * @return bool Whether the data fit
1240
	 */
1241
	public function populateGeneratorData( &$result, array $path = [] ) {
1242
		if ( $result instanceof ApiResult ) {
1243
			$data = $result->getResultData( $path );
1244
			if ( $data === null ) {
1245
				return true;
1246
			}
1247
		} else {
1248
			$data = &$result;
1249
			foreach ( $path as $key ) {
1250
				if ( !isset( $data[$key] ) ) {
1251
					// Path isn't in $result, so nothing to add, so everything
1252
					// "fits"
1253
					return true;
1254
				}
1255
				$data = &$data[$key];
1256
			}
1257
		}
1258
		foreach ( $this->mGeneratorData as $ns => $dbkeys ) {
1259
			if ( $ns === -1 ) {
1260
				$pages = [];
1261
				foreach ( $this->mSpecialTitles as $id => $title ) {
1262
					$pages[$title->getDBkey()] = $id;
1263
				}
1264
			} else {
1265
				if ( !isset( $this->mAllPages[$ns] ) ) {
1266
					// No known titles in the whole namespace. Skip it.
1267
					continue;
1268
				}
1269
				$pages = $this->mAllPages[$ns];
1270
			}
1271
			foreach ( $dbkeys as $dbkey => $genData ) {
1272
				if ( !isset( $pages[$dbkey] ) ) {
1273
					// Unknown title. Forget it.
1274
					continue;
1275
				}
1276
				$pageId = $pages[$dbkey];
1277
				if ( !isset( $data[$pageId] ) ) {
1278
					// $pageId didn't make it into the result. Ignore it.
1279
					continue;
1280
				}
1281
1282
				if ( $result instanceof ApiResult ) {
1283
					$path2 = array_merge( $path, [ $pageId ] );
1284
					foreach ( $genData as $key => $value ) {
1285
						if ( !$result->addValue( $path2, $key, $value ) ) {
1286
							return false;
1287
						}
1288
					}
1289
				} else {
1290
					$data[$pageId] = array_merge( $data[$pageId], $genData );
1291
				}
1292
			}
1293
		}
1294
1295
		// Merge data generated about redirect titles into the redirect destination
1296
		if ( $this->mRedirectMergePolicy ) {
1297
			foreach ( $this->mResolvedRedirectTitles as $titleFrom ) {
1298
				$dest = $titleFrom;
1299
				while ( isset( $this->mRedirectTitles[$dest->getPrefixedText()] ) ) {
1300
					$dest = $this->mRedirectTitles[$dest->getPrefixedText()];
1301
				}
1302
				$fromNs = $titleFrom->getNamespace();
1303
				$fromDBkey = $titleFrom->getDBkey();
1304
				$toPageId = $dest->getArticleID();
1305
				if ( isset( $data[$toPageId] ) &&
1306
					isset( $this->mGeneratorData[$fromNs][$fromDBkey] )
1307
				) {
1308
					// It is necesary to set both $data and add to $result, if an ApiResult,
1309
					// to ensure multiple redirects to the same destination are all merged.
1310
					$data[$toPageId] = call_user_func(
1311
						$this->mRedirectMergePolicy,
1312
						$data[$toPageId],
1313
						$this->mGeneratorData[$fromNs][$fromDBkey]
1314
					);
1315
					if ( $result instanceof ApiResult ) {
1316
						if ( !$result->addValue( $path, $toPageId, $data[$toPageId], ApiResult::OVERRIDE ) ) {
1317
							return false;
1318
						}
1319
					}
1320
				}
1321
			}
1322
		}
1323
1324
		return true;
1325
	}
1326
1327
	/**
1328
	 * Get the database connection (read-only)
1329
	 * @return DatabaseBase
1330
	 */
1331
	protected function getDB() {
1332
		return $this->mDbSource->getDB();
1333
	}
1334
1335
	/**
1336
	 * Returns the input array of integers with all values < 0 removed
1337
	 *
1338
	 * @param array $array
1339
	 * @return array
1340
	 */
1341
	private static function getPositiveIntegers( $array ) {
1342
		// bug 25734 API: possible issue with revids validation
1343
		// It seems with a load of revision rows, MySQL gets upset
1344
		// Remove any < 0 integers, as they can't be valid
1345
		foreach ( $array as $i => $int ) {
1346
			if ( $int < 0 ) {
1347
				unset( $array[$i] );
1348
			}
1349
		}
1350
1351
		return $array;
1352
	}
1353
1354
	public function getAllowedParams( $flags = 0 ) {
1355
		$result = [
1356
			'titles' => [
1357
				ApiBase::PARAM_ISMULTI => true,
1358
				ApiBase::PARAM_HELP_MSG => 'api-pageset-param-titles',
1359
			],
1360
			'pageids' => [
1361
				ApiBase::PARAM_TYPE => 'integer',
1362
				ApiBase::PARAM_ISMULTI => true,
1363
				ApiBase::PARAM_HELP_MSG => 'api-pageset-param-pageids',
1364
			],
1365
			'revids' => [
1366
				ApiBase::PARAM_TYPE => 'integer',
1367
				ApiBase::PARAM_ISMULTI => true,
1368
				ApiBase::PARAM_HELP_MSG => 'api-pageset-param-revids',
1369
			],
1370
			'generator' => [
1371
				ApiBase::PARAM_TYPE => null,
1372
				ApiBase::PARAM_HELP_MSG => 'api-pageset-param-generator',
1373
				ApiBase::PARAM_SUBMODULE_PARAM_PREFIX => 'g',
1374
			],
1375
			'redirects' => [
1376
				ApiBase::PARAM_DFLT => false,
1377
				ApiBase::PARAM_HELP_MSG => $this->mAllowGenerator
1378
					? 'api-pageset-param-redirects-generator'
1379
					: 'api-pageset-param-redirects-nogenerator',
1380
			],
1381
			'converttitles' => [
1382
				ApiBase::PARAM_DFLT => false,
1383
				ApiBase::PARAM_HELP_MSG => [
1384
					'api-pageset-param-converttitles',
1385
					new DeferredStringifier(
1386
						function ( IContextSource $context ) {
1387
							return $context->getLanguage()
1388
								->commaList( LanguageConverter::$languagesWithVariants );
1389
						},
1390
						$this
1391
					)
1392
				],
1393
			],
1394
		];
1395
1396
		if ( !$this->mAllowGenerator ) {
1397
			unset( $result['generator'] );
1398
		} elseif ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
1399
			$result['generator'][ApiBase::PARAM_TYPE] = 'submodule';
1400
			$result['generator'][ApiBase::PARAM_SUBMODULE_MAP] = $this->getGenerators();
1401
		}
1402
1403
		return $result;
1404
	}
1405
1406
	private static $generators = null;
1407
1408
	/**
1409
	 * Get an array of all available generators
1410
	 * @return array
1411
	 */
1412
	private function getGenerators() {
1413
		if ( self::$generators === null ) {
1414
			$query = $this->mDbSource;
1415
			if ( !( $query instanceof ApiQuery ) ) {
1416
				// If the parent container of this pageset is not ApiQuery,
1417
				// we must create it to get module manager
1418
				$query = $this->getMain()->getModuleManager()->getModule( 'query' );
1419
			}
1420
			$gens = [];
1421
			$prefix = $query->getModulePath() . '+';
1422
			$mgr = $query->getModuleManager();
1423
			foreach ( $mgr->getNamesWithClasses() as $name => $class ) {
1424
				if ( is_subclass_of( $class, 'ApiQueryGeneratorBase' ) ) {
1425
					$gens[$name] = $prefix . $name;
1426
				}
1427
			}
1428
			ksort( $gens );
1429
			self::$generators = $gens;
1430
		}
1431
1432
		return self::$generators;
1433
	}
1434
}
1435