5 use Wikimedia\Assert\Assert;
98 return $this->loadBalancer->getConnectionRef(
DB_REPLICA, [
'watchlist' ] );
148 'includeFields' => [],
149 'namespaceIds' => [],
151 'allRevisions' =>
false,
152 'usedInGenerator' =>
false
158 '$options[\'rcTypes\']',
159 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
162 !isset(
$options[
'dir'] ) || in_array(
$options[
'dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
164 'must be DIR_OLDER or DIR_NEWER'
167 !isset(
$options[
'start'] ) && !isset(
$options[
'end'] ) && $startFrom ===
null
170 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
174 '$options[\'startFrom\']',
175 'must not be provided, use $startFrom instead'
178 !isset( $startFrom ) || ( is_array( $startFrom ) &&
count( $startFrom ) === 2 ),
180 'must be a two-element array'
182 if ( array_key_exists(
'watchlistOwner',
$options ) ) {
183 Assert::parameterType(
186 '$options[\'watchlistOwner\']'
189 isset(
$options[
'watchlistOwnerToken'] ),
190 '$options[\'watchlistOwnerToken\']',
191 'must be provided when providing watchlistOwner option'
203 if ( $startFrom !==
null ) {
208 $extension->modifyWatchedItemsWithRCInfoQuery(
227 $limit = $dbOptions[
'LIMIT'] ?? INF;
230 foreach (
$res as $row ) {
231 if ( --$limit <= 0 ) {
232 $startFrom = [ $row->rc_timestamp, $row->rc_id ];
236 $target =
new TitleValue( (
int)$row->rc_namespace, $row->rc_title );
241 $this->watchedItemStore->getLatestNotificationTimestamp(
242 $row->wl_notificationtimestamp,
$user, $target
250 $extension->modifyWatchedItemsWithRCInfo(
$user,
$options, $db, $items,
$res, $startFrom );
276 if (
$user->isAnon() ) {
282 $options += [
'namespaceIds' => [] ];
285 !isset(
$options[
'sort'] ) || in_array(
$options[
'sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
286 '$options[\'sort\']',
287 'must be SORT_ASC or SORT_DESC'
290 !isset(
$options[
'filter'] ) || in_array(
291 $options[
'filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
293 '$options[\'filter\']',
294 'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
299 '$options[\'sort\']',
300 'must be provided if any of "from", "until", "startFrom" options is provided'
310 [
'wl_namespace',
'wl_title',
'wl_notificationtimestamp' ],
317 foreach (
$res as $row ) {
318 $target =
new TitleValue( (
int)$row->wl_namespace, $row->wl_title );
323 $this->watchedItemStore->getLatestNotificationTimestamp(
324 $row->wl_notificationtimestamp,
$user, $target
329 return $watchedItems;
335 $allFields = get_object_vars( $row );
336 $rcKeys = array_filter(
337 array_keys( $allFields ),
339 return substr( $key, 0, 3 ) ===
'rc_';
342 return array_intersect_key( $allFields, array_flip( $rcKeys ) );
346 $tables = [
'recentchanges',
'watchlist' ];
350 if ( in_array( self::INCLUDE_COMMENT,
$options[
'includeFields'] ) ) {
351 $tables += $this->commentStore->getJoin(
'rc_comment' )[
'tables'];
353 if ( in_array( self::INCLUDE_USER,
$options[
'includeFields'] ) ||
354 in_array( self::INCLUDE_USER_ID,
$options[
'includeFields'] ) ||
355 in_array( self::FILTER_ANON,
$options[
'filters'] ) ||
356 in_array( self::FILTER_NOT_ANON,
$options[
'filters'] ) ||
357 array_key_exists(
'onlyByUser',
$options ) || array_key_exists(
'notByUser',
$options )
359 $tables += $this->actorMigration->getJoin(
'rc_user' )[
'tables'];
372 'wl_notificationtimestamp'
380 if (
$options[
'usedInGenerator'] ) {
382 $rcIdFields = [
'rc_this_oldid' ];
384 $rcIdFields = [
'rc_cur_id' ];
387 $fields = array_merge( $fields, $rcIdFields );
389 if ( in_array( self::INCLUDE_FLAGS,
$options[
'includeFields'] ) ) {
390 $fields = array_merge( $fields, [
'rc_type',
'rc_minor',
'rc_bot' ] );
392 if ( in_array( self::INCLUDE_USER,
$options[
'includeFields'] ) ) {
393 $fields[
'rc_user_text'] = $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user_text'];
395 if ( in_array( self::INCLUDE_USER_ID,
$options[
'includeFields'] ) ) {
396 $fields[
'rc_user'] = $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user'];
398 if ( in_array( self::INCLUDE_COMMENT,
$options[
'includeFields'] ) ) {
399 $fields += $this->commentStore->getJoin(
'rc_comment' )[
'fields'];
401 if ( in_array( self::INCLUDE_PATROL_INFO,
$options[
'includeFields'] ) ) {
402 $fields = array_merge( $fields, [
'rc_patrolled',
'rc_log_type' ] );
404 if ( in_array( self::INCLUDE_SIZES,
$options[
'includeFields'] ) ) {
405 $fields = array_merge( $fields, [
'rc_old_len',
'rc_new_len' ] );
407 if ( in_array( self::INCLUDE_LOG_INFO,
$options[
'includeFields'] ) ) {
408 $fields = array_merge( $fields, [
'rc_logid',
'rc_log_type',
'rc_log_action',
'rc_params' ] );
410 if ( in_array( self::INCLUDE_TAGS,
$options[
'includeFields'] ) ) {
424 $conds = [
'wl_user' => $watchlistOwnerId ];
428 [
'rc_this_oldid=page_latest',
'rc_type=' .
RC_LOG ],
434 $conds[
'wl_namespace'] = array_map(
'intval',
$options[
'namespaceIds'] );
437 if ( array_key_exists(
'rcTypes',
$options ) ) {
438 $conds[
'rc_type'] = array_map(
'intval',
$options[
'rcTypes'] );
441 $conds = array_merge(
450 $conds[] =
'rc_timestamp > ' . $db->
addQuotes(
'' );
456 if ( $deletedPageLogCond ) {
457 $conds[] = $deletedPageLogCond;
464 if ( array_key_exists(
'watchlistOwner',
$options ) ) {
466 $watchlistOwner =
$options[
'watchlistOwner'];
467 $ownersToken = $watchlistOwner->getOption(
'watchlisttoken' );
468 $token =
$options[
'watchlistOwnerToken'];
469 if ( $ownersToken ==
'' || !hash_equals( $ownersToken, $token ) ) {
472 return $watchlistOwner->getId();
474 return $user->getId();
480 if ( in_array( self::FILTER_MINOR,
$options[
'filters'] ) ) {
481 $conds[] =
'rc_minor != 0';
482 } elseif ( in_array( self::FILTER_NOT_MINOR,
$options[
'filters'] ) ) {
483 $conds[] =
'rc_minor = 0';
486 if ( in_array( self::FILTER_BOT,
$options[
'filters'] ) ) {
487 $conds[] =
'rc_bot != 0';
488 } elseif ( in_array( self::FILTER_NOT_BOT,
$options[
'filters'] ) ) {
489 $conds[] =
'rc_bot = 0';
492 if ( in_array( self::FILTER_ANON,
$options[
'filters'] ) ) {
493 $conds[] = $this->actorMigration->isAnon(
494 $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user']
496 } elseif ( in_array( self::FILTER_NOT_ANON,
$options[
'filters'] ) ) {
497 $conds[] = $this->actorMigration->isNotAnon(
498 $this->actorMigration->getJoin(
'rc_user' )[
'fields'][
'rc_user']
502 if (
$user->useRCPatrol() ||
$user->useNPPatrol() ) {
505 if ( in_array( self::FILTER_PATROLLED,
$options[
'filters'] ) ) {
507 } elseif ( in_array( self::FILTER_NOT_PATROLLED,
$options[
'filters'] ) ) {
511 if ( in_array( self::FILTER_AUTOPATROLLED,
$options[
'filters'] ) ) {
513 } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED,
$options[
'filters'] ) ) {
518 if ( in_array( self::FILTER_UNREAD,
$options[
'filters'] ) ) {
519 $conds[] =
'rc_timestamp >= wl_notificationtimestamp';
520 } elseif ( in_array( self::FILTER_NOT_UNREAD,
$options[
'filters'] ) ) {
522 $conds[] =
'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
536 $after =
$options[
'dir'] === self::DIR_OLDER ?
'<=' :
'>=';
537 $conds[] =
'rc_timestamp ' . $after .
' ' .
541 $before =
$options[
'dir'] === self::DIR_OLDER ?
'>=' :
'<=';
542 $conds[] =
'rc_timestamp ' . $before .
' ' .
550 if ( !array_key_exists(
'onlyByUser',
$options ) && !array_key_exists(
'notByUser',
$options ) ) {
556 if ( array_key_exists(
'onlyByUser',
$options ) ) {
558 $conds[] = $this->actorMigration->getWhere( $db,
'rc_user', $byUser )[
'conds'];
559 } elseif ( array_key_exists(
'notByUser',
$options ) ) {
561 $conds[] =
'NOT(' . $this->actorMigration->getWhere( $db,
'rc_user', $byUser )[
'conds'] .
')';
566 if ( !
$user->isAllowed(
'deletedhistory' ) ) {
568 } elseif ( !
$user->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
572 $conds[] = $db->
bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask";
582 if ( !
$user->isAllowed(
'deletedhistory' ) ) {
584 } elseif ( !
$user->isAllowedAny(
'suppressrevision',
'viewsuppressed' ) ) {
590 $db->
bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask",
597 $op =
$options[
'dir'] === self::DIR_OLDER ?
'<' :
'>';
598 list( $rcTimestamp, $rcId ) = $startFrom;
603 "rc_timestamp $op $rcTimestamp",
606 "rc_timestamp = $rcTimestamp",
617 $conds = [
'wl_user' =>
$user->getId() ];
619 $conds[
'wl_namespace'] = array_map(
'intval',
$options[
'namespaceIds'] );
621 if ( isset(
$options[
'filter'] ) ) {
623 if (
$filter === self::FILTER_CHANGED ) {
624 $conds[] =
'wl_notificationtimestamp IS NOT NULL';
626 $conds[] =
'wl_notificationtimestamp IS NULL';
631 $op =
$options[
'sort'] === self::SORT_ASC ?
'>' :
'<';
635 $op =
$options[
'sort'] === self::SORT_ASC ?
'<' :
'>';
638 if ( isset(
$options[
'startFrom'] ) ) {
639 $op =
$options[
'sort'] === self::SORT_ASC ?
'>' :
'<';
674 if ( array_key_exists(
'dir',
$options ) ) {
676 $dbOptions[
'ORDER BY'] = [
'rc_timestamp' .
$sort,
'rc_id' .
$sort ];
679 if ( array_key_exists(
'limit',
$options ) ) {
680 $dbOptions[
'LIMIT'] = (int)
$options[
'limit'] + 1;
688 if ( array_key_exists(
'sort',
$options ) ) {
689 $dbOptions[
'ORDER BY'] = [
690 "wl_namespace {$options['sort']}",
691 "wl_title {$options['sort']}"
694 $dbOptions[
'ORDER BY'] =
"wl_title {$options['sort']}";
697 if ( array_key_exists(
'limit',
$options ) ) {
698 $dbOptions[
'LIMIT'] = (int)
$options[
'limit'];
705 'watchlist' => [
'JOIN',
707 'wl_namespace=rc_namespace',
713 $joinConds[
'page'] = [
'LEFT JOIN',
'rc_cur_id=page_id' ];
715 if ( in_array( self::INCLUDE_COMMENT,
$options[
'includeFields'] ) ) {
716 $joinConds += $this->commentStore->getJoin(
'rc_comment' )[
'joins'];
718 if ( in_array( self::INCLUDE_USER,
$options[
'includeFields'] ) ||
719 in_array( self::INCLUDE_USER_ID,
$options[
'includeFields'] ) ||
720 in_array( self::FILTER_ANON,
$options[
'filters'] ) ||
721 in_array( self::FILTER_NOT_ANON,
$options[
'filters'] ) ||
722 array_key_exists(
'onlyByUser',
$options ) || array_key_exists(
'notByUser',
$options )
724 $joinConds += $this->actorMigration->getJoin(
'rc_user' )[
'joins'];