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

WatchedItemQueryService::getExtensions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
use MediaWiki\Linker\LinkTarget;
4
use Wikimedia\Assert\Assert;
5
6
/**
7
 * Class performing complex database queries related to WatchedItems.
8
 *
9
 * @since 1.28
10
 *
11
 * @file
12
 * @ingroup Watchlist
13
 *
14
 * @license GNU GPL v2+
15
 */
16
class WatchedItemQueryService {
17
18
	const DIR_OLDER = 'older';
19
	const DIR_NEWER = 'newer';
20
21
	const INCLUDE_FLAGS = 'flags';
22
	const INCLUDE_USER = 'user';
23
	const INCLUDE_USER_ID = 'userid';
24
	const INCLUDE_COMMENT = 'comment';
25
	const INCLUDE_PATROL_INFO = 'patrol';
26
	const INCLUDE_SIZES = 'sizes';
27
	const INCLUDE_LOG_INFO = 'loginfo';
28
29
	// FILTER_* constants are part of public API (are used in ApiQueryWatchlist and
30
	// ApiQueryWatchlistRaw classes) and should not be changed.
31
	// Changing values of those constants will result in a breaking change in the API
32
	const FILTER_MINOR = 'minor';
33
	const FILTER_NOT_MINOR = '!minor';
34
	const FILTER_BOT = 'bot';
35
	const FILTER_NOT_BOT = '!bot';
36
	const FILTER_ANON = 'anon';
37
	const FILTER_NOT_ANON = '!anon';
38
	const FILTER_PATROLLED = 'patrolled';
39
	const FILTER_NOT_PATROLLED = '!patrolled';
40
	const FILTER_UNREAD = 'unread';
41
	const FILTER_NOT_UNREAD = '!unread';
42
	const FILTER_CHANGED = 'changed';
43
	const FILTER_NOT_CHANGED = '!changed';
44
45
	const SORT_ASC = 'ASC';
46
	const SORT_DESC = 'DESC';
47
48
	/**
49
	 * @var LoadBalancer
50
	 */
51
	private $loadBalancer;
52
53
	/** @var WatchedItemQueryServiceExtension[]|null */
54
	private $extensions = null;
55
56
	public function __construct( LoadBalancer $loadBalancer ) {
57
		$this->loadBalancer = $loadBalancer;
58
	}
59
60
	/**
61
	 * @return WatchedItemQueryServiceExtension[]
62
	 */
63
	private function getExtensions() {
64
		if ( $this->extensions === null ) {
65
			$this->extensions = [];
66
			Hooks::run( 'WatchedItemQueryServiceExtensions', [ &$this->extensions, $this ] );
67
		}
68
		return $this->extensions;
69
	}
70
71
	/**
72
	 * @return IDatabase
73
	 * @throws MWException
74
	 */
75
	private function getConnection() {
76
		return $this->loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
77
	}
78
79
	/**
80
	 * @param User $user
81
	 * @param array $options Allowed keys:
82
	 *        'includeFields'       => string[] RecentChange fields to be included in the result,
83
	 *                                 self::INCLUDE_* constants should be used
84
	 *        'filters'             => string[] optional filters to narrow down resulted items
85
	 *        'namespaceIds'        => int[] optional namespace IDs to filter by
86
	 *                                 (defaults to all namespaces)
87
	 *        'allRevisions'        => bool return multiple revisions of the same page if true,
88
	 *                                 only the most recent if false (default)
89
	 *        'rcTypes'             => int[] which types of RecentChanges to include
90
	 *                                 (defaults to all types), allowed values: RC_EDIT, RC_NEW,
91
	 *                                 RC_LOG, RC_EXTERNAL, RC_CATEGORIZE
92
	 *        'onlyByUser'          => string only list changes by a specified user
93
	 *        'notByUser'           => string do not incluide changes by a specified user
94
	 *        'dir'                 => string in which direction to enumerate, accepted values:
95
	 *                                 - DIR_OLDER list newest first
96
	 *                                 - DIR_NEWER list oldest first
97
	 *        'start'               => string (format accepted by wfTimestamp) requires 'dir' option,
98
	 *                                 timestamp to start enumerating from
99
	 *        'end'                 => string (format accepted by wfTimestamp) requires 'dir' option,
100
	 *                                 timestamp to end enumerating
101
	 *        'watchlistOwner'      => User user whose watchlist items should be listed if different
102
	 *                                 than the one specified with $user param,
103
	 *                                 requires 'watchlistOwnerToken' option
104
	 *        'watchlistOwnerToken' => string a watchlist token used to access another user's
105
	 *                                 watchlist, used with 'watchlistOwnerToken' option
106
	 *        'limit'               => int maximum numbers of items to return
107
	 *        'usedInGenerator'     => bool include only RecentChange id field required by the
108
	 *                                 generator ('rc_cur_id' or 'rc_this_oldid') if true, or all
109
	 *                                 id fields ('rc_cur_id', 'rc_this_oldid', 'rc_last_oldid')
110
	 *                                 if false (default)
111
	 * @param array|null &$startFrom Continuation value: [ string $rcTimestamp, int $rcId ]
112
	 * @return array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ),
113
	 *         where $recentChangeInfo contains the following keys:
114
	 *         - 'rc_id',
115
	 *         - 'rc_namespace',
116
	 *         - 'rc_title',
117
	 *         - 'rc_timestamp',
118
	 *         - 'rc_type',
119
	 *         - 'rc_deleted',
120
	 *         Additional keys could be added by specifying the 'includeFields' option
121
	 */
122
	public function getWatchedItemsWithRecentChangeInfo(
123
		User $user, array $options = [], &$startFrom = null
124
	) {
125
		$options += [
126
			'includeFields' => [],
127
			'namespaceIds' => [],
128
			'filters' => [],
129
			'allRevisions' => false,
130
			'usedInGenerator' => false
131
		];
132
133
		Assert::parameter(
134
			!isset( $options['rcTypes'] )
135
				|| !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
136
			'$options[\'rcTypes\']',
137
			'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
138
		);
139
		Assert::parameter(
140
			!isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
141
			'$options[\'dir\']',
142
			'must be DIR_OLDER or DIR_NEWER'
143
		);
144
		Assert::parameter(
145
			!isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null
146
				|| isset( $options['dir'] ),
147
			'$options[\'dir\']',
148
			'must be provided when providing the "start" or "end" options or the $startFrom parameter'
149
		);
150
		Assert::parameter(
151
			!isset( $options['startFrom'] ),
152
			'$options[\'startFrom\']',
153
			'must not be provided, use $startFrom instead'
154
		);
155
		Assert::parameter(
156
			!isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
157
			'$startFrom',
158
			'must be a two-element array'
159
		);
160
		if ( array_key_exists( 'watchlistOwner', $options ) ) {
161
			Assert::parameterType(
162
				User::class,
163
				$options['watchlistOwner'],
164
				'$options[\'watchlistOwner\']'
165
			);
166
			Assert::parameter(
167
				isset( $options['watchlistOwnerToken'] ),
168
				'$options[\'watchlistOwnerToken\']',
169
				'must be provided when providing watchlistOwner option'
170
			);
171
		}
172
173
		$tables = [ 'recentchanges', 'watchlist' ];
174
		if ( !$options['allRevisions'] ) {
175
			$tables[] = 'page';
176
		}
177
178
		$db = $this->getConnection();
179
180
		$fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
181
		$conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
182
		$dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
183
		$joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options );
184
185
		if ( $startFrom !== null ) {
186
			$conds[] = $this->getStartFromConds( $db, $options, $startFrom );
187
		}
188
189
		foreach ( $this->getExtensions() as $extension ) {
190
			$extension->modifyWatchedItemsWithRCInfoQuery(
191
				$user, $options, $db,
192
				$tables,
193
				$fields,
194
				$conds,
195
				$dbOptions,
196
				$joinConds
197
			);
198
		}
199
200
		$res = $db->select(
201
			$tables,
202
			$fields,
203
			$conds,
204
			__METHOD__,
205
			$dbOptions,
206
			$joinConds
207
		);
208
209
		$limit = isset( $dbOptions['LIMIT'] ) ? $dbOptions['LIMIT'] : INF;
210
		$items = [];
211
		$startFrom = null;
212
		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...
213
			if ( --$limit <= 0 ) {
214
				$startFrom = [ $row->rc_timestamp, $row->rc_id ];
215
				break;
216
			}
217
218
			$items[] = [
219
				new WatchedItem(
220
					$user,
221
					new TitleValue( (int)$row->rc_namespace, $row->rc_title ),
222
					$row->wl_notificationtimestamp
223
				),
224
				$this->getRecentChangeFieldsFromRow( $row )
225
			];
226
		}
227
228
		foreach ( $this->getExtensions() as $extension ) {
229
			$extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom );
230
		}
231
232
		return $items;
233
	}
234
235
	/**
236
	 * For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser
237
	 *
238
	 * @param User $user
239
	 * @param array $options Allowed keys:
240
	 *        'sort'         => string optional sorting by namespace ID and title
241
	 *                          one of the self::SORT_* constants
242
	 *        'namespaceIds' => int[] optional namespace IDs to filter by (defaults to all namespaces)
243
	 *        'limit'        => int maximum number of items to return
244
	 *        'filter'       => string optional filter, one of the self::FILTER_* contants
245
	 *        'from'         => LinkTarget requires 'sort' key, only return items starting from
246
	 *                          those related to the link target
247
	 *        'until'        => LinkTarget requires 'sort' key, only return items until
248
	 *                          those related to the link target
249
	 *        'startFrom'    => LinkTarget requires 'sort' key, only return items starting from
250
	 *                          those related to the link target, allows to skip some link targets
251
	 *                          specified using the form option
252
	 * @return WatchedItem[]
253
	 */
254
	public function getWatchedItemsForUser( User $user, array $options = [] ) {
255
		if ( $user->isAnon() ) {
256
			// TODO: should this just return an empty array or rather complain loud at this point
257
			// as e.g. ApiBase::getWatchlistUser does?
258
			return [];
259
		}
260
261
		$options += [ 'namespaceIds' => [] ];
262
263
		Assert::parameter(
264
			!isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
265
			'$options[\'sort\']',
266
			'must be SORT_ASC or SORT_DESC'
267
		);
268
		Assert::parameter(
269
			!isset( $options['filter'] ) || in_array(
270
				$options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
271
			),
272
			'$options[\'filter\']',
273
			'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
274
		);
275
		Assert::parameter(
276
			!isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] )
277
			|| isset( $options['sort'] ),
278
			'$options[\'sort\']',
279
			'must be provided if any of "from", "until", "startFrom" options is provided'
280
		);
281
282
		$db = $this->getConnection();
283
284
		$conds = $this->getWatchedItemsForUserQueryConds( $db, $user, $options );
285
		$dbOptions = $this->getWatchedItemsForUserQueryDbOptions( $options );
286
287
		$res = $db->select(
288
			'watchlist',
289
			[ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
290
			$conds,
291
			__METHOD__,
292
			$dbOptions
293
		);
294
295
		$watchedItems = [];
296 View Code Duplication
		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...
297
			// todo these could all be cached at some point?
298
			$watchedItems[] = new WatchedItem(
299
				$user,
300
				new TitleValue( (int)$row->wl_namespace, $row->wl_title ),
301
				$row->wl_notificationtimestamp
302
			);
303
		}
304
305
		return $watchedItems;
306
	}
307
308
	private function getRecentChangeFieldsFromRow( stdClass $row ) {
309
		// This can be simplified to single array_filter call filtering by key value,
310
		// once we stop supporting PHP 5.5
311
		$allFields = get_object_vars( $row );
312
		$rcKeys = array_filter(
313
			array_keys( $allFields ),
314
			function( $key ) {
315
				return substr( $key, 0, 3 ) === 'rc_';
316
			}
317
		);
318
		return array_intersect_key( $allFields, array_flip( $rcKeys ) );
319
	}
320
321
	private function getWatchedItemsWithRCInfoQueryFields( array $options ) {
322
		$fields = [
323
			'rc_id',
324
			'rc_namespace',
325
			'rc_title',
326
			'rc_timestamp',
327
			'rc_type',
328
			'rc_deleted',
329
			'wl_notificationtimestamp'
330
		];
331
332
		$rcIdFields = [
333
			'rc_cur_id',
334
			'rc_this_oldid',
335
			'rc_last_oldid',
336
		];
337
		if ( $options['usedInGenerator'] ) {
338
			if ( $options['allRevisions'] ) {
339
				$rcIdFields = [ 'rc_this_oldid' ];
340
			} else {
341
				$rcIdFields = [ 'rc_cur_id' ];
342
			}
343
		}
344
		$fields = array_merge( $fields, $rcIdFields );
345
346
		if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) {
347
			$fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
348
		}
349
		if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
350
			$fields[] = 'rc_user_text';
351
		}
352
		if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
353
			$fields[] = 'rc_user';
354
		}
355
		if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
356
			$fields[] = 'rc_comment';
357
		}
358
		if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
359
			$fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
360
		}
361
		if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) {
362
			$fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] );
363
		}
364
		if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
365
			$fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
366
		}
367
368
		return $fields;
369
	}
370
371
	private function getWatchedItemsWithRCInfoQueryConds(
372
		IDatabase $db,
373
		User $user,
374
		array $options
375
	) {
376
		$watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options );
377
		$conds = [ 'wl_user' => $watchlistOwnerId ];
378
379
		if ( !$options['allRevisions'] ) {
380
			$conds[] = $db->makeList(
381
				[ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ],
382
				LIST_OR
383
			);
384
		}
385
386
		if ( $options['namespaceIds'] ) {
387
			$conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
388
		}
389
390
		if ( array_key_exists( 'rcTypes', $options ) ) {
391
			$conds['rc_type'] = array_map( 'intval',  $options['rcTypes'] );
392
		}
393
394
		$conds = array_merge(
395
			$conds,
396
			$this->getWatchedItemsWithRCInfoQueryFilterConds( $user, $options )
397
		);
398
399
		$conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) );
400
401
		if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
402
			if ( $db->getType() === 'mysql' ) {
403
				// This is an index optimization for mysql
404
				$conds[] = "rc_timestamp > ''";
405
			}
406
		}
407
408
		$conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) );
409
410
		$deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user );
411
		if ( $deletedPageLogCond ) {
412
			$conds[] = $deletedPageLogCond;
413
		}
414
415
		return $conds;
416
	}
417
418
	private function getWatchlistOwnerId( User $user, array $options ) {
419
		if ( array_key_exists( 'watchlistOwner', $options ) ) {
420
			/** @var User $watchlistOwner */
421
			$watchlistOwner = $options['watchlistOwner'];
422
			$ownersToken = $watchlistOwner->getOption( 'watchlisttoken' );
423
			$token = $options['watchlistOwnerToken'];
424
			if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
425
				throw new UsageException(
426
					'Incorrect watchlist token provided -- please set a correct token in Special:Preferences',
427
					'bad_wltoken'
428
				);
429
			}
430
			return $watchlistOwner->getId();
431
		}
432
		return $user->getId();
433
	}
434
435
	private function getWatchedItemsWithRCInfoQueryFilterConds( User $user, array $options ) {
436
		$conds = [];
437
438 View Code Duplication
		if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) {
439
			$conds[] = 'rc_minor != 0';
440
		} elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) {
441
			$conds[] = 'rc_minor = 0';
442
		}
443
444 View Code Duplication
		if ( in_array( self::FILTER_BOT, $options['filters'] ) ) {
445
			$conds[] = 'rc_bot != 0';
446
		} elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) {
447
			$conds[] = 'rc_bot = 0';
448
		}
449
450 View Code Duplication
		if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
451
			$conds[] = 'rc_user = 0';
452
		} elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
453
			$conds[] = 'rc_user != 0';
454
		}
455
456
		if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
457
			// TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
458
			// right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
459 View Code Duplication
			if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
460
				$conds[] = 'rc_patrolled != 0';
461
			} elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
462
				$conds[] = 'rc_patrolled = 0';
463
			}
464
		}
465
466 View Code Duplication
		if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
467
			$conds[] = 'rc_timestamp >= wl_notificationtimestamp';
468
		} elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
469
			// TODO: should this be changed to use Database::makeList?
470
			$conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
471
		}
472
473
		return $conds;
474
	}
475
476
	private function getStartEndConds( IDatabase $db, array $options ) {
477
		if ( !isset( $options['start'] ) && ! isset( $options['end'] ) ) {
478
			return [];
479
		}
480
481
		$conds = [];
482
483 View Code Duplication
		if ( isset( $options['start'] ) ) {
484
			$after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
485
			$conds[] = 'rc_timestamp ' . $after . ' ' .
486
				$db->addQuotes( $db->timestamp( $options['start'] ) );
487
		}
488 View Code Duplication
		if ( isset( $options['end'] ) ) {
489
			$before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
490
			$conds[] = 'rc_timestamp ' . $before . ' ' .
491
				$db->addQuotes( $db->timestamp( $options['end'] ) );
492
		}
493
494
		return $conds;
495
	}
496
497
	private function getUserRelatedConds( IDatabase $db, User $user, array $options ) {
498
		if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
499
			return [];
500
		}
501
502
		$conds = [];
503
504
		if ( array_key_exists( 'onlyByUser', $options ) ) {
505
			$conds['rc_user_text'] = $options['onlyByUser'];
506
		} elseif ( array_key_exists( 'notByUser', $options ) ) {
507
			$conds[] = 'rc_user_text != ' . $db->addQuotes( $options['notByUser'] );
508
		}
509
510
		// Avoid brute force searches (bug 17342)
511
		$bitmask = 0;
512
		if ( !$user->isAllowed( 'deletedhistory' ) ) {
513
			$bitmask = Revision::DELETED_USER;
514
		} elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
515
			$bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
516
		}
517
		if ( $bitmask ) {
518
			$conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
519
		}
520
521
		return $conds;
522
	}
523
524
	private function getExtraDeletedPageLogEntryRelatedCond( IDatabase $db, User $user ) {
525
		// LogPage::DELETED_ACTION hides the affected page, too. So hide those
526
		// entirely from the watchlist, or someone could guess the title.
527
		$bitmask = 0;
528
		if ( !$user->isAllowed( 'deletedhistory' ) ) {
529
			$bitmask = LogPage::DELETED_ACTION;
530
		} elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
531
			$bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
532
		}
533 View Code Duplication
		if ( $bitmask ) {
534
			return $db->makeList( [
535
				'rc_type != ' . RC_LOG,
536
				$db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
537
			], LIST_OR );
538
		}
539
		return '';
540
	}
541
542
	private function getStartFromConds( IDatabase $db, array $options, array $startFrom ) {
543
		$op = $options['dir'] === self::DIR_OLDER ? '<' : '>';
544
		list( $rcTimestamp, $rcId ) = $startFrom;
545
		$rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) );
546
		$rcId = (int)$rcId;
547
		return $db->makeList(
548
			[
549
				"rc_timestamp $op $rcTimestamp",
550
				$db->makeList(
551
					[
552
						"rc_timestamp = $rcTimestamp",
553
						"rc_id $op= $rcId"
554
					],
555
					LIST_AND
556
				)
557
			],
558
			LIST_OR
559
		);
560
	}
561
562
	private function getWatchedItemsForUserQueryConds( IDatabase $db, User $user, array $options ) {
563
		$conds = [ 'wl_user' => $user->getId() ];
564
		if ( $options['namespaceIds'] ) {
565
			$conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
566
		}
567
		if ( isset( $options['filter'] ) ) {
568
			$filter = $options['filter'];
569
			if ( $filter ===  self::FILTER_CHANGED ) {
570
				$conds[] = 'wl_notificationtimestamp IS NOT NULL';
571
			} else {
572
				$conds[] = 'wl_notificationtimestamp IS NULL';
573
			}
574
		}
575
576 View Code Duplication
		if ( isset( $options['from'] ) ) {
577
			$op = $options['sort'] === self::SORT_ASC ? '>' : '<';
578
			$conds[] = $this->getFromUntilTargetConds( $db, $options['from'], $op );
579
		}
580 View Code Duplication
		if ( isset( $options['until'] ) ) {
581
			$op = $options['sort'] === self::SORT_ASC ? '<' : '>';
582
			$conds[] = $this->getFromUntilTargetConds( $db, $options['until'], $op );
583
		}
584 View Code Duplication
		if ( isset( $options['startFrom'] ) ) {
585
			$op = $options['sort'] === self::SORT_ASC ? '>' : '<';
586
			$conds[] = $this->getFromUntilTargetConds( $db, $options['startFrom'], $op );
587
		}
588
589
		return $conds;
590
	}
591
592
	/**
593
	 * Creates a query condition part for getting only items before or after the given link target
594
	 * (while ordering using $sort mode)
595
	 *
596
	 * @param IDatabase $db
597
	 * @param LinkTarget $target
598
	 * @param string $op comparison operator to use in the conditions
599
	 * @return string
600
	 */
601
	private function getFromUntilTargetConds( IDatabase $db, LinkTarget $target, $op ) {
602
		return $db->makeList(
603
			[
604
				"wl_namespace $op " . $target->getNamespace(),
605
				$db->makeList(
606
					[
607
						'wl_namespace = ' . $target->getNamespace(),
608
						"wl_title $op= " . $db->addQuotes( $target->getDBkey() )
609
					],
610
					LIST_AND
611
				)
612
			],
613
			LIST_OR
614
		);
615
	}
616
617
	private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) {
618
		$dbOptions = [];
619
620
		if ( array_key_exists( 'dir', $options ) ) {
621
			$sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
622
			$dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
623
		}
624
625
		if ( array_key_exists( 'limit', $options ) ) {
626
			$dbOptions['LIMIT'] = (int)$options['limit'] + 1;
627
		}
628
629
		return $dbOptions;
630
	}
631
632
	private function getWatchedItemsForUserQueryDbOptions( array $options ) {
633
		$dbOptions = [];
634
		if ( array_key_exists( 'sort', $options ) ) {
635
			$dbOptions['ORDER BY'] = [
636
				"wl_namespace {$options['sort']}",
637
				"wl_title {$options['sort']}"
638
			];
639
			if ( count( $options['namespaceIds'] ) === 1 ) {
640
				$dbOptions['ORDER BY'] = "wl_title {$options['sort']}";
641
			}
642
		}
643
		if ( array_key_exists( 'limit', $options ) ) {
644
			$dbOptions['LIMIT'] = (int)$options['limit'];
645
		}
646
		return $dbOptions;
647
	}
648
649
	private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
650
		$joinConds = [
651
			'watchlist' => [ 'INNER JOIN',
652
				[
653
					'wl_namespace=rc_namespace',
654
					'wl_title=rc_title'
655
				]
656
			]
657
		];
658
		if ( !$options['allRevisions'] ) {
659
			$joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
660
		}
661
		return $joinConds;
662
	}
663
664
}
665