Completed
Branch master (a9d73a)
by
unknown
30:07
created

ApiPageSet::getNormalizedTitlesAsResult()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 12
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 18
rs 8.8571
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] => [ '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
	public function getNormalizedTitlesAsResult( $result = null ) {
498
		global $wgContLang;
499
500
		$values = [];
501
		foreach ( $this->getNormalizedTitles() as $rawTitleStr => $titleStr ) {
502
			$encode = ( $wgContLang->normalize( $rawTitleStr ) !== $rawTitleStr );
503
			$values[] = [
504
				'fromencoded' => $encode,
505
				'from' => $encode ? rawurlencode( $rawTitleStr ) : $rawTitleStr,
506
				'to' => $titleStr
507
			];
508
		}
509
		if ( !empty( $values ) && $result ) {
510
			ApiResult::setIndexedTagName( $values, 'n' );
511
		}
512
513
		return $values;
514
	}
515
516
	/**
517
	 * Get a list of title conversions - maps a title to its converted
518
	 * version.
519
	 * @return array Array of raw_prefixed_title (string) => prefixed_title (string)
520
	 */
521
	public function getConvertedTitles() {
522
		return $this->mConvertedTitles;
523
	}
524
525
	/**
526
	 * Get a list of title conversions - maps a title to its converted
527
	 * version as a result array.
528
	 * @param ApiResult $result
529
	 * @return array Array of (from, to) strings
530
	 * @since 1.21
531
	 */
532
	public function getConvertedTitlesAsResult( $result = null ) {
533
		$values = [];
534
		foreach ( $this->getConvertedTitles() as $rawTitleStr => $titleStr ) {
535
			$values[] = [
536
				'from' => $rawTitleStr,
537
				'to' => $titleStr
538
			];
539
		}
540
		if ( !empty( $values ) && $result ) {
541
			ApiResult::setIndexedTagName( $values, 'c' );
542
		}
543
544
		return $values;
545
	}
546
547
	/**
548
	 * Get a list of interwiki titles - maps a title to its interwiki
549
	 * prefix.
550
	 * @return array Array of raw_prefixed_title (string) => interwiki_prefix (string)
551
	 */
552
	public function getInterwikiTitles() {
553
		return $this->mInterwikiTitles;
554
	}
555
556
	/**
557
	 * Get a list of interwiki titles - maps a title to its interwiki
558
	 * prefix as result.
559
	 * @param ApiResult $result
560
	 * @param bool $iwUrl
561
	 * @return array Array of raw_prefixed_title (string) => interwiki_prefix (string)
562
	 * @since 1.21
563
	 */
564
	public function getInterwikiTitlesAsResult( $result = null, $iwUrl = false ) {
565
		$values = [];
566
		foreach ( $this->getInterwikiTitles() as $rawTitleStr => $interwikiStr ) {
567
			$item = [
568
				'title' => $rawTitleStr,
569
				'iw' => $interwikiStr,
570
			];
571
			if ( $iwUrl ) {
572
				$title = Title::newFromText( $rawTitleStr );
573
				$item['url'] = $title->getFullURL( '', false, PROTO_CURRENT );
574
			}
575
			$values[] = $item;
576
		}
577
		if ( !empty( $values ) && $result ) {
578
			ApiResult::setIndexedTagName( $values, 'i' );
579
		}
580
581
		return $values;
582
	}
583
584
	/**
585
	 * Get an array of invalid/special/missing titles.
586
	 *
587
	 * @param array $invalidChecks List of types of invalid titles to include.
588
	 *   Recognized values are:
589
	 *   - invalidTitles: Titles and reasons from $this->getInvalidTitlesAndReasons()
590
	 *   - special: Titles from $this->getSpecialTitles()
591
	 *   - missingIds: ids from $this->getMissingPageIDs()
592
	 *   - missingRevIds: ids from $this->getMissingRevisionIDs()
593
	 *   - missingTitles: Titles from $this->getMissingTitles()
594
	 *   - interwikiTitles: Titles from $this->getInterwikiTitlesAsResult()
595
	 * @return array Array suitable for inclusion in the response
596
	 * @since 1.23
597
	 */
598
	public function getInvalidTitlesAndRevisions( $invalidChecks = [ 'invalidTitles',
599
		'special', 'missingIds', 'missingRevIds', 'missingTitles', 'interwikiTitles' ]
600
	) {
601
		$result = [];
602
		if ( in_array( 'invalidTitles', $invalidChecks ) ) {
603
			self::addValues( $result, $this->getInvalidTitlesAndReasons(), 'invalid' );
604
		}
605
		if ( in_array( 'special', $invalidChecks ) ) {
606
			self::addValues( $result, $this->getSpecialTitles(), 'special', 'title' );
607
		}
608
		if ( in_array( 'missingIds', $invalidChecks ) ) {
609
			self::addValues( $result, $this->getMissingPageIDs(), 'missing', 'pageid' );
610
		}
611
		if ( in_array( 'missingRevIds', $invalidChecks ) ) {
612
			self::addValues( $result, $this->getMissingRevisionIDs(), 'missing', 'revid' );
613
		}
614
		if ( in_array( 'missingTitles', $invalidChecks ) ) {
615
			self::addValues( $result, $this->getMissingTitles(), 'missing' );
616
		}
617
		if ( in_array( 'interwikiTitles', $invalidChecks ) ) {
618
			self::addValues( $result, $this->getInterwikiTitlesAsResult() );
619
		}
620
621
		return $result;
622
	}
623
624
	/**
625
	 * Get the list of valid revision IDs (requested with the revids= parameter)
626
	 * @return array Array of revID (int) => pageID (int)
627
	 */
628
	public function getRevisionIDs() {
629
		return $this->mGoodRevIDs;
630
	}
631
632
	/**
633
	 * Get the list of non-deleted revision IDs (requested with the revids= parameter)
634
	 * @return array Array of revID (int) => pageID (int)
635
	 */
636
	public function getLiveRevisionIDs() {
637
		return $this->mLiveRevIDs;
638
	}
639
640
	/**
641
	 * Get the list of revision IDs that were associated with deleted titles.
642
	 * @return array Array of revID (int) => pageID (int)
643
	 */
644
	public function getDeletedRevisionIDs() {
645
		return $this->mDeletedRevIDs;
646
	}
647
648
	/**
649
	 * Revision IDs that were not found in the database
650
	 * @return array Array of revision IDs
651
	 */
652
	public function getMissingRevisionIDs() {
653
		return $this->mMissingRevIDs;
654
	}
655
656
	/**
657
	 * Revision IDs that were not found in the database as result array.
658
	 * @param ApiResult $result
659
	 * @return array Array of revision IDs
660
	 * @since 1.21
661
	 */
662
	public function getMissingRevisionIDsAsResult( $result = null ) {
663
		$values = [];
664
		foreach ( $this->getMissingRevisionIDs() as $revid ) {
665
			$values[$revid] = [
666
				'revid' => $revid
667
			];
668
		}
669
		if ( !empty( $values ) && $result ) {
670
			ApiResult::setIndexedTagName( $values, 'rev' );
671
		}
672
673
		return $values;
674
	}
675
676
	/**
677
	 * Get the list of titles with negative namespace
678
	 * @return Title[]
679
	 */
680
	public function getSpecialTitles() {
681
		return $this->mSpecialTitles;
682
	}
683
684
	/**
685
	 * Returns the number of revisions (requested with revids= parameter).
686
	 * @return int Number of revisions.
687
	 */
688
	public function getRevisionCount() {
689
		return count( $this->getRevisionIDs() );
690
	}
691
692
	/**
693
	 * Populate this PageSet from a list of Titles
694
	 * @param array $titles Array of Title objects
695
	 */
696
	public function populateFromTitles( $titles ) {
697
		$this->initFromTitles( $titles );
698
	}
699
700
	/**
701
	 * Populate this PageSet from a list of page IDs
702
	 * @param array $pageIDs Array of page IDs
703
	 */
704
	public function populateFromPageIDs( $pageIDs ) {
705
		$this->initFromPageIds( $pageIDs );
706
	}
707
708
	/**
709
	 * Populate this PageSet from a rowset returned from the database
710
	 *
711
	 * Note that the query result must include the columns returned by
712
	 * $this->getPageTableFields().
713
	 *
714
	 * @param IDatabase $db
715
	 * @param ResultWrapper $queryResult Query result object
716
	 */
717
	public function populateFromQueryResult( $db, $queryResult ) {
718
		$this->initFromQueryResult( $queryResult );
719
	}
720
721
	/**
722
	 * Populate this PageSet from a list of revision IDs
723
	 * @param array $revIDs Array of revision IDs
724
	 */
725
	public function populateFromRevisionIDs( $revIDs ) {
726
		$this->initFromRevIDs( $revIDs );
727
	}
728
729
	/**
730
	 * Extract all requested fields from the row received from the database
731
	 * @param stdClass $row Result row
732
	 */
733
	public function processDbRow( $row ) {
734
		// Store Title object in various data structures
735
		$title = Title::newFromRow( $row );
736
737
		$pageId = intval( $row->page_id );
738
		$this->mAllPages[$row->page_namespace][$row->page_title] = $pageId;
739
		$this->mTitles[] = $title;
740
741
		if ( $this->mResolveRedirects && $row->page_is_redirect == '1' ) {
742
			$this->mPendingRedirectIDs[$pageId] = $title;
743
		} else {
744
			$this->mGoodPages[$row->page_namespace][$row->page_title] = $pageId;
745
			$this->mGoodAndMissingPages[$row->page_namespace][$row->page_title] = $pageId;
746
			$this->mGoodTitles[$pageId] = $title;
747
		}
748
749
		foreach ( $this->mRequestedPageFields as $fieldName => &$fieldValues ) {
750
			$fieldValues[$pageId] = $row->$fieldName;
751
		}
752
	}
753
754
	/**
755
	 * This method populates internal variables with page information
756
	 * based on the given array of title strings.
757
	 *
758
	 * Steps:
759
	 * #1 For each title, get data from `page` table
760
	 * #2 If page was not found in the DB, store it as missing
761
	 *
762
	 * Additionally, when resolving redirects:
763
	 * #3 If no more redirects left, stop.
764
	 * #4 For each redirect, get its target from the `redirect` table.
765
	 * #5 Substitute the original LinkBatch object with the new list
766
	 * #6 Repeat from step #1
767
	 *
768
	 * @param array $titles Array of Title objects or strings
769
	 */
770
	private function initFromTitles( $titles ) {
771
		// Get validated and normalized title objects
772
		$linkBatch = $this->processTitlesArray( $titles );
773
		if ( $linkBatch->isEmpty() ) {
774
			return;
775
		}
776
777
		$db = $this->getDB();
778
		$set = $linkBatch->constructSet( 'page', $db );
779
780
		// Get pageIDs data from the `page` table
781
		$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 778 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...
782
			__METHOD__ );
783
784
		// Hack: get the ns:titles stored in [ ns => [ titles ] ] format
785
		$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 781 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...
786
787
		// Resolve any found redirects
788
		$this->resolvePendingRedirects();
789
	}
790
791
	/**
792
	 * Does the same as initFromTitles(), but is based on page IDs instead
793
	 * @param array $pageids Array of page IDs
794
	 */
795
	private function initFromPageIds( $pageids ) {
796
		if ( !$pageids ) {
797
			return;
798
		}
799
800
		$pageids = array_map( 'intval', $pageids ); // paranoia
801
		$remaining = array_flip( $pageids );
802
803
		$pageids = self::getPositiveIntegers( $pageids );
804
805
		$res = null;
806
		if ( !empty( $pageids ) ) {
807
			$set = [
808
				'page_id' => $pageids
809
			];
810
			$db = $this->getDB();
811
812
			// Get pageIDs data from the `page` table
813
			$res = $db->select( 'page', $this->getPageTableFields(), $set,
814
				__METHOD__ );
815
		}
816
817
		$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...
818
819
		// Resolve any found redirects
820
		$this->resolvePendingRedirects();
821
	}
822
823
	/**
824
	 * Iterate through the result of the query on 'page' table,
825
	 * and for each row create and store title object and save any extra fields requested.
826
	 * @param ResultWrapper $res DB Query result
827
	 * @param array $remaining Array of either pageID or ns/title elements (optional).
828
	 *        If given, any missing items will go to $mMissingPageIDs and $mMissingTitles
829
	 * @param bool $processTitles Must be provided together with $remaining.
830
	 *        If true, treat $remaining as an array of [ns][title]
831
	 *        If false, treat it as an array of [pageIDs]
832
	 */
833
	private function initFromQueryResult( $res, &$remaining = null, $processTitles = null ) {
834
		if ( !is_null( $remaining ) && is_null( $processTitles ) ) {
835
			ApiBase::dieDebug( __METHOD__, 'Missing $processTitles parameter when $remaining is provided' );
836
		}
837
838
		$usernames = [];
839
		if ( $res ) {
840
			foreach ( $res as $row ) {
841
				$pageId = intval( $row->page_id );
842
843
				// Remove found page from the list of remaining items
844
				if ( isset( $remaining ) ) {
845
					if ( $processTitles ) {
846
						unset( $remaining[$row->page_namespace][$row->page_title] );
847
					} else {
848
						unset( $remaining[$pageId] );
849
					}
850
				}
851
852
				// Store any extra fields requested by modules
853
				$this->processDbRow( $row );
854
855
				// Need gender information
856
				if ( MWNamespace::hasGenderDistinction( $row->page_namespace ) ) {
857
					$usernames[] = $row->page_title;
858
				}
859
			}
860
		}
861
862
		if ( isset( $remaining ) ) {
863
			// Any items left in the $remaining list are added as missing
864
			if ( $processTitles ) {
865
				// The remaining titles in $remaining are non-existent pages
866
				foreach ( $remaining as $ns => $dbkeys ) {
867
					foreach ( array_keys( $dbkeys ) as $dbkey ) {
868
						$title = Title::makeTitle( $ns, $dbkey );
869
						$this->mAllPages[$ns][$dbkey] = $this->mFakePageId;
870
						$this->mMissingPages[$ns][$dbkey] = $this->mFakePageId;
871
						$this->mGoodAndMissingPages[$ns][$dbkey] = $this->mFakePageId;
872
						$this->mMissingTitles[$this->mFakePageId] = $title;
873
						$this->mFakePageId--;
874
						$this->mTitles[] = $title;
875
876
						// need gender information
877
						if ( MWNamespace::hasGenderDistinction( $ns ) ) {
878
							$usernames[] = $dbkey;
879
						}
880
					}
881
				}
882
			} else {
883
				// The remaining pageids do not exist
884
				if ( !$this->mMissingPageIDs ) {
885
					$this->mMissingPageIDs = array_keys( $remaining );
886
				} else {
887
					$this->mMissingPageIDs = array_merge( $this->mMissingPageIDs, array_keys( $remaining ) );
888
				}
889
			}
890
		}
891
892
		// Get gender information
893
		$genderCache = GenderCache::singleton();
894
		$genderCache->doQuery( $usernames, __METHOD__ );
895
	}
896
897
	/**
898
	 * Does the same as initFromTitles(), but is based on revision IDs
899
	 * instead
900
	 * @param array $revids Array of revision IDs
901
	 */
902
	private function initFromRevIDs( $revids ) {
903
		if ( !$revids ) {
904
			return;
905
		}
906
907
		$revids = array_map( 'intval', $revids ); // paranoia
908
		$db = $this->getDB();
909
		$pageids = [];
910
		$remaining = array_flip( $revids );
911
912
		$revids = self::getPositiveIntegers( $revids );
913
914
		if ( !empty( $revids ) ) {
915
			$tables = [ 'revision', 'page' ];
916
			$fields = [ 'rev_id', 'rev_page' ];
917
			$where = [ 'rev_id' => $revids, 'rev_page = page_id' ];
918
919
			// Get pageIDs data from the `page` table
920
			$res = $db->select( $tables, $fields, $where, __METHOD__ );
921
			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...
922
				$revid = intval( $row->rev_id );
923
				$pageid = intval( $row->rev_page );
924
				$this->mGoodRevIDs[$revid] = $pageid;
925
				$this->mLiveRevIDs[$revid] = $pageid;
926
				$pageids[$pageid] = '';
927
				unset( $remaining[$revid] );
928
			}
929
		}
930
931
		$this->mMissingRevIDs = array_keys( $remaining );
932
933
		// Populate all the page information
934
		$this->initFromPageIds( array_keys( $pageids ) );
935
936
		// If the user can see deleted revisions, pull out the corresponding
937
		// titles from the archive table and include them too. We ignore
938
		// ar_page_id because deleted revisions are tied by title, not page_id.
939
		if ( !empty( $this->mMissingRevIDs ) && $this->getUser()->isAllowed( 'deletedhistory' ) ) {
940
			$remaining = array_flip( $this->mMissingRevIDs );
941
			$tables = [ 'archive' ];
942
			$fields = [ 'ar_rev_id', 'ar_namespace', 'ar_title' ];
943
			$where = [ 'ar_rev_id' => $this->mMissingRevIDs ];
944
945
			$res = $db->select( $tables, $fields, $where, __METHOD__ );
946
			$titles = [];
947
			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...
948
				$revid = intval( $row->ar_rev_id );
949
				$titles[$revid] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
950
				unset( $remaining[$revid] );
951
			}
952
953
			$this->initFromTitles( $titles );
954
955
			foreach ( $titles as $revid => $title ) {
956
				$ns = $title->getNamespace();
957
				$dbkey = $title->getDBkey();
958
959
				// Handle converted titles
960
				if ( !isset( $this->mAllPages[$ns][$dbkey] ) &&
961
					isset( $this->mConvertedTitles[$title->getPrefixedText()] )
962
				) {
963
					$title = Title::newFromText( $this->mConvertedTitles[$title->getPrefixedText()] );
964
					$ns = $title->getNamespace();
965
					$dbkey = $title->getDBkey();
966
				}
967
968
				if ( isset( $this->mAllPages[$ns][$dbkey] ) ) {
969
					$this->mGoodRevIDs[$revid] = $this->mAllPages[$ns][$dbkey];
970
					$this->mDeletedRevIDs[$revid] = $this->mAllPages[$ns][$dbkey];
971
				} else {
972
					$remaining[$revid] = true;
973
				}
974
			}
975
976
			$this->mMissingRevIDs = array_keys( $remaining );
977
		}
978
	}
979
980
	/**
981
	 * Resolve any redirects in the result if redirect resolution was
982
	 * requested. This function is called repeatedly until all redirects
983
	 * have been resolved.
984
	 */
985
	private function resolvePendingRedirects() {
986
		if ( $this->mResolveRedirects ) {
987
			$db = $this->getDB();
988
			$pageFlds = $this->getPageTableFields();
989
990
			// Repeat until all redirects have been resolved
991
			// The infinite loop is prevented by keeping all known pages in $this->mAllPages
992
			while ( $this->mPendingRedirectIDs ) {
993
				// Resolve redirects by querying the pagelinks table, and repeat the process
994
				// Create a new linkBatch object for the next pass
995
				$linkBatch = $this->getRedirectTargets();
996
997
				if ( $linkBatch->isEmpty() ) {
998
					break;
999
				}
1000
1001
				$set = $linkBatch->constructSet( 'page', $db );
1002
				if ( $set === false ) {
1003
					break;
1004
				}
1005
1006
				// Get pageIDs data from the `page` table
1007
				$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 1001 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...
1008
1009
				// Hack: get the ns:titles stored in [ns => array(titles)] format
1010
				$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 1007 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...
1011
			}
1012
		}
1013
	}
1014
1015
	/**
1016
	 * Get the targets of the pending redirects from the database
1017
	 *
1018
	 * Also creates entries in the redirect table for redirects that don't
1019
	 * have one.
1020
	 * @return LinkBatch
1021
	 */
1022
	private function getRedirectTargets() {
1023
		$lb = new LinkBatch();
1024
		$db = $this->getDB();
1025
1026
		$res = $db->select(
1027
			'redirect',
1028
			[
1029
				'rd_from',
1030
				'rd_namespace',
1031
				'rd_fragment',
1032
				'rd_interwiki',
1033
				'rd_title'
1034
			], [ 'rd_from' => array_keys( $this->mPendingRedirectIDs ) ],
1035
			__METHOD__
1036
		);
1037
		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...
1038
			$rdfrom = intval( $row->rd_from );
1039
			$from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText();
1040
			$to = Title::makeTitle(
1041
				$row->rd_namespace,
1042
				$row->rd_title,
1043
				$row->rd_fragment,
1044
				$row->rd_interwiki
1045
			);
1046
			$this->mResolvedRedirectTitles[$from] = $this->mPendingRedirectIDs[$rdfrom];
1047
			unset( $this->mPendingRedirectIDs[$rdfrom] );
1048
			if ( $to->isExternal() ) {
1049
				$this->mInterwikiTitles[$to->getPrefixedText()] = $to->getInterwiki();
1050
			} elseif ( !isset( $this->mAllPages[$row->rd_namespace][$row->rd_title] ) ) {
1051
				$lb->add( $row->rd_namespace, $row->rd_title );
1052
			}
1053
			$this->mRedirectTitles[$from] = $to;
1054
		}
1055
1056
		if ( $this->mPendingRedirectIDs ) {
1057
			// We found pages that aren't in the redirect table
1058
			// Add them
1059
			foreach ( $this->mPendingRedirectIDs as $id => $title ) {
1060
				$page = WikiPage::factory( $title );
1061
				$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...
1062
				if ( !$rt ) {
1063
					// What the hell. Let's just ignore this
1064
					continue;
1065
				}
1066
				$lb->addObj( $rt );
1067
				$from = $title->getPrefixedText();
1068
				$this->mResolvedRedirectTitles[$from] = $title;
1069
				$this->mRedirectTitles[$from] = $rt;
1070
				unset( $this->mPendingRedirectIDs[$id] );
1071
			}
1072
		}
1073
1074
		return $lb;
1075
	}
1076
1077
	/**
1078
	 * Get the cache mode for the data generated by this module.
1079
	 * All PageSet users should take into account whether this returns a more-restrictive
1080
	 * cache mode than the using module itself. For possible return values and other
1081
	 * details about cache modes, see ApiMain::setCacheMode()
1082
	 *
1083
	 * Public caching will only be allowed if *all* the modules that supply
1084
	 * data for a given request return a cache mode of public.
1085
	 *
1086
	 * @param array|null $params
1087
	 * @return string
1088
	 * @since 1.21
1089
	 */
1090
	public function getCacheMode( $params = null ) {
1091
		return $this->mCacheMode;
1092
	}
1093
1094
	/**
1095
	 * Given an array of title strings, convert them into Title objects.
1096
	 * Alternatively, an array of Title objects may be given.
1097
	 * This method validates access rights for the title,
1098
	 * and appends normalization values to the output.
1099
	 *
1100
	 * @param array $titles Array of Title objects or strings
1101
	 * @return LinkBatch
1102
	 */
1103
	private function processTitlesArray( $titles ) {
1104
		$usernames = [];
1105
		$linkBatch = new LinkBatch();
1106
1107
		foreach ( $titles as $title ) {
1108
			if ( is_string( $title ) ) {
1109
				try {
1110
					$titleObj = Title::newFromTextThrow( $title, $this->mDefaultNamespace );
1111
				} catch ( MalformedTitleException $ex ) {
1112
					// Handle invalid titles gracefully
1113
					$this->mAllPages[0][$title] = $this->mFakePageId;
1114
					$this->mInvalidTitles[$this->mFakePageId] = [
1115
						'title' => $title,
1116
						'invalidreason' => $ex->getMessage(),
1117
					];
1118
					$this->mFakePageId--;
1119
					continue; // There's nothing else we can do
1120
				}
1121
			} else {
1122
				$titleObj = $title;
1123
			}
1124
			$unconvertedTitle = $titleObj->getPrefixedText();
1125
			$titleWasConverted = false;
1126
			if ( $titleObj->isExternal() ) {
1127
				// This title is an interwiki link.
1128
				$this->mInterwikiTitles[$unconvertedTitle] = $titleObj->getInterwiki();
1129
			} else {
1130
				// Variants checking
1131
				global $wgContLang;
1132
				if ( $this->mConvertTitles &&
1133
					count( $wgContLang->getVariants() ) > 1 &&
1134
					!$titleObj->exists()
1135
				) {
1136
					// Language::findVariantLink will modify titleText and titleObj into
1137
					// the canonical variant if possible
1138
					$titleText = is_string( $title ) ? $title : $titleObj->getPrefixedText();
1139
					$wgContLang->findVariantLink( $titleText, $titleObj );
1140
					$titleWasConverted = $unconvertedTitle !== $titleObj->getPrefixedText();
1141
				}
1142
1143
				if ( $titleObj->getNamespace() < 0 ) {
1144
					// Handle Special and Media pages
1145
					$titleObj = $titleObj->fixSpecialName();
1146
					$this->mSpecialTitles[$this->mFakePageId] = $titleObj;
1147
					$this->mFakePageId--;
1148
				} else {
1149
					// Regular page
1150
					$linkBatch->addObj( $titleObj );
1151
				}
1152
			}
1153
1154
			// Make sure we remember the original title that was
1155
			// given to us. This way the caller can correlate new
1156
			// titles with the originally requested when e.g. the
1157
			// namespace is localized or the capitalization is
1158
			// different
1159
			if ( $titleWasConverted ) {
1160
				$this->mConvertedTitles[$unconvertedTitle] = $titleObj->getPrefixedText();
1161
				// In this case the page can't be Special.
1162
				if ( is_string( $title ) && $title !== $unconvertedTitle ) {
1163
					$this->mNormalizedTitles[$title] = $unconvertedTitle;
1164
				}
1165
			} elseif ( is_string( $title ) && $title !== $titleObj->getPrefixedText() ) {
1166
				$this->mNormalizedTitles[$title] = $titleObj->getPrefixedText();
1167
			}
1168
1169
			// Need gender information
1170
			if ( MWNamespace::hasGenderDistinction( $titleObj->getNamespace() ) ) {
1171
				$usernames[] = $titleObj->getText();
1172
			}
1173
		}
1174
		// Get gender information
1175
		$genderCache = GenderCache::singleton();
1176
		$genderCache->doQuery( $usernames, __METHOD__ );
1177
1178
		return $linkBatch;
1179
	}
1180
1181
	/**
1182
	 * Set data for a title.
1183
	 *
1184
	 * This data may be extracted into an ApiResult using
1185
	 * self::populateGeneratorData. This should generally be limited to
1186
	 * data that is likely to be particularly useful to end users rather than
1187
	 * just being a dump of everything returned in non-generator mode.
1188
	 *
1189
	 * Redirects here will *not* be followed, even if 'redirects' was
1190
	 * specified, since in the case of multiple redirects we can't know which
1191
	 * source's data to use on the target.
1192
	 *
1193
	 * @param Title $title
1194
	 * @param array $data
1195
	 */
1196
	public function setGeneratorData( Title $title, array $data ) {
1197
		$ns = $title->getNamespace();
1198
		$dbkey = $title->getDBkey();
1199
		$this->mGeneratorData[$ns][$dbkey] = $data;
1200
	}
1201
1202
	/**
1203
	 * Controls how generator data about a redirect source is merged into
1204
	 * the generator data for the redirect target. When not set no data
1205
	 * is merged. Note that if multiple titles redirect to the same target
1206
	 * the order of operations is undefined.
1207
	 *
1208
	 * Example to include generated data from redirect in target, prefering
1209
	 * the data generated for the destination when there is a collision:
1210
	 * @code
1211
	 *   $pageSet->setRedirectMergePolicy( function( array $current, array $new ) {
1212
	 *       return $current + $new;
1213
	 *   } );
1214
	 * @endcode
1215
	 *
1216
	 * @param callable|null $callable Recieves two array arguments, first the
1217
	 *  generator data for the redirect target and second the generator data
1218
	 *  for the redirect source. Returns the resulting generator data to use
1219
	 *  for the redirect target.
1220
	 */
1221
	public function setRedirectMergePolicy( $callable ) {
1222
		$this->mRedirectMergePolicy = $callable;
1223
	}
1224
1225
	/**
1226
	 * Populate the generator data for all titles in the result
1227
	 *
1228
	 * The page data may be inserted into an ApiResult object or into an
1229
	 * associative array. The $path parameter specifies the path within the
1230
	 * ApiResult or array to find the "pages" node.
1231
	 *
1232
	 * The "pages" node itself must be an associative array mapping the page ID
1233
	 * or fake page ID values returned by this pageset (see
1234
	 * self::getAllTitlesByNamespace() and self::getSpecialTitles()) to
1235
	 * associative arrays of page data. Each of those subarrays will have the
1236
	 * data from self::setGeneratorData() merged in.
1237
	 *
1238
	 * Data that was set by self::setGeneratorData() for pages not in the
1239
	 * "pages" node will be ignored.
1240
	 *
1241
	 * @param ApiResult|array &$result
1242
	 * @param array $path
1243
	 * @return bool Whether the data fit
1244
	 */
1245
	public function populateGeneratorData( &$result, array $path = [] ) {
1246
		if ( $result instanceof ApiResult ) {
1247
			$data = $result->getResultData( $path );
1248
			if ( $data === null ) {
1249
				return true;
1250
			}
1251
		} else {
1252
			$data = &$result;
1253
			foreach ( $path as $key ) {
1254
				if ( !isset( $data[$key] ) ) {
1255
					// Path isn't in $result, so nothing to add, so everything
1256
					// "fits"
1257
					return true;
1258
				}
1259
				$data = &$data[$key];
1260
			}
1261
		}
1262
		foreach ( $this->mGeneratorData as $ns => $dbkeys ) {
1263
			if ( $ns === -1 ) {
1264
				$pages = [];
1265
				foreach ( $this->mSpecialTitles as $id => $title ) {
1266
					$pages[$title->getDBkey()] = $id;
1267
				}
1268
			} else {
1269
				if ( !isset( $this->mAllPages[$ns] ) ) {
1270
					// No known titles in the whole namespace. Skip it.
1271
					continue;
1272
				}
1273
				$pages = $this->mAllPages[$ns];
1274
			}
1275
			foreach ( $dbkeys as $dbkey => $genData ) {
1276
				if ( !isset( $pages[$dbkey] ) ) {
1277
					// Unknown title. Forget it.
1278
					continue;
1279
				}
1280
				$pageId = $pages[$dbkey];
1281
				if ( !isset( $data[$pageId] ) ) {
1282
					// $pageId didn't make it into the result. Ignore it.
1283
					continue;
1284
				}
1285
1286
				if ( $result instanceof ApiResult ) {
1287
					$path2 = array_merge( $path, [ $pageId ] );
1288
					foreach ( $genData as $key => $value ) {
1289
						if ( !$result->addValue( $path2, $key, $value ) ) {
1290
							return false;
1291
						}
1292
					}
1293
				} else {
1294
					$data[$pageId] = array_merge( $data[$pageId], $genData );
1295
				}
1296
			}
1297
		}
1298
1299
		// Merge data generated about redirect titles into the redirect destination
1300
		if ( $this->mRedirectMergePolicy ) {
1301
			foreach ( $this->mResolvedRedirectTitles as $titleFrom ) {
1302
				$dest = $titleFrom;
1303
				while ( isset( $this->mRedirectTitles[$dest->getPrefixedText()] ) ) {
1304
					$dest = $this->mRedirectTitles[$dest->getPrefixedText()];
1305
				}
1306
				$fromNs = $titleFrom->getNamespace();
1307
				$fromDBkey = $titleFrom->getDBkey();
1308
				$toPageId = $dest->getArticleID();
1309
				if ( isset( $data[$toPageId] ) &&
1310
					isset( $this->mGeneratorData[$fromNs][$fromDBkey] )
1311
				) {
1312
					// It is necesary to set both $data and add to $result, if an ApiResult,
1313
					// to ensure multiple redirects to the same destination are all merged.
1314
					$data[$toPageId] = call_user_func(
1315
						$this->mRedirectMergePolicy,
1316
						$data[$toPageId],
1317
						$this->mGeneratorData[$fromNs][$fromDBkey]
1318
					);
1319
					if ( $result instanceof ApiResult ) {
1320
						if ( !$result->addValue( $path, $toPageId, $data[$toPageId], ApiResult::OVERRIDE ) ) {
1321
							return false;
1322
						}
1323
					}
1324
				}
1325
			}
1326
		}
1327
1328
		return true;
1329
	}
1330
1331
	/**
1332
	 * Get the database connection (read-only)
1333
	 * @return DatabaseBase
1334
	 */
1335
	protected function getDB() {
1336
		return $this->mDbSource->getDB();
1337
	}
1338
1339
	/**
1340
	 * Returns the input array of integers with all values < 0 removed
1341
	 *
1342
	 * @param array $array
1343
	 * @return array
1344
	 */
1345
	private static function getPositiveIntegers( $array ) {
1346
		// bug 25734 API: possible issue with revids validation
1347
		// It seems with a load of revision rows, MySQL gets upset
1348
		// Remove any < 0 integers, as they can't be valid
1349
		foreach ( $array as $i => $int ) {
1350
			if ( $int < 0 ) {
1351
				unset( $array[$i] );
1352
			}
1353
		}
1354
1355
		return $array;
1356
	}
1357
1358
	public function getAllowedParams( $flags = 0 ) {
1359
		$result = [
1360
			'titles' => [
1361
				ApiBase::PARAM_ISMULTI => true,
1362
				ApiBase::PARAM_HELP_MSG => 'api-pageset-param-titles',
1363
			],
1364
			'pageids' => [
1365
				ApiBase::PARAM_TYPE => 'integer',
1366
				ApiBase::PARAM_ISMULTI => true,
1367
				ApiBase::PARAM_HELP_MSG => 'api-pageset-param-pageids',
1368
			],
1369
			'revids' => [
1370
				ApiBase::PARAM_TYPE => 'integer',
1371
				ApiBase::PARAM_ISMULTI => true,
1372
				ApiBase::PARAM_HELP_MSG => 'api-pageset-param-revids',
1373
			],
1374
			'generator' => [
1375
				ApiBase::PARAM_TYPE => null,
1376
				ApiBase::PARAM_HELP_MSG => 'api-pageset-param-generator',
1377
				ApiBase::PARAM_SUBMODULE_PARAM_PREFIX => 'g',
1378
			],
1379
			'redirects' => [
1380
				ApiBase::PARAM_DFLT => false,
1381
				ApiBase::PARAM_HELP_MSG => $this->mAllowGenerator
1382
					? 'api-pageset-param-redirects-generator'
1383
					: 'api-pageset-param-redirects-nogenerator',
1384
			],
1385
			'converttitles' => [
1386
				ApiBase::PARAM_DFLT => false,
1387
				ApiBase::PARAM_HELP_MSG => [
1388
					'api-pageset-param-converttitles',
1389
					new DeferredStringifier(
1390
						function ( IContextSource $context ) {
1391
							return $context->getLanguage()
1392
								->commaList( LanguageConverter::$languagesWithVariants );
1393
						},
1394
						$this
1395
					)
1396
				],
1397
			],
1398
		];
1399
1400
		if ( !$this->mAllowGenerator ) {
1401
			unset( $result['generator'] );
1402
		} elseif ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
1403
			$result['generator'][ApiBase::PARAM_TYPE] = 'submodule';
1404
			$result['generator'][ApiBase::PARAM_SUBMODULE_MAP] = $this->getGenerators();
1405
		}
1406
1407
		return $result;
1408
	}
1409
1410
	protected function handleParamNormalization( $paramName, $value, $rawValue ) {
1411
		parent::handleParamNormalization( $paramName, $value, $rawValue );
1412
1413
		if ( $paramName === 'titles' ) {
1414
			// For the 'titles' parameter, we want to split it like ApiBase would
1415
			// and add any changed titles to $this->mNormalizedTitles
1416
			$value = $this->explodeMultiValue( $value, self::LIMIT_SML2 + 1 );
1417
			$l = count( $value );
1418
			$rawValue = $this->explodeMultiValue( $rawValue, $l );
1419
			for ( $i = 0; $i < $l; $i++ ) {
1420
				if ( $value[$i] !== $rawValue[$i] ) {
1421
					$this->mNormalizedTitles[$rawValue[$i]] = $value[$i];
1422
				}
1423
			}
1424
		}
1425
	}
1426
1427
	private static $generators = null;
1428
1429
	/**
1430
	 * Get an array of all available generators
1431
	 * @return array
1432
	 */
1433
	private function getGenerators() {
1434
		if ( self::$generators === null ) {
1435
			$query = $this->mDbSource;
1436
			if ( !( $query instanceof ApiQuery ) ) {
1437
				// If the parent container of this pageset is not ApiQuery,
1438
				// we must create it to get module manager
1439
				$query = $this->getMain()->getModuleManager()->getModule( 'query' );
1440
			}
1441
			$gens = [];
1442
			$prefix = $query->getModulePath() . '+';
1443
			$mgr = $query->getModuleManager();
1444
			foreach ( $mgr->getNamesWithClasses() as $name => $class ) {
1445
				if ( is_subclass_of( $class, 'ApiQueryGeneratorBase' ) ) {
1446
					$gens[$name] = $prefix . $name;
1447
				}
1448
			}
1449
			ksort( $gens );
1450
			self::$generators = $gens;
1451
		}
1452
1453
		return self::$generators;
1454
	}
1455
}
1456