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