MediaWiki master
WatchedItemQueryService.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Watchlist;
4
5use LogPage;
19use RecentChange;
20use Wikimedia\Assert\Assert;
25
37
38 public const DIR_OLDER = 'older';
39 public const DIR_NEWER = 'newer';
40
41 public const INCLUDE_FLAGS = 'flags';
42 public const INCLUDE_USER = 'user';
43 public const INCLUDE_USER_ID = 'userid';
44 public const INCLUDE_COMMENT = 'comment';
45 public const INCLUDE_PATROL_INFO = 'patrol';
46 public const INCLUDE_AUTOPATROL_INFO = 'autopatrol';
47 public const INCLUDE_SIZES = 'sizes';
48 public const INCLUDE_LOG_INFO = 'loginfo';
49 public const INCLUDE_TAGS = 'tags';
50
51 // FILTER_* constants are part of public API (are used in ApiQueryWatchlist and
52 // ApiQueryWatchlistRaw classes) and should not be changed.
53 // Changing values of those constants will result in a breaking change in the API
54 public const FILTER_MINOR = 'minor';
55 public const FILTER_NOT_MINOR = '!minor';
56 public const FILTER_BOT = 'bot';
57 public const FILTER_NOT_BOT = '!bot';
58 public const FILTER_ANON = 'anon';
59 public const FILTER_NOT_ANON = '!anon';
60 public const FILTER_PATROLLED = 'patrolled';
61 public const FILTER_NOT_PATROLLED = '!patrolled';
62 public const FILTER_AUTOPATROLLED = 'autopatrolled';
63 public const FILTER_NOT_AUTOPATROLLED = '!autopatrolled';
64 public const FILTER_UNREAD = 'unread';
65 public const FILTER_NOT_UNREAD = '!unread';
66 public const FILTER_CHANGED = 'changed';
67 public const FILTER_NOT_CHANGED = '!changed';
68
69 public const SORT_ASC = 'ASC';
70 public const SORT_DESC = 'DESC';
71
75 private $dbProvider;
76
78 private $extensions = null;
79
81 private $commentStore;
82
84 private $watchedItemStore;
85
87 private $hookRunner;
88
90 private $userOptionsLookup;
91
93 private $tempUserConfig;
94
98 private $expiryEnabled;
99
103 private $maxQueryExecutionTime;
104
105 public function __construct(
106 IConnectionProvider $dbProvider,
107 CommentStore $commentStore,
108 WatchedItemStoreInterface $watchedItemStore,
109 HookContainer $hookContainer,
110 UserOptionsLookup $userOptionsLookup,
111 TempUserConfig $tempUserConfig,
112 bool $expiryEnabled = false,
113 int $maxQueryExecutionTime = 0
114 ) {
115 $this->dbProvider = $dbProvider;
116 $this->commentStore = $commentStore;
117 $this->watchedItemStore = $watchedItemStore;
118 $this->hookRunner = new HookRunner( $hookContainer );
119 $this->userOptionsLookup = $userOptionsLookup;
120 $this->tempUserConfig = $tempUserConfig;
121 $this->expiryEnabled = $expiryEnabled;
122 $this->maxQueryExecutionTime = $maxQueryExecutionTime;
123 }
124
128 private function getExtensions() {
129 if ( $this->extensions === null ) {
130 $this->extensions = [];
131 $this->hookRunner->onWatchedItemQueryServiceExtensions( $this->extensions, $this );
132 }
133 return $this->extensions;
134 }
135
180 User $user, array $options = [], &$startFrom = null
181 ) {
182 $options += [
183 'includeFields' => [],
184 'namespaceIds' => [],
185 'filters' => [],
186 'allRevisions' => false,
187 'usedInGenerator' => false
188 ];
189
190 Assert::parameter(
191 !isset( $options['rcTypes'] )
192 || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
193 '$options[\'rcTypes\']',
194 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
195 );
196 Assert::parameter(
197 !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
198 '$options[\'dir\']',
199 'must be DIR_OLDER or DIR_NEWER'
200 );
201 Assert::parameter(
202 ( !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null )
203 || isset( $options['dir'] ),
204 '$options[\'dir\']',
205 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
206 );
207 Assert::parameter(
208 !isset( $options['startFrom'] ),
209 '$options[\'startFrom\']',
210 'must not be provided, use $startFrom instead'
211 );
212 Assert::parameter(
213 $startFrom === null || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
214 '$startFrom',
215 'must be a two-element array'
216 );
217 if ( array_key_exists( 'watchlistOwner', $options ) ) {
218 Assert::parameterType(
219 UserIdentity::class,
220 $options['watchlistOwner'],
221 '$options[\'watchlistOwner\']'
222 );
223 Assert::parameter(
224 isset( $options['watchlistOwnerToken'] ),
225 '$options[\'watchlistOwnerToken\']',
226 'must be provided when providing watchlistOwner option'
227 );
228 }
229
230 $db = $this->dbProvider->getReplicaDatabase();
231
232 $tables = $this->getWatchedItemsWithRCInfoQueryTables( $options );
233 $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
234 $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
235 $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
236 $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options );
237
238 if ( $startFrom !== null ) {
239 $conds[] = $this->getStartFromConds( $db, $options, $startFrom );
240 }
241
242 foreach ( $this->getExtensions() as $extension ) {
243 $extension->modifyWatchedItemsWithRCInfoQuery(
244 $user, $options, $db,
245 $tables,
246 $fields,
247 $conds,
248 $dbOptions,
249 $joinConds
250 );
251 }
252
253 $res = $db->newSelectQueryBuilder()
254 ->tables( $tables )
255 ->fields( $fields )
256 ->conds( $conds )
257 ->caller( __METHOD__ )
258 ->options( $dbOptions )
259 ->joinConds( $joinConds )
260 ->fetchResultSet();
261
262 $limit = $dbOptions['LIMIT'] ?? INF;
263 $items = [];
264 $startFrom = null;
265 foreach ( $res as $row ) {
266 if ( --$limit <= 0 ) {
267 $startFrom = [ $row->rc_timestamp, $row->rc_id ];
268 break;
269 }
270
271 $target = new TitleValue( (int)$row->rc_namespace, $row->rc_title );
272 $items[] = [
273 new WatchedItem(
274 $user,
275 $target,
276 $this->watchedItemStore->getLatestNotificationTimestamp(
277 $row->wl_notificationtimestamp, $user, $target
278 ),
279 $row->we_expiry ?? null
280 ),
281 $this->getRecentChangeFieldsFromRow( $row )
282 ];
283 }
284
285 foreach ( $this->getExtensions() as $extension ) {
286 $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom );
287 }
288
289 return $items;
290 }
291
311 public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
312 if ( !$user->isRegistered() ) {
313 // TODO: should this just return an empty array or rather complain loud at this point
314 // as e.g. ApiBase::getWatchlistUser does?
315 return [];
316 }
317
318 $options += [ 'namespaceIds' => [] ];
319
320 Assert::parameter(
321 !isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
322 '$options[\'sort\']',
323 'must be SORT_ASC or SORT_DESC'
324 );
325 Assert::parameter(
326 !isset( $options['filter'] ) || in_array(
327 $options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
328 ),
329 '$options[\'filter\']',
330 'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
331 );
332 Assert::parameter(
333 ( !isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] ) )
334 || isset( $options['sort'] ),
335 '$options[\'sort\']',
336 'must be provided if any of "from", "until", "startFrom" options is provided'
337 );
338
339 $db = $this->dbProvider->getReplicaDatabase();
340
341 $queryBuilder = $db->newSelectQueryBuilder()
342 ->select( [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ] )
343 ->from( 'watchlist' )
344 ->caller( __METHOD__ );
345 $this->addQueryCondsForWatchedItemsForUser( $db, $user, $options, $queryBuilder );
346 $this->addQueryDbOptionsForWatchedItemsForUser( $options, $queryBuilder );
347
348 if ( $this->expiryEnabled ) {
349 // If expiries are enabled, join with the watchlist_expiry table and exclude expired items.
350 $queryBuilder->leftJoin( 'watchlist_expiry', null, 'wl_id = we_item' )
351 ->andWhere( $db->expr( 'we_expiry', '>', $db->timestamp() )->or( 'we_expiry', '=', null ) );
352 }
353 $res = $queryBuilder->fetchResultSet();
354
355 $watchedItems = [];
356 foreach ( $res as $row ) {
357 $target = new TitleValue( (int)$row->wl_namespace, $row->wl_title );
358 // todo these could all be cached at some point?
359 $watchedItems[] = new WatchedItem(
360 $user,
361 $target,
362 $this->watchedItemStore->getLatestNotificationTimestamp(
363 $row->wl_notificationtimestamp, $user, $target
364 ),
365 $row->we_expiry ?? null
366 );
367 }
368
369 return $watchedItems;
370 }
371
372 private function getRecentChangeFieldsFromRow( \stdClass $row ) {
373 return array_filter(
374 get_object_vars( $row ),
375 static function ( $key ) {
376 return str_starts_with( $key, 'rc_' );
377 },
378 ARRAY_FILTER_USE_KEY
379 );
380 }
381
382 private function getWatchedItemsWithRCInfoQueryTables( array $options ) {
383 $tables = [ 'recentchanges', 'watchlist' ];
384
385 if ( $this->expiryEnabled ) {
386 $tables[] = 'watchlist_expiry';
387 }
388
389 if ( !$options['allRevisions'] ) {
390 $tables[] = 'page';
391 }
392 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
393 $tables += $this->commentStore->getJoin( 'rc_comment' )['tables'];
394 }
395 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
396 in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
397 in_array( self::FILTER_ANON, $options['filters'] ) ||
398 in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
399 array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
400 ) {
401 $tables['watchlist_actor'] = 'actor';
402 }
403 return $tables;
404 }
405
406 private function getWatchedItemsWithRCInfoQueryFields( array $options ) {
407 $fields = [
408 'rc_id',
409 'rc_namespace',
410 'rc_title',
411 'rc_timestamp',
412 'rc_type',
413 'rc_deleted',
414 'wl_notificationtimestamp'
415 ];
416
417 if ( $this->expiryEnabled ) {
418 $fields[] = 'we_expiry';
419 }
420
421 $rcIdFields = [
422 'rc_cur_id',
423 'rc_this_oldid',
424 'rc_last_oldid',
425 ];
426 if ( $options['usedInGenerator'] ) {
427 if ( $options['allRevisions'] ) {
428 $rcIdFields = [ 'rc_this_oldid' ];
429 } else {
430 $rcIdFields = [ 'rc_cur_id' ];
431 }
432 }
433 $fields = array_merge( $fields, $rcIdFields );
434
435 if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) {
436 $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
437 }
438 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
439 $fields['rc_user_text'] = 'watchlist_actor.actor_name';
440 }
441 if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
442 $fields['rc_user'] = 'watchlist_actor.actor_user';
443 }
444 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
445 $fields += $this->commentStore->getJoin( 'rc_comment' )['fields'];
446 }
447 if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
448 $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
449 }
450 if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) {
451 $fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] );
452 }
453 if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
454 $fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
455 }
456 if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
457 // prefixed with rc_ to include the field in getRecentChangeFieldsFromRow
458 $fields['rc_tags'] = MediaWikiServices::getInstance()->getChangeTagsStore()
459 ->makeTagSummarySubquery( 'recentchanges' );
460 }
461
462 return $fields;
463 }
464
465 private function getWatchedItemsWithRCInfoQueryConds(
466 IReadableDatabase $db,
467 User $user,
468 array $options
469 ) {
470 $watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options );
471 $conds = [ 'wl_user' => $watchlistOwnerId ];
472
473 if ( $this->expiryEnabled ) {
474 $conds[] = $db->expr( 'we_expiry', '=', null )->or( 'we_expiry', '>', $db->timestamp() );
475 }
476
477 if ( !$options['allRevisions'] ) {
478 $conds[] = $db->makeList(
479 [ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ],
480 LIST_OR
481 );
482 }
483
484 if ( $options['namespaceIds'] ) {
485 $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
486 }
487
488 if ( array_key_exists( 'rcTypes', $options ) ) {
489 $conds['rc_type'] = array_map( 'intval', $options['rcTypes'] );
490 }
491
492 $conds = array_merge(
493 $conds,
494 $this->getWatchedItemsWithRCInfoQueryFilterConds( $db, $user, $options )
495 );
496
497 $conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) );
498
499 if ( !isset( $options['start'] ) && !isset( $options['end'] ) && $db->getType() === 'mysql' ) {
500 // This is an index optimization for mysql
501 $conds[] = $db->expr( 'rc_timestamp', '>', '' );
502 }
503
504 $conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) );
505
506 $deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user );
507 if ( $deletedPageLogCond ) {
508 $conds[] = $deletedPageLogCond;
509 }
510
511 return $conds;
512 }
513
514 private function getWatchlistOwnerId( UserIdentity $user, array $options ) {
515 if ( array_key_exists( 'watchlistOwner', $options ) ) {
517 $watchlistOwner = $options['watchlistOwner'];
518 $ownersToken =
519 $this->userOptionsLookup->getOption( $watchlistOwner, 'watchlisttoken' );
520 $token = $options['watchlistOwnerToken'];
521 if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
522 throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' );
523 }
524 return $watchlistOwner->getId();
525 }
526 return $user->getId();
527 }
528
529 private function getWatchedItemsWithRCInfoQueryFilterConds(
530 IReadableDatabase $dbr,
531 User $user,
532 array $options
533 ) {
534 $conds = [];
535
536 if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) {
537 $conds[] = 'rc_minor != 0';
538 } elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) {
539 $conds[] = 'rc_minor = 0';
540 }
541
542 if ( in_array( self::FILTER_BOT, $options['filters'] ) ) {
543 $conds[] = 'rc_bot != 0';
544 } elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) {
545 $conds[] = 'rc_bot = 0';
546 }
547
548 // Treat temporary users as 'anon', to match ChangesListSpecialPage
549 if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
550 if ( $this->tempUserConfig->isKnown() ) {
551 $conds[] = $dbr->expr( 'watchlist_actor.actor_user', '=', null )
552 ->orExpr( $this->tempUserConfig->getMatchCondition( $dbr,
553 'watchlist_actor.actor_name', IExpression::LIKE ) );
554 } else {
555 $conds[] = 'watchlist_actor.actor_user IS NULL';
556 }
557 } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
558 $conds[] = 'watchlist_actor.actor_user IS NOT NULL';
559 if ( $this->tempUserConfig->isKnown() ) {
560 $conds[] = $this->tempUserConfig->getMatchCondition( $dbr,
561 'watchlist_actor.actor_name', IExpression::NOT_LIKE );
562 }
563 }
564
565 if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
566 // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
567 // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
568 if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
569 $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
570 } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
571 $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
572 }
573
574 if ( in_array( self::FILTER_AUTOPATROLLED, $options['filters'] ) ) {
575 $conds['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
576 } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options['filters'] ) ) {
577 $conds[] = 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED;
578 }
579 }
580
581 if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
582 $conds[] = 'rc_timestamp >= wl_notificationtimestamp';
583 } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
584 // TODO: should this be changed to use Database::makeList?
585 $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
586 }
587
588 return $conds;
589 }
590
591 private function getStartEndConds( IReadableDatabase $db, array $options ) {
592 if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
593 return [];
594 }
595
596 $conds = [];
597 if ( isset( $options['start'] ) ) {
598 $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
599 $conds[] = $db->expr( 'rc_timestamp', $after, $db->timestamp( $options['start'] ) );
600 }
601 if ( isset( $options['end'] ) ) {
602 $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
603 $conds[] = $db->expr( 'rc_timestamp', $before, $db->timestamp( $options['end'] ) );
604 }
605
606 return $conds;
607 }
608
609 private function getUserRelatedConds( IReadableDatabase $db, Authority $user, array $options ) {
610 if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
611 return [];
612 }
613
614 $conds = [];
615
616 if ( array_key_exists( 'onlyByUser', $options ) ) {
617 $conds['watchlist_actor.actor_name'] = $options['onlyByUser'];
618 } elseif ( array_key_exists( 'notByUser', $options ) ) {
619 $conds[] = $db->expr( 'watchlist_actor.actor_name', '!=', $options['notByUser'] );
620 }
621
622 // Avoid brute force searches (T19342)
623 $bitmask = 0;
624 if ( !$user->isAllowed( 'deletedhistory' ) ) {
625 $bitmask = RevisionRecord::DELETED_USER;
626 } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
627 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
628 }
629 if ( $bitmask ) {
630 $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
631 }
632
633 return $conds;
634 }
635
636 private function getExtraDeletedPageLogEntryRelatedCond( IReadableDatabase $db, Authority $user ) {
637 // LogPage::DELETED_ACTION hides the affected page, too. So hide those
638 // entirely from the watchlist, or someone could guess the title.
639 $bitmask = 0;
640 if ( !$user->isAllowed( 'deletedhistory' ) ) {
641 $bitmask = LogPage::DELETED_ACTION;
642 } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
644 }
645 if ( $bitmask ) {
646 return $db->makeList( [
647 'rc_type != ' . RC_LOG,
648 $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
649 ], LIST_OR );
650 }
651 return '';
652 }
653
654 private function getStartFromConds( IReadableDatabase $db, array $options, array $startFrom ) {
655 $op = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
656 [ $rcTimestamp, $rcId ] = $startFrom;
657 $rcTimestamp = $db->timestamp( $rcTimestamp );
658 $rcId = (int)$rcId;
659 return $db->buildComparison( $op, [
660 'rc_timestamp' => $rcTimestamp,
661 'rc_id' => $rcId,
662 ] );
663 }
664
665 private function addQueryCondsForWatchedItemsForUser(
666 IReadableDatabase $db, UserIdentity $user, array $options, SelectQueryBuilder $queryBuilder
667 ) {
668 $queryBuilder->where( [ 'wl_user' => $user->getId() ] );
669 if ( $options['namespaceIds'] ) {
670 $queryBuilder->where( [ 'wl_namespace' => array_map( 'intval', $options['namespaceIds'] ) ] );
671 }
672 if ( isset( $options['filter'] ) ) {
673 $filter = $options['filter'];
674 if ( $filter === self::FILTER_CHANGED ) {
675 $queryBuilder->where( 'wl_notificationtimestamp IS NOT NULL' );
676 } else {
677 $queryBuilder->where( 'wl_notificationtimestamp IS NULL' );
678 }
679 }
680
681 if ( isset( $options['from'] ) ) {
682 $op = $options['sort'] === self::SORT_ASC ? '>=' : '<=';
683 $queryBuilder->where( $this->getFromUntilTargetConds( $db, $options['from'], $op ) );
684 }
685 if ( isset( $options['until'] ) ) {
686 $op = $options['sort'] === self::SORT_ASC ? '<=' : '>=';
687 $queryBuilder->where( $this->getFromUntilTargetConds( $db, $options['until'], $op ) );
688 }
689 if ( isset( $options['startFrom'] ) ) {
690 $op = $options['sort'] === self::SORT_ASC ? '>=' : '<=';
691 $queryBuilder->where( $this->getFromUntilTargetConds( $db, $options['startFrom'], $op ) );
692 }
693 }
694
704 private function getFromUntilTargetConds( IReadableDatabase $db, LinkTarget $target, $op ) {
705 return $db->buildComparison( $op, [
706 'wl_namespace' => $target->getNamespace(),
707 'wl_title' => $target->getDBkey(),
708 ] );
709 }
710
711 private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) {
712 $dbOptions = [];
713
714 if ( array_key_exists( 'dir', $options ) ) {
715 $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
716 $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
717 }
718
719 if ( array_key_exists( 'limit', $options ) ) {
720 $dbOptions['LIMIT'] = (int)$options['limit'] + 1;
721 }
722 if ( $this->maxQueryExecutionTime ) {
723 $dbOptions['MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime;
724 }
725 return $dbOptions;
726 }
727
728 private function addQueryDbOptionsForWatchedItemsForUser( array $options, SelectQueryBuilder $queryBuilder ) {
729 if ( array_key_exists( 'sort', $options ) ) {
730 if ( count( $options['namespaceIds'] ) !== 1 ) {
731 $queryBuilder->orderBy( 'wl_namespace', $options['sort'] );
732 }
733 $queryBuilder->orderBy( 'wl_title', $options['sort'] );
734 }
735 if ( array_key_exists( 'limit', $options ) ) {
736 $queryBuilder->limit( (int)$options['limit'] );
737 }
738 if ( $this->maxQueryExecutionTime ) {
739 $queryBuilder->setMaxExecutionTime( $this->maxQueryExecutionTime );
740 }
741 }
742
743 private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
744 $joinConds = [
745 'watchlist' => [ 'JOIN',
746 [
747 'wl_namespace=rc_namespace',
748 'wl_title=rc_title'
749 ]
750 ]
751 ];
752
753 if ( $this->expiryEnabled ) {
754 $joinConds['watchlist_expiry'] = [ 'LEFT JOIN', 'wl_id = we_item' ];
755 }
756
757 if ( !$options['allRevisions'] ) {
758 $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
759 }
760 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
761 $joinConds += $this->commentStore->getJoin( 'rc_comment' )['joins'];
762 }
763 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
764 in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
765 in_array( self::FILTER_ANON, $options['filters'] ) ||
766 in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
767 array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
768 ) {
769 $joinConds['watchlist_actor'] = [ 'JOIN', 'actor_id=rc_actor' ];
770 }
771 return $joinConds;
772 }
773
774}
776class_alias( WatchedItemQueryService::class, 'WatchedItemQueryService' );
const RC_NEW
Definition Defines.php:118
const LIST_OR
Definition Defines.php:47
const RC_LOG
Definition Defines.php:119
const RC_EXTERNAL
Definition Defines.php:120
const RC_EDIT
Definition Defines.php:117
const RC_CATEGORIZE
Definition Defines.php:121
Class to simplify the use of log pages.
Definition LogPage.php:46
const DELETED_RESTRICTED
Definition LogPage.php:50
const DELETED_ACTION
Definition LogPage.php:47
Exception used to abort API execution with an error.
Handle database storage of comments such as edit summaries and log reasons.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Page revision base class.
Represents the target of a wiki link.
Provides access to user options.
User class for the MediaWiki software.
Definition User.php:119
getWatchedItemsForUser(UserIdentity $user, array $options=[])
For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser.
__construct(IConnectionProvider $dbProvider, CommentStore $commentStore, WatchedItemStoreInterface $watchedItemStore, HookContainer $hookContainer, UserOptionsLookup $userOptionsLookup, TempUserConfig $tempUserConfig, bool $expiryEnabled=false, int $maxQueryExecutionTime=0)
getWatchedItemsWithRecentChangeInfo(User $user, array $options=[], &$startFrom=null)
Representation of a pair of user and title for watchlist entries.
Utility class for creating and reading rows in the recentchanges table.
const PRC_UNPATROLLED
const PRC_AUTOPATROLLED
Build SELECT queries with a fluent interface.
Represents the target of a wiki link.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
Interface for temporary user creation config and name matching.
Interface for objects representing user identity.
isRegistered()
This must be equivalent to getId() != 0 and is provided for code readability.
Provide primary and replica IDatabase connections.
A database connection without write operations.