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