7use Wikimedia\Assert\Assert;
84 $this->loadBalancer = $loadBalancer;
85 $this->commentStore = $commentStore;
86 $this->actorMigration = $actorMigration;
87 $this->watchedItemStore = $watchedItemStore;
88 $this->permissionManager = $permissionManager;
95 if ( $this->extensions ===
null ) {
96 $this->extensions = [];
97 Hooks::run(
'WatchedItemQueryServiceExtensions', [ &$this->extensions, $this ] );
99 return $this->extensions;
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'] ) ) {
515 $conds[] =
'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
516 } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options[
'filters'] ) ) {
517 $conds[
'rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
520 if ( in_array( self::FILTER_AUTOPATROLLED, $options[
'filters'] ) ) {
521 $conds[
'rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
522 } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options[
'filters'] ) ) {
523 $conds[] =
'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED;
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'];
This class handles the logic for the actor table migration.
static newWithMessage(ApiBase $module=null, $msg, $code=null, $data=null, $httpCode=0)
Represents a page (or page fragment) title within MediaWiki.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
useNPPatrol()
Check whether to enable new pages patrol features for this user.
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
const INCLUDE_AUTOPATROL_INFO
const FILTER_NOT_AUTOPATROLLED
getWatchedItemsForUser(UserIdentity $user, array $options=[])
For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser.
WatchedItemQueryServiceExtension[] null $extensions
const INCLUDE_PATROL_INFO
getWatchedItemsWithRCInfoQueryDbOptions(array $options)
getWatchedItemsWithRCInfoQueryFilterConds(User $user, array $options)
ILoadBalancer $loadBalancer
getWatchedItemsWithRCInfoQueryTables(array $options)
getWatchedItemsWithRCInfoQueryFields(array $options)
getWatchedItemsWithRCInfoQueryConds(IDatabase $db, User $user, array $options)
WatchedItemStoreInterface $watchedItemStore
const FILTER_AUTOPATROLLED
getFromUntilTargetConds(IDatabase $db, LinkTarget $target, $op)
Creates a query condition part for getting only items before or after the given link target (while or...
getExtraDeletedPageLogEntryRelatedCond(IDatabase $db, UserIdentity $user)
getStartEndConds(IDatabase $db, array $options)
getUserRelatedConds(IDatabase $db, UserIdentity $user, array $options)
ActorMigration $actorMigration
const FILTER_NOT_PATROLLED
__construct(ILoadBalancer $loadBalancer, CommentStore $commentStore, ActorMigration $actorMigration, WatchedItemStoreInterface $watchedItemStore, PermissionManager $permissionManager)
getWatchlistOwnerId(UserIdentity $user, array $options)
getWatchedItemsForUserQueryConds(IDatabase $db, UserIdentity $user, array $options)
PermissionManager $permissionManager
getWatchedItemsWithRecentChangeInfo(User $user, array $options=[], &$startFrom=null)
getWatchedItemsWithRCInfoQueryJoinConds(array $options)
getWatchedItemsForUserQueryDbOptions(array $options)
getRecentChangeFieldsFromRow(stdClass $row)
getStartFromConds(IDatabase $db, array $options, array $startFrom)
CommentStore $commentStore
Representation of a pair of user and title for watchlist entries.