Completed
Branch master (939199)
by
unknown
39:35
created

LinksDeletionUpdate::batchDeleteByPK()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 15
c 0
b 0
f 0
nc 6
nop 4
dl 0
loc 23
rs 8.7972
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
use MediaWiki\MediaWikiServices;
23
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...
24
25
/**
26
 * Update object handling the cleanup of links tables after a page was deleted.
27
 **/
28
class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
29
	/** @var WikiPage */
30
	protected $page;
31
	/** @var integer */
32
	protected $pageId;
33
	/** @var string */
34
	protected $timestamp;
35
36
	/** @var IDatabase */
37
	private $db;
38
39
	/**
40
	 * @param WikiPage $page Page we are updating
41
	 * @param integer|null $pageId ID of the page we are updating [optional]
42
	 * @param string|null $timestamp TS_MW timestamp of deletion
43
	 * @throws MWException
44
	 */
45
	function __construct( WikiPage $page, $pageId = null, $timestamp = null ) {
46
		parent::__construct();
47
48
		$this->page = $page;
49
		if ( $pageId ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pageId of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
50
			$this->pageId = $pageId; // page ID at time of deletion
51
		} elseif ( $page->exists() ) {
52
			$this->pageId = $page->getId();
53
		} else {
54
			throw new InvalidArgumentException( "Page ID not known. Page doesn't exist?" );
55
		}
56
57
		$this->timestamp = $timestamp ?: wfTimestampNow();
0 ignored issues
show
Documentation Bug introduced by
It seems like $timestamp ?: wfTimestampNow() can also be of type false. However, the property $timestamp is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
58
	}
59
60
	public function doUpdate() {
61
		$services = MediaWikiServices::getInstance();
62
		$config = $services->getMainConfig();
63
		$lbFactory = $services->getDBLoadBalancerFactory();
64
		$batchSize = $config->get( 'UpdateRowsPerQuery' );
65
66
		// Page may already be deleted, so don't just getId()
67
		$id = $this->pageId;
68
69
		if ( $this->ticket ) {
70
			// Make sure all links update threads see the changes of each other.
71
			// This handles the case when updates have to batched into several COMMITs.
72
			$scopedLock = LinksUpdate::acquirePageLock( $this->getDB(), $id );
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...
73
		}
74
75
		$title = $this->page->getTitle();
76
		$dbw = $this->getDB(); // convenience
77
78
		// Delete restrictions for it
79
		$dbw->delete( 'page_restrictions', [ 'pr_page' => $id ], __METHOD__ );
80
81
		// Fix category table counts
82
		$cats = $dbw->selectFieldValues(
83
			'categorylinks',
84
			'cl_to',
85
			[ 'cl_from' => $id ],
86
			__METHOD__
87
		);
88
		$catBatches = array_chunk( $cats, $batchSize );
89
		foreach ( $catBatches as $catBatch ) {
90
			$this->page->updateCategoryCounts( [], $catBatch, $id );
91 View Code Duplication
			if ( count( $catBatches ) > 1 ) {
92
				$lbFactory->commitAndWaitForReplication(
93
					__METHOD__, $this->ticket, [ 'wiki' => $dbw->getWikiID() ]
94
				);
95
			}
96
		}
97
98
		// Refresh the category table entry if it seems to have no pages. Check
99
		// master for the most up-to-date cat_pages count.
100
		if ( $title->getNamespace() === NS_CATEGORY ) {
101
			$row = $dbw->selectRow(
102
				'category',
103
				[ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
104
				[ 'cat_title' => $title->getDBkey(), 'cat_pages <= 0' ],
105
				__METHOD__
106
			);
107
			if ( $row ) {
108
				Category::newFromRow( $row, $title )->refreshCounts();
109
			}
110
		}
111
112
		$this->batchDeleteByPK(
113
			'pagelinks',
114
			[ 'pl_from' => $id ],
115
			[ 'pl_from', 'pl_namespace', 'pl_title' ],
116
			$batchSize
117
		);
118
		$this->batchDeleteByPK(
119
			'imagelinks',
120
			[ 'il_from' => $id ],
121
			[ 'il_from', 'il_to' ],
122
			$batchSize
123
		);
124
		$this->batchDeleteByPK(
125
			'categorylinks',
126
			[ 'cl_from' => $id ],
127
			[ 'cl_from', 'cl_to' ],
128
			$batchSize
129
		);
130
		$this->batchDeleteByPK(
131
			'templatelinks',
132
			[ 'tl_from' => $id ],
133
			[ 'tl_from', 'tl_namespace', 'tl_title' ],
134
			$batchSize
135
		);
136
		$this->batchDeleteByPK(
137
			'externallinks',
138
			[ 'el_from' => $id ],
139
			[ 'el_id' ],
140
			$batchSize
141
		);
142
		$this->batchDeleteByPK(
143
			'langlinks',
144
			[ 'll_from' => $id ],
145
			[ 'll_from', 'll_lang' ],
146
			$batchSize
147
		);
148
		$this->batchDeleteByPK(
149
			'iwlinks',
150
			[ 'iwl_from' => $id ],
151
			[ 'iwl_from', 'iwl_prefix', 'iwl_title' ],
152
			$batchSize
153
		);
154
155
		// Delete any redirect entry or page props entries
156
		$dbw->delete( 'redirect', [ 'rd_from' => $id ], __METHOD__ );
157
		$dbw->delete( 'page_props', [ 'pp_page' => $id ], __METHOD__ );
158
159
		// Find recentchanges entries to clean up...
160
		$rcIdsForTitle = $dbw->selectFieldValues(
161
			'recentchanges',
162
			'rc_id',
163
			[
164
				'rc_type != ' . RC_LOG,
165
				'rc_namespace' => $title->getNamespace(),
166
				'rc_title' => $title->getDBkey(),
167
				'rc_timestamp < ' .
168
					$dbw->addQuotes( $dbw->timestamp( $this->timestamp ) )
169
			],
170
			__METHOD__
171
		);
172
		$rcIdsForPage = $dbw->selectFieldValues(
173
			'recentchanges',
174
			'rc_id',
175
			[ 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ],
176
			__METHOD__
177
		);
178
179
		// T98706: delete by PK to avoid lock contention with RC delete log insertions
180
		$rcIdBatches = array_chunk( array_merge( $rcIdsForTitle, $rcIdsForPage ), $batchSize );
181
		foreach ( $rcIdBatches as $rcIdBatch ) {
182
			$dbw->delete( 'recentchanges', [ 'rc_id' => $rcIdBatch ], __METHOD__ );
183 View Code Duplication
			if ( count( $rcIdBatches ) > 1 ) {
184
				$lbFactory->commitAndWaitForReplication(
185
					__METHOD__, $this->ticket, [ 'wiki' => $dbw->getWikiID() ]
186
				);
187
			}
188
		}
189
190
		// Commit and release the lock (if set)
191
		ScopedCallback::consume( $scopedLock );
192
	}
193
194
	private function batchDeleteByPK( $table, array $conds, array $pk, $bSize ) {
195
		$services = MediaWikiServices::getInstance();
196
		$lbFactory = $services->getDBLoadBalancerFactory();
197
		$dbw = $this->getDB(); // convenience
198
199
		$res = $dbw->select( $table, $pk, $conds, __METHOD__ );
200
201
		$pkDeleteConds = [];
202
		foreach ( $res as $row ) {
203
			$pkDeleteConds[] = $dbw->makeList( (array)$row, LIST_AND );
204
			if ( count( $pkDeleteConds ) >= $bSize ) {
205
				$dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ );
206
				$lbFactory->commitAndWaitForReplication(
207
					__METHOD__, $this->ticket, [ 'wiki' => $dbw->getWikiID() ]
208
				);
209
				$pkDeleteConds = [];
210
			}
211
		}
212
213
		if ( $pkDeleteConds ) {
214
			$dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ );
215
		}
216
	}
217
218
	protected function getDB() {
219
		if ( !$this->db ) {
220
			$this->db = wfGetDB( DB_MASTER );
221
		}
222
223
		return $this->db;
224
	}
225
226
	public function getAsJobSpecification() {
227
		return [
228
			'wiki' => $this->getDB()->getWikiID(),
229
			'job'  => new JobSpecification(
230
				'deleteLinks',
231
				[ 'pageId' => $this->pageId, 'timestamp' => $this->timestamp ],
232
				[ 'removeDuplicates' => true ],
233
				$this->page->getTitle()
234
			)
235
		];
236
	}
237
}
238