MediaWiki REL1_35
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
75
78
80 private $hookRunner;
81
86
91
92 public function __construct(
93 ILoadBalancer $loadBalancer,
94 CommentStore $commentStore,
95 ActorMigration $actorMigration,
96 WatchedItemStoreInterface $watchedItemStore,
97 PermissionManager $permissionManager,
98 HookContainer $hookContainer,
99 bool $expiryEnabled = false,
100 int $maxQueryExecutionTime = 0
101 ) {
102 $this->loadBalancer = $loadBalancer;
103 $this->commentStore = $commentStore;
104 $this->actorMigration = $actorMigration;
105 $this->watchedItemStore = $watchedItemStore;
106 $this->permissionManager = $permissionManager;
107 $this->hookRunner = new HookRunner( $hookContainer );
108 $this->expiryEnabled = $expiryEnabled;
109 $this->maxQueryExecutionTime = $maxQueryExecutionTime;
110 }
111
115 private function getExtensions() {
116 if ( $this->extensions === null ) {
117 $this->extensions = [];
118 $this->hookRunner->onWatchedItemQueryServiceExtensions( $this->extensions, $this );
119 }
120 return $this->extensions;
121 }
122
126 private function getConnection() {
127 return $this->loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
128 }
129
174 User $user, array $options = [], &$startFrom = null
175 ) {
176 $options += [
177 'includeFields' => [],
178 'namespaceIds' => [],
179 'filters' => [],
180 'allRevisions' => false,
181 'usedInGenerator' => false
182 ];
183
184 Assert::parameter(
185 !isset( $options['rcTypes'] )
186 || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
187 '$options[\'rcTypes\']',
188 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
189 );
190 Assert::parameter(
191 !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
192 '$options[\'dir\']',
193 'must be DIR_OLDER or DIR_NEWER'
194 );
195 Assert::parameter(
196 !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null
197 || isset( $options['dir'] ),
198 '$options[\'dir\']',
199 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
200 );
201 Assert::parameter(
202 !isset( $options['startFrom'] ),
203 '$options[\'startFrom\']',
204 'must not be provided, use $startFrom instead'
205 );
206 Assert::parameter(
207 !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
208 '$startFrom',
209 'must be a two-element array'
210 );
211 if ( array_key_exists( 'watchlistOwner', $options ) ) {
212 Assert::parameterType(
213 User::class,
214 $options['watchlistOwner'],
215 '$options[\'watchlistOwner\']'
216 );
217 Assert::parameter(
218 isset( $options['watchlistOwnerToken'] ),
219 '$options[\'watchlistOwnerToken\']',
220 'must be provided when providing watchlistOwner option'
221 );
222 }
223
224 $db = $this->getConnection();
225
226 $tables = $this->getWatchedItemsWithRCInfoQueryTables( $options );
227 $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
228 $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
229 $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
230 $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options );
231
232 if ( $startFrom !== null ) {
233 $conds[] = $this->getStartFromConds( $db, $options, $startFrom );
234 }
235
236 foreach ( $this->getExtensions() as $extension ) {
237 $extension->modifyWatchedItemsWithRCInfoQuery(
238 $user, $options, $db,
239 $tables,
240 $fields,
241 $conds,
242 $dbOptions,
243 $joinConds
244 );
245 }
246
247 $res = $db->select(
248 $tables,
249 $fields,
250 $conds,
251 __METHOD__,
252 $dbOptions,
253 $joinConds
254 );
255
256 $limit = $dbOptions['LIMIT'] ?? INF;
257 $items = [];
258 $startFrom = null;
259 foreach ( $res as $row ) {
260 if ( --$limit <= 0 ) {
261 $startFrom = [ $row->rc_timestamp, $row->rc_id ];
262 break;
263 }
264
265 $target = new TitleValue( (int)$row->rc_namespace, $row->rc_title );
266 $items[] = [
267 new WatchedItem(
268 $user,
269 $target,
270 $this->watchedItemStore->getLatestNotificationTimestamp(
271 $row->wl_notificationtimestamp, $user, $target
272 )
273 ),
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->getConnection();
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->makeList(
343 [ 'we_expiry' => null, 'we_expiry > ' . $db->addQuotes( $db->timestamp() ) ],
344 $db::LIST_OR
345 );
346 $joinConds['watchlist_expiry'] = [ 'LEFT JOIN', 'wl_id = we_item' ];
347 }
348 $res = $db->select(
349 $tables,
350 [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
351 $conds,
352 __METHOD__,
353 $dbOptions,
354 $joinConds
355 );
356
357 $watchedItems = [];
358 foreach ( $res as $row ) {
359 $target = new TitleValue( (int)$row->wl_namespace, $row->wl_title );
360 // todo these could all be cached at some point?
361 $watchedItems[] = new WatchedItem(
362 $user,
363 $target,
364 $this->watchedItemStore->getLatestNotificationTimestamp(
365 $row->wl_notificationtimestamp, $user, $target
366 )
367 );
368 }
369
370 return $watchedItems;
371 }
372
373 private function getRecentChangeFieldsFromRow( stdClass $row ) {
374 // FIXME: This can be simplified to single array_filter call filtering by key value,
375 // now we have stopped supporting PHP 5.5
376 $allFields = get_object_vars( $row );
377 $rcKeys = array_filter(
378 array_keys( $allFields ),
379 function ( $key ) {
380 return substr( $key, 0, 3 ) === 'rc_';
381 }
382 );
383 return array_intersect_key( $allFields, array_flip( $rcKeys ) );
384 }
385
386 private function getWatchedItemsWithRCInfoQueryTables( array $options ) {
387 $tables = [ 'recentchanges', 'watchlist' ];
388
389 if ( $this->expiryEnabled ) {
390 $tables[] = 'watchlist_expiry';
391 }
392
393 if ( !$options['allRevisions'] ) {
394 $tables[] = 'page';
395 }
396 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
397 $tables += $this->commentStore->getJoin( 'rc_comment' )['tables'];
398 }
399 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
400 in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
401 in_array( self::FILTER_ANON, $options['filters'] ) ||
402 in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
403 array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
404 ) {
405 $tables += $this->actorMigration->getJoin( 'rc_user' )['tables'];
406 }
407 return $tables;
408 }
409
410 private function getWatchedItemsWithRCInfoQueryFields( array $options ) {
411 $fields = [
412 'rc_id',
413 'rc_namespace',
414 'rc_title',
415 'rc_timestamp',
416 'rc_type',
417 'rc_deleted',
418 'wl_notificationtimestamp'
419 ];
420
421 if ( $this->expiryEnabled ) {
422 $fields[] = 'we_expiry';
423 }
424
425 $rcIdFields = [
426 'rc_cur_id',
427 'rc_this_oldid',
428 'rc_last_oldid',
429 ];
430 if ( $options['usedInGenerator'] ) {
431 if ( $options['allRevisions'] ) {
432 $rcIdFields = [ 'rc_this_oldid' ];
433 } else {
434 $rcIdFields = [ 'rc_cur_id' ];
435 }
436 }
437 $fields = array_merge( $fields, $rcIdFields );
438
439 if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) {
440 $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
441 }
442 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
443 $fields['rc_user_text'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user_text'];
444 }
445 if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
446 $fields['rc_user'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user'];
447 }
448 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
449 $fields += $this->commentStore->getJoin( 'rc_comment' )['fields'];
450 }
451 if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
452 $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
453 }
454 if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) {
455 $fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] );
456 }
457 if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
458 $fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
459 }
460 if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
461 // prefixed with rc_ to include the field in getRecentChangeFieldsFromRow
462 $fields['rc_tags'] = ChangeTags::makeTagSummarySubquery( 'recentchanges' );
463 }
464
465 return $fields;
466 }
467
469 IDatabase $db,
470 User $user,
471 array $options
472 ) {
473 $watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options );
474 $conds = [ 'wl_user' => $watchlistOwnerId ];
475
476 if ( $this->expiryEnabled ) {
477 $conds[] = 'we_expiry IS NULL OR we_expiry > ' . $db->addQuotes( $db->timestamp() );
478 }
479
480 if ( !$options['allRevisions'] ) {
481 $conds[] = $db->makeList(
482 [ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ],
483 LIST_OR
484 );
485 }
486
487 if ( $options['namespaceIds'] ) {
488 $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
489 }
490
491 if ( array_key_exists( 'rcTypes', $options ) ) {
492 $conds['rc_type'] = array_map( 'intval', $options['rcTypes'] );
493 }
494
495 $conds = array_merge(
496 $conds,
497 $this->getWatchedItemsWithRCInfoQueryFilterConds( $user, $options )
498 );
499
500 $conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) );
501
502 if ( !isset( $options['start'] ) && !isset( $options['end'] ) && $db->getType() === 'mysql' ) {
503 // This is an index optimization for mysql
504 $conds[] = 'rc_timestamp > ' . $db->addQuotes( '' );
505 }
506
507 $conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) );
508
509 $deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user );
510 if ( $deletedPageLogCond ) {
511 $conds[] = $deletedPageLogCond;
512 }
513
514 return $conds;
515 }
516
517 private function getWatchlistOwnerId( UserIdentity $user, array $options ) {
518 if ( array_key_exists( 'watchlistOwner', $options ) ) {
520 $watchlistOwner = $options['watchlistOwner'];
521 $ownersToken =
522 $watchlistOwner->getOption( 'watchlisttoken' );
523 $token = $options['watchlistOwnerToken'];
524 if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
525 throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' );
526 }
527 return $watchlistOwner->getId();
528 }
529 return $user->getId();
530 }
531
532 private function getWatchedItemsWithRCInfoQueryFilterConds( User $user, array $options ) {
533 $conds = [];
534
535 if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) {
536 $conds[] = 'rc_minor != 0';
537 } elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) {
538 $conds[] = 'rc_minor = 0';
539 }
540
541 if ( in_array( self::FILTER_BOT, $options['filters'] ) ) {
542 $conds[] = 'rc_bot != 0';
543 } elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) {
544 $conds[] = 'rc_bot = 0';
545 }
546
547 if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
548 $conds[] = $this->actorMigration->isAnon(
549 $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
550 );
551 } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
552 $conds[] = $this->actorMigration->isNotAnon(
553 $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
554 );
555 }
556
557 if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
558 // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
559 // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
560 if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
561 $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
562 } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
563 $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
564 }
565
566 if ( in_array( self::FILTER_AUTOPATROLLED, $options['filters'] ) ) {
567 $conds['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
568 } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options['filters'] ) ) {
569 $conds[] = 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED;
570 }
571 }
572
573 if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
574 $conds[] = 'rc_timestamp >= wl_notificationtimestamp';
575 } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
576 // TODO: should this be changed to use Database::makeList?
577 $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
578 }
579
580 return $conds;
581 }
582
583 private function getStartEndConds( IDatabase $db, array $options ) {
584 if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
585 return [];
586 }
587
588 $conds = [];
589
590 if ( isset( $options['start'] ) ) {
591 $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
592 $conds[] = 'rc_timestamp ' . $after . ' ' .
593 $db->addQuotes( $db->timestamp( $options['start'] ) );
594 }
595 if ( isset( $options['end'] ) ) {
596 $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
597 $conds[] = 'rc_timestamp ' . $before . ' ' .
598 $db->addQuotes( $db->timestamp( $options['end'] ) );
599 }
600
601 return $conds;
602 }
603
604 private function getUserRelatedConds( IDatabase $db, UserIdentity $user, array $options ) {
605 if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
606 return [];
607 }
608
609 $conds = [];
610
611 if ( array_key_exists( 'onlyByUser', $options ) ) {
612 $byUser = User::newFromName( $options['onlyByUser'], false );
613 $conds[] = $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'];
614 } elseif ( array_key_exists( 'notByUser', $options ) ) {
615 $byUser = User::newFromName( $options['notByUser'], false );
616 $conds[] = 'NOT(' . $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'] . ')';
617 }
618
619 // Avoid brute force searches (T19342)
620 $bitmask = 0;
621 if ( !$this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
622 $bitmask = RevisionRecord::DELETED_USER;
623 } elseif ( !$this->permissionManager
624 ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
625 ) {
626 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
627 }
628 if ( $bitmask ) {
629 $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
630 }
631
632 return $conds;
633 }
634
636 // LogPage::DELETED_ACTION hides the affected page, too. So hide those
637 // entirely from the watchlist, or someone could guess the title.
638 $bitmask = 0;
639 if ( !$this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
640 $bitmask = LogPage::DELETED_ACTION;
641 } elseif ( !$this->permissionManager
642 ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
643 ) {
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( IDatabase $db, array $options, array $startFrom ) {
656 $op = $options['dir'] === self::DIR_OLDER ? '<' : '>';
657 list( $rcTimestamp, $rcId ) = $startFrom;
658 $rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) );
659 $rcId = (int)$rcId;
660 return $db->makeList(
661 [
662 "rc_timestamp $op $rcTimestamp",
663 $db->makeList(
664 [
665 "rc_timestamp = $rcTimestamp",
666 "rc_id $op= $rcId"
667 ],
669 )
670 ],
671 LIST_OR
672 );
673 }
674
676 IDatabase $db, UserIdentity $user, array $options
677 ) {
678 $conds = [ 'wl_user' => $user->getId() ];
679 if ( $options['namespaceIds'] ) {
680 $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
681 }
682 if ( isset( $options['filter'] ) ) {
683 $filter = $options['filter'];
684 if ( $filter === self::FILTER_CHANGED ) {
685 $conds[] = 'wl_notificationtimestamp IS NOT NULL';
686 } else {
687 $conds[] = 'wl_notificationtimestamp IS NULL';
688 }
689 }
690
691 if ( isset( $options['from'] ) ) {
692 $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
693 $conds[] = $this->getFromUntilTargetConds( $db, $options['from'], $op );
694 }
695 if ( isset( $options['until'] ) ) {
696 $op = $options['sort'] === self::SORT_ASC ? '<' : '>';
697 $conds[] = $this->getFromUntilTargetConds( $db, $options['until'], $op );
698 }
699 if ( isset( $options['startFrom'] ) ) {
700 $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
701 $conds[] = $this->getFromUntilTargetConds( $db, $options['startFrom'], $op );
702 }
703
704 return $conds;
705 }
706
716 private function getFromUntilTargetConds( IDatabase $db, LinkTarget $target, $op ) {
717 return $db->makeList(
718 [
719 "wl_namespace $op " . $target->getNamespace(),
720 $db->makeList(
721 [
722 'wl_namespace = ' . $target->getNamespace(),
723 "wl_title $op= " . $db->addQuotes( $target->getDBkey() )
724 ],
726 )
727 ],
728 LIST_OR
729 );
730 }
731
732 private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) {
733 $dbOptions = [];
734
735 if ( array_key_exists( 'dir', $options ) ) {
736 $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
737 $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
738 }
739
740 if ( array_key_exists( 'limit', $options ) ) {
741 $dbOptions['LIMIT'] = (int)$options['limit'] + 1;
742 }
743 if ( $this->maxQueryExecutionTime ) {
744 $dbOptions['MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime;
745 }
746 return $dbOptions;
747 }
748
749 private function getWatchedItemsForUserQueryDbOptions( array $options ) {
750 $dbOptions = [];
751 if ( array_key_exists( 'sort', $options ) ) {
752 $dbOptions['ORDER BY'] = [
753 "wl_namespace {$options['sort']}",
754 "wl_title {$options['sort']}"
755 ];
756 if ( count( $options['namespaceIds'] ) === 1 ) {
757 $dbOptions['ORDER BY'] = "wl_title {$options['sort']}";
758 }
759 }
760 if ( array_key_exists( 'limit', $options ) ) {
761 $dbOptions['LIMIT'] = (int)$options['limit'];
762 }
763 if ( $this->maxQueryExecutionTime ) {
764 $dbOptions['MAX_EXECUTION_TIME'] = $this->maxQueryExecutionTime;
765 }
766 return $dbOptions;
767 }
768
769 private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
770 $joinConds = [
771 'watchlist' => [ 'JOIN',
772 [
773 'wl_namespace=rc_namespace',
774 'wl_title=rc_title'
775 ]
776 ]
777 ];
778
779 if ( $this->expiryEnabled ) {
780 $joinConds['watchlist_expiry'] = [ 'LEFT JOIN', 'wl_id = we_item' ];
781 }
782
783 if ( !$options['allRevisions'] ) {
784 $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
785 }
786 if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
787 $joinConds += $this->commentStore->getJoin( 'rc_comment' )['joins'];
788 }
789 if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
790 in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
791 in_array( self::FILTER_ANON, $options['filters'] ) ||
792 in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
793 array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
794 ) {
795 $joinConds += $this->actorMigration->getJoin( 'rc_user' )['joins'];
796 }
797 return $joinConds;
798 }
799
800}
This class handles the logic for the actor table migration and should always be used in lieu of direc...
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
const DELETED_RESTRICTED
Definition LogPage.php:41
const DELETED_ACTION
Definition LogPage.php:38
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
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:60
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:541
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition User.php:3180
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition User.php:3171
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
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.
__construct(ILoadBalancer $loadBalancer, CommentStore $commentStore, ActorMigration $actorMigration, WatchedItemStoreInterface $watchedItemStore, PermissionManager $permissionManager, HookContainer $hookContainer, bool $expiryEnabled=false, int $maxQueryExecutionTime=0)
getExtraDeletedPageLogEntryRelatedCond(IDatabase $db, UserIdentity $user)
getStartEndConds(IDatabase $db, array $options)
getUserRelatedConds(IDatabase $db, UserIdentity $user, array $options)
getWatchlistOwnerId(UserIdentity $user, array $options)
getWatchedItemsForUserQueryConds(IDatabase $db, UserIdentity $user, array $options)
getWatchedItemsWithRecentChangeInfo(User $user, array $options=[], &$startFrom=null)
getWatchedItemsWithRCInfoQueryJoinConds(array $options)
getWatchedItemsForUserQueryDbOptions(array $options)
getStartFromConds(IDatabase $db, array $options, array $startFrom)
Representation of a pair of user and title for watchlist entries.
const RC_NEW
Definition Defines.php:133
const LIST_OR
Definition Defines.php:52
const RC_LOG
Definition Defines.php:134
const RC_EXTERNAL
Definition Defines.php:135
const LIST_AND
Definition Defines.php:49
const RC_EDIT
Definition Defines.php:132
const RC_CATEGORIZE
Definition Defines.php:136
getNamespace()
Get the namespace index.
getDBkey()
Get the main part with underscores.
Interface for objects representing user identity.
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 type of the DBMS (e.g.
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition defines.php:25