9use Wikimedia\Assert\Assert;
99 bool $expiryEnabled =
false,
100 int $maxQueryExecutionTime = 0
102 $this->loadBalancer = $loadBalancer;
103 $this->commentStore = $commentStore;
104 $this->actorMigration = $actorMigration;
105 $this->watchedItemStore = $watchedItemStore;
106 $this->permissionManager = $permissionManager;
107 $this->hookRunner =
new HookRunner( $hookContainer );
116 if ( $this->extensions ===
null ) {
117 $this->extensions = [];
118 $this->hookRunner->onWatchedItemQueryServiceExtensions( $this->extensions, $this );
120 return $this->extensions;
127 return $this->loadBalancer->getConnectionRef(
DB_REPLICA, [
'watchlist' ] );
174 User $user, array $options = [], &$startFrom =
null
177 'includeFields' => [],
178 'namespaceIds' => [],
180 'allRevisions' =>
false,
181 'usedInGenerator' => false
185 !isset( $options[
'rcTypes'] )
187 '$options[\'rcTypes\']',
188 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
191 !isset( $options[
'dir'] ) || in_array( $options[
'dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
193 'must be DIR_OLDER or DIR_NEWER'
196 !isset( $options[
'start'] ) && !isset( $options[
'end'] ) && $startFrom ===
null
197 || isset( $options[
'dir'] ),
199 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
202 !isset( $options[
'startFrom'] ),
203 '$options[\'startFrom\']',
204 'must not be provided, use $startFrom instead'
207 !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
209 'must be a two-element array'
211 if ( array_key_exists(
'watchlistOwner', $options ) ) {
212 Assert::parameterType(
214 $options[
'watchlistOwner'],
215 '$options[\'watchlistOwner\']'
218 isset( $options[
'watchlistOwnerToken'] ),
219 '$options[\'watchlistOwnerToken\']',
220 'must be provided when providing watchlistOwner option'
232 if ( $startFrom !==
null ) {
237 $extension->modifyWatchedItemsWithRCInfoQuery(
238 $user, $options, $db,
256 $limit = $dbOptions[
'LIMIT'] ?? INF;
259 foreach (
$res as $row ) {
260 if ( --$limit <= 0 ) {
261 $startFrom = [ $row->rc_timestamp, $row->rc_id ];
265 $target =
new TitleValue( (
int)$row->rc_namespace, $row->rc_title );
270 $this->watchedItemStore->getLatestNotificationTimestamp(
271 $row->wl_notificationtimestamp, $user, $target
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
370 return $watchedItems;
376 $allFields = get_object_vars( $row );
377 $rcKeys = array_filter(
378 array_keys( $allFields ),
380 return substr( $key, 0, 3 ) ===
'rc_';
383 return array_intersect_key( $allFields, array_flip( $rcKeys ) );
387 $tables = [
'recentchanges',
'watchlist' ];
389 if ( $this->expiryEnabled ) {
390 $tables[] =
'watchlist_expiry';
393 if ( !$options[
'allRevisions'] ) {
396 if ( in_array( self::INCLUDE_COMMENT, $options[
'includeFields'] ) ) {
397 $tables += $this->commentStore->getJoin(
'rc_comment' )[
'tables'];
399 if ( in_array( self::INCLUDE_USER, $options[
'includeFields'] ) ||
400 in_array( self::INCLUDE_USER_ID, $options[
'includeFields'] ) ||
401 in_array( self::FILTER_ANON, $options[
'filters'] ) ||
402 in_array( self::FILTER_NOT_ANON, $options[
'filters'] ) ||
403 array_key_exists(
'onlyByUser', $options ) || array_key_exists(
'notByUser', $options )
405 $tables += $this->actorMigration->getJoin(
'rc_user' )[
'tables'];
418 'wl_notificationtimestamp'
421 if ( $this->expiryEnabled ) {
422 $fields[] =
'we_expiry';
430 if ( $options[
'usedInGenerator'] ) {
431 if ( $options[
'allRevisions'] ) {
432 $rcIdFields = [
'rc_this_oldid' ];
434 $rcIdFields = [
'rc_cur_id' ];
437 $fields = array_merge( $fields, $rcIdFields );
439 if ( in_array( self::INCLUDE_FLAGS, $options[
'includeFields'] ) ) {
440 $fields = array_merge( $fields, [
'rc_type',
'rc_minor',
'rc_bot' ] );
442 if ( in_array( self::INCLUDE_USER, $options[
'includeFields'] ) ) {
443 $fields[
'rc_user_text'] = $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user_text'];
445 if ( in_array( self::INCLUDE_USER_ID, $options[
'includeFields'] ) ) {
446 $fields[
'rc_user'] = $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user'];
448 if ( in_array( self::INCLUDE_COMMENT, $options[
'includeFields'] ) ) {
449 $fields += $this->commentStore->getJoin(
'rc_comment' )[
'fields'];
451 if ( in_array( self::INCLUDE_PATROL_INFO, $options[
'includeFields'] ) ) {
452 $fields = array_merge( $fields, [
'rc_patrolled',
'rc_log_type' ] );
454 if ( in_array( self::INCLUDE_SIZES, $options[
'includeFields'] ) ) {
455 $fields = array_merge( $fields, [
'rc_old_len',
'rc_new_len' ] );
457 if ( in_array( self::INCLUDE_LOG_INFO, $options[
'includeFields'] ) ) {
458 $fields = array_merge( $fields, [
'rc_logid',
'rc_log_type',
'rc_log_action',
'rc_params' ] );
460 if ( in_array( self::INCLUDE_TAGS, $options[
'includeFields'] ) ) {
474 $conds = [
'wl_user' => $watchlistOwnerId ];
476 if ( $this->expiryEnabled ) {
480 if ( !$options[
'allRevisions'] ) {
482 [
'rc_this_oldid=page_latest',
'rc_type=' .
RC_LOG ],
487 if ( $options[
'namespaceIds'] ) {
488 $conds[
'wl_namespace'] = array_map(
'intval', $options[
'namespaceIds'] );
491 if ( array_key_exists(
'rcTypes', $options ) ) {
492 $conds[
'rc_type'] = array_map(
'intval', $options[
'rcTypes'] );
495 $conds = array_merge(
502 if ( !isset( $options[
'start'] ) && !isset( $options[
'end'] ) && $db->
getType() ===
'mysql' ) {
504 $conds[] =
'rc_timestamp > ' . $db->
addQuotes(
'' );
510 if ( $deletedPageLogCond ) {
511 $conds[] = $deletedPageLogCond;
518 if ( array_key_exists(
'watchlistOwner', $options ) ) {
520 $watchlistOwner = $options[
'watchlistOwner'];
522 $watchlistOwner->getOption(
'watchlisttoken' );
523 $token = $options[
'watchlistOwnerToken'];
524 if ( $ownersToken ==
'' || !hash_equals( $ownersToken, $token ) ) {
525 throw ApiUsageException::newWithMessage(
null,
'apierror-bad-watchlist-token',
'bad_wltoken' );
527 return $watchlistOwner->getId();
529 return $user->
getId();
535 if ( in_array( self::FILTER_MINOR, $options[
'filters'] ) ) {
536 $conds[] =
'rc_minor != 0';
537 } elseif ( in_array( self::FILTER_NOT_MINOR, $options[
'filters'] ) ) {
538 $conds[] =
'rc_minor = 0';
541 if ( in_array( self::FILTER_BOT, $options[
'filters'] ) ) {
542 $conds[] =
'rc_bot != 0';
543 } elseif ( in_array( self::FILTER_NOT_BOT, $options[
'filters'] ) ) {
544 $conds[] =
'rc_bot = 0';
547 if ( in_array( self::FILTER_ANON, $options[
'filters'] ) ) {
548 $conds[] = $this->actorMigration->isAnon(
549 $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user']
551 } elseif ( in_array( self::FILTER_NOT_ANON, $options[
'filters'] ) ) {
552 $conds[] = $this->actorMigration->isNotAnon(
553 $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user']
560 if ( in_array( self::FILTER_PATROLLED, $options[
'filters'] ) ) {
561 $conds[] =
'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
562 } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options[
'filters'] ) ) {
563 $conds[
'rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
566 if ( in_array( self::FILTER_AUTOPATROLLED, $options[
'filters'] ) ) {
567 $conds[
'rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
568 } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options[
'filters'] ) ) {
569 $conds[] =
'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED;
573 if ( in_array( self::FILTER_UNREAD, $options[
'filters'] ) ) {
574 $conds[] =
'rc_timestamp >= wl_notificationtimestamp';
575 } elseif ( in_array( self::FILTER_NOT_UNREAD, $options[
'filters'] ) ) {
577 $conds[] =
'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
584 if ( !isset( $options[
'start'] ) && !isset( $options[
'end'] ) ) {
590 if ( isset( $options[
'start'] ) ) {
591 $after = $options[
'dir'] === self::DIR_OLDER ?
'<=' :
'>=';
592 $conds[] =
'rc_timestamp ' . $after .
' ' .
595 if ( isset( $options[
'end'] ) ) {
596 $before = $options[
'dir'] === self::DIR_OLDER ?
'>=' :
'<=';
597 $conds[] =
'rc_timestamp ' . $before .
' ' .
605 if ( !array_key_exists(
'onlyByUser', $options ) && !array_key_exists(
'notByUser', $options ) ) {
611 if ( array_key_exists(
'onlyByUser', $options ) ) {
613 $conds[] = $this->actorMigration->getWhere( $db,
'rc_user', $byUser )[
'conds'];
614 } elseif ( array_key_exists(
'notByUser', $options ) ) {
616 $conds[] =
'NOT(' . $this->actorMigration->getWhere( $db,
'rc_user', $byUser )[
'conds'] .
')';
621 if ( !$this->permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
622 $bitmask = RevisionRecord::DELETED_USER;
623 } elseif ( !$this->permissionManager
624 ->userHasAnyRight( $user,
'suppressrevision',
'viewsuppressed' )
626 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
629 $conds[] = $db->
bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask";
639 if ( !$this->permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
641 } elseif ( !$this->permissionManager
642 ->userHasAnyRight( $user,
'suppressrevision',
'viewsuppressed' )
649 $db->
bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask",
656 $op = $options[
'dir'] === self::DIR_OLDER ?
'<' :
'>';
657 list( $rcTimestamp, $rcId ) = $startFrom;
662 "rc_timestamp $op $rcTimestamp",
665 "rc_timestamp = $rcTimestamp",
678 $conds = [
'wl_user' => $user->
getId() ];
679 if ( $options[
'namespaceIds'] ) {
680 $conds[
'wl_namespace'] = array_map(
'intval', $options[
'namespaceIds'] );
682 if ( isset( $options[
'filter'] ) ) {
683 $filter = $options[
'filter'];
684 if ( $filter === self::FILTER_CHANGED ) {
685 $conds[] =
'wl_notificationtimestamp IS NOT NULL';
687 $conds[] =
'wl_notificationtimestamp IS NULL';
691 if ( isset( $options[
'from'] ) ) {
692 $op = $options[
'sort'] === self::SORT_ASC ?
'>' :
'<';
695 if ( isset( $options[
'until'] ) ) {
696 $op = $options[
'sort'] === self::SORT_ASC ?
'<' :
'>';
699 if ( isset( $options[
'startFrom'] ) ) {
700 $op = $options[
'sort'] === self::SORT_ASC ?
'>' :
'<';
735 if ( array_key_exists(
'dir', $options ) ) {
736 $sort = $options[
'dir'] === self::DIR_OLDER ?
' DESC' :
'';
737 $dbOptions[
'ORDER BY'] = [
'rc_timestamp' . $sort,
'rc_id' . $sort ];
740 if ( array_key_exists(
'limit', $options ) ) {
741 $dbOptions[
'LIMIT'] = (int)$options[
'limit'] + 1;
743 if ( $this->maxQueryExecutionTime ) {
744 $dbOptions[
'MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime;
751 if ( array_key_exists(
'sort', $options ) ) {
752 $dbOptions[
'ORDER BY'] = [
753 "wl_namespace {$options['sort']}",
754 "wl_title {$options['sort']}"
756 if ( count( $options[
'namespaceIds'] ) === 1 ) {
757 $dbOptions[
'ORDER BY'] =
"wl_title {$options['sort']}";
760 if ( array_key_exists(
'limit', $options ) ) {
761 $dbOptions[
'LIMIT'] = (int)$options[
'limit'];
763 if ( $this->maxQueryExecutionTime ) {
764 $dbOptions[
'MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime;
771 'watchlist' => [
'JOIN',
773 'wl_namespace=rc_namespace',
779 if ( $this->expiryEnabled ) {
780 $joinConds[
'watchlist_expiry'] = [
'LEFT JOIN',
'wl_id = we_item' ];
783 if ( !$options[
'allRevisions'] ) {
784 $joinConds[
'page'] = [
'LEFT JOIN',
'rc_cur_id=page_id' ];
786 if ( in_array( self::INCLUDE_COMMENT, $options[
'includeFields'] ) ) {
787 $joinConds += $this->commentStore->getJoin(
'rc_comment' )[
'joins'];
789 if ( in_array( self::INCLUDE_USER, $options[
'includeFields'] ) ||
790 in_array( self::INCLUDE_USER_ID, $options[
'includeFields'] ) ||
791 in_array( self::FILTER_ANON, $options[
'filters'] ) ||
792 in_array( self::FILTER_NOT_ANON, $options[
'filters'] ) ||
793 array_key_exists(
'onlyByUser', $options ) || array_key_exists(
'notByUser', $options )
795 $joinConds += $this->actorMigration->getJoin(
'rc_user' )[
'joins'];
This class handles the logic for the actor table migration and should always be used in lieu of direc...
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
bool $expiryEnabled
Correlates to $wgWatchlistExpiry feature flag.
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...
int $maxQueryExecutionTime
Max query execution time.
__construct(ILoadBalancer $loadBalancer, CommentStore $commentStore, ActorMigration $actorMigration, WatchedItemStoreInterface $watchedItemStore, PermissionManager $permissionManager, HookContainer $hookContainer, bool $expiryEnabled=false, int $maxQueryExecutionTime=0)
getExtraDeletedPageLogEntryRelatedCond(IDatabase $db, UserIdentity $user)
getStartEndConds(IDatabase $db, array $options)
getUserRelatedConds(IDatabase $db, UserIdentity $user, array $options)
ActorMigration $actorMigration
const FILTER_NOT_PATROLLED
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.