Completed
Branch master (227f0c)
by
unknown
30:54
created

LinksUpdate::getExistingImages()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 0
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Updater for link tracking tables after a page edit.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
23
/**
24
 * Class the manages updates of *_link tables as well as similar extension-managed tables
25
 *
26
 * @note: LinksUpdate is managed by DeferredUpdates::execute(). Do not run this in a transaction.
27
 *
28
 * See docs/deferred.txt
29
 */
30
class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
31
	// @todo make members protected, but make sure extensions don't break
32
33
	/** @var int Page ID of the article linked from */
34
	public $mId;
35
36
	/** @var Title Title object of the article linked from */
37
	public $mTitle;
38
39
	/** @var ParserOutput */
40
	public $mParserOutput;
41
42
	/** @var array Map of title strings to IDs for the links in the document */
43
	public $mLinks;
44
45
	/** @var array DB keys of the images used, in the array key only */
46
	public $mImages;
47
48
	/** @var array Map of title strings to IDs for the template references, including broken ones */
49
	public $mTemplates;
50
51
	/** @var array URLs of external links, array key only */
52
	public $mExternals;
53
54
	/** @var array Map of category names to sort keys */
55
	public $mCategories;
56
57
	/** @var array Map of language codes to titles */
58
	public $mInterlangs;
59
60
	/** @var array 2-D map of (prefix => DBK => 1) */
61
	public $mInterwikis;
62
63
	/** @var array Map of arbitrary name to value */
64
	public $mProperties;
65
66
	/** @var bool Whether to queue jobs for recursive updates */
67
	public $mRecursive;
68
69
	/** @var Revision Revision for which this update has been triggered */
70
	private $mRevision;
71
72
	/**
73
	 * @var null|array Added links if calculated.
74
	 */
75
	private $linkInsertions = null;
76
77
	/**
78
	 * @var null|array Deleted links if calculated.
79
	 */
80
	private $linkDeletions = null;
81
82
	/**
83
	 * @var User|null
84
	 */
85
	private $user;
86
87
	/**
88
	 * Constructor
89
	 *
90
	 * @param Title $title Title of the page we're updating
91
	 * @param ParserOutput $parserOutput Output from a full parse of this page
92
	 * @param bool $recursive Queue jobs for recursive updates?
93
	 * @throws MWException
94
	 */
95
	function __construct( Title $title, ParserOutput $parserOutput, $recursive = true ) {
96
		// Implicit transactions are disabled as they interfere with batching
97
		parent::__construct( false );
98
99
		$this->mTitle = $title;
100
		$this->mId = $title->getArticleID( Title::GAID_FOR_UPDATE );
101
102
		if ( !$this->mId ) {
103
			throw new InvalidArgumentException(
104
				"The Title object yields no ID. Perhaps the page doesn't exist?"
105
			);
106
		}
107
108
		$this->mParserOutput = $parserOutput;
109
110
		$this->mLinks = $parserOutput->getLinks();
111
		$this->mImages = $parserOutput->getImages();
112
		$this->mTemplates = $parserOutput->getTemplates();
113
		$this->mExternals = $parserOutput->getExternalLinks();
114
		$this->mCategories = $parserOutput->getCategories();
115
		$this->mProperties = $parserOutput->getProperties();
116
		$this->mInterwikis = $parserOutput->getInterwikiLinks();
117
118
		# Convert the format of the interlanguage links
119
		# I didn't want to change it in the ParserOutput, because that array is passed all
120
		# the way back to the skin, so either a skin API break would be required, or an
121
		# inefficient back-conversion.
122
		$ill = $parserOutput->getLanguageLinks();
123
		$this->mInterlangs = [];
124
		foreach ( $ill as $link ) {
125
			list( $key, $title ) = explode( ':', $link, 2 );
126
			$this->mInterlangs[$key] = $title;
127
		}
128
129
		foreach ( $this->mCategories as &$sortkey ) {
130
			# If the sortkey is longer then 255 bytes,
131
			# it truncated by DB, and then doesn't get
132
			# matched when comparing existing vs current
133
			# categories, causing bug 25254.
134
			# Also. substr behaves weird when given "".
135
			if ( $sortkey !== '' ) {
136
				$sortkey = substr( $sortkey, 0, 255 );
137
			}
138
		}
139
140
		$this->mRecursive = $recursive;
141
142
		Hooks::run( 'LinksUpdateConstructed', [ &$this ] );
143
	}
144
145
	/**
146
	 * Update link tables with outgoing links from an updated article
147
	 *
148
	 * @note: this is managed by DeferredUpdates::execute(). Do not run this in a transaction.
149
	 */
150
	public function doUpdate() {
151
		// Make sure all links update threads see the changes of each other.
152
		// This handles the case when updates have to batched into several COMMITs.
153
		$scopedLock = self::acquirePageLock( $this->mDb, $this->mId );
154
155
		Hooks::run( 'LinksUpdate', [ &$this ] );
156
		$this->doIncrementalUpdate();
157
158
		$this->mDb->onTransactionIdle( function() use ( &$scopedLock ) {
159
			Hooks::run( 'LinksUpdateComplete', [ &$this ] );
160
			// Release the lock *after* the final COMMIT for correctness
161
			ScopedCallback::consume( $scopedLock );
162
		} );
163
	}
164
165
	/**
166
	 * Acquire a lock for performing link table updates for a page on a DB
167
	 *
168
	 * @param IDatabase $dbw
169
	 * @param integer $pageId
170
	 * @return ScopedCallback|null Returns null on failure
171
	 * @throws RuntimeException
172
	 * @since 1.27
173
	 */
174
	public static function acquirePageLock( IDatabase $dbw, $pageId ) {
175
		$scopedLock = $dbw->getScopedLockAndFlush(
176
			"LinksUpdate:pageid:$pageId",
177
			__METHOD__,
178
			15
179
		);
180
		if ( !$scopedLock ) {
181
			throw new RuntimeException( "Could not acquire lock on page #$pageId." );
182
		}
183
184
		return $scopedLock;
185
	}
186
187
	protected function doIncrementalUpdate() {
188
		# Page links
189
		$existing = $this->getExistingLinks();
190
		$this->linkDeletions = $this->getLinkDeletions( $existing );
191
		$this->linkInsertions = $this->getLinkInsertions( $existing );
192
		$this->incrTableUpdate( 'pagelinks', 'pl', $this->linkDeletions, $this->linkInsertions );
193
194
		# Image links
195
		$existing = $this->getExistingImages();
196
		$imageDeletes = $this->getImageDeletions( $existing );
197
		$this->incrTableUpdate( 'imagelinks', 'il', $imageDeletes,
198
			$this->getImageInsertions( $existing ) );
199
200
		# Invalidate all image description pages which had links added or removed
201
		$imageUpdates = $imageDeletes + array_diff_key( $this->mImages, $existing );
202
		$this->invalidateImageDescriptions( $imageUpdates );
203
204
		# External links
205
		$existing = $this->getExistingExternals();
206
		$this->incrTableUpdate( 'externallinks', 'el', $this->getExternalDeletions( $existing ),
207
			$this->getExternalInsertions( $existing ) );
208
209
		# Language links
210
		$existing = $this->getExistingInterlangs();
211
		$this->incrTableUpdate( 'langlinks', 'll', $this->getInterlangDeletions( $existing ),
212
			$this->getInterlangInsertions( $existing ) );
213
214
		# Inline interwiki links
215
		$existing = $this->getExistingInterwikis();
216
		$this->incrTableUpdate( 'iwlinks', 'iwl', $this->getInterwikiDeletions( $existing ),
217
			$this->getInterwikiInsertions( $existing ) );
218
219
		# Template links
220
		$existing = $this->getExistingTemplates();
221
		$this->incrTableUpdate( 'templatelinks', 'tl', $this->getTemplateDeletions( $existing ),
222
			$this->getTemplateInsertions( $existing ) );
223
224
		# Category links
225
		$existing = $this->getExistingCategories();
226
		$categoryDeletes = $this->getCategoryDeletions( $existing );
227
		$this->incrTableUpdate( 'categorylinks', 'cl', $categoryDeletes,
228
			$this->getCategoryInsertions( $existing ) );
229
230
		# Invalidate all categories which were added, deleted or changed (set symmetric difference)
231
		$categoryInserts = array_diff_assoc( $this->mCategories, $existing );
232
		$categoryUpdates = $categoryInserts + $categoryDeletes;
233
		$this->invalidateCategories( $categoryUpdates );
234
		$this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
235
236
		# Page properties
237
		$existing = $this->getExistingProperties();
238
		$propertiesDeletes = $this->getPropertyDeletions( $existing );
239
		$this->incrTableUpdate( 'page_props', 'pp', $propertiesDeletes,
240
			$this->getPropertyInsertions( $existing ) );
241
242
		# Invalidate the necessary pages
243
		$changed = $propertiesDeletes + array_diff_assoc( $this->mProperties, $existing );
244
		$this->invalidateProperties( $changed );
245
246
		# Update the links table freshness for this title
247
		$this->updateLinksTimestamp();
248
249
		# Refresh links of all pages including this page
250
		# This will be in a separate transaction
251
		if ( $this->mRecursive ) {
252
			$this->queueRecursiveJobs();
253
		}
254
255
	}
256
257
	/**
258
	 * Queue recursive jobs for this page
259
	 *
260
	 * Which means do LinksUpdate on all pages that include the current page,
261
	 * using the job queue.
262
	 */
263
	protected function queueRecursiveJobs() {
264
		self::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
265
		if ( $this->mTitle->getNamespace() == NS_FILE ) {
266
			// Process imagelinks in case the title is or was a redirect
267
			self::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
268
		}
269
270
		$bc = $this->mTitle->getBacklinkCache();
271
		// Get jobs for cascade-protected backlinks for a high priority queue.
272
		// If meta-templates change to using a new template, the new template
273
		// should be implicitly protected as soon as possible, if applicable.
274
		// These jobs duplicate a subset of the above ones, but can run sooner.
275
		// Which ever runs first generally no-ops the other one.
276
		$jobs = [];
277
		foreach ( $bc->getCascadeProtectedLinks() as $title ) {
0 ignored issues
show
Bug introduced by
The expression $bc->getCascadeProtectedLinks() of type object<TitleArrayFromResult>|null 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...
278
			$jobs[] = RefreshLinksJob::newPrioritized( $title, [] );
279
		}
280
		JobQueueGroup::singleton()->push( $jobs );
281
	}
282
283
	/**
284
	 * Queue a RefreshLinks job for any table.
285
	 *
286
	 * @param Title $title Title to do job for
287
	 * @param string $table Table to use (e.g. 'templatelinks')
288
	 */
289
	public static function queueRecursiveJobsForTable( Title $title, $table ) {
290
		if ( $title->getBacklinkCache()->hasLinks( $table ) ) {
291
			$job = new RefreshLinksJob(
292
				$title,
293
				[
294
					'table' => $table,
295
					'recursive' => true,
296
				] + Job::newRootJobParams( // "overall" refresh links job info
297
					"refreshlinks:{$table}:{$title->getPrefixedText()}"
298
				)
299
			);
300
301
			JobQueueGroup::singleton()->push( $job );
302
		}
303
	}
304
305
	/**
306
	 * @param array $cats
307
	 */
308
	function invalidateCategories( $cats ) {
309
		$this->invalidatePages( NS_CATEGORY, array_keys( $cats ) );
310
	}
311
312
	/**
313
	 * Update all the appropriate counts in the category table.
314
	 * @param array $added Associative array of category name => sort key
315
	 * @param array $deleted Associative array of category name => sort key
316
	 */
317
	function updateCategoryCounts( $added, $deleted ) {
318
		$a = WikiPage::factory( $this->mTitle );
319
		$a->updateCategoryCounts(
320
			array_keys( $added ), array_keys( $deleted )
321
		);
322
	}
323
324
	/**
325
	 * @param array $images
326
	 */
327
	function invalidateImageDescriptions( $images ) {
328
		$this->invalidatePages( NS_FILE, array_keys( $images ) );
329
	}
330
331
	/**
332
	 * Update a table by doing a delete query then an insert query
333
	 * @param string $table Table name
334
	 * @param string $prefix Field name prefix
335
	 * @param array $deletions
336
	 * @param array $insertions Rows to insert
337
	 */
338
	private function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
339
		$bSize = RequestContext::getMain()->getConfig()->get( 'UpdateRowsPerQuery' );
340
341
		if ( $table === 'page_props' ) {
342
			$fromField = 'pp_page';
343
		} else {
344
			$fromField = "{$prefix}_from";
345
		}
346
347
		$deleteWheres = []; // list of WHERE clause arrays for each DB delete() call
348
		if ( $table === 'pagelinks' || $table === 'templatelinks' || $table === 'iwlinks' ) {
349
			$baseKey =  ( $table === 'iwlinks' ) ? 'iwl_prefix' : "{$prefix}_namespace";
350
351
			$curBatchSize = 0;
352
			$curDeletionBatch = [];
353
			$deletionBatches = [];
354
			foreach ( $deletions as $ns => $dbKeys ) {
355
				foreach ( $dbKeys as $dbKey => $unused ) {
356
					$curDeletionBatch[$ns][$dbKey] = 1;
357
					if ( ++$curBatchSize >= $bSize ) {
358
						$deletionBatches[] = $curDeletionBatch;
359
						$curDeletionBatch = [];
360
						$curBatchSize = 0;
361
					}
362
				}
363
			}
364
			if ( $curDeletionBatch ) {
365
				$deletionBatches[] = $curDeletionBatch;
366
			}
367
368
			foreach ( $deletionBatches as $deletionBatch ) {
369
				$deleteWheres[] = [
370
					$fromField => $this->mId,
371
					$this->mDb->makeWhereFrom2d( $deletionBatch, $baseKey, "{$prefix}_title" )
372
				];
373
			}
374
		} else {
375
			if ( $table === 'langlinks' ) {
376
				$toField = 'll_lang';
377
			} elseif ( $table === 'page_props' ) {
378
				$toField = 'pp_propname';
379
			} else {
380
				$toField = $prefix . '_to';
381
			}
382
383
			$deletionBatches = array_chunk( array_keys( $deletions ), $bSize );
384
			foreach ( $deletionBatches as $deletionBatch ) {
385
				$deleteWheres[] = [ $fromField => $this->mId, $toField => $deletionBatch ];
386
			}
387
		}
388
389 View Code Duplication
		foreach ( $deleteWheres as $deleteWhere ) {
390
			$this->mDb->delete( $table, $deleteWhere, __METHOD__ );
391
			$this->mDb->commit( __METHOD__, 'flush' );
392
			wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] );
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLBFactory() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
393
		}
394
395
		$insertBatches = array_chunk( $insertions, $bSize );
396 View Code Duplication
		foreach ( $insertBatches as $insertBatch ) {
397
			$this->mDb->insert( $table, $insertBatch, __METHOD__, 'IGNORE' );
398
			$this->mDb->commit( __METHOD__, 'flush' );
399
			wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] );
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLBFactory() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
400
		}
401
402
		if ( count( $insertions ) ) {
403
			Hooks::run( 'LinksUpdateAfterInsert', [ $this, $table, $insertions ] );
404
		}
405
	}
406
407
	/**
408
	 * Get an array of pagelinks insertions for passing to the DB
409
	 * Skips the titles specified by the 2-D array $existing
410
	 * @param array $existing
411
	 * @return array
412
	 */
413 View Code Duplication
	private function getLinkInsertions( $existing = [] ) {
414
		$arr = [];
415
		foreach ( $this->mLinks as $ns => $dbkeys ) {
416
			$diffs = isset( $existing[$ns] )
417
				? array_diff_key( $dbkeys, $existing[$ns] )
418
				: $dbkeys;
419
			foreach ( $diffs as $dbk => $id ) {
420
				$arr[] = [
421
					'pl_from' => $this->mId,
422
					'pl_from_namespace' => $this->mTitle->getNamespace(),
423
					'pl_namespace' => $ns,
424
					'pl_title' => $dbk
425
				];
426
			}
427
		}
428
429
		return $arr;
430
	}
431
432
	/**
433
	 * Get an array of template insertions. Like getLinkInsertions()
434
	 * @param array $existing
435
	 * @return array
436
	 */
437 View Code Duplication
	private function getTemplateInsertions( $existing = [] ) {
438
		$arr = [];
439
		foreach ( $this->mTemplates as $ns => $dbkeys ) {
440
			$diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
441
			foreach ( $diffs as $dbk => $id ) {
442
				$arr[] = [
443
					'tl_from' => $this->mId,
444
					'tl_from_namespace' => $this->mTitle->getNamespace(),
445
					'tl_namespace' => $ns,
446
					'tl_title' => $dbk
447
				];
448
			}
449
		}
450
451
		return $arr;
452
	}
453
454
	/**
455
	 * Get an array of image insertions
456
	 * Skips the names specified in $existing
457
	 * @param array $existing
458
	 * @return array
459
	 */
460
	private function getImageInsertions( $existing = [] ) {
461
		$arr = [];
462
		$diffs = array_diff_key( $this->mImages, $existing );
463
		foreach ( $diffs as $iname => $dummy ) {
464
			$arr[] = [
465
				'il_from' => $this->mId,
466
				'il_from_namespace' => $this->mTitle->getNamespace(),
467
				'il_to' => $iname
468
			];
469
		}
470
471
		return $arr;
472
	}
473
474
	/**
475
	 * Get an array of externallinks insertions. Skips the names specified in $existing
476
	 * @param array $existing
477
	 * @return array
478
	 */
479
	private function getExternalInsertions( $existing = [] ) {
480
		$arr = [];
481
		$diffs = array_diff_key( $this->mExternals, $existing );
482
		foreach ( $diffs as $url => $dummy ) {
483
			foreach ( wfMakeUrlIndexes( $url ) as $index ) {
484
				$arr[] = [
485
					'el_id' => $this->mDb->nextSequenceValue( 'externallinks_el_id_seq' ),
486
					'el_from' => $this->mId,
487
					'el_to' => $url,
488
					'el_index' => $index,
489
				];
490
			}
491
		}
492
493
		return $arr;
494
	}
495
496
	/**
497
	 * Get an array of category insertions
498
	 *
499
	 * @param array $existing Mapping existing category names to sort keys. If both
500
	 * match a link in $this, the link will be omitted from the output
501
	 *
502
	 * @return array
503
	 */
504
	private function getCategoryInsertions( $existing = [] ) {
505
		global $wgContLang, $wgCategoryCollation;
506
		$diffs = array_diff_assoc( $this->mCategories, $existing );
507
		$arr = [];
508
		foreach ( $diffs as $name => $prefix ) {
509
			$nt = Title::makeTitleSafe( NS_CATEGORY, $name );
510
			$wgContLang->findVariantLink( $name, $nt, true );
511
512 View Code Duplication
			if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
513
				$type = 'subcat';
514
			} elseif ( $this->mTitle->getNamespace() == NS_FILE ) {
515
				$type = 'file';
516
			} else {
517
				$type = 'page';
518
			}
519
520
			# Treat custom sortkeys as a prefix, so that if multiple
521
			# things are forced to sort as '*' or something, they'll
522
			# sort properly in the category rather than in page_id
523
			# order or such.
524
			$sortkey = Collation::singleton()->getSortKey(
525
				$this->mTitle->getCategorySortkey( $prefix ) );
526
527
			$arr[] = [
528
				'cl_from' => $this->mId,
529
				'cl_to' => $name,
530
				'cl_sortkey' => $sortkey,
531
				'cl_timestamp' => $this->mDb->timestamp(),
532
				'cl_sortkey_prefix' => $prefix,
533
				'cl_collation' => $wgCategoryCollation,
534
				'cl_type' => $type,
535
			];
536
		}
537
538
		return $arr;
539
	}
540
541
	/**
542
	 * Get an array of interlanguage link insertions
543
	 *
544
	 * @param array $existing Mapping existing language codes to titles
545
	 *
546
	 * @return array
547
	 */
548
	private function getInterlangInsertions( $existing = [] ) {
549
		$diffs = array_diff_assoc( $this->mInterlangs, $existing );
550
		$arr = [];
551
		foreach ( $diffs as $lang => $title ) {
552
			$arr[] = [
553
				'll_from' => $this->mId,
554
				'll_lang' => $lang,
555
				'll_title' => $title
556
			];
557
		}
558
559
		return $arr;
560
	}
561
562
	/**
563
	 * Get an array of page property insertions
564
	 * @param array $existing
565
	 * @return array
566
	 */
567
	function getPropertyInsertions( $existing = [] ) {
568
		$diffs = array_diff_assoc( $this->mProperties, $existing );
569
570
		$arr = [];
571
		foreach ( array_keys( $diffs ) as $name ) {
572
			$arr[] = $this->getPagePropRowData( $name );
573
		}
574
575
		return $arr;
576
	}
577
578
	/**
579
	 * Returns an associative array to be used for inserting a row into
580
	 * the page_props table. Besides the given property name, this will
581
	 * include the page id from $this->mId and any property value from
582
	 * $this->mProperties.
583
	 *
584
	 * The array returned will include the pp_sortkey field if this
585
	 * is present in the database (as indicated by $wgPagePropsHaveSortkey).
586
	 * The sortkey value is currently determined by getPropertySortKeyValue().
587
	 *
588
	 * @note this assumes that $this->mProperties[$prop] is defined.
589
	 *
590
	 * @param string $prop The name of the property.
591
	 *
592
	 * @return array
593
	 */
594
	private function getPagePropRowData( $prop ) {
595
		global $wgPagePropsHaveSortkey;
596
597
		$value = $this->mProperties[$prop];
598
599
		$row = [
600
			'pp_page' => $this->mId,
601
			'pp_propname' => $prop,
602
			'pp_value' => $value,
603
		];
604
605
		if ( $wgPagePropsHaveSortkey ) {
606
			$row['pp_sortkey'] = $this->getPropertySortKeyValue( $value );
607
		}
608
609
		return $row;
610
	}
611
612
	/**
613
	 * Determines the sort key for the given property value.
614
	 * This will return $value if it is a float or int,
615
	 * 1 or resp. 0 if it is a bool, and null otherwise.
616
	 *
617
	 * @note In the future, we may allow the sortkey to be specified explicitly
618
	 *       in ParserOutput::setProperty.
619
	 *
620
	 * @param mixed $value
621
	 *
622
	 * @return float|null
623
	 */
624
	private function getPropertySortKeyValue( $value ) {
625
		if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
626
			return floatval( $value );
627
		}
628
629
		return null;
630
	}
631
632
	/**
633
	 * Get an array of interwiki insertions for passing to the DB
634
	 * Skips the titles specified by the 2-D array $existing
635
	 * @param array $existing
636
	 * @return array
637
	 */
638
	private function getInterwikiInsertions( $existing = [] ) {
639
		$arr = [];
640
		foreach ( $this->mInterwikis as $prefix => $dbkeys ) {
641
			$diffs = isset( $existing[$prefix] )
642
				? array_diff_key( $dbkeys, $existing[$prefix] )
643
				: $dbkeys;
644
645
			foreach ( $diffs as $dbk => $id ) {
646
				$arr[] = [
647
					'iwl_from' => $this->mId,
648
					'iwl_prefix' => $prefix,
649
					'iwl_title' => $dbk
650
				];
651
			}
652
		}
653
654
		return $arr;
655
	}
656
657
	/**
658
	 * Given an array of existing links, returns those links which are not in $this
659
	 * and thus should be deleted.
660
	 * @param array $existing
661
	 * @return array
662
	 */
663 View Code Duplication
	private function getLinkDeletions( $existing ) {
664
		$del = [];
665
		foreach ( $existing as $ns => $dbkeys ) {
666
			if ( isset( $this->mLinks[$ns] ) ) {
667
				$del[$ns] = array_diff_key( $existing[$ns], $this->mLinks[$ns] );
668
			} else {
669
				$del[$ns] = $existing[$ns];
670
			}
671
		}
672
673
		return $del;
674
	}
675
676
	/**
677
	 * Given an array of existing templates, returns those templates which are not in $this
678
	 * and thus should be deleted.
679
	 * @param array $existing
680
	 * @return array
681
	 */
682 View Code Duplication
	private function getTemplateDeletions( $existing ) {
683
		$del = [];
684
		foreach ( $existing as $ns => $dbkeys ) {
685
			if ( isset( $this->mTemplates[$ns] ) ) {
686
				$del[$ns] = array_diff_key( $existing[$ns], $this->mTemplates[$ns] );
687
			} else {
688
				$del[$ns] = $existing[$ns];
689
			}
690
		}
691
692
		return $del;
693
	}
694
695
	/**
696
	 * Given an array of existing images, returns those images which are not in $this
697
	 * and thus should be deleted.
698
	 * @param array $existing
699
	 * @return array
700
	 */
701
	private function getImageDeletions( $existing ) {
702
		return array_diff_key( $existing, $this->mImages );
703
	}
704
705
	/**
706
	 * Given an array of existing external links, returns those links which are not
707
	 * in $this and thus should be deleted.
708
	 * @param array $existing
709
	 * @return array
710
	 */
711
	private function getExternalDeletions( $existing ) {
712
		return array_diff_key( $existing, $this->mExternals );
713
	}
714
715
	/**
716
	 * Given an array of existing categories, returns those categories which are not in $this
717
	 * and thus should be deleted.
718
	 * @param array $existing
719
	 * @return array
720
	 */
721
	private function getCategoryDeletions( $existing ) {
722
		return array_diff_assoc( $existing, $this->mCategories );
723
	}
724
725
	/**
726
	 * Given an array of existing interlanguage links, returns those links which are not
727
	 * in $this and thus should be deleted.
728
	 * @param array $existing
729
	 * @return array
730
	 */
731
	private function getInterlangDeletions( $existing ) {
732
		return array_diff_assoc( $existing, $this->mInterlangs );
733
	}
734
735
	/**
736
	 * Get array of properties which should be deleted.
737
	 * @param array $existing
738
	 * @return array
739
	 */
740
	function getPropertyDeletions( $existing ) {
741
		return array_diff_assoc( $existing, $this->mProperties );
742
	}
743
744
	/**
745
	 * Given an array of existing interwiki links, returns those links which are not in $this
746
	 * and thus should be deleted.
747
	 * @param array $existing
748
	 * @return array
749
	 */
750 View Code Duplication
	private function getInterwikiDeletions( $existing ) {
751
		$del = [];
752
		foreach ( $existing as $prefix => $dbkeys ) {
753
			if ( isset( $this->mInterwikis[$prefix] ) ) {
754
				$del[$prefix] = array_diff_key( $existing[$prefix], $this->mInterwikis[$prefix] );
755
			} else {
756
				$del[$prefix] = $existing[$prefix];
757
			}
758
		}
759
760
		return $del;
761
	}
762
763
	/**
764
	 * Get an array of existing links, as a 2-D array
765
	 *
766
	 * @return array
767
	 */
768
	private function getExistingLinks() {
769
		$res = $this->mDb->select( 'pagelinks', [ 'pl_namespace', 'pl_title' ],
770
			[ 'pl_from' => $this->mId ], __METHOD__, $this->mOptions );
771
		$arr = [];
772
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean 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...
773
			if ( !isset( $arr[$row->pl_namespace] ) ) {
774
				$arr[$row->pl_namespace] = [];
775
			}
776
			$arr[$row->pl_namespace][$row->pl_title] = 1;
777
		}
778
779
		return $arr;
780
	}
781
782
	/**
783
	 * Get an array of existing templates, as a 2-D array
784
	 *
785
	 * @return array
786
	 */
787
	private function getExistingTemplates() {
788
		$res = $this->mDb->select( 'templatelinks', [ 'tl_namespace', 'tl_title' ],
789
			[ 'tl_from' => $this->mId ], __METHOD__, $this->mOptions );
790
		$arr = [];
791
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean 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...
792
			if ( !isset( $arr[$row->tl_namespace] ) ) {
793
				$arr[$row->tl_namespace] = [];
794
			}
795
			$arr[$row->tl_namespace][$row->tl_title] = 1;
796
		}
797
798
		return $arr;
799
	}
800
801
	/**
802
	 * Get an array of existing images, image names in the keys
803
	 *
804
	 * @return array
805
	 */
806
	private function getExistingImages() {
807
		$res = $this->mDb->select( 'imagelinks', [ 'il_to' ],
808
			[ 'il_from' => $this->mId ], __METHOD__, $this->mOptions );
809
		$arr = [];
810
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean 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...
811
			$arr[$row->il_to] = 1;
812
		}
813
814
		return $arr;
815
	}
816
817
	/**
818
	 * Get an array of existing external links, URLs in the keys
819
	 *
820
	 * @return array
821
	 */
822
	private function getExistingExternals() {
823
		$res = $this->mDb->select( 'externallinks', [ 'el_to' ],
824
			[ 'el_from' => $this->mId ], __METHOD__, $this->mOptions );
825
		$arr = [];
826
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean 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...
827
			$arr[$row->el_to] = 1;
828
		}
829
830
		return $arr;
831
	}
832
833
	/**
834
	 * Get an array of existing categories, with the name in the key and sort key in the value.
835
	 *
836
	 * @return array
837
	 */
838
	private function getExistingCategories() {
839
		$res = $this->mDb->select( 'categorylinks', [ 'cl_to', 'cl_sortkey_prefix' ],
840
			[ 'cl_from' => $this->mId ], __METHOD__, $this->mOptions );
841
		$arr = [];
842
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean 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...
843
			$arr[$row->cl_to] = $row->cl_sortkey_prefix;
844
		}
845
846
		return $arr;
847
	}
848
849
	/**
850
	 * Get an array of existing interlanguage links, with the language code in the key and the
851
	 * title in the value.
852
	 *
853
	 * @return array
854
	 */
855
	private function getExistingInterlangs() {
856
		$res = $this->mDb->select( 'langlinks', [ 'll_lang', 'll_title' ],
857
			[ 'll_from' => $this->mId ], __METHOD__, $this->mOptions );
858
		$arr = [];
859
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean 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...
860
			$arr[$row->ll_lang] = $row->ll_title;
861
		}
862
863
		return $arr;
864
	}
865
866
	/**
867
	 * Get an array of existing inline interwiki links, as a 2-D array
868
	 * @return array (prefix => array(dbkey => 1))
869
	 */
870
	protected function getExistingInterwikis() {
871
		$res = $this->mDb->select( 'iwlinks', [ 'iwl_prefix', 'iwl_title' ],
872
			[ 'iwl_from' => $this->mId ], __METHOD__, $this->mOptions );
873
		$arr = [];
874
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean 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...
875
			if ( !isset( $arr[$row->iwl_prefix] ) ) {
876
				$arr[$row->iwl_prefix] = [];
877
			}
878
			$arr[$row->iwl_prefix][$row->iwl_title] = 1;
879
		}
880
881
		return $arr;
882
	}
883
884
	/**
885
	 * Get an array of existing categories, with the name in the key and sort key in the value.
886
	 *
887
	 * @return array Array of property names and values
888
	 */
889
	private function getExistingProperties() {
890
		$res = $this->mDb->select( 'page_props', [ 'pp_propname', 'pp_value' ],
891
			[ 'pp_page' => $this->mId ], __METHOD__, $this->mOptions );
892
		$arr = [];
893
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean 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...
894
			$arr[$row->pp_propname] = $row->pp_value;
895
		}
896
897
		return $arr;
898
	}
899
900
	/**
901
	 * Return the title object of the page being updated
902
	 * @return Title
903
	 */
904
	public function getTitle() {
905
		return $this->mTitle;
906
	}
907
908
	/**
909
	 * Returns parser output
910
	 * @since 1.19
911
	 * @return ParserOutput
912
	 */
913
	public function getParserOutput() {
914
		return $this->mParserOutput;
915
	}
916
917
	/**
918
	 * Return the list of images used as generated by the parser
919
	 * @return array
920
	 */
921
	public function getImages() {
922
		return $this->mImages;
923
	}
924
925
	/**
926
	 * Set the revision corresponding to this LinksUpdate
927
	 *
928
	 * @since 1.27
929
	 *
930
	 * @param Revision $revision
931
	 */
932
	public function setRevision( Revision $revision ) {
933
		$this->mRevision = $revision;
934
	}
935
936
	/**
937
	 * @since 1.28
938
	 * @return null|Revision
939
	 */
940
	public function getRevision() {
941
		return $this->mRevision;
942
	}
943
944
	/**
945
	 * Set the User who triggered this LinksUpdate
946
	 *
947
	 * @since 1.27
948
	 * @param User $user
949
	 */
950
	public function setTriggeringUser( User $user ) {
951
		$this->user = $user;
952
	}
953
954
	/**
955
	 * @since 1.27
956
	 * @return null|User
957
	 */
958
	public function getTriggeringUser() {
959
		return $this->user;
960
	}
961
962
	/**
963
	 * Invalidate any necessary link lists related to page property changes
964
	 * @param array $changed
965
	 */
966
	private function invalidateProperties( $changed ) {
967
		global $wgPagePropLinkInvalidations;
968
969
		foreach ( $changed as $name => $value ) {
970
			if ( isset( $wgPagePropLinkInvalidations[$name] ) ) {
971
				$inv = $wgPagePropLinkInvalidations[$name];
972
				if ( !is_array( $inv ) ) {
973
					$inv = [ $inv ];
974
				}
975
				foreach ( $inv as $table ) {
976
					DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->mTitle, $table ) );
977
				}
978
			}
979
		}
980
	}
981
982
	/**
983
	 * Fetch page links added by this LinksUpdate.  Only available after the update is complete.
984
	 * @since 1.22
985
	 * @return null|array Array of Titles
986
	 */
987
	public function getAddedLinks() {
988
		if ( $this->linkInsertions === null ) {
989
			return null;
990
		}
991
		$result = [];
992
		foreach ( $this->linkInsertions as $insertion ) {
993
			$result[] = Title::makeTitle( $insertion['pl_namespace'], $insertion['pl_title'] );
994
		}
995
996
		return $result;
997
	}
998
999
	/**
1000
	 * Fetch page links removed by this LinksUpdate.  Only available after the update is complete.
1001
	 * @since 1.22
1002
	 * @return null|array Array of Titles
1003
	 */
1004
	public function getRemovedLinks() {
1005
		if ( $this->linkDeletions === null ) {
1006
			return null;
1007
		}
1008
		$result = [];
1009
		foreach ( $this->linkDeletions as $ns => $titles ) {
1010
			foreach ( $titles as $title => $unused ) {
1011
				$result[] = Title::makeTitle( $ns, $title );
1012
			}
1013
		}
1014
1015
		return $result;
1016
	}
1017
1018
	/**
1019
	 * Update links table freshness
1020
	 */
1021
	protected function updateLinksTimestamp() {
1022
		if ( $this->mId ) {
1023
			// The link updates made here only reflect the freshness of the parser output
1024
			$timestamp = $this->mParserOutput->getCacheTime();
1025
			$this->mDb->update( 'page',
1026
				[ 'page_links_updated' => $this->mDb->timestamp( $timestamp ) ],
1027
				[ 'page_id' => $this->mId ],
1028
				__METHOD__
1029
			);
1030
		}
1031
	}
1032
1033
	public function getAsJobSpecification() {
1034
		if ( $this->user ) {
1035
			$userInfo = [
1036
				'userId' => $this->user->getId(),
1037
				'userName' => $this->user->getName(),
1038
			];
1039
		} else {
1040
			$userInfo = false;
1041
		}
1042
1043
		if ( $this->mRevision ) {
1044
			$triggeringRevisionId = $this->mRevision->getId();
1045
		} else {
1046
			$triggeringRevisionId = false;
1047
		}
1048
1049
		return [
1050
			'wiki' => $this->mDb->getWikiID(),
1051
			'job'  => new JobSpecification(
1052
				'refreshLinksPrioritized',
1053
				[
1054
					// Reuse the parser cache if it was saved
1055
					'rootJobTimestamp' => $this->mParserOutput->getCacheTime(),
1056
					'useRecursiveLinksUpdate' => $this->mRecursive,
1057
					'triggeringUser' => $userInfo,
1058
					'triggeringRevisionId' => $triggeringRevisionId,
1059
				],
1060
				[ 'removeDuplicates' => true ],
1061
				$this->getTitle()
1062
			)
1063
		];
1064
	}
1065
}
1066