Completed
Branch master (90e9fc)
by
unknown
29:23
created

LinksDeletionUpdate::getDB()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 7
rs 9.4285
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
24
/**
25
 * Update object handling the cleanup of links tables after a page was deleted.
26
 **/
27
class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
28
	/** @var WikiPage */
29
	protected $page;
30
	/** @var integer */
31
	protected $pageId;
32
	/** @var string */
33
	protected $timestamp;
34
35
	/** @var IDatabase */
36
	private $db;
37
38
	/**
39
	 * @param WikiPage $page Page we are updating
40
	 * @param integer|null $pageId ID of the page we are updating [optional]
41
	 * @param string|null $timestamp TS_MW timestamp of deletion
42
	 * @throws MWException
43
	 */
44
	function __construct( WikiPage $page, $pageId = null, $timestamp = null ) {
45
		parent::__construct();
46
47
		$this->page = $page;
48
		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...
49
			$this->pageId = $pageId; // page ID at time of deletion
50
		} elseif ( $page->exists() ) {
51
			$this->pageId = $page->getId();
52
		} else {
53
			throw new InvalidArgumentException( "Page ID not known. Page doesn't exist?" );
54
		}
55
56
		$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...
57
	}
58
59
	public function doUpdate() {
60
		$services = MediaWikiServices::getInstance();
61
		$config = $services->getMainConfig();
62
		$lbFactory = $services->getDBLoadBalancerFactory();
63
		$batchSize = $config->get( 'UpdateRowsPerQuery' );
64
65
		// Page may already be deleted, so don't just getId()
66
		$id = $this->pageId;
67
68
		if ( $this->ticket ) {
69
			// Make sure all links update threads see the changes of each other.
70
			// This handles the case when updates have to batched into several COMMITs.
71
			$scopedLock = LinksUpdate::acquirePageLock( $this->getDB(), $id );
72
		}
73
74
		$title = $this->page->getTitle();
75
		$dbw = $this->getDB(); // convenience
76
77
		// Delete restrictions for it
78
		$dbw->delete( 'page_restrictions', [ 'pr_page' => $id ], __METHOD__ );
79
80
		// Fix category table counts
81
		$cats = $dbw->selectFieldValues(
82
			'categorylinks',
83
			'cl_to',
84
			[ 'cl_from' => $id ],
85
			__METHOD__
86
		);
87
		$catBatches = array_chunk( $cats, $batchSize );
88
		foreach ( $catBatches as $catBatch ) {
89
			$this->page->updateCategoryCounts( [], $catBatch, $id );
90 View Code Duplication
			if ( count( $catBatches ) > 1 ) {
91
				$lbFactory->commitAndWaitForReplication(
92
					__METHOD__, $this->ticket, [ 'wiki' => $dbw->getWikiID() ]
93
				);
94
			}
95
		}
96
97
		// Refresh the category table entry if it seems to have no pages. Check
98
		// master for the most up-to-date cat_pages count.
99
		if ( $title->getNamespace() === NS_CATEGORY ) {
100
			$row = $dbw->selectRow(
101
				'category',
102
				[ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
103
				[ 'cat_title' => $title->getDBkey(), 'cat_pages <= 0' ],
104
				__METHOD__
105
			);
106
			if ( $row ) {
107
				Category::newFromRow( $row, $title )->refreshCounts();
0 ignored issues
show
Bug introduced by
It seems like $row defined by $dbw->selectRow('categor...ges <= 0'), __METHOD__) on line 100 can also be of type boolean; however, Category::newFromRow() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
108
			}
109
		}
110
111
		// If using cascading deletes, we can skip some explicit deletes
112
		if ( !$dbw->cascadingDeletes() ) {
0 ignored issues
show
Bug introduced by
The method cascadingDeletes() does not exist on IDatabase. Did you maybe mean delete()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
113
			// Delete outgoing links
114
			$this->batchDeleteByPK(
115
				'pagelinks',
116
				[ 'pl_from' => $id ],
117
				[ 'pl_from', 'pl_namespace', 'pl_title' ],
118
				$batchSize
119
			);
120
			$this->batchDeleteByPK(
121
				'imagelinks',
122
				[ 'il_from' => $id ],
123
				[ 'il_from', 'il_to' ],
124
				$batchSize
125
			);
126
			$this->batchDeleteByPK(
127
				'categorylinks',
128
				[ 'cl_from' => $id ],
129
				[ 'cl_from', 'cl_to' ],
130
				$batchSize
131
			);
132
			$this->batchDeleteByPK(
133
				'templatelinks',
134
				[ 'tl_from' => $id ],
135
				[ 'tl_from', 'tl_namespace', 'tl_title' ],
136
				$batchSize
137
			);
138
			$this->batchDeleteByPK(
139
				'externallinks',
140
				[ 'el_from' => $id ],
141
				[ 'el_id' ],
142
				$batchSize
143
			);
144
			$this->batchDeleteByPK(
145
				'langlinks',
146
				[ 'll_from' => $id ],
147
				[ 'll_from', 'll_lang' ],
148
				$batchSize
149
			);
150
			$this->batchDeleteByPK(
151
				'iwlinks',
152
				[ 'iwl_from' => $id ],
153
				[ 'iwl_from', 'iwl_prefix', 'iwl_title' ],
154
				$batchSize
155
			);
156
			// Delete any redirect entry or page props entries
157
			$dbw->delete( 'redirect', [ 'rd_from' => $id ], __METHOD__ );
158
			$dbw->delete( 'page_props', [ 'pp_page' => $id ], __METHOD__ );
159
		}
160
161
		// If using cleanup triggers, we can skip some manual deletes
162
		if ( !$dbw->cleanupTriggers() ) {
163
			// Find recentchanges entries to clean up...
164
			$rcIdsForTitle = $dbw->selectFieldValues(
165
				'recentchanges',
166
				'rc_id',
167
				[
168
					'rc_type != ' . RC_LOG,
169
					'rc_namespace' => $title->getNamespace(),
170
					'rc_title' => $title->getDBkey(),
171
					'rc_timestamp < ' .
172
						$dbw->addQuotes( $dbw->timestamp( $this->timestamp ) )
173
				],
174
				__METHOD__
175
			);
176
			$rcIdsForPage = $dbw->selectFieldValues(
177
				'recentchanges',
178
				'rc_id',
179
				[ 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ],
180
				__METHOD__
181
			);
182
183
			// T98706: delete by PK to avoid lock contention with RC delete log insertions
184
			$rcIdBatches = array_chunk( array_merge( $rcIdsForTitle, $rcIdsForPage ), $batchSize );
185
			foreach ( $rcIdBatches as $rcIdBatch ) {
186
				$dbw->delete( 'recentchanges', [ 'rc_id' => $rcIdBatch ], __METHOD__ );
187 View Code Duplication
				if ( count( $rcIdBatches ) > 1 ) {
188
					$lbFactory->commitAndWaitForReplication(
189
						__METHOD__, $this->ticket, [ 'wiki' => $dbw->getWikiID() ]
190
					);
191
				}
192
			}
193
		}
194
195
		// Commit and release the lock (if set)
196
		ScopedCallback::consume( $scopedLock );
197
	}
198
199
	private function batchDeleteByPK( $table, array $conds, array $pk, $bSize ) {
200
		$services = MediaWikiServices::getInstance();
201
		$lbFactory = $services->getDBLoadBalancerFactory();
202
		$dbw = $this->getDB(); // convenience
203
204
		$res = $dbw->select( $table, $pk, $conds, __METHOD__ );
205
206
		$pkDeleteConds = [];
207
		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...
208
			$pkDeleteConds[] = $dbw->makeList( (array)$row, LIST_AND );
209
			if ( count( $pkDeleteConds ) >= $bSize ) {
210
				$dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ );
211
				$lbFactory->commitAndWaitForReplication(
212
					__METHOD__, $this->ticket, [ 'wiki' => $dbw->getWikiID() ]
213
				);
214
				$pkDeleteConds = [];
215
			}
216
		}
217
218
		if ( $pkDeleteConds ) {
219
			$dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ );
220
		}
221
	}
222
223
	protected function getDB() {
224
		if ( !$this->db ) {
225
			$this->db = wfGetDB( DB_MASTER );
226
		}
227
228
		return $this->db;
229
	}
230
231
	public function getAsJobSpecification() {
232
		return [
233
			'wiki' => $this->getDB()->getWikiID(),
234
			'job'  => new JobSpecification(
235
				'deleteLinks',
236
				[ 'pageId' => $this->pageId, 'timestamp' => $this->timestamp ],
237
				[ 'removeDuplicates' => true ],
238
				$this->page->getTitle()
239
			)
240
		];
241
	}
242
}
243