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