MediaWiki master
WatchedItemQueryService.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Watchlist;
4
5use ChangeTags;
6use LogPage;
19use RecentChange;
20use Wikimedia\Assert\Assert;
25
37
38 public const DIR_OLDER = 'older';
39 public const DIR_NEWER = 'newer';
40
41 public const INCLUDE_FLAGS = 'flags';
42 public const INCLUDE_USER = 'user';
43 public const INCLUDE_USER_ID = 'userid';
44 public const INCLUDE_COMMENT = 'comment';
45 public const INCLUDE_PATROL_INFO = 'patrol';
46 public const INCLUDE_AUTOPATROL_INFO = 'autopatrol';
47 public const INCLUDE_SIZES = 'sizes';
48 public const INCLUDE_LOG_INFO = 'loginfo';
49 public const INCLUDE_TAGS = 'tags';
50
51 // FILTER_* constants are part of public API (are used in ApiQueryWatchlist and
52 // ApiQueryWatchlistRaw classes) and should not be changed.
53 // Changing values of those constants will result in a breaking change in the API
54 public const FILTER_MINOR = 'minor';
55 public const FILTER_NOT_MINOR = '!minor';
56 public const FILTER_BOT = 'bot';
57 public const FILTER_NOT_BOT = '!bot';
58 public const FILTER_ANON = 'anon';
59 public const FILTER_NOT_ANON = '!anon';
60 public const FILTER_PATROLLED = 'patrolled';
61 public const FILTER_NOT_PATROLLED = '!patrolled';
62 public const FILTER_AUTOPATROLLED = 'autopatrolled';
63 public const FILTER_NOT_AUTOPATROLLED = '!autopatrolled';
64 public const FILTER_UNREAD = 'unread';
65 public const FILTER_NOT_UNREAD = '!unread';
66 public const FILTER_CHANGED = 'changed';
67 public const FILTER_NOT_CHANGED = '!changed';
68
69 public const SORT_ASC = 'ASC';
70 public const SORT_DESC = 'DESC';
71
75 private $dbProvider;
76
78 private $extensions = null;
79
81 private $commentStore;
82
84 private $watchedItemStore;
85
87 private $hookRunner;
88
90 private $userOptionsLookup;
91
93 private $tempUserConfig;
94
98 private $expiryEnabled;
99
103 private $maxQueryExecutionTime;
104
105 public function __construct(
106 IConnectionProvider $dbProvider,
107 CommentStore $commentStore,
108 WatchedItemStoreInterface $watchedItemStore,
109 HookContainer $hookContainer,
110 UserOptionsLookup $userOptionsLookup,
111 TempUserConfig $tempUserConfig,
112 bool $expiryEnabled = false,
113 int $maxQueryExecutionTime = 0
114 ) {
115 $this->dbProvider = $dbProvider;
116 $this->commentStore = $commentStore;
117 $this->watchedItemStore = $watchedItemStore;
118 $this->hookRunner = new HookRunner( $hookContainer );
119 $this->userOptionsLookup = $userOptionsLookup;
120 $this->tempUserConfig = $tempUserConfig;
121 $this->expiryEnabled = $expiryEnabled;
122 $this->maxQueryExecutionTime = $maxQueryExecutionTime;
123 }
124
128 private function getExtensions() {
129 if ( $this->extensions === null ) {
130 $this->extensions = [];
131 $this->hookRunner->onWatchedItemQueryServiceExtensions( $this->extensions, $this );
132 }
133 return $this->extensions;
134 }
135
180 User $user, array $options = [], &$startFrom = null
181 ) {
182 $options += [
183 'includeFields' => [],
184 'namespaceIds' => [],
185 'filters' => [],
186 'allRevisions' => false,
187 'usedInGenerator' => false
188 ];
189
190 Assert::parameter(
191 !isset( $options['rcTypes'] )
192 || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
193 '$options[\'rcTypes\']',
194 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
195 );
196 Assert::parameter(
197 !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
198 '$options[\'dir\']',
199 'must be DIR_OLDER or DIR_NEWER'
200 );
201 Assert::parameter(
202 ( !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null )
203 || isset( $options['dir'] ),
204 '$options[\'dir\']',
205 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
206 );
207 Assert::parameter(
208 !isset( $options['startFrom'] ),
209 '$options[\'startFrom\']',
210 'must not be provided, use $startFrom instead'
211 );
212 Assert::parameter(
213 !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
214 '$startFrom',
215 'must be a two-element array'
216 );
217 if ( array_key_exists( 'watchlistOwner', $options ) ) {
218 Assert::parameterType(
219 UserIdentity::class,
220 $options['watchlistOwner'],
221 '$options[\'watchlistOwner\']'
222 );
223 Assert::parameter(
224 isset( $options['watchlistOwnerToken'] ),
225 '$options[\'watchlistOwnerToken\']',
226 'must be provided when providing watchlistOwner option'
227 );
228 }
229
230 $db = $this->dbProvider->getReplicaDatabase();
231
232 $tables = $this->getWatchedItemsWithRCInfoQueryTables( $options );
233 $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
234 $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
235 $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
236 $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options );
237
238 if ( $startFrom !== null ) {
239 $conds[] = $this->getStartFromConds( $db, $options, $startFrom );
240 }
241
242 foreach ( $this->getExtensions() as $extension ) {
243 $extension->modifyWatchedItemsWithRCInfoQuery(
244 $user, $options, $db,
245 $tables,
246 $fields,
247 $conds,
248 $dbOptions,
249 $joinConds
250 );
251 }
252
253 $res = $db->newSelectQueryBuilder()
254 ->tables( $tables )
255 ->fields( $fields )
256 ->conds( $conds )
257 ->caller( __METHOD__ )
258 ->options( $dbOptions )
259 ->joinConds( $joinConds )
260 ->fetchResultSet();
261
262 $limit = $dbOptions['LIMIT'] ?? INF;
263 $items = [];
264 $startFrom = null;
265 foreach ( $res as $row ) {
266 if ( --$limit <= 0 ) {
267 $startFrom = [ $row->rc_timestamp, $row->rc_id ];
268 break;
269 }
270
271 $target = new TitleValue( (int)$row->rc_namespace, $row->rc_title );
272 $items[] = [
273 new WatchedItem(
274 $user,
275 $target,
276 $this->watchedItemStore->getLatestNotificationTimestamp(
277 $row->wl_notificationtimestamp, $user, $target
278 ),
279 $row->we_expiry ?? null
280 ),
281 $this->getRecentChangeFieldsFromRow( $row )
282 ];
283 }
284
285 foreach ( $this->getExtensions() as $extension ) {
286 $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom );
287 }
288
289 return $items;
290 }
291
311 public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
312 if ( !$user->isRegistered() ) {
313 // TODO: should this just return an empty array or rather complain loud at this point
314 // as e.g. ApiBase::getWatchlistUser does?
315 return [];
316 }
317
318 $options += [ 'namespaceIds' => [] ];
319
320 Assert::parameter(
321 !isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
322 '$options[\'sort\']',
323 'must be SORT_ASC or SORT_DESC'
324 );
325 Assert::parameter(
326 !isset( $options['filter'] ) || in_array(
327 $options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
328 ),
329 '$options[\'filter\']',
330 'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
331 );
332 Assert::parameter(
333 ( !isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] ) )
334 || isset( $options['sort'] ),
335 '$options[\'sort\']',
336 'must be provided if any of "from", "until", "startFrom" options is provided'
337 );
338
339 $db = $this->dbProvider->getReplicaDatabase();
340
341 $queryBuilder = $db->newSelectQueryBuilder()
342 ->select( [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ] )
343 ->from( 'watchlist' )
344 ->caller( __METHOD__ );
345 $this->addQueryCondsForWatchedItemsForUser( $db, $user, $options, $queryBuilder );
346 $this->addQueryDbOptionsForWatchedItemsForUser( $options, $queryBuilder );
347
348 if ( $this->expiryEnabled ) {
349 // If expiries are enabled, join with the watchlist_expiry table and exclude expired items.
350 $queryBuilder->leftJoin( 'watchlist_expiry', null, 'wl_id = we_item' )
351 ->andWhere( $db->expr( 'we_expiry', '>', $db->timestamp() )->or( 'we_expiry', '=', null ) );
352 }
353 $res = $queryBuilder->fetchResultSet();
354
355 $watchedItems = [];
356 foreach ( $res as $row ) {
357 $target = new TitleValue( (int)$row->wl_namespace, $row->wl_title );
358 // todo these could all be cached at some point?
359 $watchedItems[] = new WatchedItem(
360 $user,
361 $target,
362 $this->watchedItemStore->getLatestNotificationTimestamp(
363 $row->wl_notificationtimestamp, $user, $target
364 ),
365 $row->we_expiry ?? null
366 );
367 }
368
369 return $watchedItems;
370 }
371
372 private function getRecentChangeFieldsFromRow( \stdClass $row ) {
373 return array_filter(
374 get_object_vars( $row ),
375 static function ( $key ) {
376 return str_starts_with( $key, 'rc_' );
377 },
378 ARRAY_FILTER_USE_KEY
379 );
380 }
381
382 private function getWatchedItemsWithRCInfoQueryTables( array $options ) {
383 $tables = [ 'recentchanges', 'watchlist' ];
384
385 if ( $this->expiryEnabled ) {
386 $tables[] = 'watchlist_expiry';
387 }
388
389 if ( !$options['allRevisions'] ) {
390 $tables[] = 'page';
391 }
392 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
393 $tables += $this->commentStore->getJoin( 'rc_comment' )['tables'];
394 }
395 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
396 in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
397 in_array( self::FILTER_ANON, $options['filters'] ) ||
398 in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
399 array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
400 ) {
401 $tables['watchlist_actor'] = 'actor';
402 }
403 return $tables;
404 }
405
406 private function getWatchedItemsWithRCInfoQueryFields( array $options ) {
407 $fields = [
408 'rc_id',
409 'rc_namespace',
410 'rc_title',
411 'rc_timestamp',
412 'rc_type',
413 'rc_deleted',
414 'wl_notificationtimestamp'
415 ];
416
417 if ( $this->expiryEnabled ) {
418 $fields[] = 'we_expiry';
419 }
420
421 $rcIdFields = [
422 'rc_cur_id',
423 'rc_this_oldid',
424 'rc_last_oldid',
425 ];
426 if ( $options['usedInGenerator'] ) {
427 if ( $options['allRevisions'] ) {
428 $rcIdFields = [ 'rc_this_oldid' ];
429 } else {
430 $rcIdFields = [ 'rc_cur_id' ];
431 }
432 }
433 $fields = array_merge( $fields, $rcIdFields );
434
435 if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) {
436 $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
437 }
438 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
439 $fields['rc_user_text'] = 'watchlist_actor.actor_name';
440 }
441 if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
442 $fields['rc_user'] = 'watchlist_actor.actor_user';
443 }
444 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
445 $fields += $this->commentStore->getJoin( 'rc_comment' )['fields'];
446 }
447 if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
448 $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
449 }
450 if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) {
451 $fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] );
452 }
453 if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
454 $fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
455 }
456 if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
457 // prefixed with rc_ to include the field in getRecentChangeFieldsFromRow
458 $fields['rc_tags'] = ChangeTags::makeTagSummarySubquery( 'recentchanges' );
459 }
460
461 return $fields;
462 }
463
464 private function getWatchedItemsWithRCInfoQueryConds(
465 IReadableDatabase $db,
466 User $user,
467 array $options
468 ) {
469 $watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options );
470 $conds = [ 'wl_user' => $watchlistOwnerId ];
471
472 if ( $this->expiryEnabled ) {
473 $conds[] = $db->expr( 'we_expiry', '=', null )->or( 'we_expiry', '>', $db->timestamp() );
474 }
475
476 if ( !$options['allRevisions'] ) {
477 $conds[] = $db->makeList(
478 [ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ],
479 LIST_OR
480 );
481 }
482
483 if ( $options['namespaceIds'] ) {
484 $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
485 }
486
487 if ( array_key_exists( 'rcTypes', $options ) ) {
488 $conds['rc_type'] = array_map( 'intval', $options['rcTypes'] );
489 }
490
491 $conds = array_merge(
492 $conds,
493 $this->getWatchedItemsWithRCInfoQueryFilterConds( $db, $user, $options )
494 );
495
496 $conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) );
497
498 if ( !isset( $options['start'] ) && !isset( $options['end'] ) && $db->getType() === 'mysql' ) {
499 // This is an index optimization for mysql
500 $conds[] = $db->expr( 'rc_timestamp', '>', '' );
501 }
502
503 $conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) );
504
505 $deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user );
506 if ( $deletedPageLogCond ) {
507 $conds[] = $deletedPageLogCond;
508 }
509
510 return $conds;
511 }
512
513 private function getWatchlistOwnerId( UserIdentity $user, array $options ) {
514 if ( array_key_exists( 'watchlistOwner', $options ) ) {
516 $watchlistOwner = $options['watchlistOwner'];
517 $ownersToken =
518 $this->userOptionsLookup->getOption( $watchlistOwner, 'watchlisttoken' );
519 $token = $options['watchlistOwnerToken'];
520 if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
521 throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' );
522 }
523 return $watchlistOwner->getId();
524 }
525 return $user->getId();
526 }
527
528 private function getWatchedItemsWithRCInfoQueryFilterConds(
529 IReadableDatabase $dbr,
530 User $user,
531 array $options
532 ) {
533 $conds = [];
534
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';
539 }
540
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';
545 }
546
547 // Treat temporary users as 'anon', to match ChangesListSpecialPage
548 if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
549 if ( $this->tempUserConfig->isKnown() ) {
550 $conds[] = $dbr->expr( 'watchlist_actor.actor_user', '=', null )
551 ->orExpr( $this->tempUserConfig->getMatchCondition( $dbr,
552 'watchlist_actor.actor_name', IExpression::LIKE ) );
553 } else {
554 $conds[] = 'watchlist_actor.actor_user IS NULL';
555 }
556 } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
557 $conds[] = 'watchlist_actor.actor_user IS NOT NULL';
558 if ( $this->tempUserConfig->isKnown() ) {
559 $conds[] = $this->tempUserConfig->getMatchCondition( $dbr,
560 'watchlist_actor.actor_name', IExpression::NOT_LIKE );
561 }
562 }
563
564 if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
565 // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
566 // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
567 if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
568 $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
569 } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
570 $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
571 }
572
573 if ( in_array( self::FILTER_AUTOPATROLLED, $options['filters'] ) ) {
574 $conds['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
575 } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options['filters'] ) ) {
576 $conds[] = 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED;
577 }
578 }
579
580 if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
581 $conds[] = 'rc_timestamp >= wl_notificationtimestamp';
582 } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
583 // TODO: should this be changed to use Database::makeList?
584 $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
585 }
586
587 return $conds;
588 }
589
590 private function getStartEndConds( IReadableDatabase $db, array $options ) {
591 if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
592 return [];
593 }
594
595 $conds = [];
596 if ( isset( $options['start'] ) ) {
597 $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
598 $conds[] = $db->expr( 'rc_timestamp', $after, $db->timestamp( $options['start'] ) );
599 }
600 if ( isset( $options['end'] ) ) {
601 $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
602 $conds[] = $db->expr( 'rc_timestamp', $before, $db->timestamp( $options['end'] ) );
603 }
604
605 return $conds;
606 }
607
608 private function getUserRelatedConds( IReadableDatabase $db, Authority $user, array $options ) {
609 if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
610 return [];
611 }
612
613 $conds = [];
614
615 if ( array_key_exists( 'onlyByUser', $options ) ) {
616 $conds['watchlist_actor.actor_name'] = $options['onlyByUser'];
617 } elseif ( array_key_exists( 'notByUser', $options ) ) {
618 $conds[] = $db->expr( 'watchlist_actor.actor_name', '!=', $options['notByUser'] );
619 }
620
621 // Avoid brute force searches (T19342)
622 $bitmask = 0;
623 if ( !$user->isAllowed( 'deletedhistory' ) ) {
624 $bitmask = RevisionRecord::DELETED_USER;
625 } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
626 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
627 }
628 if ( $bitmask ) {
629 $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
630 }
631
632 return $conds;
633 }
634
635 private function getExtraDeletedPageLogEntryRelatedCond( IReadableDatabase $db, Authority $user ) {
636 // LogPage::DELETED_ACTION hides the affected page, too. So hide those
637 // entirely from the watchlist, or someone could guess the title.
638 $bitmask = 0;
639 if ( !$user->isAllowed( 'deletedhistory' ) ) {
640 $bitmask = LogPage::DELETED_ACTION;
641 } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
643 }
644 if ( $bitmask ) {
645 return $db->makeList( [
646 'rc_type != ' . RC_LOG,
647 $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
648 ], LIST_OR );
649 }
650 return '';
651 }
652
653 private function getStartFromConds( IReadableDatabase $db, array $options, array $startFrom ) {
654 $op = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
655 [ $rcTimestamp, $rcId ] = $startFrom;
656 $rcTimestamp = $db->timestamp( $rcTimestamp );
657 $rcId = (int)$rcId;
658 return $db->buildComparison( $op, [
659 'rc_timestamp' => $rcTimestamp,
660 'rc_id' => $rcId,
661 ] );
662 }
663
664 private function addQueryCondsForWatchedItemsForUser(
665 IReadableDatabase $db, UserIdentity $user, array $options, SelectQueryBuilder $queryBuilder
666 ) {
667 $queryBuilder->where( [ 'wl_user' => $user->getId() ] );
668 if ( $options['namespaceIds'] ) {
669 $queryBuilder->where( [ 'wl_namespace' => array_map( 'intval', $options['namespaceIds'] ) ] );
670 }
671 if ( isset( $options['filter'] ) ) {
672 $filter = $options['filter'];
673 if ( $filter === self::FILTER_CHANGED ) {
674 $queryBuilder->where( 'wl_notificationtimestamp IS NOT NULL' );
675 } else {
676 $queryBuilder->where( 'wl_notificationtimestamp IS NULL' );
677 }
678 }
679
680 if ( isset( $options['from'] ) ) {
681 $op = $options['sort'] === self::SORT_ASC ? '>=' : '<=';
682 $queryBuilder->where( $this->getFromUntilTargetConds( $db, $options['from'], $op ) );
683 }
684 if ( isset( $options['until'] ) ) {
685 $op = $options['sort'] === self::SORT_ASC ? '<=' : '>=';
686 $queryBuilder->where( $this->getFromUntilTargetConds( $db, $options['until'], $op ) );
687 }
688 if ( isset( $options['startFrom'] ) ) {
689 $op = $options['sort'] === self::SORT_ASC ? '>=' : '<=';
690 $queryBuilder->where( $this->getFromUntilTargetConds( $db, $options['startFrom'], $op ) );
691 }
692 }
693
703 private function getFromUntilTargetConds( IReadableDatabase $db, LinkTarget $target, $op ) {
704 return $db->buildComparison( $op, [
705 'wl_namespace' => $target->getNamespace(),
706 'wl_title' => $target->getDBkey(),
707 ] );
708 }
709
710 private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) {
711 $dbOptions = [];
712
713 if ( array_key_exists( 'dir', $options ) ) {
714 $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
715 $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
716 }
717
718 if ( array_key_exists( 'limit', $options ) ) {
719 $dbOptions['LIMIT'] = (int)$options['limit'] + 1;
720 }
721 if ( $this->maxQueryExecutionTime ) {
722 $dbOptions['MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime;
723 }
724 return $dbOptions;
725 }
726
727 private function addQueryDbOptionsForWatchedItemsForUser( array $options, SelectQueryBuilder $queryBuilder ) {
728 if ( array_key_exists( 'sort', $options ) ) {
729 if ( count( $options['namespaceIds'] ) !== 1 ) {
730 $queryBuilder->orderBy( 'wl_namespace', $options['sort'] );
731 }
732 $queryBuilder->orderBy( 'wl_title', $options['sort'] );
733 }
734 if ( array_key_exists( 'limit', $options ) ) {
735 $queryBuilder->limit( (int)$options['limit'] );
736 }
737 if ( $this->maxQueryExecutionTime ) {
738 $queryBuilder->setMaxExecutionTime( $this->maxQueryExecutionTime );
739 }
740 }
741
742 private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
743 $joinConds = [
744 'watchlist' => [ 'JOIN',
745 [
746 'wl_namespace=rc_namespace',
747 'wl_title=rc_title'
748 ]
749 ]
750 ];
751
752 if ( $this->expiryEnabled ) {
753 $joinConds['watchlist_expiry'] = [ 'LEFT JOIN', 'wl_id = we_item' ];
754 }
755
756 if ( !$options['allRevisions'] ) {
757 $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
758 }
759 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
760 $joinConds += $this->commentStore->getJoin( 'rc_comment' )['joins'];
761 }
762 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
763 in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
764 in_array( self::FILTER_ANON, $options['filters'] ) ||
765 in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
766 array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
767 ) {
768 $joinConds['watchlist_actor'] = [ 'JOIN', 'actor_id=rc_actor' ];
769 }
770 return $joinConds;
771 }
772
773}
775class_alias( WatchedItemQueryService::class, 'WatchedItemQueryService' );
const RC_NEW
Definition Defines.php:118
const LIST_OR
Definition Defines.php:47
const RC_LOG
Definition Defines.php:119
const RC_EXTERNAL
Definition Defines.php:120
const RC_EDIT
Definition Defines.php:117
const RC_CATEGORIZE
Definition Defines.php:121
Recent changes tagging.
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Class to simplify the use of log pages.
Definition LogPage.php:45
const DELETED_RESTRICTED
Definition LogPage.php:49
const DELETED_ACTION
Definition LogPage.php:46
Exception used to abort API execution with an error.
Handle database storage of comments such as edit summaries and log reasons.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Page revision base class.
Represents the target of a wiki link.
Provides access to user options.
internal since 1.36
Definition User.php:93
getWatchedItemsForUser(UserIdentity $user, array $options=[])
For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser.
__construct(IConnectionProvider $dbProvider, CommentStore $commentStore, WatchedItemStoreInterface $watchedItemStore, HookContainer $hookContainer, UserOptionsLookup $userOptionsLookup, TempUserConfig $tempUserConfig, bool $expiryEnabled=false, int $maxQueryExecutionTime=0)
getWatchedItemsWithRecentChangeInfo(User $user, array $options=[], &$startFrom=null)
Representation of a pair of user and title for watchlist entries.
Utility class for creating and reading rows in the recentchanges table.
const PRC_UNPATROLLED
const PRC_AUTOPATROLLED
Build SELECT queries with a fluent interface.
Represents the target of a wiki link.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
Interface for temporary user creation config and name matching.
Interface for objects representing user identity.
isRegistered()
This must be equivalent to getId() != 0 and is provided for code readability.
Provide primary and replica IDatabase connections.
A database connection without write operations.