MediaWiki REL1_37
WatchedItemQueryService.php
Go to the documentation of this file.
1<?php
2
9use Wikimedia\Assert\Assert;
12
24
25 public const DIR_OLDER = 'older';
26 public const DIR_NEWER = 'newer';
27
28 public const INCLUDE_FLAGS = 'flags';
29 public const INCLUDE_USER = 'user';
30 public const INCLUDE_USER_ID = 'userid';
31 public const INCLUDE_COMMENT = 'comment';
32 public const INCLUDE_PATROL_INFO = 'patrol';
33 public const INCLUDE_AUTOPATROL_INFO = 'autopatrol';
34 public const INCLUDE_SIZES = 'sizes';
35 public const INCLUDE_LOG_INFO = 'loginfo';
36 public const INCLUDE_TAGS = 'tags';
37
38 // FILTER_* constants are part of public API (are used in ApiQueryWatchlist and
39 // ApiQueryWatchlistRaw classes) and should not be changed.
40 // Changing values of those constants will result in a breaking change in the API
41 public const FILTER_MINOR = 'minor';
42 public const FILTER_NOT_MINOR = '!minor';
43 public const FILTER_BOT = 'bot';
44 public const FILTER_NOT_BOT = '!bot';
45 public const FILTER_ANON = 'anon';
46 public const FILTER_NOT_ANON = '!anon';
47 public const FILTER_PATROLLED = 'patrolled';
48 public const FILTER_NOT_PATROLLED = '!patrolled';
49 public const FILTER_AUTOPATROLLED = 'autopatrolled';
50 public const FILTER_NOT_AUTOPATROLLED = '!autopatrolled';
51 public const FILTER_UNREAD = 'unread';
52 public const FILTER_NOT_UNREAD = '!unread';
53 public const FILTER_CHANGED = 'changed';
54 public const FILTER_NOT_CHANGED = '!changed';
55
56 public const SORT_ASC = 'ASC';
57 public const SORT_DESC = 'DESC';
58
63
65 private $extensions = null;
66
69
72
74 private $hookRunner;
75
80
85
86 public function __construct(
87 ILoadBalancer $loadBalancer,
88 CommentStore $commentStore,
89 WatchedItemStoreInterface $watchedItemStore,
90 HookContainer $hookContainer,
91 bool $expiryEnabled = false,
92 int $maxQueryExecutionTime = 0
93 ) {
94 $this->loadBalancer = $loadBalancer;
95 $this->commentStore = $commentStore;
96 $this->watchedItemStore = $watchedItemStore;
97 $this->hookRunner = new HookRunner( $hookContainer );
98 $this->expiryEnabled = $expiryEnabled;
99 $this->maxQueryExecutionTime = $maxQueryExecutionTime;
100 }
101
105 private function getExtensions() {
106 if ( $this->extensions === null ) {
107 $this->extensions = [];
108 $this->hookRunner->onWatchedItemQueryServiceExtensions( $this->extensions, $this );
109 }
110 return $this->extensions;
111 }
112
116 private function getConnection() {
117 return $this->loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
118 }
119
164 User $user, array $options = [], &$startFrom = null
165 ) {
166 $options += [
167 'includeFields' => [],
168 'namespaceIds' => [],
169 'filters' => [],
170 'allRevisions' => false,
171 'usedInGenerator' => false
172 ];
173
174 Assert::parameter(
175 !isset( $options['rcTypes'] )
176 || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
177 '$options[\'rcTypes\']',
178 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
179 );
180 Assert::parameter(
181 !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
182 '$options[\'dir\']',
183 'must be DIR_OLDER or DIR_NEWER'
184 );
185 Assert::parameter(
186 !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null
187 || isset( $options['dir'] ),
188 '$options[\'dir\']',
189 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
190 );
191 Assert::parameter(
192 !isset( $options['startFrom'] ),
193 '$options[\'startFrom\']',
194 'must not be provided, use $startFrom instead'
195 );
196 Assert::parameter(
197 !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
198 '$startFrom',
199 'must be a two-element array'
200 );
201 if ( array_key_exists( 'watchlistOwner', $options ) ) {
202 Assert::parameterType(
203 User::class,
204 $options['watchlistOwner'],
205 '$options[\'watchlistOwner\']'
206 );
207 Assert::parameter(
208 isset( $options['watchlistOwnerToken'] ),
209 '$options[\'watchlistOwnerToken\']',
210 'must be provided when providing watchlistOwner option'
211 );
212 }
213
214 $db = $this->getConnection();
215
216 $tables = $this->getWatchedItemsWithRCInfoQueryTables( $options );
217 $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
218 $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
219 $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
220 $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options );
221
222 if ( $startFrom !== null ) {
223 $conds[] = $this->getStartFromConds( $db, $options, $startFrom );
224 }
225
226 foreach ( $this->getExtensions() as $extension ) {
227 $extension->modifyWatchedItemsWithRCInfoQuery(
228 $user, $options, $db,
229 $tables,
230 $fields,
231 $conds,
232 $dbOptions,
233 $joinConds
234 );
235 }
236
237 $res = $db->select(
238 $tables,
239 $fields,
240 $conds,
241 __METHOD__,
242 $dbOptions,
243 $joinConds
244 );
245
246 $limit = $dbOptions['LIMIT'] ?? INF;
247 $items = [];
248 $startFrom = null;
249 foreach ( $res as $row ) {
250 if ( --$limit <= 0 ) {
251 $startFrom = [ $row->rc_timestamp, $row->rc_id ];
252 break;
253 }
254
255 $target = new TitleValue( (int)$row->rc_namespace, $row->rc_title );
256 $items[] = [
257 new WatchedItem(
258 $user,
259 $target,
260 $this->watchedItemStore->getLatestNotificationTimestamp(
261 $row->wl_notificationtimestamp, $user, $target
262 ),
263 $row->we_expiry ?? null
264 ),
265 $this->getRecentChangeFieldsFromRow( $row )
266 ];
267 }
268
269 foreach ( $this->getExtensions() as $extension ) {
270 $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom );
271 }
272
273 return $items;
274 }
275
295 public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
296 if ( !$user->isRegistered() ) {
297 // TODO: should this just return an empty array or rather complain loud at this point
298 // as e.g. ApiBase::getWatchlistUser does?
299 return [];
300 }
301
302 $options += [ 'namespaceIds' => [] ];
303
304 Assert::parameter(
305 !isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
306 '$options[\'sort\']',
307 'must be SORT_ASC or SORT_DESC'
308 );
309 Assert::parameter(
310 !isset( $options['filter'] ) || in_array(
311 $options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
312 ),
313 '$options[\'filter\']',
314 'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
315 );
316 Assert::parameter(
317 !isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] )
318 || isset( $options['sort'] ),
319 '$options[\'sort\']',
320 'must be provided if any of "from", "until", "startFrom" options is provided'
321 );
322
323 $db = $this->getConnection();
324
325 $conds = $this->getWatchedItemsForUserQueryConds( $db, $user, $options );
326 $dbOptions = $this->getWatchedItemsForUserQueryDbOptions( $options );
327
328 $tables = 'watchlist';
329 $joinConds = [];
330 if ( $this->expiryEnabled ) {
331 // If expiries are enabled, join with the watchlist_expiry table and exclude expired items.
332 $tables = [ 'watchlist', 'watchlist_expiry' ];
333 $conds[] = $db->makeList(
334 [ 'we_expiry' => null, 'we_expiry > ' . $db->addQuotes( $db->timestamp() ) ],
335 $db::LIST_OR
336 );
337 $joinConds['watchlist_expiry'] = [ 'LEFT JOIN', 'wl_id = we_item' ];
338 }
339 $res = $db->select(
340 $tables,
341 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
342 $conds,
343 __METHOD__,
344 $dbOptions,
345 $joinConds
346 );
347
348 $watchedItems = [];
349 foreach ( $res as $row ) {
350 $target = new TitleValue( (int)$row->wl_namespace, $row->wl_title );
351 // todo these could all be cached at some point?
352 $watchedItems[] = new WatchedItem(
353 $user,
354 $target,
355 $this->watchedItemStore->getLatestNotificationTimestamp(
356 $row->wl_notificationtimestamp, $user, $target
357 ),
358 $row->we_expiry ?? null
359 );
360 }
361
362 return $watchedItems;
363 }
364
365 private function getRecentChangeFieldsFromRow( stdClass $row ) {
366 return array_filter(
367 get_object_vars( $row ),
368 static function ( $key ) {
369 return substr( $key, 0, 3 ) === 'rc_';
370 },
371 ARRAY_FILTER_USE_KEY
372 );
373 }
374
375 private function getWatchedItemsWithRCInfoQueryTables( array $options ) {
376 $tables = [ 'recentchanges', 'watchlist' ];
377
378 if ( $this->expiryEnabled ) {
379 $tables[] = 'watchlist_expiry';
380 }
381
382 if ( !$options['allRevisions'] ) {
383 $tables[] = 'page';
384 }
385 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
386 $tables += $this->commentStore->getJoin( 'rc_comment' )['tables'];
387 }
388 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
389 in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
390 in_array( self::FILTER_ANON, $options['filters'] ) ||
391 in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
392 array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
393 ) {
394 $tables['watchlist_actor'] = 'actor';
395 }
396 return $tables;
397 }
398
399 private function getWatchedItemsWithRCInfoQueryFields( array $options ) {
400 $fields = [
401 'rc_id',
402 'rc_namespace',
403 'rc_title',
404 'rc_timestamp',
405 'rc_type',
406 'rc_deleted',
407 'wl_notificationtimestamp'
408 ];
409
410 if ( $this->expiryEnabled ) {
411 $fields[] = 'we_expiry';
412 }
413
414 $rcIdFields = [
415 'rc_cur_id',
416 'rc_this_oldid',
417 'rc_last_oldid',
418 ];
419 if ( $options['usedInGenerator'] ) {
420 if ( $options['allRevisions'] ) {
421 $rcIdFields = [ 'rc_this_oldid' ];
422 } else {
423 $rcIdFields = [ 'rc_cur_id' ];
424 }
425 }
426 $fields = array_merge( $fields, $rcIdFields );
427
428 if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) {
429 $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
430 }
431 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
432 $fields['rc_user_text'] = 'watchlist_actor.actor_name';
433 }
434 if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
435 $fields['rc_user'] = 'watchlist_actor.actor_user';
436 }
437 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
438 $fields += $this->commentStore->getJoin( 'rc_comment' )['fields'];
439 }
440 if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
441 $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
442 }
443 if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) {
444 $fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] );
445 }
446 if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
447 $fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
448 }
449 if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
450 // prefixed with rc_ to include the field in getRecentChangeFieldsFromRow
451 $fields['rc_tags'] = ChangeTags::makeTagSummarySubquery( 'recentchanges' );
452 }
453
454 return $fields;
455 }
456
458 IDatabase $db,
459 User $user,
460 array $options
461 ) {
462 $watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options );
463 $conds = [ 'wl_user' => $watchlistOwnerId ];
464
465 if ( $this->expiryEnabled ) {
466 $conds[] = 'we_expiry IS NULL OR we_expiry > ' . $db->addQuotes( $db->timestamp() );
467 }
468
469 if ( !$options['allRevisions'] ) {
470 $conds[] = $db->makeList(
471 [ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ],
472 LIST_OR
473 );
474 }
475
476 if ( $options['namespaceIds'] ) {
477 $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
478 }
479
480 if ( array_key_exists( 'rcTypes', $options ) ) {
481 $conds['rc_type'] = array_map( 'intval', $options['rcTypes'] );
482 }
483
484 $conds = array_merge(
485 $conds,
486 $this->getWatchedItemsWithRCInfoQueryFilterConds( $user, $options )
487 );
488
489 $conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) );
490
491 if ( !isset( $options['start'] ) && !isset( $options['end'] ) && $db->getType() === 'mysql' ) {
492 // This is an index optimization for mysql
493 $conds[] = 'rc_timestamp > ' . $db->addQuotes( '' );
494 }
495
496 $conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) );
497
498 $deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user );
499 if ( $deletedPageLogCond ) {
500 $conds[] = $deletedPageLogCond;
501 }
502
503 return $conds;
504 }
505
506 private function getWatchlistOwnerId( UserIdentity $user, array $options ) {
507 if ( array_key_exists( 'watchlistOwner', $options ) ) {
509 $watchlistOwner = $options['watchlistOwner'];
510 $ownersToken =
511 $watchlistOwner->getOption( 'watchlisttoken' );
512 $token = $options['watchlistOwnerToken'];
513 if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
514 throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' );
515 }
516 return $watchlistOwner->getId();
517 }
518 return $user->getId();
519 }
520
521 private function getWatchedItemsWithRCInfoQueryFilterConds( User $user, array $options ) {
522 $conds = [];
523
524 if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) {
525 $conds[] = 'rc_minor != 0';
526 } elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) {
527 $conds[] = 'rc_minor = 0';
528 }
529
530 if ( in_array( self::FILTER_BOT, $options['filters'] ) ) {
531 $conds[] = 'rc_bot != 0';
532 } elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) {
533 $conds[] = 'rc_bot = 0';
534 }
535
536 if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
537 $conds[] = 'watchlist_actor.actor_user IS NULL';
538 } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
539 $conds[] = 'watchlist_actor.actor_user IS NOT NULL';
540 }
541
542 if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
543 // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
544 // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
545 if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
546 $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
547 } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
548 $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
549 }
550
551 if ( in_array( self::FILTER_AUTOPATROLLED, $options['filters'] ) ) {
552 $conds['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
553 } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options['filters'] ) ) {
554 $conds[] = 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED;
555 }
556 }
557
558 if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
559 $conds[] = 'rc_timestamp >= wl_notificationtimestamp';
560 } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
561 // TODO: should this be changed to use Database::makeList?
562 $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
563 }
564
565 return $conds;
566 }
567
568 private function getStartEndConds( IDatabase $db, array $options ) {
569 if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
570 return [];
571 }
572
573 $conds = [];
574
575 if ( isset( $options['start'] ) ) {
576 $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
577 $conds[] = 'rc_timestamp ' . $after . ' ' .
578 $db->addQuotes( $db->timestamp( $options['start'] ) );
579 }
580 if ( isset( $options['end'] ) ) {
581 $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
582 $conds[] = 'rc_timestamp ' . $before . ' ' .
583 $db->addQuotes( $db->timestamp( $options['end'] ) );
584 }
585
586 return $conds;
587 }
588
589 private function getUserRelatedConds( IDatabase $db, Authority $user, array $options ) {
590 if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
591 return [];
592 }
593
594 $conds = [];
595
596 if ( array_key_exists( 'onlyByUser', $options ) ) {
597 $conds['watchlist_actor.actor_name'] = $options['onlyByUser'];
598 } elseif ( array_key_exists( 'notByUser', $options ) ) {
599 $conds[] = 'watchlist_actor.actor_name<>' . $db->addQuotes( $options['notByUser'] );
600 }
601
602 // Avoid brute force searches (T19342)
603 $bitmask = 0;
604 if ( !$user->isAllowed( 'deletedhistory' ) ) {
605 $bitmask = RevisionRecord::DELETED_USER;
606 } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
607 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
608 }
609 if ( $bitmask ) {
610 $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
611 }
612
613 return $conds;
614 }
615
617 // LogPage::DELETED_ACTION hides the affected page, too. So hide those
618 // entirely from the watchlist, or someone could guess the title.
619 $bitmask = 0;
620 if ( !$user->isAllowed( 'deletedhistory' ) ) {
621 $bitmask = LogPage::DELETED_ACTION;
622 } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
624 }
625 if ( $bitmask ) {
626 return $db->makeList( [
627 'rc_type != ' . RC_LOG,
628 $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
629 ], LIST_OR );
630 }
631 return '';
632 }
633
634 private function getStartFromConds( IDatabase $db, array $options, array $startFrom ) {
635 $op = $options['dir'] === self::DIR_OLDER ? '<' : '>';
636 list( $rcTimestamp, $rcId ) = $startFrom;
637 $rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) );
638 $rcId = (int)$rcId;
639 return $db->makeList(
640 [
641 "rc_timestamp $op $rcTimestamp",
642 $db->makeList(
643 [
644 "rc_timestamp = $rcTimestamp",
645 "rc_id $op= $rcId"
646 ],
648 )
649 ],
650 LIST_OR
651 );
652 }
653
655 IDatabase $db, UserIdentity $user, array $options
656 ) {
657 $conds = [ 'wl_user' => $user->getId() ];
658 if ( $options['namespaceIds'] ) {
659 $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
660 }
661 if ( isset( $options['filter'] ) ) {
662 $filter = $options['filter'];
663 if ( $filter === self::FILTER_CHANGED ) {
664 $conds[] = 'wl_notificationtimestamp IS NOT NULL';
665 } else {
666 $conds[] = 'wl_notificationtimestamp IS NULL';
667 }
668 }
669
670 if ( isset( $options['from'] ) ) {
671 $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
672 $conds[] = $this->getFromUntilTargetConds( $db, $options['from'], $op );
673 }
674 if ( isset( $options['until'] ) ) {
675 $op = $options['sort'] === self::SORT_ASC ? '<' : '>';
676 $conds[] = $this->getFromUntilTargetConds( $db, $options['until'], $op );
677 }
678 if ( isset( $options['startFrom'] ) ) {
679 $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
680 $conds[] = $this->getFromUntilTargetConds( $db, $options['startFrom'], $op );
681 }
682
683 return $conds;
684 }
685
695 private function getFromUntilTargetConds( IDatabase $db, LinkTarget $target, $op ) {
696 return $db->makeList(
697 [
698 "wl_namespace $op " . $target->getNamespace(),
699 $db->makeList(
700 [
701 'wl_namespace = ' . $target->getNamespace(),
702 "wl_title $op= " . $db->addQuotes( $target->getDBkey() )
703 ],
705 )
706 ],
707 LIST_OR
708 );
709 }
710
711 private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) {
712 $dbOptions = [];
713
714 if ( array_key_exists( 'dir', $options ) ) {
715 $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
716 $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
717 }
718
719 if ( array_key_exists( 'limit', $options ) ) {
720 $dbOptions['LIMIT'] = (int)$options['limit'] + 1;
721 }
722 if ( $this->maxQueryExecutionTime ) {
723 $dbOptions['MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime;
724 }
725 return $dbOptions;
726 }
727
728 private function getWatchedItemsForUserQueryDbOptions( array $options ) {
729 $dbOptions = [];
730 if ( array_key_exists( 'sort', $options ) ) {
731 $dbOptions['ORDER BY'] = [
732 "wl_namespace {$options['sort']}",
733 "wl_title {$options['sort']}"
734 ];
735 if ( count( $options['namespaceIds'] ) === 1 ) {
736 $dbOptions['ORDER BY'] = "wl_title {$options['sort']}";
737 }
738 }
739 if ( array_key_exists( 'limit', $options ) ) {
740 $dbOptions['LIMIT'] = (int)$options['limit'];
741 }
742 if ( $this->maxQueryExecutionTime ) {
743 $dbOptions['MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime;
744 }
745 return $dbOptions;
746 }
747
748 private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
749 $joinConds = [
750 'watchlist' => [ 'JOIN',
751 [
752 'wl_namespace=rc_namespace',
753 'wl_title=rc_title'
754 ]
755 ]
756 ];
757
758 if ( $this->expiryEnabled ) {
759 $joinConds['watchlist_expiry'] = [ 'LEFT JOIN', 'wl_id = we_item' ];
760 }
761
762 if ( !$options['allRevisions'] ) {
763 $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
764 }
765 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
766 $joinConds += $this->commentStore->getJoin( 'rc_comment' )['joins'];
767 }
768 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
769 in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
770 in_array( self::FILTER_ANON, $options['filters'] ) ||
771 in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
772 array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
773 ) {
774 $joinConds['watchlist_actor'] = [ 'JOIN', 'actor_id=rc_actor' ];
775 }
776 return $joinConds;
777 }
778
779}
const RC_NEW
Definition Defines.php:116
const LIST_OR
Definition Defines.php:46
const RC_LOG
Definition Defines.php:117
const RC_EXTERNAL
Definition Defines.php:118
const LIST_AND
Definition Defines.php:43
const RC_EDIT
Definition Defines.php:115
const RC_CATEGORIZE
Definition Defines.php:119
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Handle database storage of comments such as edit summaries and log reasons.
const DELETED_RESTRICTED
Definition LogPage.php:42
const DELETED_ACTION
Definition LogPage.php:39
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Page revision base class.
Represents a page (or page fragment) title within MediaWiki.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:69
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:3050
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:3041
bool $expiryEnabled
Correlates to $wgWatchlistExpiry feature flag.
getWatchedItemsForUser(UserIdentity $user, array $options=[])
For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser.
WatchedItemQueryServiceExtension[] null $extensions
__construct(ILoadBalancer $loadBalancer, CommentStore $commentStore, WatchedItemStoreInterface $watchedItemStore, HookContainer $hookContainer, bool $expiryEnabled=false, int $maxQueryExecutionTime=0)
getWatchedItemsWithRCInfoQueryDbOptions(array $options)
getWatchedItemsWithRCInfoQueryFilterConds(User $user, array $options)
getWatchedItemsWithRCInfoQueryTables(array $options)
getWatchedItemsWithRCInfoQueryFields(array $options)
getWatchedItemsWithRCInfoQueryConds(IDatabase $db, User $user, array $options)
WatchedItemStoreInterface $watchedItemStore
getFromUntilTargetConds(IDatabase $db, LinkTarget $target, $op)
Creates a query condition part for getting only items before or after the given link target (while or...
int $maxQueryExecutionTime
Max query execution time.
getStartEndConds(IDatabase $db, array $options)
getWatchlistOwnerId(UserIdentity $user, array $options)
getWatchedItemsForUserQueryConds(IDatabase $db, UserIdentity $user, array $options)
getWatchedItemsWithRecentChangeInfo(User $user, array $options=[], &$startFrom=null)
getWatchedItemsWithRCInfoQueryJoinConds(array $options)
getExtraDeletedPageLogEntryRelatedCond(IDatabase $db, Authority $user)
getWatchedItemsForUserQueryDbOptions(array $options)
getUserRelatedConds(IDatabase $db, Authority $user, array $options)
getStartFromConds(IDatabase $db, array $options, array $startFrom)
Representation of a pair of user and title for watchlist entries.
getNamespace()
Get the namespace index.
getDBkey()
Get the main part with underscores.
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)
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
bitAnd( $fieldLeft, $fieldRight)
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
getType()
Get the RDBMS type of the server (e.g.
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition defines.php:25