5 use Wikimedia\Assert\Assert;
94 return $this->loadBalancer->getConnectionRef(
DB_REPLICA, [
'watchlist' ] );
144 'includeFields' => [],
145 'namespaceIds' => [],
147 'allRevisions' =>
false,
148 'usedInGenerator' =>
false
154 '$options[\'rcTypes\']',
155 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
158 !isset(
$options[
'dir'] ) || in_array(
$options[
'dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
160 'must be DIR_OLDER or DIR_NEWER'
163 !isset(
$options[
'start'] ) && !isset(
$options[
'end'] ) && $startFrom ===
null
166 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
170 '$options[\'startFrom\']',
171 'must not be provided, use $startFrom instead'
174 !isset( $startFrom ) || ( is_array( $startFrom ) &&
count( $startFrom ) === 2 ),
176 'must be a two-element array'
178 if ( array_key_exists(
'watchlistOwner',
$options ) ) {
179 Assert::parameterType(
182 '$options[\'watchlistOwner\']'
185 isset(
$options[
'watchlistOwnerToken'] ),
186 '$options[\'watchlistOwnerToken\']',
187 'must be provided when providing watchlistOwner option'
199 if ( $startFrom !==
null ) {
204 $extension->modifyWatchedItemsWithRCInfoQuery(
223 $limit = $dbOptions[
'LIMIT'] ?? INF;
226 foreach (
$res as $row ) {
227 if ( --$limit <= 0 ) {
228 $startFrom = [ $row->rc_timestamp, $row->rc_id ];
235 new TitleValue( (
int)$row->rc_namespace, $row->rc_title ),
236 $row->wl_notificationtimestamp
243 $extension->modifyWatchedItemsWithRCInfo(
$user,
$options, $db, $items,
$res, $startFrom );
269 if (
$user->isAnon() ) {
275 $options += [
'namespaceIds' => [] ];
278 !isset(
$options[
'sort'] ) || in_array(
$options[
'sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
279 '$options[\'sort\']',
280 'must be SORT_ASC or SORT_DESC'
283 !isset(
$options[
'filter'] ) || in_array(
284 $options[
'filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
286 '$options[\'filter\']',
287 'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
292 '$options[\'sort\']',
293 'must be provided if any of "from", "until", "startFrom" options is provided'
303 [
'wl_namespace',
'wl_title',
'wl_notificationtimestamp' ],
310 foreach (
$res as $row ) {
314 new TitleValue( (
int)$row->wl_namespace, $row->wl_title ),
315 $row->wl_notificationtimestamp
319 return $watchedItems;
325 $allFields = get_object_vars( $row );
326 $rcKeys = array_filter(
327 array_keys( $allFields ),
329 return substr( $key, 0, 3 ) ===
'rc_';
332 return array_intersect_key( $allFields, array_flip( $rcKeys ) );
336 $tables = [
'recentchanges',
'watchlist' ];
340 if ( in_array( self::INCLUDE_COMMENT,
$options[
'includeFields'] ) ) {
341 $tables += $this->commentStore->getJoin(
'rc_comment' )[
'tables'];
343 if ( in_array( self::INCLUDE_TAGS,
$options[
'includeFields'] ) ) {
346 if ( in_array( self::INCLUDE_USER,
$options[
'includeFields'] ) ||
347 in_array( self::INCLUDE_USER_ID,
$options[
'includeFields'] ) ||
348 in_array( self::FILTER_ANON,
$options[
'filters'] ) ||
349 in_array( self::FILTER_NOT_ANON,
$options[
'filters'] ) ||
350 array_key_exists(
'onlyByUser',
$options ) || array_key_exists(
'notByUser',
$options )
352 $tables += $this->actorMigration->getJoin(
'rc_user' )[
'tables'];
365 'wl_notificationtimestamp'
373 if (
$options[
'usedInGenerator'] ) {
375 $rcIdFields = [
'rc_this_oldid' ];
377 $rcIdFields = [
'rc_cur_id' ];
380 $fields = array_merge( $fields, $rcIdFields );
382 if ( in_array( self::INCLUDE_FLAGS,
$options[
'includeFields'] ) ) {
383 $fields = array_merge( $fields, [
'rc_type',
'rc_minor',
'rc_bot' ] );
385 if ( in_array( self::INCLUDE_USER,
$options[
'includeFields'] ) ) {
386 $fields[
'rc_user_text'] = $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user_text'];
388 if ( in_array( self::INCLUDE_USER_ID,
$options[
'includeFields'] ) ) {
389 $fields[
'rc_user'] = $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user'];
391 if ( in_array( self::INCLUDE_COMMENT,
$options[
'includeFields'] ) ) {
392 $fields += $this->commentStore->getJoin(
'rc_comment' )[
'fields'];
394 if ( in_array( self::INCLUDE_PATROL_INFO,
$options[
'includeFields'] ) ) {
395 $fields = array_merge( $fields, [
'rc_patrolled',
'rc_log_type' ] );
397 if ( in_array( self::INCLUDE_SIZES,
$options[
'includeFields'] ) ) {
398 $fields = array_merge( $fields, [
'rc_old_len',
'rc_new_len' ] );
400 if ( in_array( self::INCLUDE_LOG_INFO,
$options[
'includeFields'] ) ) {
401 $fields = array_merge( $fields, [
'rc_logid',
'rc_log_type',
'rc_log_action',
'rc_params' ] );
403 if ( in_array( self::INCLUDE_TAGS,
$options[
'includeFields'] ) ) {
405 $fields[
'rc_tags'] =
'ts_tags';
417 $conds = [
'wl_user' => $watchlistOwnerId ];
421 [
'rc_this_oldid=page_latest',
'rc_type=' .
RC_LOG ],
427 $conds[
'wl_namespace'] = array_map(
'intval',
$options[
'namespaceIds'] );
430 if ( array_key_exists(
'rcTypes',
$options ) ) {
431 $conds[
'rc_type'] = array_map(
'intval',
$options[
'rcTypes'] );
434 $conds = array_merge(
442 if ( $db->
getType() ===
'mysql' ) {
444 $conds[] =
'rc_timestamp > ' . $db->
addQuotes(
'' );
451 if ( $deletedPageLogCond ) {
452 $conds[] = $deletedPageLogCond;
459 if ( array_key_exists(
'watchlistOwner',
$options ) ) {
461 $watchlistOwner =
$options[
'watchlistOwner'];
462 $ownersToken = $watchlistOwner->getOption(
'watchlisttoken' );
463 $token =
$options[
'watchlistOwnerToken'];
464 if ( $ownersToken ==
'' || !hash_equals( $ownersToken, $token ) ) {
467 return $watchlistOwner->getId();
469 return $user->getId();
475 if ( in_array( self::FILTER_MINOR,
$options[
'filters'] ) ) {
476 $conds[] =
'rc_minor != 0';
477 } elseif ( in_array( self::FILTER_NOT_MINOR,
$options[
'filters'] ) ) {
478 $conds[] =
'rc_minor = 0';
481 if ( in_array( self::FILTER_BOT,
$options[
'filters'] ) ) {
482 $conds[] =
'rc_bot != 0';
483 } elseif ( in_array( self::FILTER_NOT_BOT,
$options[
'filters'] ) ) {
484 $conds[] =
'rc_bot = 0';
487 if ( in_array( self::FILTER_ANON,
$options[
'filters'] ) ) {
488 $conds[] = $this->actorMigration->isAnon(
489 $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user']
491 } elseif ( in_array( self::FILTER_NOT_ANON,
$options[
'filters'] ) ) {
492 $conds[] = $this->actorMigration->isNotAnon(
493 $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user']
497 if (
$user->useRCPatrol() ||
$user->useNPPatrol() ) {
500 if ( in_array( self::FILTER_PATROLLED,
$options[
'filters'] ) ) {
502 } elseif ( in_array( self::FILTER_NOT_PATROLLED,
$options[
'filters'] ) ) {
506 if ( in_array( self::FILTER_AUTOPATROLLED,
$options[
'filters'] ) ) {
508 } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED,
$options[
'filters'] ) ) {
513 if ( in_array( self::FILTER_UNREAD,
$options[
'filters'] ) ) {
514 $conds[] =
'rc_timestamp >= wl_notificationtimestamp';
515 } elseif ( in_array( self::FILTER_NOT_UNREAD,
$options[
'filters'] ) ) {
517 $conds[] =
'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
531 $after =
$options[
'dir'] === self::DIR_OLDER ?
'<=' :
'>=';
532 $conds[] =
'rc_timestamp ' . $after .
' ' .
536 $before =
$options[
'dir'] === self::DIR_OLDER ?
'>=' :
'<=';
537 $conds[] =
'rc_timestamp ' . $before .
' ' .
545 if ( !array_key_exists(
'onlyByUser',
$options ) && !array_key_exists(
'notByUser',
$options ) ) {
551 if ( array_key_exists(
'onlyByUser',
$options ) ) {
553 $conds[] = $this->actorMigration->getWhere( $db,
'rc_user', $byUser )[
'conds'];
554 } elseif ( array_key_exists(
'notByUser',
$options ) ) {
556 $conds[] =
'NOT(' . $this->actorMigration->getWhere( $db,
'rc_user', $byUser )[
'conds'] .
')';
561 if ( !
$user->isAllowed(
'deletedhistory' ) ) {
563 } elseif ( !
$user->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
567 $conds[] = $db->
bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask";
577 if ( !
$user->isAllowed(
'deletedhistory' ) ) {
579 } elseif ( !
$user->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
585 $db->
bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask",
592 $op =
$options[
'dir'] === self::DIR_OLDER ?
'<' :
'>';
593 list( $rcTimestamp, $rcId ) = $startFrom;
598 "rc_timestamp $op $rcTimestamp",
601 "rc_timestamp = $rcTimestamp",
612 $conds = [
'wl_user' =>
$user->getId() ];
614 $conds[
'wl_namespace'] = array_map(
'intval',
$options[
'namespaceIds'] );
616 if ( isset(
$options[
'filter'] ) ) {
618 if ( $filter === self::FILTER_CHANGED ) {
619 $conds[] =
'wl_notificationtimestamp IS NOT NULL';
621 $conds[] =
'wl_notificationtimestamp IS NULL';
626 $op =
$options[
'sort'] === self::SORT_ASC ?
'>' :
'<';
630 $op =
$options[
'sort'] === self::SORT_ASC ?
'<' :
'>';
633 if ( isset(
$options[
'startFrom'] ) ) {
634 $op =
$options[
'sort'] === self::SORT_ASC ?
'>' :
'<';
669 if ( array_key_exists(
'dir',
$options ) ) {
671 $dbOptions[
'ORDER BY'] = [
'rc_timestamp' .
$sort,
'rc_id' .
$sort ];
674 if ( array_key_exists(
'limit',
$options ) ) {
675 $dbOptions[
'LIMIT'] = (int)
$options[
'limit'] + 1;
683 if ( array_key_exists(
'sort',
$options ) ) {
684 $dbOptions[
'ORDER BY'] = [
685 "wl_namespace {$options['sort']}",
686 "wl_title {$options['sort']}"
689 $dbOptions[
'ORDER BY'] =
"wl_title {$options['sort']}";
692 if ( array_key_exists(
'limit',
$options ) ) {
693 $dbOptions[
'LIMIT'] = (int)
$options[
'limit'];
700 'watchlist' => [
'INNER JOIN',
702 'wl_namespace=rc_namespace',
708 $joinConds[
'page'] = [
'LEFT JOIN',
'rc_cur_id=page_id' ];
710 if ( in_array( self::INCLUDE_COMMENT,
$options[
'includeFields'] ) ) {
711 $joinConds += $this->commentStore->getJoin(
'rc_comment' )[
'joins'];
713 if ( in_array( self::INCLUDE_TAGS,
$options[
'includeFields'] ) ) {
714 $joinConds[
'tag_summary'] = [
'LEFT JOIN', [
'rc_id=ts_rc_id' ] ];
716 if ( in_array( self::INCLUDE_USER,
$options[
'includeFields'] ) ||
717 in_array( self::INCLUDE_USER_ID,
$options[
'includeFields'] ) ||
718 in_array( self::FILTER_ANON,
$options[
'filters'] ) ||
719 in_array( self::FILTER_NOT_ANON,
$options[
'filters'] ) ||
720 array_key_exists(
'onlyByUser',
$options ) || array_key_exists(
'notByUser',
$options )
722 $joinConds += $this->actorMigration->getJoin(
'rc_user' )[
'joins'];