WatchedItemQueryService.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. <?php
  2. use MediaWiki\Linker\LinkTarget;
  3. use MediaWiki\Permissions\PermissionManager;
  4. use MediaWiki\Revision\RevisionRecord;
  5. use MediaWiki\User\UserIdentity;
  6. use Wikimedia\Assert\Assert;
  7. use Wikimedia\Rdbms\IDatabase;
  8. use Wikimedia\Rdbms\ILoadBalancer;
  9. /**
  10. * Class performing complex database queries related to WatchedItems.
  11. *
  12. * @since 1.28
  13. *
  14. * @file
  15. * @ingroup Watchlist
  16. *
  17. * @license GPL-2.0-or-later
  18. */
  19. class WatchedItemQueryService {
  20. const DIR_OLDER = 'older';
  21. const DIR_NEWER = 'newer';
  22. const INCLUDE_FLAGS = 'flags';
  23. const INCLUDE_USER = 'user';
  24. const INCLUDE_USER_ID = 'userid';
  25. const INCLUDE_COMMENT = 'comment';
  26. const INCLUDE_PATROL_INFO = 'patrol';
  27. const INCLUDE_AUTOPATROL_INFO = 'autopatrol';
  28. const INCLUDE_SIZES = 'sizes';
  29. const INCLUDE_LOG_INFO = 'loginfo';
  30. const INCLUDE_TAGS = 'tags';
  31. // FILTER_* constants are part of public API (are used in ApiQueryWatchlist and
  32. // ApiQueryWatchlistRaw classes) and should not be changed.
  33. // Changing values of those constants will result in a breaking change in the API
  34. const FILTER_MINOR = 'minor';
  35. const FILTER_NOT_MINOR = '!minor';
  36. const FILTER_BOT = 'bot';
  37. const FILTER_NOT_BOT = '!bot';
  38. const FILTER_ANON = 'anon';
  39. const FILTER_NOT_ANON = '!anon';
  40. const FILTER_PATROLLED = 'patrolled';
  41. const FILTER_NOT_PATROLLED = '!patrolled';
  42. const FILTER_AUTOPATROLLED = 'autopatrolled';
  43. const FILTER_NOT_AUTOPATROLLED = '!autopatrolled';
  44. const FILTER_UNREAD = 'unread';
  45. const FILTER_NOT_UNREAD = '!unread';
  46. const FILTER_CHANGED = 'changed';
  47. const FILTER_NOT_CHANGED = '!changed';
  48. const SORT_ASC = 'ASC';
  49. const SORT_DESC = 'DESC';
  50. /**
  51. * @var ILoadBalancer
  52. */
  53. private $loadBalancer;
  54. /** @var WatchedItemQueryServiceExtension[]|null */
  55. private $extensions = null;
  56. /** @var CommentStore */
  57. private $commentStore;
  58. /** @var ActorMigration */
  59. private $actorMigration;
  60. /** @var WatchedItemStoreInterface */
  61. private $watchedItemStore;
  62. /** @var PermissionManager */
  63. private $permissionManager;
  64. public function __construct(
  65. ILoadBalancer $loadBalancer,
  66. CommentStore $commentStore,
  67. ActorMigration $actorMigration,
  68. WatchedItemStoreInterface $watchedItemStore,
  69. PermissionManager $permissionManager
  70. ) {
  71. $this->loadBalancer = $loadBalancer;
  72. $this->commentStore = $commentStore;
  73. $this->actorMigration = $actorMigration;
  74. $this->watchedItemStore = $watchedItemStore;
  75. $this->permissionManager = $permissionManager;
  76. }
  77. /**
  78. * @return WatchedItemQueryServiceExtension[]
  79. */
  80. private function getExtensions() {
  81. if ( $this->extensions === null ) {
  82. $this->extensions = [];
  83. Hooks::run( 'WatchedItemQueryServiceExtensions', [ &$this->extensions, $this ] );
  84. }
  85. return $this->extensions;
  86. }
  87. /**
  88. * @return IDatabase
  89. */
  90. private function getConnection() {
  91. return $this->loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
  92. }
  93. /**
  94. * @param User $user
  95. * @param array $options Allowed keys:
  96. * 'includeFields' => string[] RecentChange fields to be included in the result,
  97. * self::INCLUDE_* constants should be used
  98. * 'filters' => string[] optional filters to narrow down resulted items
  99. * 'namespaceIds' => int[] optional namespace IDs to filter by
  100. * (defaults to all namespaces)
  101. * 'allRevisions' => bool return multiple revisions of the same page if true,
  102. * only the most recent if false (default)
  103. * 'rcTypes' => int[] which types of RecentChanges to include
  104. * (defaults to all types), allowed values: RC_EDIT, RC_NEW,
  105. * RC_LOG, RC_EXTERNAL, RC_CATEGORIZE
  106. * 'onlyByUser' => string only list changes by a specified user
  107. * 'notByUser' => string do not incluide changes by a specified user
  108. * 'dir' => string in which direction to enumerate, accepted values:
  109. * - DIR_OLDER list newest first
  110. * - DIR_NEWER list oldest first
  111. * 'start' => string (format accepted by wfTimestamp) requires 'dir' option,
  112. * timestamp to start enumerating from
  113. * 'end' => string (format accepted by wfTimestamp) requires 'dir' option,
  114. * timestamp to end enumerating
  115. * 'watchlistOwner' => User user whose watchlist items should be listed if different
  116. * than the one specified with $user param, requires
  117. * 'watchlistOwnerToken' option
  118. * 'watchlistOwnerToken' => string a watchlist token used to access another user's
  119. * watchlist, used with 'watchlistOwnerToken' option
  120. * 'limit' => int maximum numbers of items to return
  121. * 'usedInGenerator' => bool include only RecentChange id field required by the
  122. * generator ('rc_cur_id' or 'rc_this_oldid') if true, or all
  123. * id fields ('rc_cur_id', 'rc_this_oldid', 'rc_last_oldid')
  124. * if false (default)
  125. * @param array|null &$startFrom Continuation value: [ string $rcTimestamp, int $rcId ]
  126. * @return array[] Array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ),
  127. * where $recentChangeInfo contains the following keys:
  128. * - 'rc_id',
  129. * - 'rc_namespace',
  130. * - 'rc_title',
  131. * - 'rc_timestamp',
  132. * - 'rc_type',
  133. * - 'rc_deleted',
  134. * Additional keys could be added by specifying the 'includeFields' option
  135. */
  136. public function getWatchedItemsWithRecentChangeInfo(
  137. User $user, array $options = [], &$startFrom = null
  138. ) {
  139. $options += [
  140. 'includeFields' => [],
  141. 'namespaceIds' => [],
  142. 'filters' => [],
  143. 'allRevisions' => false,
  144. 'usedInGenerator' => false
  145. ];
  146. Assert::parameter(
  147. !isset( $options['rcTypes'] )
  148. || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
  149. '$options[\'rcTypes\']',
  150. 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
  151. );
  152. Assert::parameter(
  153. !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
  154. '$options[\'dir\']',
  155. 'must be DIR_OLDER or DIR_NEWER'
  156. );
  157. Assert::parameter(
  158. !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null
  159. || isset( $options['dir'] ),
  160. '$options[\'dir\']',
  161. 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
  162. );
  163. Assert::parameter(
  164. !isset( $options['startFrom'] ),
  165. '$options[\'startFrom\']',
  166. 'must not be provided, use $startFrom instead'
  167. );
  168. Assert::parameter(
  169. !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
  170. '$startFrom',
  171. 'must be a two-element array'
  172. );
  173. if ( array_key_exists( 'watchlistOwner', $options ) ) {
  174. Assert::parameterType(
  175. User::class,
  176. $options['watchlistOwner'],
  177. '$options[\'watchlistOwner\']'
  178. );
  179. Assert::parameter(
  180. isset( $options['watchlistOwnerToken'] ),
  181. '$options[\'watchlistOwnerToken\']',
  182. 'must be provided when providing watchlistOwner option'
  183. );
  184. }
  185. $db = $this->getConnection();
  186. $tables = $this->getWatchedItemsWithRCInfoQueryTables( $options );
  187. $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
  188. $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
  189. $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
  190. $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options );
  191. if ( $startFrom !== null ) {
  192. $conds[] = $this->getStartFromConds( $db, $options, $startFrom );
  193. }
  194. foreach ( $this->getExtensions() as $extension ) {
  195. $extension->modifyWatchedItemsWithRCInfoQuery(
  196. $user, $options, $db,
  197. $tables,
  198. $fields,
  199. $conds,
  200. $dbOptions,
  201. $joinConds
  202. );
  203. }
  204. $res = $db->select(
  205. $tables,
  206. $fields,
  207. $conds,
  208. __METHOD__,
  209. $dbOptions,
  210. $joinConds
  211. );
  212. $limit = $dbOptions['LIMIT'] ?? INF;
  213. $items = [];
  214. $startFrom = null;
  215. foreach ( $res as $row ) {
  216. if ( --$limit <= 0 ) {
  217. $startFrom = [ $row->rc_timestamp, $row->rc_id ];
  218. break;
  219. }
  220. $target = new TitleValue( (int)$row->rc_namespace, $row->rc_title );
  221. $items[] = [
  222. new WatchedItem(
  223. $user,
  224. $target,
  225. $this->watchedItemStore->getLatestNotificationTimestamp(
  226. $row->wl_notificationtimestamp, $user, $target
  227. )
  228. ),
  229. $this->getRecentChangeFieldsFromRow( $row )
  230. ];
  231. }
  232. foreach ( $this->getExtensions() as $extension ) {
  233. $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom );
  234. }
  235. return $items;
  236. }
  237. /**
  238. * For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser
  239. *
  240. * @param UserIdentity $user
  241. * @param array $options Allowed keys:
  242. * 'sort' => string optional sorting by namespace ID and title
  243. * one of the self::SORT_* constants
  244. * 'namespaceIds' => int[] optional namespace IDs to filter by (defaults to all namespaces)
  245. * 'limit' => int maximum number of items to return
  246. * 'filter' => string optional filter, one of the self::FILTER_* contants
  247. * 'from' => LinkTarget requires 'sort' key, only return items starting from
  248. * those related to the link target
  249. * 'until' => LinkTarget requires 'sort' key, only return items until
  250. * those related to the link target
  251. * 'startFrom' => LinkTarget requires 'sort' key, only return items starting from
  252. * those related to the link target, allows to skip some link targets
  253. * specified using the form option
  254. * @return WatchedItem[]
  255. */
  256. public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
  257. if ( !$user->isRegistered() ) {
  258. // TODO: should this just return an empty array or rather complain loud at this point
  259. // as e.g. ApiBase::getWatchlistUser does?
  260. return [];
  261. }
  262. $options += [ 'namespaceIds' => [] ];
  263. Assert::parameter(
  264. !isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
  265. '$options[\'sort\']',
  266. 'must be SORT_ASC or SORT_DESC'
  267. );
  268. Assert::parameter(
  269. !isset( $options['filter'] ) || in_array(
  270. $options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
  271. ),
  272. '$options[\'filter\']',
  273. 'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
  274. );
  275. Assert::parameter(
  276. !isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] )
  277. || isset( $options['sort'] ),
  278. '$options[\'sort\']',
  279. 'must be provided if any of "from", "until", "startFrom" options is provided'
  280. );
  281. $db = $this->getConnection();
  282. $conds = $this->getWatchedItemsForUserQueryConds( $db, $user, $options );
  283. $dbOptions = $this->getWatchedItemsForUserQueryDbOptions( $options );
  284. $res = $db->select(
  285. 'watchlist',
  286. [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
  287. $conds,
  288. __METHOD__,
  289. $dbOptions
  290. );
  291. $watchedItems = [];
  292. foreach ( $res as $row ) {
  293. $target = new TitleValue( (int)$row->wl_namespace, $row->wl_title );
  294. // todo these could all be cached at some point?
  295. $watchedItems[] = new WatchedItem(
  296. $user,
  297. $target,
  298. $this->watchedItemStore->getLatestNotificationTimestamp(
  299. $row->wl_notificationtimestamp, $user, $target
  300. )
  301. );
  302. }
  303. return $watchedItems;
  304. }
  305. private function getRecentChangeFieldsFromRow( stdClass $row ) {
  306. // FIXME: This can be simplified to single array_filter call filtering by key value,
  307. // now we have stopped supporting PHP 5.5
  308. $allFields = get_object_vars( $row );
  309. $rcKeys = array_filter(
  310. array_keys( $allFields ),
  311. function ( $key ) {
  312. return substr( $key, 0, 3 ) === 'rc_';
  313. }
  314. );
  315. return array_intersect_key( $allFields, array_flip( $rcKeys ) );
  316. }
  317. private function getWatchedItemsWithRCInfoQueryTables( array $options ) {
  318. $tables = [ 'recentchanges', 'watchlist' ];
  319. if ( !$options['allRevisions'] ) {
  320. $tables[] = 'page';
  321. }
  322. if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
  323. $tables += $this->commentStore->getJoin( 'rc_comment' )['tables'];
  324. }
  325. if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
  326. in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
  327. in_array( self::FILTER_ANON, $options['filters'] ) ||
  328. in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
  329. array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
  330. ) {
  331. $tables += $this->actorMigration->getJoin( 'rc_user' )['tables'];
  332. }
  333. return $tables;
  334. }
  335. private function getWatchedItemsWithRCInfoQueryFields( array $options ) {
  336. $fields = [
  337. 'rc_id',
  338. 'rc_namespace',
  339. 'rc_title',
  340. 'rc_timestamp',
  341. 'rc_type',
  342. 'rc_deleted',
  343. 'wl_notificationtimestamp'
  344. ];
  345. $rcIdFields = [
  346. 'rc_cur_id',
  347. 'rc_this_oldid',
  348. 'rc_last_oldid',
  349. ];
  350. if ( $options['usedInGenerator'] ) {
  351. if ( $options['allRevisions'] ) {
  352. $rcIdFields = [ 'rc_this_oldid' ];
  353. } else {
  354. $rcIdFields = [ 'rc_cur_id' ];
  355. }
  356. }
  357. $fields = array_merge( $fields, $rcIdFields );
  358. if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) {
  359. $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
  360. }
  361. if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
  362. $fields['rc_user_text'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user_text'];
  363. }
  364. if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
  365. $fields['rc_user'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user'];
  366. }
  367. if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
  368. $fields += $this->commentStore->getJoin( 'rc_comment' )['fields'];
  369. }
  370. if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
  371. $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
  372. }
  373. if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) {
  374. $fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] );
  375. }
  376. if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
  377. $fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
  378. }
  379. if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
  380. // prefixed with rc_ to include the field in getRecentChangeFieldsFromRow
  381. $fields['rc_tags'] = ChangeTags::makeTagSummarySubquery( 'recentchanges' );
  382. }
  383. return $fields;
  384. }
  385. private function getWatchedItemsWithRCInfoQueryConds(
  386. IDatabase $db,
  387. User $user,
  388. array $options
  389. ) {
  390. $watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options );
  391. $conds = [ 'wl_user' => $watchlistOwnerId ];
  392. if ( !$options['allRevisions'] ) {
  393. $conds[] = $db->makeList(
  394. [ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ],
  395. LIST_OR
  396. );
  397. }
  398. if ( $options['namespaceIds'] ) {
  399. $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
  400. }
  401. if ( array_key_exists( 'rcTypes', $options ) ) {
  402. $conds['rc_type'] = array_map( 'intval', $options['rcTypes'] );
  403. }
  404. $conds = array_merge(
  405. $conds,
  406. $this->getWatchedItemsWithRCInfoQueryFilterConds( $user, $options )
  407. );
  408. $conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) );
  409. if ( !isset( $options['start'] ) && !isset( $options['end'] ) && $db->getType() === 'mysql' ) {
  410. // This is an index optimization for mysql
  411. $conds[] = 'rc_timestamp > ' . $db->addQuotes( '' );
  412. }
  413. $conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) );
  414. $deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user );
  415. if ( $deletedPageLogCond ) {
  416. $conds[] = $deletedPageLogCond;
  417. }
  418. return $conds;
  419. }
  420. private function getWatchlistOwnerId( UserIdentity $user, array $options ) {
  421. if ( array_key_exists( 'watchlistOwner', $options ) ) {
  422. /** @var User $watchlistOwner */
  423. $watchlistOwner = $options['watchlistOwner'];
  424. $ownersToken =
  425. $watchlistOwner->getOption( 'watchlisttoken' );
  426. $token = $options['watchlistOwnerToken'];
  427. if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
  428. throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' );
  429. }
  430. return $watchlistOwner->getId();
  431. }
  432. return $user->getId();
  433. }
  434. private function getWatchedItemsWithRCInfoQueryFilterConds( User $user, array $options ) {
  435. $conds = [];
  436. if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) {
  437. $conds[] = 'rc_minor != 0';
  438. } elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) {
  439. $conds[] = 'rc_minor = 0';
  440. }
  441. if ( in_array( self::FILTER_BOT, $options['filters'] ) ) {
  442. $conds[] = 'rc_bot != 0';
  443. } elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) {
  444. $conds[] = 'rc_bot = 0';
  445. }
  446. if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
  447. $conds[] = $this->actorMigration->isAnon(
  448. $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
  449. );
  450. } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
  451. $conds[] = $this->actorMigration->isNotAnon(
  452. $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
  453. );
  454. }
  455. if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
  456. // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
  457. // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
  458. if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
  459. $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
  460. } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
  461. $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
  462. }
  463. if ( in_array( self::FILTER_AUTOPATROLLED, $options['filters'] ) ) {
  464. $conds['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
  465. } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options['filters'] ) ) {
  466. $conds[] = 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED;
  467. }
  468. }
  469. if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
  470. $conds[] = 'rc_timestamp >= wl_notificationtimestamp';
  471. } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
  472. // TODO: should this be changed to use Database::makeList?
  473. $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
  474. }
  475. return $conds;
  476. }
  477. private function getStartEndConds( IDatabase $db, array $options ) {
  478. if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
  479. return [];
  480. }
  481. $conds = [];
  482. if ( isset( $options['start'] ) ) {
  483. $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
  484. $conds[] = 'rc_timestamp ' . $after . ' ' .
  485. $db->addQuotes( $db->timestamp( $options['start'] ) );
  486. }
  487. if ( isset( $options['end'] ) ) {
  488. $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
  489. $conds[] = 'rc_timestamp ' . $before . ' ' .
  490. $db->addQuotes( $db->timestamp( $options['end'] ) );
  491. }
  492. return $conds;
  493. }
  494. private function getUserRelatedConds( IDatabase $db, UserIdentity $user, array $options ) {
  495. if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
  496. return [];
  497. }
  498. $conds = [];
  499. if ( array_key_exists( 'onlyByUser', $options ) ) {
  500. $byUser = User::newFromName( $options['onlyByUser'], false );
  501. $conds[] = $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'];
  502. } elseif ( array_key_exists( 'notByUser', $options ) ) {
  503. $byUser = User::newFromName( $options['notByUser'], false );
  504. $conds[] = 'NOT(' . $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'] . ')';
  505. }
  506. // Avoid brute force searches (T19342)
  507. $bitmask = 0;
  508. if ( !$this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
  509. $bitmask = RevisionRecord::DELETED_USER;
  510. } elseif ( !$this->permissionManager
  511. ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
  512. ) {
  513. $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
  514. }
  515. if ( $bitmask ) {
  516. $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
  517. }
  518. return $conds;
  519. }
  520. private function getExtraDeletedPageLogEntryRelatedCond( IDatabase $db, UserIdentity $user ) {
  521. // LogPage::DELETED_ACTION hides the affected page, too. So hide those
  522. // entirely from the watchlist, or someone could guess the title.
  523. $bitmask = 0;
  524. if ( !$this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
  525. $bitmask = LogPage::DELETED_ACTION;
  526. } elseif ( !$this->permissionManager
  527. ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
  528. ) {
  529. $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
  530. }
  531. if ( $bitmask ) {
  532. return $db->makeList( [
  533. 'rc_type != ' . RC_LOG,
  534. $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
  535. ], LIST_OR );
  536. }
  537. return '';
  538. }
  539. private function getStartFromConds( IDatabase $db, array $options, array $startFrom ) {
  540. $op = $options['dir'] === self::DIR_OLDER ? '<' : '>';
  541. list( $rcTimestamp, $rcId ) = $startFrom;
  542. $rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) );
  543. $rcId = (int)$rcId;
  544. return $db->makeList(
  545. [
  546. "rc_timestamp $op $rcTimestamp",
  547. $db->makeList(
  548. [
  549. "rc_timestamp = $rcTimestamp",
  550. "rc_id $op= $rcId"
  551. ],
  552. LIST_AND
  553. )
  554. ],
  555. LIST_OR
  556. );
  557. }
  558. private function getWatchedItemsForUserQueryConds(
  559. IDatabase $db, UserIdentity $user, array $options
  560. ) {
  561. $conds = [ 'wl_user' => $user->getId() ];
  562. if ( $options['namespaceIds'] ) {
  563. $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
  564. }
  565. if ( isset( $options['filter'] ) ) {
  566. $filter = $options['filter'];
  567. if ( $filter === self::FILTER_CHANGED ) {
  568. $conds[] = 'wl_notificationtimestamp IS NOT NULL';
  569. } else {
  570. $conds[] = 'wl_notificationtimestamp IS NULL';
  571. }
  572. }
  573. if ( isset( $options['from'] ) ) {
  574. $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
  575. $conds[] = $this->getFromUntilTargetConds( $db, $options['from'], $op );
  576. }
  577. if ( isset( $options['until'] ) ) {
  578. $op = $options['sort'] === self::SORT_ASC ? '<' : '>';
  579. $conds[] = $this->getFromUntilTargetConds( $db, $options['until'], $op );
  580. }
  581. if ( isset( $options['startFrom'] ) ) {
  582. $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
  583. $conds[] = $this->getFromUntilTargetConds( $db, $options['startFrom'], $op );
  584. }
  585. return $conds;
  586. }
  587. /**
  588. * Creates a query condition part for getting only items before or after the given link target
  589. * (while ordering using $sort mode)
  590. *
  591. * @param IDatabase $db
  592. * @param LinkTarget $target
  593. * @param string $op comparison operator to use in the conditions
  594. * @return string
  595. */
  596. private function getFromUntilTargetConds( IDatabase $db, LinkTarget $target, $op ) {
  597. return $db->makeList(
  598. [
  599. "wl_namespace $op " . $target->getNamespace(),
  600. $db->makeList(
  601. [
  602. 'wl_namespace = ' . $target->getNamespace(),
  603. "wl_title $op= " . $db->addQuotes( $target->getDBkey() )
  604. ],
  605. LIST_AND
  606. )
  607. ],
  608. LIST_OR
  609. );
  610. }
  611. private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) {
  612. $dbOptions = [];
  613. if ( array_key_exists( 'dir', $options ) ) {
  614. $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
  615. $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
  616. }
  617. if ( array_key_exists( 'limit', $options ) ) {
  618. $dbOptions['LIMIT'] = (int)$options['limit'] + 1;
  619. }
  620. return $dbOptions;
  621. }
  622. private function getWatchedItemsForUserQueryDbOptions( array $options ) {
  623. $dbOptions = [];
  624. if ( array_key_exists( 'sort', $options ) ) {
  625. $dbOptions['ORDER BY'] = [
  626. "wl_namespace {$options['sort']}",
  627. "wl_title {$options['sort']}"
  628. ];
  629. if ( count( $options['namespaceIds'] ) === 1 ) {
  630. $dbOptions['ORDER BY'] = "wl_title {$options['sort']}";
  631. }
  632. }
  633. if ( array_key_exists( 'limit', $options ) ) {
  634. $dbOptions['LIMIT'] = (int)$options['limit'];
  635. }
  636. return $dbOptions;
  637. }
  638. private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
  639. $joinConds = [
  640. 'watchlist' => [ 'JOIN',
  641. [
  642. 'wl_namespace=rc_namespace',
  643. 'wl_title=rc_title'
  644. ]
  645. ]
  646. ];
  647. if ( !$options['allRevisions'] ) {
  648. $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
  649. }
  650. if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
  651. $joinConds += $this->commentStore->getJoin( 'rc_comment' )['joins'];
  652. }
  653. if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
  654. in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
  655. in_array( self::FILTER_ANON, $options['filters'] ) ||
  656. in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
  657. array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
  658. ) {
  659. $joinConds += $this->actorMigration->getJoin( 'rc_user' )['joins'];
  660. }
  661. return $joinConds;
  662. }
  663. }