Completed
Branch master (715cbe)
by
unknown
51:55
created

LinksUpdate::doIncrementalUpdate()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 90
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 62
nc 2
nop 0
dl 0
loc 90
rs 8.5454
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * 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
use MediaWiki\MediaWikiServices;
24
use Wikimedia\ScopedCallback;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ScopedCallback.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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