10 use Wikimedia\Assert\Assert;
106 $this->hookRunner =
new HookRunner( $hookContainer );
115 if ( $this->extensions ===
null ) {
116 $this->extensions = [];
117 $this->hookRunner->onWatchedItemQueryServiceExtensions( $this->extensions, $this );
126 return $this->loadBalancer->getConnectionRef(
DB_REPLICA, [
'watchlist' ] );
173 User $user, array $options = [], &$startFrom =
null
176 'includeFields' => [],
177 'namespaceIds' => [],
179 'allRevisions' =>
false,
180 'usedInGenerator' => false
184 !isset( $options[
'rcTypes'] )
186 '$options[\'rcTypes\']',
187 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
190 !isset( $options[
'dir'] ) || in_array( $options[
'dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
192 'must be DIR_OLDER or DIR_NEWER'
195 !isset( $options[
'start'] ) && !isset( $options[
'end'] ) && $startFrom ===
null
196 || isset( $options[
'dir'] ),
198 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
201 !isset( $options[
'startFrom'] ),
202 '$options[\'startFrom\']',
203 'must not be provided, use $startFrom instead'
206 !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
208 'must be a two-element array'
210 if ( array_key_exists(
'watchlistOwner', $options ) ) {
211 Assert::parameterType(
213 $options[
'watchlistOwner'],
214 '$options[\'watchlistOwner\']'
217 isset( $options[
'watchlistOwnerToken'] ),
218 '$options[\'watchlistOwnerToken\']',
219 'must be provided when providing watchlistOwner option'
231 if ( $startFrom !==
null ) {
236 $extension->modifyWatchedItemsWithRCInfoQuery(
237 $user, $options, $db,
255 $limit = $dbOptions[
'LIMIT'] ?? INF;
258 foreach (
$res as $row ) {
259 if ( --$limit <= 0 ) {
260 $startFrom = [ $row->rc_timestamp, $row->rc_id ];
264 $target =
new TitleValue( (
int)$row->rc_namespace, $row->rc_title );
269 $this->watchedItemStore->getLatestNotificationTimestamp(
270 $row->wl_notificationtimestamp, $user, $target
272 $row->we_expiry ??
null
279 $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items,
$res, $startFrom );
311 $options += [
'namespaceIds' => [] ];
314 !isset( $options[
'sort'] ) || in_array( $options[
'sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
315 '$options[\'sort\']',
316 'must be SORT_ASC or SORT_DESC'
319 !isset( $options[
'filter'] ) || in_array(
320 $options[
'filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
322 '$options[\'filter\']',
323 'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
326 !isset( $options[
'from'] ) && !isset( $options[
'until'] ) && !isset( $options[
'startFrom'] )
327 || isset( $options[
'sort'] ),
328 '$options[\'sort\']',
329 'must be provided if any of "from", "until", "startFrom" options is provided'
337 $tables =
'watchlist';
339 if ( $this->expiryEnabled ) {
341 $tables = [
'watchlist',
'watchlist_expiry' ];
342 $conds[] = $db->makeList(
343 [
'we_expiry' =>
null,
'we_expiry > ' . $db->addQuotes( $db->timestamp() ) ],
346 $joinConds[
'watchlist_expiry'] = [
'LEFT JOIN',
'wl_id = we_item' ];
350 [
'wl_namespace',
'wl_title',
'wl_notificationtimestamp' ],
358 foreach (
$res as $row ) {
359 $target =
new TitleValue( (
int)$row->wl_namespace, $row->wl_title );
364 $this->watchedItemStore->getLatestNotificationTimestamp(
365 $row->wl_notificationtimestamp, $user, $target
367 $row->we_expiry ??
null
371 return $watchedItems;
377 $allFields = get_object_vars( $row );
378 $rcKeys = array_filter(
379 array_keys( $allFields ),
381 return substr( $key, 0, 3 ) ===
'rc_';
384 return array_intersect_key( $allFields, array_flip( $rcKeys ) );
388 $tables = [
'recentchanges',
'watchlist' ];
390 if ( $this->expiryEnabled ) {
391 $tables[] =
'watchlist_expiry';
394 if ( !$options[
'allRevisions'] ) {
397 if ( in_array( self::INCLUDE_COMMENT, $options[
'includeFields'] ) ) {
398 $tables += $this->commentStore->getJoin(
'rc_comment' )[
'tables'];
400 if ( in_array( self::INCLUDE_USER, $options[
'includeFields'] ) ||
401 in_array( self::INCLUDE_USER_ID, $options[
'includeFields'] ) ||
402 in_array( self::FILTER_ANON, $options[
'filters'] ) ||
403 in_array( self::FILTER_NOT_ANON, $options[
'filters'] ) ||
404 array_key_exists(
'onlyByUser', $options ) || array_key_exists(
'notByUser', $options )
406 $tables += $this->actorMigration->getJoin(
'rc_user' )[
'tables'];
419 'wl_notificationtimestamp'
422 if ( $this->expiryEnabled ) {
423 $fields[] =
'we_expiry';
431 if ( $options[
'usedInGenerator'] ) {
432 if ( $options[
'allRevisions'] ) {
433 $rcIdFields = [
'rc_this_oldid' ];
435 $rcIdFields = [
'rc_cur_id' ];
438 $fields = array_merge( $fields, $rcIdFields );
440 if ( in_array( self::INCLUDE_FLAGS, $options[
'includeFields'] ) ) {
441 $fields = array_merge( $fields, [
'rc_type',
'rc_minor',
'rc_bot' ] );
443 if ( in_array( self::INCLUDE_USER, $options[
'includeFields'] ) ) {
444 $fields[
'rc_user_text'] = $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user_text'];
446 if ( in_array( self::INCLUDE_USER_ID, $options[
'includeFields'] ) ) {
447 $fields[
'rc_user'] = $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user'];
449 if ( in_array( self::INCLUDE_COMMENT, $options[
'includeFields'] ) ) {
450 $fields += $this->commentStore->getJoin(
'rc_comment' )[
'fields'];
452 if ( in_array( self::INCLUDE_PATROL_INFO, $options[
'includeFields'] ) ) {
453 $fields = array_merge( $fields, [
'rc_patrolled',
'rc_log_type' ] );
455 if ( in_array( self::INCLUDE_SIZES, $options[
'includeFields'] ) ) {
456 $fields = array_merge( $fields, [
'rc_old_len',
'rc_new_len' ] );
458 if ( in_array( self::INCLUDE_LOG_INFO, $options[
'includeFields'] ) ) {
459 $fields = array_merge( $fields, [
'rc_logid',
'rc_log_type',
'rc_log_action',
'rc_params' ] );
461 if ( in_array( self::INCLUDE_TAGS, $options[
'includeFields'] ) ) {
475 $conds = [
'wl_user' => $watchlistOwnerId ];
477 if ( $this->expiryEnabled ) {
481 if ( !$options[
'allRevisions'] ) {
483 [
'rc_this_oldid=page_latest',
'rc_type=' .
RC_LOG ],
488 if ( $options[
'namespaceIds'] ) {
489 $conds[
'wl_namespace'] = array_map(
'intval', $options[
'namespaceIds'] );
492 if ( array_key_exists(
'rcTypes', $options ) ) {
493 $conds[
'rc_type'] = array_map(
'intval', $options[
'rcTypes'] );
496 $conds = array_merge(
503 if ( !isset( $options[
'start'] ) && !isset( $options[
'end'] ) && $db->
getType() ===
'mysql' ) {
505 $conds[] =
'rc_timestamp > ' . $db->
addQuotes(
'' );
511 if ( $deletedPageLogCond ) {
512 $conds[] = $deletedPageLogCond;
519 if ( array_key_exists(
'watchlistOwner', $options ) ) {
521 $watchlistOwner = $options[
'watchlistOwner'];
523 $watchlistOwner->getOption(
'watchlisttoken' );
524 $token = $options[
'watchlistOwnerToken'];
525 if ( $ownersToken ==
'' || !hash_equals( $ownersToken, $token ) ) {
528 return $watchlistOwner->getId();
530 return $user->
getId();
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';
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';
548 if ( in_array( self::FILTER_ANON, $options[
'filters'] ) ) {
549 $conds[] = $this->actorMigration->isAnon(
550 $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user']
552 } elseif ( in_array( self::FILTER_NOT_ANON, $options[
'filters'] ) ) {
553 $conds[] = $this->actorMigration->isNotAnon(
554 $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user']
561 if ( in_array( self::FILTER_PATROLLED, $options[
'filters'] ) ) {
563 } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options[
'filters'] ) ) {
567 if ( in_array( self::FILTER_AUTOPATROLLED, $options[
'filters'] ) ) {
569 } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options[
'filters'] ) ) {
574 if ( in_array( self::FILTER_UNREAD, $options[
'filters'] ) ) {
575 $conds[] =
'rc_timestamp >= wl_notificationtimestamp';
576 } elseif ( in_array( self::FILTER_NOT_UNREAD, $options[
'filters'] ) ) {
578 $conds[] =
'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
585 if ( !isset( $options[
'start'] ) && !isset( $options[
'end'] ) ) {
591 if ( isset( $options[
'start'] ) ) {
592 $after = $options[
'dir'] === self::DIR_OLDER ?
'<=' :
'>=';
593 $conds[] =
'rc_timestamp ' . $after .
' ' .
596 if ( isset( $options[
'end'] ) ) {
597 $before = $options[
'dir'] === self::DIR_OLDER ?
'>=' :
'<=';
598 $conds[] =
'rc_timestamp ' . $before .
' ' .
606 if ( !array_key_exists(
'onlyByUser', $options ) && !array_key_exists(
'notByUser', $options ) ) {
612 if ( array_key_exists(
'onlyByUser', $options ) ) {
613 $byUser = $this->userFactory->newFromName(
614 $options[
'onlyByUser'],
615 UserFactory::RIGOR_NONE
617 $conds[] = $this->actorMigration->getWhere( $db,
'rc_user', $byUser )[
'conds'];
618 } elseif ( array_key_exists(
'notByUser', $options ) ) {
619 $byUser = $this->userFactory->newFromName(
620 $options[
'notByUser'],
621 UserFactory::RIGOR_NONE
623 $conds[] =
'NOT(' . $this->actorMigration->getWhere( $db,
'rc_user', $byUser )[
'conds'] .
')';
628 if ( !$this->permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
629 $bitmask = RevisionRecord::DELETED_USER;
630 } elseif ( !$this->permissionManager
631 ->userHasAnyRight( $user,
'suppressrevision',
'viewsuppressed' )
633 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
636 $conds[] = $db->
bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask";
646 if ( !$this->permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
648 } elseif ( !$this->permissionManager
649 ->userHasAnyRight( $user,
'suppressrevision',
'viewsuppressed' )
656 $db->
bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask",
663 $op = $options[
'dir'] === self::DIR_OLDER ?
'<' :
'>';
664 list( $rcTimestamp, $rcId ) = $startFrom;
669 "rc_timestamp $op $rcTimestamp",
672 "rc_timestamp = $rcTimestamp",
685 $conds = [
'wl_user' => $user->
getId() ];
686 if ( $options[
'namespaceIds'] ) {
687 $conds[
'wl_namespace'] = array_map(
'intval', $options[
'namespaceIds'] );
689 if ( isset( $options[
'filter'] ) ) {
690 $filter = $options[
'filter'];
691 if ( $filter === self::FILTER_CHANGED ) {
692 $conds[] =
'wl_notificationtimestamp IS NOT NULL';
694 $conds[] =
'wl_notificationtimestamp IS NULL';
698 if ( isset( $options[
'from'] ) ) {
699 $op = $options[
'sort'] === self::SORT_ASC ?
'>' :
'<';
702 if ( isset( $options[
'until'] ) ) {
703 $op = $options[
'sort'] === self::SORT_ASC ?
'<' :
'>';
706 if ( isset( $options[
'startFrom'] ) ) {
707 $op = $options[
'sort'] === self::SORT_ASC ?
'>' :
'<';
742 if ( array_key_exists(
'dir', $options ) ) {
743 $sort = $options[
'dir'] === self::DIR_OLDER ?
' DESC' :
'';
744 $dbOptions[
'ORDER BY'] = [
'rc_timestamp' . $sort,
'rc_id' . $sort ];
747 if ( array_key_exists(
'limit', $options ) ) {
748 $dbOptions[
'LIMIT'] = (int)$options[
'limit'] + 1;
756 if ( array_key_exists(
'sort', $options ) ) {
757 $dbOptions[
'ORDER BY'] = [
758 "wl_namespace {$options['sort']}",
759 "wl_title {$options['sort']}"
761 if ( count( $options[
'namespaceIds'] ) === 1 ) {
762 $dbOptions[
'ORDER BY'] =
"wl_title {$options['sort']}";
765 if ( array_key_exists(
'limit', $options ) ) {
766 $dbOptions[
'LIMIT'] = (int)$options[
'limit'];
773 'watchlist' => [
'JOIN',
775 'wl_namespace=rc_namespace',
781 if ( $this->expiryEnabled ) {
782 $joinConds[
'watchlist_expiry'] = [
'LEFT JOIN',
'wl_id = we_item' ];
785 if ( !$options[
'allRevisions'] ) {
786 $joinConds[
'page'] = [
'LEFT JOIN',
'rc_cur_id=page_id' ];
788 if ( in_array( self::INCLUDE_COMMENT, $options[
'includeFields'] ) ) {
789 $joinConds += $this->commentStore->getJoin(
'rc_comment' )[
'joins'];
791 if ( in_array( self::INCLUDE_USER, $options[
'includeFields'] ) ||
792 in_array( self::INCLUDE_USER_ID, $options[
'includeFields'] ) ||
793 in_array( self::FILTER_ANON, $options[
'filters'] ) ||
794 in_array( self::FILTER_NOT_ANON, $options[
'filters'] ) ||
795 array_key_exists(
'onlyByUser', $options ) || array_key_exists(
'notByUser', $options )
797 $joinConds += $this->actorMigration->getJoin(
'rc_user' )[
'joins'];