MediaWiki  master
WatchedItemQueryService.php
Go to the documentation of this file.
1 <?php
2 
10 use 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 $actorMigration;
73 
76 
79 
81  private $hookRunner;
82 
84  private $userFactory;
85 
89  private $expiryEnabled;
90 
91  public function __construct(
97  HookContainer $hookContainer,
99  bool $expiryEnabled = false
100  ) {
101  $this->loadBalancer = $loadBalancer;
102  $this->commentStore = $commentStore;
103  $this->actorMigration = $actorMigration;
104  $this->watchedItemStore = $watchedItemStore;
105  $this->permissionManager = $permissionManager;
106  $this->hookRunner = new HookRunner( $hookContainer );
107  $this->userFactory = $userFactory;
108  $this->expiryEnabled = $expiryEnabled;
109  }
110 
114  private function getExtensions() {
115  if ( $this->extensions === null ) {
116  $this->extensions = [];
117  $this->hookRunner->onWatchedItemQueryServiceExtensions( $this->extensions, $this );
118  }
119  return $this->extensions;
120  }
121 
125  private function getConnection() {
126  return $this->loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
127  }
128 
173  User $user, array $options = [], &$startFrom = null
174  ) {
175  $options += [
176  'includeFields' => [],
177  'namespaceIds' => [],
178  'filters' => [],
179  'allRevisions' => false,
180  'usedInGenerator' => false
181  ];
182 
183  Assert::parameter(
184  !isset( $options['rcTypes'] )
185  || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
186  '$options[\'rcTypes\']',
187  'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
188  );
189  Assert::parameter(
190  !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
191  '$options[\'dir\']',
192  'must be DIR_OLDER or DIR_NEWER'
193  );
194  Assert::parameter(
195  !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null
196  || isset( $options['dir'] ),
197  '$options[\'dir\']',
198  'must be provided when providing the "start" or "end" options or the $startFrom parameter'
199  );
200  Assert::parameter(
201  !isset( $options['startFrom'] ),
202  '$options[\'startFrom\']',
203  'must not be provided, use $startFrom instead'
204  );
205  Assert::parameter(
206  !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
207  '$startFrom',
208  'must be a two-element array'
209  );
210  if ( array_key_exists( 'watchlistOwner', $options ) ) {
211  Assert::parameterType(
212  User::class,
213  $options['watchlistOwner'],
214  '$options[\'watchlistOwner\']'
215  );
216  Assert::parameter(
217  isset( $options['watchlistOwnerToken'] ),
218  '$options[\'watchlistOwnerToken\']',
219  'must be provided when providing watchlistOwner option'
220  );
221  }
222 
223  $db = $this->getConnection();
224 
225  $tables = $this->getWatchedItemsWithRCInfoQueryTables( $options );
226  $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
227  $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
228  $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
229  $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options );
230 
231  if ( $startFrom !== null ) {
232  $conds[] = $this->getStartFromConds( $db, $options, $startFrom );
233  }
234 
235  foreach ( $this->getExtensions() as $extension ) {
236  $extension->modifyWatchedItemsWithRCInfoQuery(
237  $user, $options, $db,
238  $tables,
239  $fields,
240  $conds,
241  $dbOptions,
242  $joinConds
243  );
244  }
245 
246  $res = $db->select(
247  $tables,
248  $fields,
249  $conds,
250  __METHOD__,
251  $dbOptions,
252  $joinConds
253  );
254 
255  $limit = $dbOptions['LIMIT'] ?? INF;
256  $items = [];
257  $startFrom = null;
258  foreach ( $res as $row ) {
259  if ( --$limit <= 0 ) {
260  $startFrom = [ $row->rc_timestamp, $row->rc_id ];
261  break;
262  }
263 
264  $target = new TitleValue( (int)$row->rc_namespace, $row->rc_title );
265  $items[] = [
266  new WatchedItem(
267  $user,
268  $target,
269  $this->watchedItemStore->getLatestNotificationTimestamp(
270  $row->wl_notificationtimestamp, $user, $target
271  ),
272  $row->we_expiry ?? null
273  ),
274  $this->getRecentChangeFieldsFromRow( $row )
275  ];
276  }
277 
278  foreach ( $this->getExtensions() as $extension ) {
279  $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom );
280  }
281 
282  return $items;
283  }
284 
304  public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
305  if ( !$user->isRegistered() ) {
306  // TODO: should this just return an empty array or rather complain loud at this point
307  // as e.g. ApiBase::getWatchlistUser does?
308  return [];
309  }
310 
311  $options += [ 'namespaceIds' => [] ];
312 
313  Assert::parameter(
314  !isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
315  '$options[\'sort\']',
316  'must be SORT_ASC or SORT_DESC'
317  );
318  Assert::parameter(
319  !isset( $options['filter'] ) || in_array(
320  $options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
321  ),
322  '$options[\'filter\']',
323  'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
324  );
325  Assert::parameter(
326  !isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] )
327  || isset( $options['sort'] ),
328  '$options[\'sort\']',
329  'must be provided if any of "from", "until", "startFrom" options is provided'
330  );
331 
332  $db = $this->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() ) ],
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  $row->we_expiry ?? null
368  );
369  }
370 
371  return $watchedItems;
372  }
373 
374  private function getRecentChangeFieldsFromRow( stdClass $row ) {
375  // FIXME: This can be simplified to single array_filter call filtering by key value,
376  // now we have stopped supporting PHP 5.5
377  $allFields = get_object_vars( $row );
378  $rcKeys = array_filter(
379  array_keys( $allFields ),
380  function ( $key ) {
381  return substr( $key, 0, 3 ) === 'rc_';
382  }
383  );
384  return array_intersect_key( $allFields, array_flip( $rcKeys ) );
385  }
386 
387  private function getWatchedItemsWithRCInfoQueryTables( array $options ) {
388  $tables = [ 'recentchanges', 'watchlist' ];
389 
390  if ( $this->expiryEnabled ) {
391  $tables[] = 'watchlist_expiry';
392  }
393 
394  if ( !$options['allRevisions'] ) {
395  $tables[] = 'page';
396  }
397  if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
398  $tables += $this->commentStore->getJoin( 'rc_comment' )['tables'];
399  }
400  if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
401  in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
402  in_array( self::FILTER_ANON, $options['filters'] ) ||
403  in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
404  array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
405  ) {
406  $tables += $this->actorMigration->getJoin( 'rc_user' )['tables'];
407  }
408  return $tables;
409  }
410 
411  private function getWatchedItemsWithRCInfoQueryFields( array $options ) {
412  $fields = [
413  'rc_id',
414  'rc_namespace',
415  'rc_title',
416  'rc_timestamp',
417  'rc_type',
418  'rc_deleted',
419  'wl_notificationtimestamp'
420  ];
421 
422  if ( $this->expiryEnabled ) {
423  $fields[] = 'we_expiry';
424  }
425 
426  $rcIdFields = [
427  'rc_cur_id',
428  'rc_this_oldid',
429  'rc_last_oldid',
430  ];
431  if ( $options['usedInGenerator'] ) {
432  if ( $options['allRevisions'] ) {
433  $rcIdFields = [ 'rc_this_oldid' ];
434  } else {
435  $rcIdFields = [ 'rc_cur_id' ];
436  }
437  }
438  $fields = array_merge( $fields, $rcIdFields );
439 
440  if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) {
441  $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
442  }
443  if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
444  $fields['rc_user_text'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user_text'];
445  }
446  if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
447  $fields['rc_user'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user'];
448  }
449  if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
450  $fields += $this->commentStore->getJoin( 'rc_comment' )['fields'];
451  }
452  if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
453  $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
454  }
455  if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) {
456  $fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] );
457  }
458  if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
459  $fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
460  }
461  if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
462  // prefixed with rc_ to include the field in getRecentChangeFieldsFromRow
463  $fields['rc_tags'] = ChangeTags::makeTagSummarySubquery( 'recentchanges' );
464  }
465 
466  return $fields;
467  }
468 
470  IDatabase $db,
471  User $user,
472  array $options
473  ) {
474  $watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options );
475  $conds = [ 'wl_user' => $watchlistOwnerId ];
476 
477  if ( $this->expiryEnabled ) {
478  $conds[] = 'we_expiry IS NULL OR we_expiry > ' . $db->addQuotes( $db->timestamp() );
479  }
480 
481  if ( !$options['allRevisions'] ) {
482  $conds[] = $db->makeList(
483  [ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ],
484  LIST_OR
485  );
486  }
487 
488  if ( $options['namespaceIds'] ) {
489  $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
490  }
491 
492  if ( array_key_exists( 'rcTypes', $options ) ) {
493  $conds['rc_type'] = array_map( 'intval', $options['rcTypes'] );
494  }
495 
496  $conds = array_merge(
497  $conds,
498  $this->getWatchedItemsWithRCInfoQueryFilterConds( $user, $options )
499  );
500 
501  $conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) );
502 
503  if ( !isset( $options['start'] ) && !isset( $options['end'] ) && $db->getType() === 'mysql' ) {
504  // This is an index optimization for mysql
505  $conds[] = 'rc_timestamp > ' . $db->addQuotes( '' );
506  }
507 
508  $conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) );
509 
510  $deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user );
511  if ( $deletedPageLogCond ) {
512  $conds[] = $deletedPageLogCond;
513  }
514 
515  return $conds;
516  }
517 
518  private function getWatchlistOwnerId( UserIdentity $user, array $options ) {
519  if ( array_key_exists( 'watchlistOwner', $options ) ) {
521  $watchlistOwner = $options['watchlistOwner'];
522  $ownersToken =
523  $watchlistOwner->getOption( 'watchlisttoken' );
524  $token = $options['watchlistOwnerToken'];
525  if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
526  throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' );
527  }
528  return $watchlistOwner->getId();
529  }
530  return $user->getId();
531  }
532 
533  private function getWatchedItemsWithRCInfoQueryFilterConds( User $user, array $options ) {
534  $conds = [];
535 
536  if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) {
537  $conds[] = 'rc_minor != 0';
538  } elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) {
539  $conds[] = 'rc_minor = 0';
540  }
541 
542  if ( in_array( self::FILTER_BOT, $options['filters'] ) ) {
543  $conds[] = 'rc_bot != 0';
544  } elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) {
545  $conds[] = 'rc_bot = 0';
546  }
547 
548  if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
549  $conds[] = $this->actorMigration->isAnon(
550  $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
551  );
552  } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
553  $conds[] = $this->actorMigration->isNotAnon(
554  $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
555  );
556  }
557 
558  if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
559  // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
560  // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
561  if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
562  $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
563  } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
564  $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
565  }
566 
567  if ( in_array( self::FILTER_AUTOPATROLLED, $options['filters'] ) ) {
568  $conds['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
569  } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options['filters'] ) ) {
570  $conds[] = 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED;
571  }
572  }
573 
574  if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
575  $conds[] = 'rc_timestamp >= wl_notificationtimestamp';
576  } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
577  // TODO: should this be changed to use Database::makeList?
578  $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
579  }
580 
581  return $conds;
582  }
583 
584  private function getStartEndConds( IDatabase $db, array $options ) {
585  if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
586  return [];
587  }
588 
589  $conds = [];
590 
591  if ( isset( $options['start'] ) ) {
592  $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
593  $conds[] = 'rc_timestamp ' . $after . ' ' .
594  $db->addQuotes( $db->timestamp( $options['start'] ) );
595  }
596  if ( isset( $options['end'] ) ) {
597  $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
598  $conds[] = 'rc_timestamp ' . $before . ' ' .
599  $db->addQuotes( $db->timestamp( $options['end'] ) );
600  }
601 
602  return $conds;
603  }
604 
605  private function getUserRelatedConds( IDatabase $db, UserIdentity $user, array $options ) {
606  if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
607  return [];
608  }
609 
610  $conds = [];
611 
612  if ( array_key_exists( 'onlyByUser', $options ) ) {
613  $byUser = $this->userFactory->newFromName(
614  $options['onlyByUser'],
615  UserFactory::RIGOR_NONE
616  );
617  $conds[] = $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'];
618  } elseif ( array_key_exists( 'notByUser', $options ) ) {
619  $byUser = $this->userFactory->newFromName(
620  $options['notByUser'],
621  UserFactory::RIGOR_NONE
622  );
623  $conds[] = 'NOT(' . $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'] . ')';
624  }
625 
626  // Avoid brute force searches (T19342)
627  $bitmask = 0;
628  if ( !$this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
629  $bitmask = RevisionRecord::DELETED_USER;
630  } elseif ( !$this->permissionManager
631  ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
632  ) {
633  $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
634  }
635  if ( $bitmask ) {
636  $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
637  }
638 
639  return $conds;
640  }
641 
643  // LogPage::DELETED_ACTION hides the affected page, too. So hide those
644  // entirely from the watchlist, or someone could guess the title.
645  $bitmask = 0;
646  if ( !$this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
647  $bitmask = LogPage::DELETED_ACTION;
648  } elseif ( !$this->permissionManager
649  ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
650  ) {
652  }
653  if ( $bitmask ) {
654  return $db->makeList( [
655  'rc_type != ' . RC_LOG,
656  $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
657  ], LIST_OR );
658  }
659  return '';
660  }
661 
662  private function getStartFromConds( IDatabase $db, array $options, array $startFrom ) {
663  $op = $options['dir'] === self::DIR_OLDER ? '<' : '>';
664  list( $rcTimestamp, $rcId ) = $startFrom;
665  $rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) );
666  $rcId = (int)$rcId;
667  return $db->makeList(
668  [
669  "rc_timestamp $op $rcTimestamp",
670  $db->makeList(
671  [
672  "rc_timestamp = $rcTimestamp",
673  "rc_id $op= $rcId"
674  ],
675  LIST_AND
676  )
677  ],
678  LIST_OR
679  );
680  }
681 
683  IDatabase $db, UserIdentity $user, array $options
684  ) {
685  $conds = [ 'wl_user' => $user->getId() ];
686  if ( $options['namespaceIds'] ) {
687  $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
688  }
689  if ( isset( $options['filter'] ) ) {
690  $filter = $options['filter'];
691  if ( $filter === self::FILTER_CHANGED ) {
692  $conds[] = 'wl_notificationtimestamp IS NOT NULL';
693  } else {
694  $conds[] = 'wl_notificationtimestamp IS NULL';
695  }
696  }
697 
698  if ( isset( $options['from'] ) ) {
699  $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
700  $conds[] = $this->getFromUntilTargetConds( $db, $options['from'], $op );
701  }
702  if ( isset( $options['until'] ) ) {
703  $op = $options['sort'] === self::SORT_ASC ? '<' : '>';
704  $conds[] = $this->getFromUntilTargetConds( $db, $options['until'], $op );
705  }
706  if ( isset( $options['startFrom'] ) ) {
707  $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
708  $conds[] = $this->getFromUntilTargetConds( $db, $options['startFrom'], $op );
709  }
710 
711  return $conds;
712  }
713 
723  private function getFromUntilTargetConds( IDatabase $db, LinkTarget $target, $op ) {
724  return $db->makeList(
725  [
726  "wl_namespace $op " . $target->getNamespace(),
727  $db->makeList(
728  [
729  'wl_namespace = ' . $target->getNamespace(),
730  "wl_title $op= " . $db->addQuotes( $target->getDBkey() )
731  ],
732  LIST_AND
733  )
734  ],
735  LIST_OR
736  );
737  }
738 
739  private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) {
740  $dbOptions = [];
741 
742  if ( array_key_exists( 'dir', $options ) ) {
743  $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
744  $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
745  }
746 
747  if ( array_key_exists( 'limit', $options ) ) {
748  $dbOptions['LIMIT'] = (int)$options['limit'] + 1;
749  }
750 
751  return $dbOptions;
752  }
753 
754  private function getWatchedItemsForUserQueryDbOptions( array $options ) {
755  $dbOptions = [];
756  if ( array_key_exists( 'sort', $options ) ) {
757  $dbOptions['ORDER BY'] = [
758  "wl_namespace {$options['sort']}",
759  "wl_title {$options['sort']}"
760  ];
761  if ( count( $options['namespaceIds'] ) === 1 ) {
762  $dbOptions['ORDER BY'] = "wl_title {$options['sort']}";
763  }
764  }
765  if ( array_key_exists( 'limit', $options ) ) {
766  $dbOptions['LIMIT'] = (int)$options['limit'];
767  }
768  return $dbOptions;
769  }
770 
771  private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
772  $joinConds = [
773  'watchlist' => [ 'JOIN',
774  [
775  'wl_namespace=rc_namespace',
776  'wl_title=rc_title'
777  ]
778  ]
779  ];
780 
781  if ( $this->expiryEnabled ) {
782  $joinConds['watchlist_expiry'] = [ 'LEFT JOIN', 'wl_id = we_item' ];
783  }
784 
785  if ( !$options['allRevisions'] ) {
786  $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
787  }
788  if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
789  $joinConds += $this->commentStore->getJoin( 'rc_comment' )['joins'];
790  }
791  if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
792  in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
793  in_array( self::FILTER_ANON, $options['filters'] ) ||
794  in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
795  array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
796  ) {
797  $joinConds += $this->actorMigration->getJoin( 'rc_user' )['joins'];
798  }
799  return $joinConds;
800  }
801 
802 }
WatchedItemQueryService\getConnection
getConnection()
Definition: WatchedItemQueryService.php:125
LIST_OR
const LIST_OR
Definition: Defines.php:45
ChangeTags\makeTagSummarySubquery
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Definition: ChangeTags.php:941
WatchedItemQueryService\getFromUntilTargetConds
getFromUntilTargetConds(IDatabase $db, LinkTarget $target, $op)
Creates a query condition part for getting only items before or after the given link target (while or...
Definition: WatchedItemQueryService.php:723
WatchedItemQueryService\getWatchedItemsWithRCInfoQueryConds
getWatchedItemsWithRCInfoQueryConds(IDatabase $db, User $user, array $options)
Definition: WatchedItemQueryService.php:469
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:45
WatchedItemQueryService\getWatchedItemsWithRCInfoQueryJoinConds
getWatchedItemsWithRCInfoQueryJoinConds(array $options)
Definition: WatchedItemQueryService.php:771
WatchedItemQueryServiceExtension
Definition: WatchedItemQueryServiceExtension.php:17
ApiUsageException\newWithMessage
static newWithMessage(?ApiBase $module, $msg, $code=null, $data=null, $httpCode=0, Throwable $previous=null)
Definition: ApiUsageException.php:68
WatchedItemQueryService\$commentStore
CommentStore $commentStore
Definition: WatchedItemQueryService.php:69
RC_EDIT
const RC_EDIT
Definition: Defines.php:125
WatchedItemQueryService\INCLUDE_COMMENT
const INCLUDE_COMMENT
Definition: WatchedItemQueryService.php:32
WatchedItemQueryService\FILTER_NOT_AUTOPATROLLED
const FILTER_NOT_AUTOPATROLLED
Definition: WatchedItemQueryService.php:51
WatchedItemQueryService\FILTER_PATROLLED
const FILTER_PATROLLED
Definition: WatchedItemQueryService.php:48
WatchedItemQueryService\$userFactory
UserFactory $userFactory
Definition: WatchedItemQueryService.php:84
WatchedItemQueryService\getWatchedItemsWithRCInfoQueryFields
getWatchedItemsWithRCInfoQueryFields(array $options)
Definition: WatchedItemQueryService.php:411
LIST_AND
const LIST_AND
Definition: Defines.php:42
WatchedItemQueryService\getStartEndConds
getStartEndConds(IDatabase $db, array $options)
Definition: WatchedItemQueryService.php:584
WatchedItemQueryService\getWatchedItemsWithRCInfoQueryDbOptions
getWatchedItemsWithRCInfoQueryDbOptions(array $options)
Definition: WatchedItemQueryService.php:739
CommentStore
Handle database storage of comments such as edit summaries and log reasons.
Definition: CommentStore.php:42
WatchedItemQueryService\$hookRunner
HookRunner $hookRunner
Definition: WatchedItemQueryService.php:81
WatchedItemQueryService\getWatchedItemsForUserQueryDbOptions
getWatchedItemsForUserQueryDbOptions(array $options)
Definition: WatchedItemQueryService.php:754
User\useNPPatrol
useNPPatrol()
Check whether to enable new pages patrol features for this user.
Definition: User.php:3061
WatchedItemQueryService\getWatchedItemsWithRecentChangeInfo
getWatchedItemsWithRecentChangeInfo(User $user, array $options=[], &$startFrom=null)
Definition: WatchedItemQueryService.php:172
ActorMigration
This class handles the logic for the actor table migration and should always be used in lieu of direc...
Definition: ActorMigration.php:41
RC_NEW
const RC_NEW
Definition: Defines.php:126
$res
$res
Definition: testCompression.php:57
User\useRCPatrol
useRCPatrol()
Check whether to enable recent changes patrol features for this user.
Definition: User.php:3052
WatchedItemQueryService\$actorMigration
ActorMigration $actorMigration
Definition: WatchedItemQueryService.php:72
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:32
WatchedItemQueryService\INCLUDE_TAGS
const INCLUDE_TAGS
Definition: WatchedItemQueryService.php:37
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
WatchedItemQueryService\getWatchlistOwnerId
getWatchlistOwnerId(UserIdentity $user, array $options)
Definition: WatchedItemQueryService.php:518
WatchedItemQueryService\getWatchedItemsForUserQueryConds
getWatchedItemsForUserQueryConds(IDatabase $db, UserIdentity $user, array $options)
Definition: WatchedItemQueryService.php:682
WatchedItemQueryService\INCLUDE_LOG_INFO
const INCLUDE_LOG_INFO
Definition: WatchedItemQueryService.php:36
RC_LOG
const RC_LOG
Definition: Defines.php:127
MediaWiki\Linker\LinkTarget\getNamespace
getNamespace()
Get the namespace index.
Wikimedia\Rdbms\IDatabase\timestamp
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
WatchedItemQueryService\$watchedItemStore
WatchedItemStoreInterface $watchedItemStore
Definition: WatchedItemQueryService.php:75
WatchedItemQueryService\getWatchedItemsWithRCInfoQueryFilterConds
getWatchedItemsWithRCInfoQueryFilterConds(User $user, array $options)
Definition: WatchedItemQueryService.php:533
MediaWiki\User\UserIdentity\isRegistered
isRegistered()
WatchedItemQueryService\FILTER_MINOR
const FILTER_MINOR
Definition: WatchedItemQueryService.php:42
WatchedItemQueryService\FILTER_NOT_CHANGED
const FILTER_NOT_CHANGED
Definition: WatchedItemQueryService.php:55
WatchedItemQueryService\INCLUDE_AUTOPATROL_INFO
const INCLUDE_AUTOPATROL_INFO
Definition: WatchedItemQueryService.php:34
WatchedItemQueryService\INCLUDE_USER
const INCLUDE_USER
Definition: WatchedItemQueryService.php:30
WatchedItemQueryService\FILTER_NOT_BOT
const FILTER_NOT_BOT
Definition: WatchedItemQueryService.php:45
WatchedItemQueryService
Definition: WatchedItemQueryService.php:24
WatchedItemQueryService\INCLUDE_PATROL_INFO
const INCLUDE_PATROL_INFO
Definition: WatchedItemQueryService.php:33
WatchedItemQueryService\$permissionManager
PermissionManager $permissionManager
Definition: WatchedItemQueryService.php:78
WatchedItemQueryService\getWatchedItemsForUser
getWatchedItemsForUser(UserIdentity $user, array $options=[])
For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser.
Definition: WatchedItemQueryService.php:304
WatchedItemQueryService\$expiryEnabled
bool $expiryEnabled
Correlates to $wgWatchlistExpiry feature flag.
Definition: WatchedItemQueryService.php:89
WatchedItemQueryService\FILTER_BOT
const FILTER_BOT
Definition: WatchedItemQueryService.php:44
WatchedItemQueryService\INCLUDE_USER_ID
const INCLUDE_USER_ID
Definition: WatchedItemQueryService.php:31
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
LogPage\DELETED_ACTION
const DELETED_ACTION
Definition: LogPage.php:38
WatchedItemQueryService\$loadBalancer
ILoadBalancer $loadBalancer
Definition: WatchedItemQueryService.php:63
WatchedItemQueryService\DIR_NEWER
const DIR_NEWER
Definition: WatchedItemQueryService.php:27
MediaWiki\Permissions\PermissionManager
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Definition: PermissionManager.php:51
RC_EXTERNAL
const RC_EXTERNAL
Definition: Defines.php:128
WatchedItemQueryService\SORT_DESC
const SORT_DESC
Definition: WatchedItemQueryService.php:58
WatchedItemQueryService\getRecentChangeFieldsFromRow
getRecentChangeFieldsFromRow(stdClass $row)
Definition: WatchedItemQueryService.php:374
WatchedItem
Representation of a pair of user and title for watchlist entries.
Definition: WatchedItem.php:35
WatchedItemQueryService\FILTER_NOT_PATROLLED
const FILTER_NOT_PATROLLED
Definition: WatchedItemQueryService.php:49
MediaWiki\Linker\LinkTarget\getDBkey
getDBkey()
Get the main part with underscores.
WatchedItemQueryService\SORT_ASC
const SORT_ASC
Definition: WatchedItemQueryService.php:57
WatchedItemQueryService\FILTER_NOT_MINOR
const FILTER_NOT_MINOR
Definition: WatchedItemQueryService.php:43
WatchedItemQueryService\INCLUDE_FLAGS
const INCLUDE_FLAGS
Definition: WatchedItemQueryService.php:29
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:85
WatchedItemQueryService\__construct
__construct(ILoadBalancer $loadBalancer, CommentStore $commentStore, ActorMigration $actorMigration, WatchedItemStoreInterface $watchedItemStore, PermissionManager $permissionManager, HookContainer $hookContainer, UserFactory $userFactory, bool $expiryEnabled=false)
Definition: WatchedItemQueryService.php:91
Wikimedia\Rdbms\IDatabase\bitAnd
bitAnd( $fieldLeft, $fieldRight)
WatchedItemQueryService\DIR_OLDER
const DIR_OLDER
Definition: WatchedItemQueryService.php:26
WatchedItemQueryService\getUserRelatedConds
getUserRelatedConds(IDatabase $db, UserIdentity $user, array $options)
Definition: WatchedItemQueryService.php:605
WatchedItemQueryService\getExtensions
getExtensions()
Definition: WatchedItemQueryService.php:114
MediaWiki\User\UserIdentity\getId
getId()
RC_CATEGORIZE
const RC_CATEGORIZE
Definition: Defines.php:129
Wikimedia\Rdbms\IDatabase\addQuotes
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
WatchedItemQueryService\FILTER_CHANGED
const FILTER_CHANGED
Definition: WatchedItemQueryService.php:54
WatchedItemQueryService\FILTER_AUTOPATROLLED
const FILTER_AUTOPATROLLED
Definition: WatchedItemQueryService.php:50
WatchedItemQueryService\FILTER_NOT_UNREAD
const FILTER_NOT_UNREAD
Definition: WatchedItemQueryService.php:53
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:83
WatchedItemQueryService\FILTER_NOT_ANON
const FILTER_NOT_ANON
Definition: WatchedItemQueryService.php:47
Wikimedia\Rdbms\IDatabase\getType
getType()
Get the type of the DBMS (e.g.
WatchedItemQueryService\getWatchedItemsWithRCInfoQueryTables
getWatchedItemsWithRCInfoQueryTables(array $options)
Definition: WatchedItemQueryService.php:387
LogPage\DELETED_RESTRICTED
const DELETED_RESTRICTED
Definition: LogPage.php:41
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:571
WatchedItemQueryService\INCLUDE_SIZES
const INCLUDE_SIZES
Definition: WatchedItemQueryService.php:35
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
WatchedItemQueryService\getExtraDeletedPageLogEntryRelatedCond
getExtraDeletedPageLogEntryRelatedCond(IDatabase $db, UserIdentity $user)
Definition: WatchedItemQueryService.php:642
WatchedItemStoreInterface
Definition: WatchedItemStoreInterface.php:30
WatchedItemQueryService\FILTER_ANON
const FILTER_ANON
Definition: WatchedItemQueryService.php:46
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:63
WatchedItemQueryService\FILTER_UNREAD
const FILTER_UNREAD
Definition: WatchedItemQueryService.php:52
WatchedItemQueryService\$extensions
WatchedItemQueryServiceExtension[] null $extensions
Definition: WatchedItemQueryService.php:66
Wikimedia\Rdbms\IDatabase\makeList
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
MediaWiki\User\UserFactory
Creates User objects.
Definition: UserFactory.php:41
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
TitleValue
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:40
WatchedItemQueryService\getStartFromConds
getStartFromConds(IDatabase $db, array $options, array $startFrom)
Definition: WatchedItemQueryService.php:662