MediaWiki REL1_40
WatchedItemQueryService.php
Go to the documentation of this file.
1<?php
2
11use Wikimedia\Assert\Assert;
14
26
27 public const DIR_OLDER = 'older';
28 public const DIR_NEWER = 'newer';
29
30 public const INCLUDE_FLAGS = 'flags';
31 public const INCLUDE_USER = 'user';
32 public const INCLUDE_USER_ID = 'userid';
33 public const INCLUDE_COMMENT = 'comment';
34 public const INCLUDE_PATROL_INFO = 'patrol';
35 public const INCLUDE_AUTOPATROL_INFO = 'autopatrol';
36 public const INCLUDE_SIZES = 'sizes';
37 public const INCLUDE_LOG_INFO = 'loginfo';
38 public const INCLUDE_TAGS = 'tags';
39
40 // FILTER_* constants are part of public API (are used in ApiQueryWatchlist and
41 // ApiQueryWatchlistRaw classes) and should not be changed.
42 // Changing values of those constants will result in a breaking change in the API
43 public const FILTER_MINOR = 'minor';
44 public const FILTER_NOT_MINOR = '!minor';
45 public const FILTER_BOT = 'bot';
46 public const FILTER_NOT_BOT = '!bot';
47 public const FILTER_ANON = 'anon';
48 public const FILTER_NOT_ANON = '!anon';
49 public const FILTER_PATROLLED = 'patrolled';
50 public const FILTER_NOT_PATROLLED = '!patrolled';
51 public const FILTER_AUTOPATROLLED = 'autopatrolled';
52 public const FILTER_NOT_AUTOPATROLLED = '!autopatrolled';
53 public const FILTER_UNREAD = 'unread';
54 public const FILTER_NOT_UNREAD = '!unread';
55 public const FILTER_CHANGED = 'changed';
56 public const FILTER_NOT_CHANGED = '!changed';
57
58 public const SORT_ASC = 'ASC';
59 public const SORT_DESC = 'DESC';
60
64 private $loadBalancer;
65
67 private $extensions = null;
68
70 private $commentStore;
71
73 private $watchedItemStore;
74
76 private $hookRunner;
77
79 private $userOptionsLookup;
80
84 private $expiryEnabled;
85
89 private $maxQueryExecutionTime;
90
91 public function __construct(
92 ILoadBalancer $loadBalancer,
93 CommentStore $commentStore,
94 WatchedItemStoreInterface $watchedItemStore,
95 HookContainer $hookContainer,
96 UserOptionsLookup $userOptionsLookup,
97 bool $expiryEnabled = false,
98 int $maxQueryExecutionTime = 0
99 ) {
100 $this->loadBalancer = $loadBalancer;
101 $this->commentStore = $commentStore;
102 $this->watchedItemStore = $watchedItemStore;
103 $this->hookRunner = new HookRunner( $hookContainer );
104 $this->userOptionsLookup = $userOptionsLookup;
105 $this->expiryEnabled = $expiryEnabled;
106 $this->maxQueryExecutionTime = $maxQueryExecutionTime;
107 }
108
112 private function getExtensions() {
113 if ( $this->extensions === null ) {
114 $this->extensions = [];
115 $this->hookRunner->onWatchedItemQueryServiceExtensions( $this->extensions, $this );
116 }
117 return $this->extensions;
118 }
119
123 private function getConnection() {
124 return $this->loadBalancer->getConnectionRef( DB_REPLICA );
125 }
126
171 User $user, array $options = [], &$startFrom = null
172 ) {
173 $options += [
174 'includeFields' => [],
175 'namespaceIds' => [],
176 'filters' => [],
177 'allRevisions' => false,
178 'usedInGenerator' => false
179 ];
180
181 Assert::parameter(
182 !isset( $options['rcTypes'] )
183 || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
184 '$options[\'rcTypes\']',
185 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
186 );
187 Assert::parameter(
188 !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
189 '$options[\'dir\']',
190 'must be DIR_OLDER or DIR_NEWER'
191 );
192 Assert::parameter(
193 !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null
194 || isset( $options['dir'] ),
195 '$options[\'dir\']',
196 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
197 );
198 Assert::parameter(
199 !isset( $options['startFrom'] ),
200 '$options[\'startFrom\']',
201 'must not be provided, use $startFrom instead'
202 );
203 Assert::parameter(
204 !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
205 '$startFrom',
206 'must be a two-element array'
207 );
208 if ( array_key_exists( 'watchlistOwner', $options ) ) {
209 Assert::parameterType(
210 UserIdentity::class,
211 $options['watchlistOwner'],
212 '$options[\'watchlistOwner\']'
213 );
214 Assert::parameter(
215 isset( $options['watchlistOwnerToken'] ),
216 '$options[\'watchlistOwnerToken\']',
217 'must be provided when providing watchlistOwner option'
218 );
219 }
220
221 $db = $this->getConnection();
222
223 $tables = $this->getWatchedItemsWithRCInfoQueryTables( $options );
224 $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
225 $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
226 $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
227 $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options );
228
229 if ( $startFrom !== null ) {
230 $conds[] = $this->getStartFromConds( $db, $options, $startFrom );
231 }
232
233 foreach ( $this->getExtensions() as $extension ) {
234 $extension->modifyWatchedItemsWithRCInfoQuery(
235 $user, $options, $db,
236 $tables,
237 $fields,
238 $conds,
239 $dbOptions,
240 $joinConds
241 );
242 }
243
244 $res = $db->select(
245 $tables,
246 $fields,
247 $conds,
248 __METHOD__,
249 $dbOptions,
250 $joinConds
251 );
252
253 $limit = $dbOptions['LIMIT'] ?? INF;
254 $items = [];
255 $startFrom = null;
256 foreach ( $res as $row ) {
257 if ( --$limit <= 0 ) {
258 $startFrom = [ $row->rc_timestamp, $row->rc_id ];
259 break;
260 }
261
262 $target = new TitleValue( (int)$row->rc_namespace, $row->rc_title );
263 $items[] = [
264 new WatchedItem(
265 $user,
266 $target,
267 $this->watchedItemStore->getLatestNotificationTimestamp(
268 $row->wl_notificationtimestamp, $user, $target
269 ),
270 $row->we_expiry ?? null
271 ),
272 $this->getRecentChangeFieldsFromRow( $row )
273 ];
274 }
275
276 foreach ( $this->getExtensions() as $extension ) {
277 $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom );
278 }
279
280 return $items;
281 }
282
302 public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
303 if ( !$user->isRegistered() ) {
304 // TODO: should this just return an empty array or rather complain loud at this point
305 // as e.g. ApiBase::getWatchlistUser does?
306 return [];
307 }
308
309 $options += [ 'namespaceIds' => [] ];
310
311 Assert::parameter(
312 !isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
313 '$options[\'sort\']',
314 'must be SORT_ASC or SORT_DESC'
315 );
316 Assert::parameter(
317 !isset( $options['filter'] ) || in_array(
318 $options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
319 ),
320 '$options[\'filter\']',
321 'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
322 );
323 Assert::parameter(
324 !isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] )
325 || isset( $options['sort'] ),
326 '$options[\'sort\']',
327 'must be provided if any of "from", "until", "startFrom" options is provided'
328 );
329
330 $db = $this->getConnection();
331
332 $conds = $this->getWatchedItemsForUserQueryConds( $db, $user, $options );
333 $dbOptions = $this->getWatchedItemsForUserQueryDbOptions( $options );
334
335 $tables = 'watchlist';
336 $joinConds = [];
337 if ( $this->expiryEnabled ) {
338 // If expiries are enabled, join with the watchlist_expiry table and exclude expired items.
339 $tables = [ 'watchlist', 'watchlist_expiry' ];
340 $conds[] = $db->makeList(
341 [ 'we_expiry' => null, 'we_expiry > ' . $db->addQuotes( $db->timestamp() ) ],
342 $db::LIST_OR
343 );
344 $joinConds['watchlist_expiry'] = [ 'LEFT JOIN', 'wl_id = we_item' ];
345 }
346 $res = $db->select(
347 $tables,
348 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
349 $conds,
350 __METHOD__,
351 $dbOptions,
352 $joinConds
353 );
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 IDatabase $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[] = 'we_expiry IS NULL OR we_expiry > ' . $db->addQuotes( $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( $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[] = 'rc_timestamp > ' . $db->addQuotes( '' );
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( User $user, array $options ) {
529 $conds = [];
530
531 if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) {
532 $conds[] = 'rc_minor != 0';
533 } elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) {
534 $conds[] = 'rc_minor = 0';
535 }
536
537 if ( in_array( self::FILTER_BOT, $options['filters'] ) ) {
538 $conds[] = 'rc_bot != 0';
539 } elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) {
540 $conds[] = 'rc_bot = 0';
541 }
542
543 if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
544 $conds[] = 'watchlist_actor.actor_user IS NULL';
545 } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
546 $conds[] = 'watchlist_actor.actor_user IS NOT NULL';
547 }
548
549 if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
550 // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
551 // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
552 if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
553 $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
554 } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
555 $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
556 }
557
558 if ( in_array( self::FILTER_AUTOPATROLLED, $options['filters'] ) ) {
559 $conds['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
560 } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options['filters'] ) ) {
561 $conds[] = 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED;
562 }
563 }
564
565 if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
566 $conds[] = 'rc_timestamp >= wl_notificationtimestamp';
567 } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
568 // TODO: should this be changed to use Database::makeList?
569 $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
570 }
571
572 return $conds;
573 }
574
575 private function getStartEndConds( IDatabase $db, array $options ) {
576 if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
577 return [];
578 }
579
580 $conds = [];
581
582 if ( isset( $options['start'] ) ) {
583 $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
584 $conds[] = 'rc_timestamp ' . $after . ' ' .
585 $db->addQuotes( $db->timestamp( $options['start'] ) );
586 }
587 if ( isset( $options['end'] ) ) {
588 $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
589 $conds[] = 'rc_timestamp ' . $before . ' ' .
590 $db->addQuotes( $db->timestamp( $options['end'] ) );
591 }
592
593 return $conds;
594 }
595
596 private function getUserRelatedConds( IDatabase $db, Authority $user, array $options ) {
597 if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
598 return [];
599 }
600
601 $conds = [];
602
603 if ( array_key_exists( 'onlyByUser', $options ) ) {
604 $conds['watchlist_actor.actor_name'] = $options['onlyByUser'];
605 } elseif ( array_key_exists( 'notByUser', $options ) ) {
606 $conds[] = 'watchlist_actor.actor_name<>' . $db->addQuotes( $options['notByUser'] );
607 }
608
609 // Avoid brute force searches (T19342)
610 $bitmask = 0;
611 if ( !$user->isAllowed( 'deletedhistory' ) ) {
612 $bitmask = RevisionRecord::DELETED_USER;
613 } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
614 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
615 }
616 if ( $bitmask ) {
617 $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
618 }
619
620 return $conds;
621 }
622
623 private function getExtraDeletedPageLogEntryRelatedCond( IDatabase $db, Authority $user ) {
624 // LogPage::DELETED_ACTION hides the affected page, too. So hide those
625 // entirely from the watchlist, or someone could guess the title.
626 $bitmask = 0;
627 if ( !$user->isAllowed( 'deletedhistory' ) ) {
628 $bitmask = LogPage::DELETED_ACTION;
629 } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
631 }
632 if ( $bitmask ) {
633 return $db->makeList( [
634 'rc_type != ' . RC_LOG,
635 $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
636 ], LIST_OR );
637 }
638 return '';
639 }
640
641 private function getStartFromConds( IDatabase $db, array $options, array $startFrom ) {
642 $op = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
643 [ $rcTimestamp, $rcId ] = $startFrom;
644 $rcTimestamp = $db->timestamp( $rcTimestamp );
645 $rcId = (int)$rcId;
646 return $db->buildComparison( $op, [
647 'rc_timestamp' => $rcTimestamp,
648 'rc_id' => $rcId,
649 ] );
650 }
651
652 private function getWatchedItemsForUserQueryConds(
653 IDatabase $db, UserIdentity $user, array $options
654 ) {
655 $conds = [ 'wl_user' => $user->getId() ];
656 if ( $options['namespaceIds'] ) {
657 $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
658 }
659 if ( isset( $options['filter'] ) ) {
660 $filter = $options['filter'];
661 if ( $filter === self::FILTER_CHANGED ) {
662 $conds[] = 'wl_notificationtimestamp IS NOT NULL';
663 } else {
664 $conds[] = 'wl_notificationtimestamp IS NULL';
665 }
666 }
667
668 if ( isset( $options['from'] ) ) {
669 $op = $options['sort'] === self::SORT_ASC ? '>=' : '<=';
670 $conds[] = $this->getFromUntilTargetConds( $db, $options['from'], $op );
671 }
672 if ( isset( $options['until'] ) ) {
673 $op = $options['sort'] === self::SORT_ASC ? '<=' : '>=';
674 $conds[] = $this->getFromUntilTargetConds( $db, $options['until'], $op );
675 }
676 if ( isset( $options['startFrom'] ) ) {
677 $op = $options['sort'] === self::SORT_ASC ? '>=' : '<=';
678 $conds[] = $this->getFromUntilTargetConds( $db, $options['startFrom'], $op );
679 }
680
681 return $conds;
682 }
683
693 private function getFromUntilTargetConds( IDatabase $db, LinkTarget $target, $op ) {
694 return $db->buildComparison( $op, [
695 'wl_namespace' => $target->getNamespace(),
696 'wl_title' => $target->getDBkey(),
697 ] );
698 }
699
700 private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) {
701 $dbOptions = [];
702
703 if ( array_key_exists( 'dir', $options ) ) {
704 $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
705 $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
706 }
707
708 if ( array_key_exists( 'limit', $options ) ) {
709 $dbOptions['LIMIT'] = (int)$options['limit'] + 1;
710 }
711 if ( $this->maxQueryExecutionTime ) {
712 $dbOptions['MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime;
713 }
714 return $dbOptions;
715 }
716
717 private function getWatchedItemsForUserQueryDbOptions( array $options ) {
718 $dbOptions = [];
719 if ( array_key_exists( 'sort', $options ) ) {
720 $dbOptions['ORDER BY'] = [
721 "wl_namespace {$options['sort']}",
722 "wl_title {$options['sort']}"
723 ];
724 if ( count( $options['namespaceIds'] ) === 1 ) {
725 $dbOptions['ORDER BY'] = "wl_title {$options['sort']}";
726 }
727 }
728 if ( array_key_exists( 'limit', $options ) ) {
729 $dbOptions['LIMIT'] = (int)$options['limit'];
730 }
731 if ( $this->maxQueryExecutionTime ) {
732 $dbOptions['MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime;
733 }
734 return $dbOptions;
735 }
736
737 private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
738 $joinConds = [
739 'watchlist' => [ 'JOIN',
740 [
741 'wl_namespace=rc_namespace',
742 'wl_title=rc_title'
743 ]
744 ]
745 ];
746
747 if ( $this->expiryEnabled ) {
748 $joinConds['watchlist_expiry'] = [ 'LEFT JOIN', 'wl_id = we_item' ];
749 }
750
751 if ( !$options['allRevisions'] ) {
752 $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
753 }
754 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
755 $joinConds += $this->commentStore->getJoin( 'rc_comment' )['joins'];
756 }
757 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
758 in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
759 in_array( self::FILTER_ANON, $options['filters'] ) ||
760 in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
761 array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
762 ) {
763 $joinConds['watchlist_actor'] = [ 'JOIN', 'actor_id=rc_actor' ];
764 }
765 return $joinConds;
766 }
767
768}
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:45
const DELETED_ACTION
Definition LogPage.php:42
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.
Provides access to user options.
const PRC_UNPATROLLED
const PRC_AUTOPATROLLED
Represents a page (or page fragment) title within MediaWiki.
internal since 1.36
Definition User.php:71
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:2385
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:2375
__construct(ILoadBalancer $loadBalancer, CommentStore $commentStore, WatchedItemStoreInterface $watchedItemStore, HookContainer $hookContainer, UserOptionsLookup $userOptionsLookup, bool $expiryEnabled=false, int $maxQueryExecutionTime=0)
getWatchedItemsForUser(UserIdentity $user, array $options=[])
For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser.
getWatchedItemsWithRecentChangeInfo(User $user, array $options=[], &$startFrom=null)
Representation of a pair of user and title for watchlist entries.
getNamespace()
Get the namespace index.
getDBkey()
Get the main part of the link target, in canonical database form.
This interface represents the authority associated the current execution context, such as a web reque...
Definition Authority.php:37
isAllowed(string $permission)
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 objects representing user identity.
getId( $wikiId=self::LOCAL)
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:36
This class is a delegate to ILBFactory for a given database cluster.
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.
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,...
const DB_REPLICA
Definition defines.php:26