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

LinksUpdate::getAddedProperties()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 3
rs 10
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 null|array Added properties if calculated.
84
	 */
85
	private $propertyInsertions = null;
86
87
	/**
88
	 * @var null|array Deleted properties if calculated.
89
	 */
90
	private $propertyDeletions = null;
91
92
	/**
93
	 * @var User|null
94
	 */
95
	private $user;
96
97
	/**
98
	 * Constructor
99
	 *
100
	 * @param Title $title Title of the page we're updating
101
	 * @param ParserOutput $parserOutput Output from a full parse of this page
102
	 * @param bool $recursive Queue jobs for recursive updates?
103
	 * @throws MWException
104
	 */
105
	function __construct( Title $title, ParserOutput $parserOutput, $recursive = true ) {
106
		// Implicit transactions are disabled as they interfere with batching
107
		parent::__construct( false );
108
109
		$this->mTitle = $title;
110
		$this->mId = $title->getArticleID( Title::GAID_FOR_UPDATE );
111
112
		if ( !$this->mId ) {
113
			throw new InvalidArgumentException(
114
				"The Title object yields no ID. Perhaps the page doesn't exist?"
115
			);
116
		}
117
118
		$this->mParserOutput = $parserOutput;
119
120
		$this->mLinks = $parserOutput->getLinks();
121
		$this->mImages = $parserOutput->getImages();
122
		$this->mTemplates = $parserOutput->getTemplates();
123
		$this->mExternals = $parserOutput->getExternalLinks();
124
		$this->mCategories = $parserOutput->getCategories();
125
		$this->mProperties = $parserOutput->getProperties();
126
		$this->mInterwikis = $parserOutput->getInterwikiLinks();
127
128
		# Convert the format of the interlanguage links
129
		# I didn't want to change it in the ParserOutput, because that array is passed all
130
		# the way back to the skin, so either a skin API break would be required, or an
131
		# inefficient back-conversion.
132
		$ill = $parserOutput->getLanguageLinks();
133
		$this->mInterlangs = [];
134
		foreach ( $ill as $link ) {
135
			list( $key, $title ) = explode( ':', $link, 2 );
136
			$this->mInterlangs[$key] = $title;
137
		}
138
139
		foreach ( $this->mCategories as &$sortkey ) {
140
			# If the sortkey is longer then 255 bytes,
141
			# it truncated by DB, and then doesn't get
142
			# matched when comparing existing vs current
143
			# categories, causing bug 25254.
144
			# Also. substr behaves weird when given "".
145
			if ( $sortkey !== '' ) {
146
				$sortkey = substr( $sortkey, 0, 255 );
147
			}
148
		}
149
150
		$this->mRecursive = $recursive;
151
152
		Hooks::run( 'LinksUpdateConstructed', [ &$this ] );
153
	}
154
155
	/**
156
	 * Update link tables with outgoing links from an updated article
157
	 *
158
	 * @note: this is managed by DeferredUpdates::execute(). Do not run this in a transaction.
159
	 */
160
	public function doUpdate() {
161
		// Make sure all links update threads see the changes of each other.
162
		// This handles the case when updates have to batched into several COMMITs.
163
		$scopedLock = self::acquirePageLock( $this->mDb, $this->mId );
164
165
		Hooks::run( 'LinksUpdate', [ &$this ] );
166
		$this->doIncrementalUpdate();
167
168
		// Commit and release the lock
169
		ScopedCallback::consume( $scopedLock );
170
		// Run post-commit hooks without DBO_TRX
171
		$this->mDb->onTransactionIdle( function() {
172
			Hooks::run( 'LinksUpdateComplete', [ &$this ] );
173
		} );
174
	}
175
176
	/**
177
	 * Acquire a lock for performing link table updates for a page on a DB
178
	 *
179
	 * @param IDatabase $dbw
180
	 * @param integer $pageId
181
	 * @param string $why One of (job, atomicity)
182
	 * @return ScopedCallback
183
	 * @throws RuntimeException
184
	 * @since 1.27
185
	 */
186
	public static function acquirePageLock( IDatabase $dbw, $pageId, $why = 'atomicity' ) {
187
		$key = "LinksUpdate:$why:pageid:$pageId";
188
		$scopedLock = $dbw->getScopedLockAndFlush( $key, __METHOD__, 15 );
189
		if ( !$scopedLock ) {
190
			throw new RuntimeException( "Could not acquire lock '$key'." );
191
		}
192
193
		return $scopedLock;
194
	}
195
196
	protected function doIncrementalUpdate() {
197
		# Page links
198
		$existing = $this->getExistingLinks();
199
		$this->linkDeletions = $this->getLinkDeletions( $existing );
200
		$this->linkInsertions = $this->getLinkInsertions( $existing );
201
		$this->incrTableUpdate( 'pagelinks', 'pl', $this->linkDeletions, $this->linkInsertions );
202
203
		# Image links
204
		$existing = $this->getExistingImages();
205
		$imageDeletes = $this->getImageDeletions( $existing );
206
		$this->incrTableUpdate( 'imagelinks', 'il', $imageDeletes,
207
			$this->getImageInsertions( $existing ) );
208
209
		# Invalidate all image description pages which had links added or removed
210
		$imageUpdates = $imageDeletes + array_diff_key( $this->mImages, $existing );
211
		$this->invalidateImageDescriptions( $imageUpdates );
212
213
		# External links
214
		$existing = $this->getExistingExternals();
215
		$this->incrTableUpdate( 'externallinks', 'el', $this->getExternalDeletions( $existing ),
216
			$this->getExternalInsertions( $existing ) );
217
218
		# Language links
219
		$existing = $this->getExistingInterlangs();
220
		$this->incrTableUpdate( 'langlinks', 'll', $this->getInterlangDeletions( $existing ),
221
			$this->getInterlangInsertions( $existing ) );
222
223
		# Inline interwiki links
224
		$existing = $this->getExistingInterwikis();
225
		$this->incrTableUpdate( 'iwlinks', 'iwl', $this->getInterwikiDeletions( $existing ),
226
			$this->getInterwikiInsertions( $existing ) );
227
228
		# Template links
229
		$existing = $this->getExistingTemplates();
230
		$this->incrTableUpdate( 'templatelinks', 'tl', $this->getTemplateDeletions( $existing ),
231
			$this->getTemplateInsertions( $existing ) );
232
233
		# Category links
234
		$existing = $this->getExistingCategories();
235
		$categoryDeletes = $this->getCategoryDeletions( $existing );
236
		$this->incrTableUpdate( 'categorylinks', 'cl', $categoryDeletes,
237
			$this->getCategoryInsertions( $existing ) );
238
239
		# Invalidate all categories which were added, deleted or changed (set symmetric difference)
240
		$categoryInserts = array_diff_assoc( $this->mCategories, $existing );
241
		$categoryUpdates = $categoryInserts + $categoryDeletes;
242
		$this->invalidateCategories( $categoryUpdates );
243
		$this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
244
245
		# Page properties
246
		$existing = $this->getExistingProperties();
247
		$this->propertyDeletions = $this->getPropertyDeletions( $existing );
248
		$this->incrTableUpdate( 'page_props', 'pp', $this->propertyDeletions,
249
			$this->getPropertyInsertions( $existing ) );
250
251
		# Invalidate the necessary pages
252
		$this->propertyInsertions = array_diff_assoc( $this->mProperties, $existing );
253
		$changed = $this->propertyDeletions + $this->propertyInsertions;
254
		$this->invalidateProperties( $changed );
255
256
		# Refresh links of all pages including this page
257
		# This will be in a separate transaction
258
		if ( $this->mRecursive ) {
259
			$this->queueRecursiveJobs();
260
		}
261
262
		# Update the links table freshness for this title
263
		$this->updateLinksTimestamp();
264
	}
265
266
	/**
267
	 * Queue recursive jobs for this page
268
	 *
269
	 * Which means do LinksUpdate on all pages that include the current page,
270
	 * using the job queue.
271
	 */
272
	protected function queueRecursiveJobs() {
273
		self::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
274
		if ( $this->mTitle->getNamespace() == NS_FILE ) {
275
			// Process imagelinks in case the title is or was a redirect
276
			self::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
277
		}
278
279
		$bc = $this->mTitle->getBacklinkCache();
280
		// Get jobs for cascade-protected backlinks for a high priority queue.
281
		// If meta-templates change to using a new template, the new template
282
		// should be implicitly protected as soon as possible, if applicable.
283
		// These jobs duplicate a subset of the above ones, but can run sooner.
284
		// Which ever runs first generally no-ops the other one.
285
		$jobs = [];
286
		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...
287
			$jobs[] = RefreshLinksJob::newPrioritized( $title, [] );
288
		}
289
		JobQueueGroup::singleton()->push( $jobs );
290
	}
291
292
	/**
293
	 * Queue a RefreshLinks job for any table.
294
	 *
295
	 * @param Title $title Title to do job for
296
	 * @param string $table Table to use (e.g. 'templatelinks')
297
	 */
298
	public static function queueRecursiveJobsForTable( Title $title, $table ) {
299
		if ( $title->getBacklinkCache()->hasLinks( $table ) ) {
300
			$job = new RefreshLinksJob(
301
				$title,
302
				[
303
					'table' => $table,
304
					'recursive' => true,
305
				] + Job::newRootJobParams( // "overall" refresh links job info
306
					"refreshlinks:{$table}:{$title->getPrefixedText()}"
307
				)
308
			);
309
310
			JobQueueGroup::singleton()->push( $job );
311
		}
312
	}
313
314
	/**
315
	 * @param array $cats
316
	 */
317
	function invalidateCategories( $cats ) {
318
		PurgeJobUtils::invalidatePages( $this->mDb, NS_CATEGORY, array_keys( $cats ) );
319
	}
320
321
	/**
322
	 * Update all the appropriate counts in the category table.
323
	 * @param array $added Associative array of category name => sort key
324
	 * @param array $deleted Associative array of category name => sort key
325
	 */
326
	function updateCategoryCounts( $added, $deleted ) {
327
		$a = WikiPage::factory( $this->mTitle );
328
		$a->updateCategoryCounts(
329
			array_keys( $added ), array_keys( $deleted )
330
		);
331
	}
332
333
	/**
334
	 * @param array $images
335
	 */
336
	function invalidateImageDescriptions( $images ) {
337
		PurgeJobUtils::invalidatePages( $this->mDb, NS_FILE, array_keys( $images ) );
338
	}
339
340
	/**
341
	 * Update a table by doing a delete query then an insert query
342
	 * @param string $table Table name
343
	 * @param string $prefix Field name prefix
344
	 * @param array $deletions
345
	 * @param array $insertions Rows to insert
346
	 */
347
	private function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
348
		$bSize = RequestContext::getMain()->getConfig()->get( 'UpdateRowsPerQuery' );
349
		$factory = wfGetLBFactory();
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...
350
351
		if ( $table === 'page_props' ) {
352
			$fromField = 'pp_page';
353
		} else {
354
			$fromField = "{$prefix}_from";
355
		}
356
357
		$deleteWheres = []; // list of WHERE clause arrays for each DB delete() call
358
		if ( $table === 'pagelinks' || $table === 'templatelinks' || $table === 'iwlinks' ) {
359
			$baseKey =  ( $table === 'iwlinks' ) ? 'iwl_prefix' : "{$prefix}_namespace";
360
361
			$curBatchSize = 0;
362
			$curDeletionBatch = [];
363
			$deletionBatches = [];
364
			foreach ( $deletions as $ns => $dbKeys ) {
365
				foreach ( $dbKeys as $dbKey => $unused ) {
366
					$curDeletionBatch[$ns][$dbKey] = 1;
367
					if ( ++$curBatchSize >= $bSize ) {
368
						$deletionBatches[] = $curDeletionBatch;
369
						$curDeletionBatch = [];
370
						$curBatchSize = 0;
371
					}
372
				}
373
			}
374
			if ( $curDeletionBatch ) {
375
				$deletionBatches[] = $curDeletionBatch;
376
			}
377
378
			foreach ( $deletionBatches as $deletionBatch ) {
379
				$deleteWheres[] = [
380
					$fromField => $this->mId,
381
					$this->mDb->makeWhereFrom2d( $deletionBatch, $baseKey, "{$prefix}_title" )
382
				];
383
			}
384
		} else {
385
			if ( $table === 'langlinks' ) {
386
				$toField = 'll_lang';
387
			} elseif ( $table === 'page_props' ) {
388
				$toField = 'pp_propname';
389
			} else {
390
				$toField = $prefix . '_to';
391
			}
392
393
			$deletionBatches = array_chunk( array_keys( $deletions ), $bSize );
394
			foreach ( $deletionBatches as $deletionBatch ) {
395
				$deleteWheres[] = [ $fromField => $this->mId, $toField => $deletionBatch ];
396
			}
397
		}
398
399
		foreach ( $deleteWheres as $deleteWhere ) {
400
			$this->mDb->delete( $table, $deleteWhere, __METHOD__ );
401
			$factory->commitAndWaitForReplication(
402
				__METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
403
			);
404
		}
405
406
		$insertBatches = array_chunk( $insertions, $bSize );
407
		foreach ( $insertBatches as $insertBatch ) {
408
			$this->mDb->insert( $table, $insertBatch, __METHOD__, 'IGNORE' );
409
			$factory->commitAndWaitForReplication(
410
				__METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
411
			);
412
		}
413
414
		if ( count( $insertions ) ) {
415
			Hooks::run( 'LinksUpdateAfterInsert', [ $this, $table, $insertions ] );
416
		}
417
	}
418
419
	/**
420
	 * Get an array of pagelinks insertions for passing to the DB
421
	 * Skips the titles specified by the 2-D array $existing
422
	 * @param array $existing
423
	 * @return array
424
	 */
425 View Code Duplication
	private function getLinkInsertions( $existing = [] ) {
426
		$arr = [];
427
		foreach ( $this->mLinks as $ns => $dbkeys ) {
428
			$diffs = isset( $existing[$ns] )
429
				? array_diff_key( $dbkeys, $existing[$ns] )
430
				: $dbkeys;
431
			foreach ( $diffs as $dbk => $id ) {
432
				$arr[] = [
433
					'pl_from' => $this->mId,
434
					'pl_from_namespace' => $this->mTitle->getNamespace(),
435
					'pl_namespace' => $ns,
436
					'pl_title' => $dbk
437
				];
438
			}
439
		}
440
441
		return $arr;
442
	}
443
444
	/**
445
	 * Get an array of template insertions. Like getLinkInsertions()
446
	 * @param array $existing
447
	 * @return array
448
	 */
449 View Code Duplication
	private function getTemplateInsertions( $existing = [] ) {
450
		$arr = [];
451
		foreach ( $this->mTemplates as $ns => $dbkeys ) {
452
			$diffs = isset( $existing[$ns] ) ? array_diff_key( $dbkeys, $existing[$ns] ) : $dbkeys;
453
			foreach ( $diffs as $dbk => $id ) {
454
				$arr[] = [
455
					'tl_from' => $this->mId,
456
					'tl_from_namespace' => $this->mTitle->getNamespace(),
457
					'tl_namespace' => $ns,
458
					'tl_title' => $dbk
459
				];
460
			}
461
		}
462
463
		return $arr;
464
	}
465
466
	/**
467
	 * Get an array of image insertions
468
	 * Skips the names specified in $existing
469
	 * @param array $existing
470
	 * @return array
471
	 */
472
	private function getImageInsertions( $existing = [] ) {
473
		$arr = [];
474
		$diffs = array_diff_key( $this->mImages, $existing );
475
		foreach ( $diffs as $iname => $dummy ) {
476
			$arr[] = [
477
				'il_from' => $this->mId,
478
				'il_from_namespace' => $this->mTitle->getNamespace(),
479
				'il_to' => $iname
480
			];
481
		}
482
483
		return $arr;
484
	}
485
486
	/**
487
	 * Get an array of externallinks insertions. Skips the names specified in $existing
488
	 * @param array $existing
489
	 * @return array
490
	 */
491
	private function getExternalInsertions( $existing = [] ) {
492
		$arr = [];
493
		$diffs = array_diff_key( $this->mExternals, $existing );
494
		foreach ( $diffs as $url => $dummy ) {
495
			foreach ( wfMakeUrlIndexes( $url ) as $index ) {
496
				$arr[] = [
497
					'el_id' => $this->mDb->nextSequenceValue( 'externallinks_el_id_seq' ),
498
					'el_from' => $this->mId,
499
					'el_to' => $url,
500
					'el_index' => $index,
501
				];
502
			}
503
		}
504
505
		return $arr;
506
	}
507
508
	/**
509
	 * Get an array of category insertions
510
	 *
511
	 * @param array $existing Mapping existing category names to sort keys. If both
512
	 * match a link in $this, the link will be omitted from the output
513
	 *
514
	 * @return array
515
	 */
516
	private function getCategoryInsertions( $existing = [] ) {
517
		global $wgContLang, $wgCategoryCollation;
518
		$diffs = array_diff_assoc( $this->mCategories, $existing );
519
		$arr = [];
520
		foreach ( $diffs as $name => $prefix ) {
521
			$nt = Title::makeTitleSafe( NS_CATEGORY, $name );
522
			$wgContLang->findVariantLink( $name, $nt, true );
523
524 View Code Duplication
			if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
525
				$type = 'subcat';
526
			} elseif ( $this->mTitle->getNamespace() == NS_FILE ) {
527
				$type = 'file';
528
			} else {
529
				$type = 'page';
530
			}
531
532
			# Treat custom sortkeys as a prefix, so that if multiple
533
			# things are forced to sort as '*' or something, they'll
534
			# sort properly in the category rather than in page_id
535
			# order or such.
536
			$sortkey = Collation::singleton()->getSortKey(
537
				$this->mTitle->getCategorySortkey( $prefix ) );
538
539
			$arr[] = [
540
				'cl_from' => $this->mId,
541
				'cl_to' => $name,
542
				'cl_sortkey' => $sortkey,
543
				'cl_timestamp' => $this->mDb->timestamp(),
544
				'cl_sortkey_prefix' => $prefix,
545
				'cl_collation' => $wgCategoryCollation,
546
				'cl_type' => $type,
547
			];
548
		}
549
550
		return $arr;
551
	}
552
553
	/**
554
	 * Get an array of interlanguage link insertions
555
	 *
556
	 * @param array $existing Mapping existing language codes to titles
557
	 *
558
	 * @return array
559
	 */
560
	private function getInterlangInsertions( $existing = [] ) {
561
		$diffs = array_diff_assoc( $this->mInterlangs, $existing );
562
		$arr = [];
563
		foreach ( $diffs as $lang => $title ) {
564
			$arr[] = [
565
				'll_from' => $this->mId,
566
				'll_lang' => $lang,
567
				'll_title' => $title
568
			];
569
		}
570
571
		return $arr;
572
	}
573
574
	/**
575
	 * Get an array of page property insertions
576
	 * @param array $existing
577
	 * @return array
578
	 */
579
	function getPropertyInsertions( $existing = [] ) {
580
		$diffs = array_diff_assoc( $this->mProperties, $existing );
581
582
		$arr = [];
583
		foreach ( array_keys( $diffs ) as $name ) {
584
			$arr[] = $this->getPagePropRowData( $name );
585
		}
586
587
		return $arr;
588
	}
589
590
	/**
591
	 * Returns an associative array to be used for inserting a row into
592
	 * the page_props table. Besides the given property name, this will
593
	 * include the page id from $this->mId and any property value from
594
	 * $this->mProperties.
595
	 *
596
	 * The array returned will include the pp_sortkey field if this
597
	 * is present in the database (as indicated by $wgPagePropsHaveSortkey).
598
	 * The sortkey value is currently determined by getPropertySortKeyValue().
599
	 *
600
	 * @note this assumes that $this->mProperties[$prop] is defined.
601
	 *
602
	 * @param string $prop The name of the property.
603
	 *
604
	 * @return array
605
	 */
606
	private function getPagePropRowData( $prop ) {
607
		global $wgPagePropsHaveSortkey;
608
609
		$value = $this->mProperties[$prop];
610
611
		$row = [
612
			'pp_page' => $this->mId,
613
			'pp_propname' => $prop,
614
			'pp_value' => $value,
615
		];
616
617
		if ( $wgPagePropsHaveSortkey ) {
618
			$row['pp_sortkey'] = $this->getPropertySortKeyValue( $value );
619
		}
620
621
		return $row;
622
	}
623
624
	/**
625
	 * Determines the sort key for the given property value.
626
	 * This will return $value if it is a float or int,
627
	 * 1 or resp. 0 if it is a bool, and null otherwise.
628
	 *
629
	 * @note In the future, we may allow the sortkey to be specified explicitly
630
	 *       in ParserOutput::setProperty.
631
	 *
632
	 * @param mixed $value
633
	 *
634
	 * @return float|null
635
	 */
636
	private function getPropertySortKeyValue( $value ) {
637
		if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
638
			return floatval( $value );
639
		}
640
641
		return null;
642
	}
643
644
	/**
645
	 * Get an array of interwiki insertions for passing to the DB
646
	 * Skips the titles specified by the 2-D array $existing
647
	 * @param array $existing
648
	 * @return array
649
	 */
650
	private function getInterwikiInsertions( $existing = [] ) {
651
		$arr = [];
652
		foreach ( $this->mInterwikis as $prefix => $dbkeys ) {
653
			$diffs = isset( $existing[$prefix] )
654
				? array_diff_key( $dbkeys, $existing[$prefix] )
655
				: $dbkeys;
656
657
			foreach ( $diffs as $dbk => $id ) {
658
				$arr[] = [
659
					'iwl_from' => $this->mId,
660
					'iwl_prefix' => $prefix,
661
					'iwl_title' => $dbk
662
				];
663
			}
664
		}
665
666
		return $arr;
667
	}
668
669
	/**
670
	 * Given an array of existing links, returns those links which are not in $this
671
	 * and thus should be deleted.
672
	 * @param array $existing
673
	 * @return array
674
	 */
675 View Code Duplication
	private function getLinkDeletions( $existing ) {
676
		$del = [];
677
		foreach ( $existing as $ns => $dbkeys ) {
678
			if ( isset( $this->mLinks[$ns] ) ) {
679
				$del[$ns] = array_diff_key( $existing[$ns], $this->mLinks[$ns] );
680
			} else {
681
				$del[$ns] = $existing[$ns];
682
			}
683
		}
684
685
		return $del;
686
	}
687
688
	/**
689
	 * Given an array of existing templates, returns those templates which are not in $this
690
	 * and thus should be deleted.
691
	 * @param array $existing
692
	 * @return array
693
	 */
694 View Code Duplication
	private function getTemplateDeletions( $existing ) {
695
		$del = [];
696
		foreach ( $existing as $ns => $dbkeys ) {
697
			if ( isset( $this->mTemplates[$ns] ) ) {
698
				$del[$ns] = array_diff_key( $existing[$ns], $this->mTemplates[$ns] );
699
			} else {
700
				$del[$ns] = $existing[$ns];
701
			}
702
		}
703
704
		return $del;
705
	}
706
707
	/**
708
	 * Given an array of existing images, returns those images which are not in $this
709
	 * and thus should be deleted.
710
	 * @param array $existing
711
	 * @return array
712
	 */
713
	private function getImageDeletions( $existing ) {
714
		return array_diff_key( $existing, $this->mImages );
715
	}
716
717
	/**
718
	 * Given an array of existing external links, returns those links which are not
719
	 * in $this and thus should be deleted.
720
	 * @param array $existing
721
	 * @return array
722
	 */
723
	private function getExternalDeletions( $existing ) {
724
		return array_diff_key( $existing, $this->mExternals );
725
	}
726
727
	/**
728
	 * Given an array of existing categories, returns those categories which are not in $this
729
	 * and thus should be deleted.
730
	 * @param array $existing
731
	 * @return array
732
	 */
733
	private function getCategoryDeletions( $existing ) {
734
		return array_diff_assoc( $existing, $this->mCategories );
735
	}
736
737
	/**
738
	 * Given an array of existing interlanguage links, returns those links which are not
739
	 * in $this and thus should be deleted.
740
	 * @param array $existing
741
	 * @return array
742
	 */
743
	private function getInterlangDeletions( $existing ) {
744
		return array_diff_assoc( $existing, $this->mInterlangs );
745
	}
746
747
	/**
748
	 * Get array of properties which should be deleted.
749
	 * @param array $existing
750
	 * @return array
751
	 */
752
	function getPropertyDeletions( $existing ) {
753
		return array_diff_assoc( $existing, $this->mProperties );
754
	}
755
756
	/**
757
	 * Given an array of existing interwiki links, returns those links which are not in $this
758
	 * and thus should be deleted.
759
	 * @param array $existing
760
	 * @return array
761
	 */
762 View Code Duplication
	private function getInterwikiDeletions( $existing ) {
763
		$del = [];
764
		foreach ( $existing as $prefix => $dbkeys ) {
765
			if ( isset( $this->mInterwikis[$prefix] ) ) {
766
				$del[$prefix] = array_diff_key( $existing[$prefix], $this->mInterwikis[$prefix] );
767
			} else {
768
				$del[$prefix] = $existing[$prefix];
769
			}
770
		}
771
772
		return $del;
773
	}
774
775
	/**
776
	 * Get an array of existing links, as a 2-D array
777
	 *
778
	 * @return array
779
	 */
780
	private function getExistingLinks() {
781
		$res = $this->mDb->select( 'pagelinks', [ 'pl_namespace', 'pl_title' ],
782
			[ 'pl_from' => $this->mId ], __METHOD__, $this->mOptions );
783
		$arr = [];
784
		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...
785
			if ( !isset( $arr[$row->pl_namespace] ) ) {
786
				$arr[$row->pl_namespace] = [];
787
			}
788
			$arr[$row->pl_namespace][$row->pl_title] = 1;
789
		}
790
791
		return $arr;
792
	}
793
794
	/**
795
	 * Get an array of existing templates, as a 2-D array
796
	 *
797
	 * @return array
798
	 */
799
	private function getExistingTemplates() {
800
		$res = $this->mDb->select( 'templatelinks', [ 'tl_namespace', 'tl_title' ],
801
			[ 'tl_from' => $this->mId ], __METHOD__, $this->mOptions );
802
		$arr = [];
803
		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...
804
			if ( !isset( $arr[$row->tl_namespace] ) ) {
805
				$arr[$row->tl_namespace] = [];
806
			}
807
			$arr[$row->tl_namespace][$row->tl_title] = 1;
808
		}
809
810
		return $arr;
811
	}
812
813
	/**
814
	 * Get an array of existing images, image names in the keys
815
	 *
816
	 * @return array
817
	 */
818
	private function getExistingImages() {
819
		$res = $this->mDb->select( 'imagelinks', [ 'il_to' ],
820
			[ 'il_from' => $this->mId ], __METHOD__, $this->mOptions );
821
		$arr = [];
822
		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...
823
			$arr[$row->il_to] = 1;
824
		}
825
826
		return $arr;
827
	}
828
829
	/**
830
	 * Get an array of existing external links, URLs in the keys
831
	 *
832
	 * @return array
833
	 */
834
	private function getExistingExternals() {
835
		$res = $this->mDb->select( 'externallinks', [ 'el_to' ],
836
			[ 'el_from' => $this->mId ], __METHOD__, $this->mOptions );
837
		$arr = [];
838
		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...
839
			$arr[$row->el_to] = 1;
840
		}
841
842
		return $arr;
843
	}
844
845
	/**
846
	 * Get an array of existing categories, with the name in the key and sort key in the value.
847
	 *
848
	 * @return array
849
	 */
850
	private function getExistingCategories() {
851
		$res = $this->mDb->select( 'categorylinks', [ 'cl_to', 'cl_sortkey_prefix' ],
852
			[ 'cl_from' => $this->mId ], __METHOD__, $this->mOptions );
853
		$arr = [];
854
		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...
855
			$arr[$row->cl_to] = $row->cl_sortkey_prefix;
856
		}
857
858
		return $arr;
859
	}
860
861
	/**
862
	 * Get an array of existing interlanguage links, with the language code in the key and the
863
	 * title in the value.
864
	 *
865
	 * @return array
866
	 */
867
	private function getExistingInterlangs() {
868
		$res = $this->mDb->select( 'langlinks', [ 'll_lang', 'll_title' ],
869
			[ 'll_from' => $this->mId ], __METHOD__, $this->mOptions );
870
		$arr = [];
871
		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...
872
			$arr[$row->ll_lang] = $row->ll_title;
873
		}
874
875
		return $arr;
876
	}
877
878
	/**
879
	 * Get an array of existing inline interwiki links, as a 2-D array
880
	 * @return array (prefix => array(dbkey => 1))
881
	 */
882
	protected function getExistingInterwikis() {
883
		$res = $this->mDb->select( 'iwlinks', [ 'iwl_prefix', 'iwl_title' ],
884
			[ 'iwl_from' => $this->mId ], __METHOD__, $this->mOptions );
885
		$arr = [];
886
		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...
887
			if ( !isset( $arr[$row->iwl_prefix] ) ) {
888
				$arr[$row->iwl_prefix] = [];
889
			}
890
			$arr[$row->iwl_prefix][$row->iwl_title] = 1;
891
		}
892
893
		return $arr;
894
	}
895
896
	/**
897
	 * Get an array of existing categories, with the name in the key and sort key in the value.
898
	 *
899
	 * @return array Array of property names and values
900
	 */
901
	private function getExistingProperties() {
902
		$res = $this->mDb->select( 'page_props', [ 'pp_propname', 'pp_value' ],
903
			[ 'pp_page' => $this->mId ], __METHOD__, $this->mOptions );
904
		$arr = [];
905
		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...
906
			$arr[$row->pp_propname] = $row->pp_value;
907
		}
908
909
		return $arr;
910
	}
911
912
	/**
913
	 * Return the title object of the page being updated
914
	 * @return Title
915
	 */
916
	public function getTitle() {
917
		return $this->mTitle;
918
	}
919
920
	/**
921
	 * Returns parser output
922
	 * @since 1.19
923
	 * @return ParserOutput
924
	 */
925
	public function getParserOutput() {
926
		return $this->mParserOutput;
927
	}
928
929
	/**
930
	 * Return the list of images used as generated by the parser
931
	 * @return array
932
	 */
933
	public function getImages() {
934
		return $this->mImages;
935
	}
936
937
	/**
938
	 * Set the revision corresponding to this LinksUpdate
939
	 *
940
	 * @since 1.27
941
	 *
942
	 * @param Revision $revision
943
	 */
944
	public function setRevision( Revision $revision ) {
945
		$this->mRevision = $revision;
946
	}
947
948
	/**
949
	 * @since 1.28
950
	 * @return null|Revision
951
	 */
952
	public function getRevision() {
953
		return $this->mRevision;
954
	}
955
956
	/**
957
	 * Set the User who triggered this LinksUpdate
958
	 *
959
	 * @since 1.27
960
	 * @param User $user
961
	 */
962
	public function setTriggeringUser( User $user ) {
963
		$this->user = $user;
964
	}
965
966
	/**
967
	 * @since 1.27
968
	 * @return null|User
969
	 */
970
	public function getTriggeringUser() {
971
		return $this->user;
972
	}
973
974
	/**
975
	 * Invalidate any necessary link lists related to page property changes
976
	 * @param array $changed
977
	 */
978
	private function invalidateProperties( $changed ) {
979
		global $wgPagePropLinkInvalidations;
980
981
		foreach ( $changed as $name => $value ) {
982
			if ( isset( $wgPagePropLinkInvalidations[$name] ) ) {
983
				$inv = $wgPagePropLinkInvalidations[$name];
984
				if ( !is_array( $inv ) ) {
985
					$inv = [ $inv ];
986
				}
987
				foreach ( $inv as $table ) {
988
					DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->mTitle, $table ) );
989
				}
990
			}
991
		}
992
	}
993
994
	/**
995
	 * Fetch page links added by this LinksUpdate.  Only available after the update is complete.
996
	 * @since 1.22
997
	 * @return null|array Array of Titles
998
	 */
999
	public function getAddedLinks() {
1000
		if ( $this->linkInsertions === null ) {
1001
			return null;
1002
		}
1003
		$result = [];
1004
		foreach ( $this->linkInsertions as $insertion ) {
1005
			$result[] = Title::makeTitle( $insertion['pl_namespace'], $insertion['pl_title'] );
1006
		}
1007
1008
		return $result;
1009
	}
1010
1011
	/**
1012
	 * Fetch page links removed by this LinksUpdate.  Only available after the update is complete.
1013
	 * @since 1.22
1014
	 * @return null|array Array of Titles
1015
	 */
1016
	public function getRemovedLinks() {
1017
		if ( $this->linkDeletions === null ) {
1018
			return null;
1019
		}
1020
		$result = [];
1021
		foreach ( $this->linkDeletions as $ns => $titles ) {
1022
			foreach ( $titles as $title => $unused ) {
1023
				$result[] = Title::makeTitle( $ns, $title );
1024
			}
1025
		}
1026
1027
		return $result;
1028
	}
1029
1030
	/**
1031
	 * Fetch page properties added by this LinksUpdate.
1032
	 * Only available after the update is complete.
1033
	 * @since 1.28
1034
	 * @return null|array
1035
	 */
1036
	public function getAddedProperties() {
1037
		return $this->propertyInsertions;
1038
	}
1039
1040
	/**
1041
	 * Fetch page properties removed by this LinksUpdate.
1042
	 * Only available after the update is complete.
1043
	 * @since 1.28
1044
	 * @return null|array
1045
	 */
1046
	public function getRemovedProperties() {
1047
		return $this->propertyDeletions;
1048
	}
1049
1050
	/**
1051
	 * Update links table freshness
1052
	 */
1053
	protected function updateLinksTimestamp() {
1054
		if ( $this->mId ) {
1055
			// The link updates made here only reflect the freshness of the parser output
1056
			$timestamp = $this->mParserOutput->getCacheTime();
1057
			$this->mDb->update( 'page',
1058
				[ 'page_links_updated' => $this->mDb->timestamp( $timestamp ) ],
1059
				[ 'page_id' => $this->mId ],
1060
				__METHOD__
1061
			);
1062
		}
1063
	}
1064
1065
	public function getAsJobSpecification() {
1066
		if ( $this->user ) {
1067
			$userInfo = [
1068
				'userId' => $this->user->getId(),
1069
				'userName' => $this->user->getName(),
1070
			];
1071
		} else {
1072
			$userInfo = false;
1073
		}
1074
1075
		if ( $this->mRevision ) {
1076
			$triggeringRevisionId = $this->mRevision->getId();
1077
		} else {
1078
			$triggeringRevisionId = false;
1079
		}
1080
1081
		return [
1082
			'wiki' => $this->mDb->getWikiID(),
1083
			'job'  => new JobSpecification(
1084
				'refreshLinksPrioritized',
1085
				[
1086
					// Reuse the parser cache if it was saved
1087
					'rootJobTimestamp' => $this->mParserOutput->getCacheTime(),
1088
					'useRecursiveLinksUpdate' => $this->mRecursive,
1089
					'triggeringUser' => $userInfo,
1090
					'triggeringRevisionId' => $triggeringRevisionId,
1091
				],
1092
				[ 'removeDuplicates' => true ],
1093
				$this->getTitle()
1094
			)
1095
		];
1096
	}
1097
}
1098