7 use Wikimedia\Assert\Assert;
95 if ( $this->extensions ===
null ) {
96 $this->extensions = [];
97 Hooks::run(
'WatchedItemQueryServiceExtensions', [ &$this->extensions, $this ] );
106 return $this->loadBalancer->getConnectionRef(
DB_REPLICA, [
'watchlist' ] );
153 User $user, array $options = [], &$startFrom =
null
156 'includeFields' => [],
157 'namespaceIds' => [],
159 'allRevisions' =>
false,
160 'usedInGenerator' => false
164 !isset( $options[
'rcTypes'] )
166 '$options[\'rcTypes\']',
167 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
170 !isset( $options[
'dir'] ) || in_array( $options[
'dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
172 'must be DIR_OLDER or DIR_NEWER'
175 !isset( $options[
'start'] ) && !isset( $options[
'end'] ) && $startFrom ===
null
176 || isset( $options[
'dir'] ),
178 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
181 !isset( $options[
'startFrom'] ),
182 '$options[\'startFrom\']',
183 'must not be provided, use $startFrom instead'
186 !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
188 'must be a two-element array'
190 if ( array_key_exists(
'watchlistOwner', $options ) ) {
191 Assert::parameterType(
193 $options[
'watchlistOwner'],
194 '$options[\'watchlistOwner\']'
197 isset( $options[
'watchlistOwnerToken'] ),
198 '$options[\'watchlistOwnerToken\']',
199 'must be provided when providing watchlistOwner option'
211 if ( $startFrom !==
null ) {
216 $extension->modifyWatchedItemsWithRCInfoQuery(
217 $user, $options, $db,
235 $limit = $dbOptions[
'LIMIT'] ?? INF;
238 foreach (
$res as $row ) {
239 if ( --$limit <= 0 ) {
240 $startFrom = [ $row->rc_timestamp, $row->rc_id ];
244 $target =
new TitleValue( (
int)$row->rc_namespace, $row->rc_title );
249 $this->watchedItemStore->getLatestNotificationTimestamp(
250 $row->wl_notificationtimestamp, $user, $target
258 $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items,
$res, $startFrom );
290 $options += [
'namespaceIds' => [] ];
293 !isset( $options[
'sort'] ) || in_array( $options[
'sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
294 '$options[\'sort\']',
295 'must be SORT_ASC or SORT_DESC'
298 !isset( $options[
'filter'] ) || in_array(
299 $options[
'filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
301 '$options[\'filter\']',
302 'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
305 !isset( $options[
'from'] ) && !isset( $options[
'until'] ) && !isset( $options[
'startFrom'] )
306 || isset( $options[
'sort'] ),
307 '$options[\'sort\']',
308 'must be provided if any of "from", "until", "startFrom" options is provided'
318 [
'wl_namespace',
'wl_title',
'wl_notificationtimestamp' ],
325 foreach (
$res as $row ) {
326 $target =
new TitleValue( (
int)$row->wl_namespace, $row->wl_title );
331 $this->watchedItemStore->getLatestNotificationTimestamp(
332 $row->wl_notificationtimestamp, $user, $target
337 return $watchedItems;
343 $allFields = get_object_vars( $row );
344 $rcKeys = array_filter(
345 array_keys( $allFields ),
347 return substr( $key, 0, 3 ) ===
'rc_';
350 return array_intersect_key( $allFields, array_flip( $rcKeys ) );
354 $tables = [
'recentchanges',
'watchlist' ];
355 if ( !$options[
'allRevisions'] ) {
358 if ( in_array( self::INCLUDE_COMMENT, $options[
'includeFields'] ) ) {
359 $tables += $this->commentStore->getJoin(
'rc_comment' )[
'tables'];
361 if ( in_array( self::INCLUDE_USER, $options[
'includeFields'] ) ||
362 in_array( self::INCLUDE_USER_ID, $options[
'includeFields'] ) ||
363 in_array( self::FILTER_ANON, $options[
'filters'] ) ||
364 in_array( self::FILTER_NOT_ANON, $options[
'filters'] ) ||
365 array_key_exists(
'onlyByUser', $options ) || array_key_exists(
'notByUser', $options )
367 $tables += $this->actorMigration->getJoin(
'rc_user' )[
'tables'];
380 'wl_notificationtimestamp'
388 if ( $options[
'usedInGenerator'] ) {
389 if ( $options[
'allRevisions'] ) {
390 $rcIdFields = [
'rc_this_oldid' ];
392 $rcIdFields = [
'rc_cur_id' ];
395 $fields = array_merge( $fields, $rcIdFields );
397 if ( in_array( self::INCLUDE_FLAGS, $options[
'includeFields'] ) ) {
398 $fields = array_merge( $fields, [
'rc_type',
'rc_minor',
'rc_bot' ] );
400 if ( in_array( self::INCLUDE_USER, $options[
'includeFields'] ) ) {
401 $fields[
'rc_user_text'] = $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user_text'];
403 if ( in_array( self::INCLUDE_USER_ID, $options[
'includeFields'] ) ) {
404 $fields[
'rc_user'] = $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user'];
406 if ( in_array( self::INCLUDE_COMMENT, $options[
'includeFields'] ) ) {
407 $fields += $this->commentStore->getJoin(
'rc_comment' )[
'fields'];
409 if ( in_array( self::INCLUDE_PATROL_INFO, $options[
'includeFields'] ) ) {
410 $fields = array_merge( $fields, [
'rc_patrolled',
'rc_log_type' ] );
412 if ( in_array( self::INCLUDE_SIZES, $options[
'includeFields'] ) ) {
413 $fields = array_merge( $fields, [
'rc_old_len',
'rc_new_len' ] );
415 if ( in_array( self::INCLUDE_LOG_INFO, $options[
'includeFields'] ) ) {
416 $fields = array_merge( $fields, [
'rc_logid',
'rc_log_type',
'rc_log_action',
'rc_params' ] );
418 if ( in_array( self::INCLUDE_TAGS, $options[
'includeFields'] ) ) {
432 $conds = [
'wl_user' => $watchlistOwnerId ];
434 if ( !$options[
'allRevisions'] ) {
436 [
'rc_this_oldid=page_latest',
'rc_type=' .
RC_LOG ],
441 if ( $options[
'namespaceIds'] ) {
442 $conds[
'wl_namespace'] = array_map(
'intval', $options[
'namespaceIds'] );
445 if ( array_key_exists(
'rcTypes', $options ) ) {
446 $conds[
'rc_type'] = array_map(
'intval', $options[
'rcTypes'] );
449 $conds = array_merge(
456 if ( !isset( $options[
'start'] ) && !isset( $options[
'end'] ) && $db->
getType() ===
'mysql' ) {
458 $conds[] =
'rc_timestamp > ' . $db->
addQuotes(
'' );
464 if ( $deletedPageLogCond ) {
465 $conds[] = $deletedPageLogCond;
472 if ( array_key_exists(
'watchlistOwner', $options ) ) {
474 $watchlistOwner = $options[
'watchlistOwner'];
476 $watchlistOwner->getOption(
'watchlisttoken' );
477 $token = $options[
'watchlistOwnerToken'];
478 if ( $ownersToken ==
'' || !hash_equals( $ownersToken, $token ) ) {
481 return $watchlistOwner->getId();
483 return $user->
getId();
489 if ( in_array( self::FILTER_MINOR, $options[
'filters'] ) ) {
490 $conds[] =
'rc_minor != 0';
491 } elseif ( in_array( self::FILTER_NOT_MINOR, $options[
'filters'] ) ) {
492 $conds[] =
'rc_minor = 0';
495 if ( in_array( self::FILTER_BOT, $options[
'filters'] ) ) {
496 $conds[] =
'rc_bot != 0';
497 } elseif ( in_array( self::FILTER_NOT_BOT, $options[
'filters'] ) ) {
498 $conds[] =
'rc_bot = 0';
501 if ( in_array( self::FILTER_ANON, $options[
'filters'] ) ) {
502 $conds[] = $this->actorMigration->isAnon(
503 $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user']
505 } elseif ( in_array( self::FILTER_NOT_ANON, $options[
'filters'] ) ) {
506 $conds[] = $this->actorMigration->isNotAnon(
507 $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user']
514 if ( in_array( self::FILTER_PATROLLED, $options[
'filters'] ) ) {
516 } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options[
'filters'] ) ) {
520 if ( in_array( self::FILTER_AUTOPATROLLED, $options[
'filters'] ) ) {
522 } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options[
'filters'] ) ) {
527 if ( in_array( self::FILTER_UNREAD, $options[
'filters'] ) ) {
528 $conds[] =
'rc_timestamp >= wl_notificationtimestamp';
529 } elseif ( in_array( self::FILTER_NOT_UNREAD, $options[
'filters'] ) ) {
531 $conds[] =
'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
538 if ( !isset( $options[
'start'] ) && !isset( $options[
'end'] ) ) {
544 if ( isset( $options[
'start'] ) ) {
545 $after = $options[
'dir'] === self::DIR_OLDER ?
'<=' :
'>=';
546 $conds[] =
'rc_timestamp ' . $after .
' ' .
549 if ( isset( $options[
'end'] ) ) {
550 $before = $options[
'dir'] === self::DIR_OLDER ?
'>=' :
'<=';
551 $conds[] =
'rc_timestamp ' . $before .
' ' .
559 if ( !array_key_exists(
'onlyByUser', $options ) && !array_key_exists(
'notByUser', $options ) ) {
565 if ( array_key_exists(
'onlyByUser', $options ) ) {
567 $conds[] = $this->actorMigration->getWhere( $db,
'rc_user', $byUser )[
'conds'];
568 } elseif ( array_key_exists(
'notByUser', $options ) ) {
570 $conds[] =
'NOT(' . $this->actorMigration->getWhere( $db,
'rc_user', $byUser )[
'conds'] .
')';
575 if ( !$this->permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
576 $bitmask = RevisionRecord::DELETED_USER;
577 } elseif ( !$this->permissionManager
578 ->userHasAnyRight( $user,
'suppressrevision',
'viewsuppressed' )
580 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
583 $conds[] = $db->
bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask";
593 if ( !$this->permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
595 } elseif ( !$this->permissionManager
596 ->userHasAnyRight( $user,
'suppressrevision',
'viewsuppressed' )
603 $db->
bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask",
610 $op = $options[
'dir'] === self::DIR_OLDER ?
'<' :
'>';
611 list( $rcTimestamp, $rcId ) = $startFrom;
616 "rc_timestamp $op $rcTimestamp",
619 "rc_timestamp = $rcTimestamp",
632 $conds = [
'wl_user' => $user->
getId() ];
633 if ( $options[
'namespaceIds'] ) {
634 $conds[
'wl_namespace'] = array_map(
'intval', $options[
'namespaceIds'] );
636 if ( isset( $options[
'filter'] ) ) {
638 if (
$filter === self::FILTER_CHANGED ) {
639 $conds[] =
'wl_notificationtimestamp IS NOT NULL';
641 $conds[] =
'wl_notificationtimestamp IS NULL';
645 if ( isset( $options[
'from'] ) ) {
646 $op = $options[
'sort'] === self::SORT_ASC ?
'>' :
'<';
649 if ( isset( $options[
'until'] ) ) {
650 $op = $options[
'sort'] === self::SORT_ASC ?
'<' :
'>';
653 if ( isset( $options[
'startFrom'] ) ) {
654 $op = $options[
'sort'] === self::SORT_ASC ?
'>' :
'<';
689 if ( array_key_exists(
'dir', $options ) ) {
690 $sort = $options[
'dir'] === self::DIR_OLDER ?
' DESC' :
'';
691 $dbOptions[
'ORDER BY'] = [
'rc_timestamp' .
$sort,
'rc_id' .
$sort ];
694 if ( array_key_exists(
'limit', $options ) ) {
695 $dbOptions[
'LIMIT'] = (int)$options[
'limit'] + 1;
703 if ( array_key_exists(
'sort', $options ) ) {
704 $dbOptions[
'ORDER BY'] = [
705 "wl_namespace {$options['sort']}",
706 "wl_title {$options['sort']}"
708 if ( count( $options[
'namespaceIds'] ) === 1 ) {
709 $dbOptions[
'ORDER BY'] =
"wl_title {$options['sort']}";
712 if ( array_key_exists(
'limit', $options ) ) {
713 $dbOptions[
'LIMIT'] = (int)$options[
'limit'];
720 'watchlist' => [
'JOIN',
722 'wl_namespace=rc_namespace',
727 if ( !$options[
'allRevisions'] ) {
728 $joinConds[
'page'] = [
'LEFT JOIN',
'rc_cur_id=page_id' ];
730 if ( in_array( self::INCLUDE_COMMENT, $options[
'includeFields'] ) ) {
731 $joinConds += $this->commentStore->getJoin(
'rc_comment' )[
'joins'];
733 if ( in_array( self::INCLUDE_USER, $options[
'includeFields'] ) ||
734 in_array( self::INCLUDE_USER_ID, $options[
'includeFields'] ) ||
735 in_array( self::FILTER_ANON, $options[
'filters'] ) ||
736 in_array( self::FILTER_NOT_ANON, $options[
'filters'] ) ||
737 array_key_exists(
'onlyByUser', $options ) || array_key_exists(
'notByUser', $options )
739 $joinConds += $this->actorMigration->getJoin(
'rc_user' )[
'joins'];