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