Completed
Branch master (8ab5d1)
by
unknown
31:47
created

getRecentChangeFieldsFromRow()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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