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