|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
use Wikimedia\Assert\Assert; |
|
4
|
|
|
|
|
5
|
|
|
/** |
|
6
|
|
|
* Class performing complex database queries related to WatchedItems. |
|
7
|
|
|
* |
|
8
|
|
|
* @since 1.28 |
|
9
|
|
|
* |
|
10
|
|
|
* @file |
|
11
|
|
|
* @ingroup Watchlist |
|
12
|
|
|
* |
|
13
|
|
|
* @license GNU GPL v2+ |
|
14
|
|
|
*/ |
|
15
|
|
|
class WatchedItemQueryService { |
|
16
|
|
|
|
|
17
|
|
|
const DIR_OLDER = 'older'; |
|
18
|
|
|
const DIR_NEWER = 'newer'; |
|
19
|
|
|
|
|
20
|
|
|
const INCLUDE_FLAGS = 'flags'; |
|
21
|
|
|
const INCLUDE_USER = 'user'; |
|
22
|
|
|
const INCLUDE_USER_ID = 'userid'; |
|
23
|
|
|
const INCLUDE_COMMENT = 'comment'; |
|
24
|
|
|
const INCLUDE_PATROL_INFO = 'patrol'; |
|
25
|
|
|
const INCLUDE_SIZES = 'sizes'; |
|
26
|
|
|
const INCLUDE_LOG_INFO = 'loginfo'; |
|
27
|
|
|
|
|
28
|
|
|
// FILTER_* constants are part of public API (are used |
|
29
|
|
|
// in ApiQueryWatchlist class) and should not be changed. |
|
30
|
|
|
// Changing values of those constants will result in a breaking change in the API |
|
31
|
|
|
const FILTER_MINOR = 'minor'; |
|
32
|
|
|
const FILTER_NOT_MINOR = '!minor'; |
|
33
|
|
|
const FILTER_BOT = 'bot'; |
|
34
|
|
|
const FILTER_NOT_BOT = '!bot'; |
|
35
|
|
|
const FILTER_ANON = 'anon'; |
|
36
|
|
|
const FILTER_NOT_ANON = '!anon'; |
|
37
|
|
|
const FILTER_PATROLLED = 'patrolled'; |
|
38
|
|
|
const FILTER_NOT_PATROLLED = '!patrolled'; |
|
39
|
|
|
const FILTER_UNREAD = 'unread'; |
|
40
|
|
|
const FILTER_NOT_UNREAD = '!unread'; |
|
41
|
|
|
|
|
42
|
|
|
/** |
|
43
|
|
|
* @var LoadBalancer |
|
44
|
|
|
*/ |
|
45
|
|
|
private $loadBalancer; |
|
46
|
|
|
|
|
47
|
|
|
public function __construct( LoadBalancer $loadBalancer ) { |
|
48
|
|
|
$this->loadBalancer = $loadBalancer; |
|
49
|
|
|
} |
|
50
|
|
|
|
|
51
|
|
|
/** |
|
52
|
|
|
* @return DatabaseBase |
|
53
|
|
|
* @throws MWException |
|
54
|
|
|
*/ |
|
55
|
|
|
private function getConnection() { |
|
56
|
|
|
return $this->loadBalancer->getConnection( DB_SLAVE, [ 'watchlist' ] ); |
|
57
|
|
|
} |
|
58
|
|
|
|
|
59
|
|
|
/** |
|
60
|
|
|
* @param DatabaseBase $connection |
|
61
|
|
|
* @throws MWException |
|
62
|
|
|
*/ |
|
63
|
|
|
private function reuseConnection( DatabaseBase $connection ) { |
|
64
|
|
|
$this->loadBalancer->reuseConnection( $connection ); |
|
65
|
|
|
} |
|
66
|
|
|
|
|
67
|
|
|
/** |
|
68
|
|
|
* @param User $user |
|
69
|
|
|
* @param array $options Allowed keys: |
|
70
|
|
|
* 'includeFields' => string[] RecentChange fields to be included in the result, |
|
71
|
|
|
* self::INCLUDE_* constants should be used |
|
72
|
|
|
* 'filters' => string[] optional filters to narrow down resulted items |
|
73
|
|
|
* 'namespaceIds' => int[] optional namespace IDs to filter by |
|
74
|
|
|
* (defaults to all namespaces) |
|
75
|
|
|
* 'allRevisions' => bool return multiple revisions of the same page if true, |
|
76
|
|
|
* only the most recent if false (default) |
|
77
|
|
|
* 'rcTypes' => int[] which types of RecentChanges to include |
|
78
|
|
|
* (defaults to all types), allowed values: RC_EDIT, RC_NEW, |
|
79
|
|
|
* RC_LOG, RC_EXTERNAL, RC_CATEGORIZE |
|
80
|
|
|
* 'onlyByUser' => string only list changes by a specified user |
|
81
|
|
|
* 'notByUser' => string do not incluide changes by a specified user |
|
82
|
|
|
* 'dir' => string in which direction to enumerate, accepted values: |
|
83
|
|
|
* - DIR_OLDER list newest first |
|
84
|
|
|
* - DIR_NEWER list oldest first |
|
85
|
|
|
* 'start' => string (format accepted by wfTimestamp) requires 'dir' option, |
|
86
|
|
|
* timestamp to start enumerating from |
|
87
|
|
|
* 'end' => string (format accepted by wfTimestamp) requires 'dir' option, |
|
88
|
|
|
* timestamp to end enumerating |
|
89
|
|
|
* 'startFrom' => [ string $rcTimestamp, int $rcId ] requires 'dir' option, |
|
90
|
|
|
* return items starting from the RecentChange specified by this, |
|
91
|
|
|
* $rcTimestamp should be in the format accepted by wfTimestamp |
|
92
|
|
|
* 'watchlistOwner' => User user whose watchlist items should be listed if different |
|
93
|
|
|
* than the one specified with $user param, |
|
94
|
|
|
* requires 'watchlistOwnerToken' option |
|
95
|
|
|
* 'watchlistOwnerToken' => string a watchlist token used to access another user's |
|
96
|
|
|
* watchlist, used with 'watchlistOwnerToken' option |
|
97
|
|
|
* 'limit' => int maximum numbers of items to return |
|
98
|
|
|
* 'usedInGenerator' => bool include only RecentChange id field required by the |
|
99
|
|
|
* generator ('rc_cur_id' or 'rc_this_oldid') if true, or all |
|
100
|
|
|
* id fields ('rc_cur_id', 'rc_this_oldid', 'rc_last_oldid') |
|
101
|
|
|
* if false (default) |
|
102
|
|
|
* @return array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ), |
|
103
|
|
|
* where $recentChangeInfo contains the following keys: |
|
104
|
|
|
* - 'rc_id', |
|
105
|
|
|
* - 'rc_namespace', |
|
106
|
|
|
* - 'rc_title', |
|
107
|
|
|
* - 'rc_timestamp', |
|
108
|
|
|
* - 'rc_type', |
|
109
|
|
|
* - 'rc_deleted', |
|
110
|
|
|
* Additional keys could be added by specifying the 'includeFields' option |
|
111
|
|
|
*/ |
|
112
|
|
|
public function getWatchedItemsWithRecentChangeInfo( User $user, array $options = [] ) { |
|
113
|
|
|
$options += [ |
|
114
|
|
|
'includeFields' => [], |
|
115
|
|
|
'namespaceIds' => [], |
|
116
|
|
|
'filters' => [], |
|
117
|
|
|
'allRevisions' => false, |
|
118
|
|
|
'usedInGenerator' => false |
|
119
|
|
|
]; |
|
120
|
|
|
|
|
121
|
|
|
Assert::parameter( |
|
122
|
|
|
!isset( $options['rcTypes'] ) |
|
123
|
|
|
|| !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ), |
|
124
|
|
|
'$options[\'rcTypes\']', |
|
125
|
|
|
'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE' |
|
126
|
|
|
); |
|
127
|
|
|
Assert::parameter( |
|
128
|
|
|
!isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ), |
|
129
|
|
|
'$options[\'dir\']', |
|
130
|
|
|
'must be DIR_OLDER or DIR_NEWER' |
|
131
|
|
|
); |
|
132
|
|
|
Assert::parameter( |
|
133
|
|
|
!isset( $options['start'] ) && !isset( $options['end'] ) && !isset( $options['startFrom'] ) |
|
134
|
|
|
|| isset( $options['dir'] ), |
|
135
|
|
|
'$options[\'dir\']', |
|
136
|
|
|
'must be provided when providing any of options: start, end, startFrom' |
|
137
|
|
|
); |
|
138
|
|
|
Assert::parameter( |
|
139
|
|
|
!isset( $options['startFrom'] ) |
|
140
|
|
|
|| ( is_array( $options['startFrom'] ) && count( $options['startFrom'] ) === 2 ), |
|
141
|
|
|
'$options[\'startFrom\']', |
|
142
|
|
|
'must be a two-element array' |
|
143
|
|
|
); |
|
144
|
|
|
if ( array_key_exists( 'watchlistOwner', $options ) ) { |
|
145
|
|
|
Assert::parameterType( |
|
146
|
|
|
User::class, |
|
147
|
|
|
$options['watchlistOwner'], |
|
148
|
|
|
'$options[\'watchlistOwner\']' |
|
149
|
|
|
); |
|
150
|
|
|
Assert::parameter( |
|
151
|
|
|
isset( $options['watchlistOwnerToken'] ), |
|
152
|
|
|
'$options[\'watchlistOwnerToken\']', |
|
153
|
|
|
'must be provided when providing watchlistOwner option' |
|
154
|
|
|
); |
|
155
|
|
|
} |
|
156
|
|
|
|
|
157
|
|
|
$tables = [ 'recentchanges', 'watchlist' ]; |
|
158
|
|
|
if ( !$options['allRevisions'] ) { |
|
159
|
|
|
$tables[] = 'page'; |
|
160
|
|
|
} |
|
161
|
|
|
|
|
162
|
|
|
$db = $this->getConnection(); |
|
163
|
|
|
|
|
164
|
|
|
$fields = $this->getFields( $options ); |
|
165
|
|
|
$conds = $this->getConds( $db, $user, $options ); |
|
166
|
|
|
$dbOptions = $this->getDbOptions( $options ); |
|
167
|
|
|
$joinConds = $this->getJoinConds( $options ); |
|
168
|
|
|
|
|
169
|
|
|
$res = $db->select( |
|
170
|
|
|
$tables, |
|
171
|
|
|
$fields, |
|
172
|
|
|
$conds, |
|
173
|
|
|
__METHOD__, |
|
174
|
|
|
$dbOptions, |
|
175
|
|
|
$joinConds |
|
176
|
|
|
); |
|
177
|
|
|
|
|
178
|
|
|
$this->reuseConnection( $db ); |
|
179
|
|
|
|
|
180
|
|
|
$items = []; |
|
181
|
|
|
foreach ( $res as $row ) { |
|
|
|
|
|
|
182
|
|
|
$items[] = [ |
|
183
|
|
|
new WatchedItem( |
|
184
|
|
|
$user, |
|
185
|
|
|
new TitleValue( (int)$row->rc_namespace, $row->rc_title ), |
|
186
|
|
|
$row->wl_notificationtimestamp |
|
187
|
|
|
), |
|
188
|
|
|
$this->getRecentChangeFieldsFromRow( $row ) |
|
189
|
|
|
]; |
|
190
|
|
|
} |
|
191
|
|
|
|
|
192
|
|
|
return $items; |
|
193
|
|
|
} |
|
194
|
|
|
|
|
195
|
|
|
private function getRecentChangeFieldsFromRow( stdClass $row ) { |
|
196
|
|
|
// This can be simplified to single array_filter call filtering by key value, |
|
197
|
|
|
// once we stop supporting PHP 5.5 |
|
198
|
|
|
$allFields = get_object_vars( $row ); |
|
199
|
|
|
$rcKeys = array_filter( |
|
200
|
|
|
array_keys( $allFields ), |
|
201
|
|
|
function( $key ) { |
|
202
|
|
|
return substr( $key, 0, 3 ) === 'rc_'; |
|
203
|
|
|
} |
|
204
|
|
|
); |
|
205
|
|
|
return array_intersect_key( $allFields, array_flip( $rcKeys ) ); |
|
206
|
|
|
} |
|
207
|
|
|
|
|
208
|
|
|
private function getFields( array $options ) { |
|
209
|
|
|
$fields = [ |
|
210
|
|
|
'rc_id', |
|
211
|
|
|
'rc_namespace', |
|
212
|
|
|
'rc_title', |
|
213
|
|
|
'rc_timestamp', |
|
214
|
|
|
'rc_type', |
|
215
|
|
|
'rc_deleted', |
|
216
|
|
|
'wl_notificationtimestamp' |
|
217
|
|
|
]; |
|
218
|
|
|
|
|
219
|
|
|
$rcIdFields = [ |
|
220
|
|
|
'rc_cur_id', |
|
221
|
|
|
'rc_this_oldid', |
|
222
|
|
|
'rc_last_oldid', |
|
223
|
|
|
]; |
|
224
|
|
|
if ( $options['usedInGenerator'] ) { |
|
225
|
|
|
if ( $options['allRevisions'] ) { |
|
226
|
|
|
$rcIdFields = [ 'rc_this_oldid' ]; |
|
227
|
|
|
} else { |
|
228
|
|
|
$rcIdFields = [ 'rc_cur_id' ]; |
|
229
|
|
|
} |
|
230
|
|
|
} |
|
231
|
|
|
$fields = array_merge( $fields, $rcIdFields ); |
|
232
|
|
|
|
|
233
|
|
|
if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) { |
|
234
|
|
|
$fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] ); |
|
235
|
|
|
} |
|
236
|
|
|
if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) { |
|
237
|
|
|
$fields[] = 'rc_user_text'; |
|
238
|
|
|
} |
|
239
|
|
|
if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) { |
|
240
|
|
|
$fields[] = 'rc_user'; |
|
241
|
|
|
} |
|
242
|
|
|
if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) { |
|
243
|
|
|
$fields[] = 'rc_comment'; |
|
244
|
|
|
} |
|
245
|
|
|
if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) { |
|
246
|
|
|
$fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] ); |
|
247
|
|
|
} |
|
248
|
|
|
if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) { |
|
249
|
|
|
$fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] ); |
|
250
|
|
|
} |
|
251
|
|
|
if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) { |
|
252
|
|
|
$fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] ); |
|
253
|
|
|
} |
|
254
|
|
|
|
|
255
|
|
|
return $fields; |
|
256
|
|
|
} |
|
257
|
|
|
|
|
258
|
|
|
private function getConds( DatabaseBase $db, User $user, array $options ) { |
|
259
|
|
|
$watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options ); |
|
260
|
|
|
$conds = [ 'wl_user' => $watchlistOwnerId ]; |
|
261
|
|
|
|
|
262
|
|
|
if ( !$options['allRevisions'] ) { |
|
263
|
|
|
$conds[] = $db->makeList( |
|
264
|
|
|
[ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ], |
|
265
|
|
|
LIST_OR |
|
266
|
|
|
); |
|
267
|
|
|
} |
|
268
|
|
|
|
|
269
|
|
|
if ( $options['namespaceIds'] ) { |
|
270
|
|
|
$conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] ); |
|
271
|
|
|
} |
|
272
|
|
|
|
|
273
|
|
|
if ( array_key_exists( 'rcTypes', $options ) ) { |
|
274
|
|
|
$conds['rc_type'] = array_map( 'intval', $options['rcTypes'] ); |
|
275
|
|
|
} |
|
276
|
|
|
|
|
277
|
|
|
$conds = array_merge( $conds, $this->getFilterConds( $user, $options ) ); |
|
278
|
|
|
|
|
279
|
|
|
$conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) ); |
|
280
|
|
|
|
|
281
|
|
|
if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) { |
|
282
|
|
|
if ( $db->getType() === 'mysql' ) { |
|
283
|
|
|
// This is an index optimization for mysql |
|
284
|
|
|
$conds[] = "rc_timestamp > ''"; |
|
285
|
|
|
} |
|
286
|
|
|
} |
|
287
|
|
|
|
|
288
|
|
|
$conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) ); |
|
289
|
|
|
|
|
290
|
|
|
$deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user ); |
|
291
|
|
|
if ( $deletedPageLogCond ) { |
|
292
|
|
|
$conds[] = $deletedPageLogCond; |
|
293
|
|
|
} |
|
294
|
|
|
|
|
295
|
|
|
if ( array_key_exists( 'startFrom', $options ) ) { |
|
296
|
|
|
$conds[] = $this->getStartFromConds( $db, $options ); |
|
297
|
|
|
} |
|
298
|
|
|
|
|
299
|
|
|
return $conds; |
|
300
|
|
|
} |
|
301
|
|
|
|
|
302
|
|
|
private function getWatchlistOwnerId( User $user, array $options ) { |
|
303
|
|
|
if ( array_key_exists( 'watchlistOwner', $options ) ) { |
|
304
|
|
|
/** @var User $watchlistOwner */ |
|
305
|
|
|
$watchlistOwner = $options['watchlistOwner']; |
|
306
|
|
|
$ownersToken = $watchlistOwner->getOption( 'watchlisttoken' ); |
|
307
|
|
|
$token = $options['watchlistOwnerToken']; |
|
308
|
|
|
if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) { |
|
309
|
|
|
throw new UsageException( |
|
310
|
|
|
'Incorrect watchlist token provided -- please set a correct token in Special:Preferences', |
|
311
|
|
|
'bad_wltoken' |
|
312
|
|
|
); |
|
313
|
|
|
} |
|
314
|
|
|
return $watchlistOwner->getId(); |
|
315
|
|
|
} |
|
316
|
|
|
return $user->getId(); |
|
317
|
|
|
} |
|
318
|
|
|
|
|
319
|
|
|
private function getFilterConds( User $user, array $options ) { |
|
320
|
|
|
$conds = []; |
|
321
|
|
|
|
|
322
|
|
View Code Duplication |
if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) { |
|
323
|
|
|
$conds[] = 'rc_minor != 0'; |
|
324
|
|
|
} elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) { |
|
325
|
|
|
$conds[] = 'rc_minor = 0'; |
|
326
|
|
|
} |
|
327
|
|
|
|
|
328
|
|
View Code Duplication |
if ( in_array( self::FILTER_BOT, $options['filters'] ) ) { |
|
329
|
|
|
$conds[] = 'rc_bot != 0'; |
|
330
|
|
|
} elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) { |
|
331
|
|
|
$conds[] = 'rc_bot = 0'; |
|
332
|
|
|
} |
|
333
|
|
|
|
|
334
|
|
View Code Duplication |
if ( in_array( self::FILTER_ANON, $options['filters'] ) ) { |
|
335
|
|
|
$conds[] = 'rc_user = 0'; |
|
336
|
|
|
} elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) { |
|
337
|
|
|
$conds[] = 'rc_user != 0'; |
|
338
|
|
|
} |
|
339
|
|
|
|
|
340
|
|
|
if ( $user->useRCPatrol() || $user->useNPPatrol() ) { |
|
341
|
|
|
// TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol |
|
342
|
|
|
// right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does? |
|
343
|
|
View Code Duplication |
if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) { |
|
344
|
|
|
$conds[] = 'rc_patrolled != 0'; |
|
345
|
|
|
} elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) { |
|
346
|
|
|
$conds[] = 'rc_patrolled = 0'; |
|
347
|
|
|
} |
|
348
|
|
|
} |
|
349
|
|
|
|
|
350
|
|
View Code Duplication |
if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) { |
|
351
|
|
|
$conds[] = 'rc_timestamp >= wl_notificationtimestamp'; |
|
352
|
|
|
} elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) { |
|
353
|
|
|
// TODO: should this be changed to use Database::makeList? |
|
354
|
|
|
$conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp'; |
|
355
|
|
|
} |
|
356
|
|
|
|
|
357
|
|
|
return $conds; |
|
358
|
|
|
} |
|
359
|
|
|
|
|
360
|
|
|
private function getStartEndConds( DatabaseBase $db, array $options ) { |
|
361
|
|
|
if ( !isset( $options['start'] ) && ! isset( $options['end'] ) ) { |
|
362
|
|
|
return []; |
|
363
|
|
|
} |
|
364
|
|
|
|
|
365
|
|
|
$conds = []; |
|
366
|
|
|
|
|
367
|
|
View Code Duplication |
if ( isset( $options['start'] ) ) { |
|
368
|
|
|
$after = $options['dir'] === self::DIR_OLDER ? '<=' : '>='; |
|
369
|
|
|
$conds[] = 'rc_timestamp ' . $after . ' ' . $db->addQuotes( $options['start'] ); |
|
370
|
|
|
} |
|
371
|
|
View Code Duplication |
if ( isset( $options['end'] ) ) { |
|
372
|
|
|
$before = $options['dir'] === self::DIR_OLDER ? '>=' : '<='; |
|
373
|
|
|
$conds[] = 'rc_timestamp ' . $before . ' ' . $db->addQuotes( $options['end'] ); |
|
374
|
|
|
} |
|
375
|
|
|
|
|
376
|
|
|
return $conds; |
|
377
|
|
|
} |
|
378
|
|
|
|
|
379
|
|
|
private function getUserRelatedConds( DatabaseBase $db, User $user, array $options ) { |
|
380
|
|
|
if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) { |
|
381
|
|
|
return []; |
|
382
|
|
|
} |
|
383
|
|
|
|
|
384
|
|
|
$conds = []; |
|
385
|
|
|
|
|
386
|
|
|
if ( array_key_exists( 'onlyByUser', $options ) ) { |
|
387
|
|
|
$conds['rc_user_text'] = $options['onlyByUser']; |
|
388
|
|
|
} elseif ( array_key_exists( 'notByUser', $options ) ) { |
|
389
|
|
|
$conds[] = 'rc_user_text != ' . $db->addQuotes( $options['notByUser'] ); |
|
390
|
|
|
} |
|
391
|
|
|
|
|
392
|
|
|
// Avoid brute force searches (bug 17342) |
|
393
|
|
|
$bitmask = 0; |
|
394
|
|
|
if ( !$user->isAllowed( 'deletedhistory' ) ) { |
|
395
|
|
|
$bitmask = Revision::DELETED_USER; |
|
396
|
|
|
} elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { |
|
397
|
|
|
$bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; |
|
398
|
|
|
} |
|
399
|
|
|
if ( $bitmask ) { |
|
400
|
|
|
$conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask"; |
|
401
|
|
|
} |
|
402
|
|
|
|
|
403
|
|
|
return $conds; |
|
404
|
|
|
} |
|
405
|
|
|
|
|
406
|
|
|
private function getExtraDeletedPageLogEntryRelatedCond( DatabaseBase $db, User $user ) { |
|
407
|
|
|
// LogPage::DELETED_ACTION hides the affected page, too. So hide those |
|
408
|
|
|
// entirely from the watchlist, or someone could guess the title. |
|
409
|
|
|
$bitmask = 0; |
|
410
|
|
|
if ( !$user->isAllowed( 'deletedhistory' ) ) { |
|
411
|
|
|
$bitmask = LogPage::DELETED_ACTION; |
|
412
|
|
|
} elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { |
|
413
|
|
|
$bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED; |
|
414
|
|
|
} |
|
415
|
|
View Code Duplication |
if ( $bitmask ) { |
|
416
|
|
|
return $db->makeList( [ |
|
417
|
|
|
'rc_type != ' . RC_LOG, |
|
418
|
|
|
$db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask", |
|
419
|
|
|
], LIST_OR ); |
|
420
|
|
|
} |
|
421
|
|
|
return ''; |
|
422
|
|
|
} |
|
423
|
|
|
|
|
424
|
|
|
private function getStartFromConds( DatabaseBase $db, array $options ) { |
|
425
|
|
|
$op = $options['dir'] === self::DIR_OLDER ? '<' : '>'; |
|
426
|
|
|
list( $rcTimestamp, $rcId ) = $options['startFrom']; |
|
427
|
|
|
$rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) ); |
|
|
|
|
|
|
428
|
|
|
$rcId = (int)$rcId; |
|
429
|
|
|
return $db->makeList( |
|
430
|
|
|
[ |
|
431
|
|
|
"rc_timestamp $op $rcTimestamp", |
|
432
|
|
|
$db->makeList( |
|
433
|
|
|
[ |
|
434
|
|
|
"rc_timestamp = $rcTimestamp", |
|
435
|
|
|
"rc_id $op= $rcId" |
|
436
|
|
|
], |
|
437
|
|
|
LIST_AND |
|
438
|
|
|
) |
|
439
|
|
|
], |
|
440
|
|
|
LIST_OR |
|
441
|
|
|
); |
|
442
|
|
|
} |
|
443
|
|
|
|
|
444
|
|
|
private function getDbOptions( array $options ) { |
|
445
|
|
|
$dbOptions = []; |
|
446
|
|
|
|
|
447
|
|
|
if ( array_key_exists( 'dir', $options ) ) { |
|
448
|
|
|
$sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : ''; |
|
449
|
|
|
$dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ]; |
|
450
|
|
|
} |
|
451
|
|
|
|
|
452
|
|
|
if ( array_key_exists( 'limit', $options ) ) { |
|
453
|
|
|
$dbOptions['LIMIT'] = (int)$options['limit']; |
|
454
|
|
|
} |
|
455
|
|
|
|
|
456
|
|
|
return $dbOptions; |
|
457
|
|
|
} |
|
458
|
|
|
|
|
459
|
|
|
private function getJoinConds( array $options ) { |
|
460
|
|
|
$joinConds = [ |
|
461
|
|
|
'watchlist' => [ 'INNER JOIN', |
|
462
|
|
|
[ |
|
463
|
|
|
'wl_namespace=rc_namespace', |
|
464
|
|
|
'wl_title=rc_title' |
|
465
|
|
|
] |
|
466
|
|
|
] |
|
467
|
|
|
]; |
|
468
|
|
|
if ( !$options['allRevisions'] ) { |
|
469
|
|
|
$joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ]; |
|
470
|
|
|
} |
|
471
|
|
|
return $joinConds; |
|
472
|
|
|
} |
|
473
|
|
|
|
|
474
|
|
|
} |
|
475
|
|
|
|
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
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:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.