This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
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 Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; |
||
4 | use MediaWiki\Linker\LinkTarget; |
||
5 | use Wikimedia\Assert\Assert; |
||
6 | use Wikimedia\ScopedCallback; |
||
0 ignored issues
–
show
|
|||
7 | |||
8 | /** |
||
9 | * Storage layer class for WatchedItems. |
||
10 | * Database interaction. |
||
11 | * |
||
12 | * @author Addshore |
||
13 | * |
||
14 | * @since 1.27 |
||
15 | */ |
||
16 | class WatchedItemStore implements StatsdAwareInterface { |
||
17 | |||
18 | const SORT_DESC = 'DESC'; |
||
19 | const SORT_ASC = 'ASC'; |
||
20 | |||
21 | /** |
||
22 | * @var LoadBalancer |
||
23 | */ |
||
24 | private $loadBalancer; |
||
25 | |||
26 | /** |
||
27 | * @var HashBagOStuff |
||
28 | */ |
||
29 | private $cache; |
||
30 | |||
31 | /** |
||
32 | * @var array[] Looks like $cacheIndex[Namespace ID][Target DB Key][User Id] => 'key' |
||
33 | * The index is needed so that on mass changes all relevant items can be un-cached. |
||
34 | * For example: Clearing a users watchlist of all items or updating notification timestamps |
||
35 | * for all users watching a single target. |
||
36 | */ |
||
37 | private $cacheIndex = []; |
||
38 | |||
39 | /** |
||
40 | * @var callable|null |
||
41 | */ |
||
42 | private $deferredUpdatesAddCallableUpdateCallback; |
||
43 | |||
44 | /** |
||
45 | * @var callable|null |
||
46 | */ |
||
47 | private $revisionGetTimestampFromIdCallback; |
||
48 | |||
49 | /** |
||
50 | * @var StatsdDataFactoryInterface |
||
51 | */ |
||
52 | private $stats; |
||
53 | |||
54 | /** |
||
55 | * @param LoadBalancer $loadBalancer |
||
56 | * @param HashBagOStuff $cache |
||
57 | */ |
||
58 | public function __construct( |
||
59 | LoadBalancer $loadBalancer, |
||
60 | HashBagOStuff $cache |
||
61 | ) { |
||
62 | $this->loadBalancer = $loadBalancer; |
||
63 | $this->cache = $cache; |
||
64 | $this->stats = new NullStatsdDataFactory(); |
||
65 | $this->deferredUpdatesAddCallableUpdateCallback = [ 'DeferredUpdates', 'addCallableUpdate' ]; |
||
66 | $this->revisionGetTimestampFromIdCallback = [ 'Revision', 'getTimestampFromId' ]; |
||
67 | } |
||
68 | |||
69 | public function setStatsdDataFactory( StatsdDataFactoryInterface $stats ) { |
||
70 | $this->stats = $stats; |
||
71 | } |
||
72 | |||
73 | /** |
||
74 | * Overrides the DeferredUpdates::addCallableUpdate callback |
||
75 | * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined. |
||
76 | * |
||
77 | * @param callable $callback |
||
78 | * |
||
79 | * @see DeferredUpdates::addCallableUpdate for callback signiture |
||
80 | * |
||
81 | * @return ScopedCallback to reset the overridden value |
||
82 | * @throws MWException |
||
83 | */ |
||
84 | View Code Duplication | public function overrideDeferredUpdatesAddCallableUpdateCallback( callable $callback ) { |
|
85 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
||
86 | throw new MWException( |
||
87 | 'Cannot override DeferredUpdates::addCallableUpdate callback in operation.' |
||
88 | ); |
||
89 | } |
||
90 | $previousValue = $this->deferredUpdatesAddCallableUpdateCallback; |
||
91 | $this->deferredUpdatesAddCallableUpdateCallback = $callback; |
||
92 | return new ScopedCallback( function() use ( $previousValue ) { |
||
93 | $this->deferredUpdatesAddCallableUpdateCallback = $previousValue; |
||
94 | } ); |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * Overrides the Revision::getTimestampFromId callback |
||
99 | * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined. |
||
100 | * |
||
101 | * @param callable $callback |
||
102 | * @see Revision::getTimestampFromId for callback signiture |
||
103 | * |
||
104 | * @return ScopedCallback to reset the overridden value |
||
105 | * @throws MWException |
||
106 | */ |
||
107 | View Code Duplication | public function overrideRevisionGetTimestampFromIdCallback( callable $callback ) { |
|
108 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
||
109 | throw new MWException( |
||
110 | 'Cannot override Revision::getTimestampFromId callback in operation.' |
||
111 | ); |
||
112 | } |
||
113 | $previousValue = $this->revisionGetTimestampFromIdCallback; |
||
114 | $this->revisionGetTimestampFromIdCallback = $callback; |
||
115 | return new ScopedCallback( function() use ( $previousValue ) { |
||
116 | $this->revisionGetTimestampFromIdCallback = $previousValue; |
||
117 | } ); |
||
118 | } |
||
119 | |||
120 | private function getCacheKey( User $user, LinkTarget $target ) { |
||
121 | return $this->cache->makeKey( |
||
122 | (string)$target->getNamespace(), |
||
123 | $target->getDBkey(), |
||
124 | (string)$user->getId() |
||
125 | ); |
||
126 | } |
||
127 | |||
128 | private function cache( WatchedItem $item ) { |
||
129 | $user = $item->getUser(); |
||
130 | $target = $item->getLinkTarget(); |
||
131 | $key = $this->getCacheKey( $user, $target ); |
||
132 | $this->cache->set( $key, $item ); |
||
133 | $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] = $key; |
||
134 | $this->stats->increment( 'WatchedItemStore.cache' ); |
||
135 | } |
||
136 | |||
137 | private function uncache( User $user, LinkTarget $target ) { |
||
138 | $this->cache->delete( $this->getCacheKey( $user, $target ) ); |
||
139 | unset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] ); |
||
140 | $this->stats->increment( 'WatchedItemStore.uncache' ); |
||
141 | } |
||
142 | |||
143 | private function uncacheLinkTarget( LinkTarget $target ) { |
||
144 | $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget' ); |
||
145 | if ( !isset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] ) ) { |
||
146 | return; |
||
147 | } |
||
148 | foreach ( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] as $key ) { |
||
149 | $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget.items' ); |
||
150 | $this->cache->delete( $key ); |
||
151 | } |
||
152 | } |
||
153 | |||
154 | private function uncacheUser( User $user ) { |
||
155 | $this->stats->increment( 'WatchedItemStore.uncacheUser' ); |
||
156 | foreach ( $this->cacheIndex as $ns => $dbKeyArray ) { |
||
157 | foreach ( $dbKeyArray as $dbKey => $userArray ) { |
||
158 | if ( isset( $userArray[$user->getId()] ) ) { |
||
159 | $this->stats->increment( 'WatchedItemStore.uncacheUser.items' ); |
||
160 | $this->cache->delete( $userArray[$user->getId()] ); |
||
161 | } |
||
162 | } |
||
163 | } |
||
164 | } |
||
165 | |||
166 | /** |
||
167 | * @param User $user |
||
168 | * @param LinkTarget $target |
||
169 | * |
||
170 | * @return WatchedItem|false |
||
171 | */ |
||
172 | private function getCached( User $user, LinkTarget $target ) { |
||
173 | return $this->cache->get( $this->getCacheKey( $user, $target ) ); |
||
174 | } |
||
175 | |||
176 | /** |
||
177 | * Return an array of conditions to select or update the appropriate database |
||
178 | * row. |
||
179 | * |
||
180 | * @param User $user |
||
181 | * @param LinkTarget $target |
||
182 | * |
||
183 | * @return array |
||
184 | */ |
||
185 | private function dbCond( User $user, LinkTarget $target ) { |
||
186 | return [ |
||
187 | 'wl_user' => $user->getId(), |
||
188 | 'wl_namespace' => $target->getNamespace(), |
||
189 | 'wl_title' => $target->getDBkey(), |
||
190 | ]; |
||
191 | } |
||
192 | |||
193 | /** |
||
194 | * @param int $dbIndex DB_MASTER or DB_REPLICA |
||
195 | * |
||
196 | * @return IDatabase |
||
197 | * @throws MWException |
||
198 | */ |
||
199 | private function getConnectionRef( $dbIndex ) { |
||
200 | return $this->loadBalancer->getConnectionRef( $dbIndex, [ 'watchlist' ] ); |
||
201 | } |
||
202 | |||
203 | /** |
||
204 | * Count the number of individual items that are watched by the user. |
||
205 | * If a subject and corresponding talk page are watched this will return 2. |
||
206 | * |
||
207 | * @param User $user |
||
208 | * |
||
209 | * @return int |
||
210 | */ |
||
211 | public function countWatchedItems( User $user ) { |
||
212 | $dbr = $this->getConnectionRef( DB_REPLICA ); |
||
213 | $return = (int)$dbr->selectField( |
||
214 | 'watchlist', |
||
215 | 'COUNT(*)', |
||
216 | [ |
||
217 | 'wl_user' => $user->getId() |
||
218 | ], |
||
219 | __METHOD__ |
||
220 | ); |
||
221 | |||
222 | return $return; |
||
223 | } |
||
224 | |||
225 | /** |
||
226 | * @param LinkTarget $target |
||
227 | * |
||
228 | * @return int |
||
229 | */ |
||
230 | public function countWatchers( LinkTarget $target ) { |
||
231 | $dbr = $this->getConnectionRef( DB_REPLICA ); |
||
232 | $return = (int)$dbr->selectField( |
||
233 | 'watchlist', |
||
234 | 'COUNT(*)', |
||
235 | [ |
||
236 | 'wl_namespace' => $target->getNamespace(), |
||
237 | 'wl_title' => $target->getDBkey(), |
||
238 | ], |
||
239 | __METHOD__ |
||
240 | ); |
||
241 | |||
242 | return $return; |
||
243 | } |
||
244 | |||
245 | /** |
||
246 | * Number of page watchers who also visited a "recent" edit |
||
247 | * |
||
248 | * @param LinkTarget $target |
||
249 | * @param mixed $threshold timestamp accepted by wfTimestamp |
||
250 | * |
||
251 | * @return int |
||
252 | * @throws DBUnexpectedError |
||
253 | * @throws MWException |
||
254 | */ |
||
255 | public function countVisitingWatchers( LinkTarget $target, $threshold ) { |
||
256 | $dbr = $this->getConnectionRef( DB_REPLICA ); |
||
257 | $visitingWatchers = (int)$dbr->selectField( |
||
258 | 'watchlist', |
||
259 | 'COUNT(*)', |
||
260 | [ |
||
261 | 'wl_namespace' => $target->getNamespace(), |
||
262 | 'wl_title' => $target->getDBkey(), |
||
263 | 'wl_notificationtimestamp >= ' . |
||
264 | $dbr->addQuotes( $dbr->timestamp( $threshold ) ) . |
||
265 | ' OR wl_notificationtimestamp IS NULL' |
||
266 | ], |
||
267 | __METHOD__ |
||
268 | ); |
||
269 | |||
270 | return $visitingWatchers; |
||
271 | } |
||
272 | |||
273 | /** |
||
274 | * @param LinkTarget[] $targets |
||
275 | * @param array $options Allowed keys: |
||
276 | * 'minimumWatchers' => int |
||
277 | * |
||
278 | * @return array multi dimensional like $return[$namespaceId][$titleString] = int $watchers |
||
279 | * All targets will be present in the result. 0 either means no watchers or the number |
||
280 | * of watchers was below the minimumWatchers option if passed. |
||
281 | */ |
||
282 | public function countWatchersMultiple( array $targets, array $options = [] ) { |
||
283 | $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ]; |
||
284 | |||
285 | $dbr = $this->getConnectionRef( DB_REPLICA ); |
||
286 | |||
287 | if ( array_key_exists( 'minimumWatchers', $options ) ) { |
||
288 | $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$options['minimumWatchers']; |
||
289 | } |
||
290 | |||
291 | $lb = new LinkBatch( $targets ); |
||
292 | $res = $dbr->select( |
||
293 | 'watchlist', |
||
294 | [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ], |
||
295 | [ $lb->constructSet( 'wl', $dbr ) ], |
||
296 | __METHOD__, |
||
297 | $dbOptions |
||
298 | ); |
||
299 | |||
300 | $watchCounts = []; |
||
301 | foreach ( $targets as $linkTarget ) { |
||
302 | $watchCounts[$linkTarget->getNamespace()][$linkTarget->getDBkey()] = 0; |
||
303 | } |
||
304 | |||
305 | 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.
![]() |
|||
306 | $watchCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers; |
||
307 | } |
||
308 | |||
309 | return $watchCounts; |
||
310 | } |
||
311 | |||
312 | /** |
||
313 | * Number of watchers of each page who have visited recent edits to that page |
||
314 | * |
||
315 | * @param array $targetsWithVisitThresholds array of pairs (LinkTarget $target, mixed $threshold), |
||
316 | * $threshold is: |
||
317 | * - a timestamp of the recent edit if $target exists (format accepted by wfTimestamp) |
||
318 | * - null if $target doesn't exist |
||
319 | * @param int|null $minimumWatchers |
||
320 | * @return array multi-dimensional like $return[$namespaceId][$titleString] = $watchers, |
||
321 | * where $watchers is an int: |
||
322 | * - if the page exists, number of users watching who have visited the page recently |
||
323 | * - if the page doesn't exist, number of users that have the page on their watchlist |
||
324 | * - 0 means there are no visiting watchers or their number is below the minimumWatchers |
||
325 | * option (if passed). |
||
326 | */ |
||
327 | public function countVisitingWatchersMultiple( |
||
328 | array $targetsWithVisitThresholds, |
||
329 | $minimumWatchers = null |
||
330 | ) { |
||
331 | $dbr = $this->getConnectionRef( DB_REPLICA ); |
||
332 | |||
333 | $conds = $this->getVisitingWatchersCondition( $dbr, $targetsWithVisitThresholds ); |
||
334 | |||
335 | $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ]; |
||
336 | if ( $minimumWatchers !== null ) { |
||
337 | $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$minimumWatchers; |
||
338 | } |
||
339 | $res = $dbr->select( |
||
340 | 'watchlist', |
||
341 | [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ], |
||
342 | $conds, |
||
343 | __METHOD__, |
||
344 | $dbOptions |
||
345 | ); |
||
346 | |||
347 | $watcherCounts = []; |
||
348 | foreach ( $targetsWithVisitThresholds as list( $target ) ) { |
||
349 | /* @var LinkTarget $target */ |
||
350 | $watcherCounts[$target->getNamespace()][$target->getDBkey()] = 0; |
||
351 | } |
||
352 | |||
353 | 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.
![]() |
|||
354 | $watcherCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers; |
||
355 | } |
||
356 | |||
357 | return $watcherCounts; |
||
358 | } |
||
359 | |||
360 | /** |
||
361 | * Generates condition for the query used in a batch count visiting watchers. |
||
362 | * |
||
363 | * @param IDatabase $db |
||
364 | * @param array $targetsWithVisitThresholds array of pairs (LinkTarget, last visit threshold) |
||
365 | * @return string |
||
366 | */ |
||
367 | private function getVisitingWatchersCondition( |
||
368 | IDatabase $db, |
||
369 | array $targetsWithVisitThresholds |
||
370 | ) { |
||
371 | $missingTargets = []; |
||
372 | $namespaceConds = []; |
||
373 | foreach ( $targetsWithVisitThresholds as list( $target, $threshold ) ) { |
||
374 | if ( $threshold === null ) { |
||
375 | $missingTargets[] = $target; |
||
376 | continue; |
||
377 | } |
||
378 | /* @var LinkTarget $target */ |
||
379 | $namespaceConds[$target->getNamespace()][] = $db->makeList( [ |
||
380 | 'wl_title = ' . $db->addQuotes( $target->getDBkey() ), |
||
381 | $db->makeList( [ |
||
382 | 'wl_notificationtimestamp >= ' . $db->addQuotes( $db->timestamp( $threshold ) ), |
||
383 | 'wl_notificationtimestamp IS NULL' |
||
384 | ], LIST_OR ) |
||
385 | ], LIST_AND ); |
||
386 | } |
||
387 | |||
388 | $conds = []; |
||
389 | foreach ( $namespaceConds as $namespace => $pageConds ) { |
||
390 | $conds[] = $db->makeList( [ |
||
391 | 'wl_namespace = ' . $namespace, |
||
392 | '(' . $db->makeList( $pageConds, LIST_OR ) . ')' |
||
393 | ], LIST_AND ); |
||
394 | } |
||
395 | |||
396 | if ( $missingTargets ) { |
||
397 | $lb = new LinkBatch( $missingTargets ); |
||
398 | $conds[] = $lb->constructSet( 'wl', $db ); |
||
399 | } |
||
400 | |||
401 | return $db->makeList( $conds, LIST_OR ); |
||
402 | } |
||
403 | |||
404 | /** |
||
405 | * Get an item (may be cached) |
||
406 | * |
||
407 | * @param User $user |
||
408 | * @param LinkTarget $target |
||
409 | * |
||
410 | * @return WatchedItem|false |
||
411 | */ |
||
412 | public function getWatchedItem( User $user, LinkTarget $target ) { |
||
413 | if ( $user->isAnon() ) { |
||
414 | return false; |
||
415 | } |
||
416 | |||
417 | $cached = $this->getCached( $user, $target ); |
||
418 | if ( $cached ) { |
||
419 | $this->stats->increment( 'WatchedItemStore.getWatchedItem.cached' ); |
||
420 | return $cached; |
||
421 | } |
||
422 | $this->stats->increment( 'WatchedItemStore.getWatchedItem.load' ); |
||
423 | return $this->loadWatchedItem( $user, $target ); |
||
424 | } |
||
425 | |||
426 | /** |
||
427 | * Loads an item from the db |
||
428 | * |
||
429 | * @param User $user |
||
430 | * @param LinkTarget $target |
||
431 | * |
||
432 | * @return WatchedItem|false |
||
433 | */ |
||
434 | public function loadWatchedItem( User $user, LinkTarget $target ) { |
||
435 | // Only loggedin user can have a watchlist |
||
436 | if ( $user->isAnon() ) { |
||
437 | return false; |
||
438 | } |
||
439 | |||
440 | $dbr = $this->getConnectionRef( DB_REPLICA ); |
||
441 | $row = $dbr->selectRow( |
||
442 | 'watchlist', |
||
443 | 'wl_notificationtimestamp', |
||
444 | $this->dbCond( $user, $target ), |
||
445 | __METHOD__ |
||
446 | ); |
||
447 | |||
448 | if ( !$row ) { |
||
449 | return false; |
||
450 | } |
||
451 | |||
452 | $item = new WatchedItem( |
||
453 | $user, |
||
454 | $target, |
||
455 | $row->wl_notificationtimestamp |
||
456 | ); |
||
457 | $this->cache( $item ); |
||
458 | |||
459 | return $item; |
||
460 | } |
||
461 | |||
462 | /** |
||
463 | * @param User $user |
||
464 | * @param array $options Allowed keys: |
||
465 | * 'forWrite' => bool defaults to false |
||
466 | * 'sort' => string optional sorting by namespace ID and title |
||
467 | * one of the self::SORT_* constants |
||
468 | * |
||
469 | * @return WatchedItem[] |
||
470 | */ |
||
471 | public function getWatchedItemsForUser( User $user, array $options = [] ) { |
||
472 | $options += [ 'forWrite' => false ]; |
||
473 | |||
474 | $dbOptions = []; |
||
475 | if ( array_key_exists( 'sort', $options ) ) { |
||
476 | Assert::parameter( |
||
477 | ( in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ) ), |
||
478 | '$options[\'sort\']', |
||
479 | 'must be SORT_ASC or SORT_DESC' |
||
480 | ); |
||
481 | $dbOptions['ORDER BY'] = [ |
||
482 | "wl_namespace {$options['sort']}", |
||
483 | "wl_title {$options['sort']}" |
||
484 | ]; |
||
485 | } |
||
486 | $db = $this->getConnectionRef( $options['forWrite'] ? DB_MASTER : DB_REPLICA ); |
||
487 | |||
488 | $res = $db->select( |
||
489 | 'watchlist', |
||
490 | [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], |
||
491 | [ 'wl_user' => $user->getId() ], |
||
492 | __METHOD__, |
||
493 | $dbOptions |
||
494 | ); |
||
495 | |||
496 | $watchedItems = []; |
||
497 | 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.
![]() |
|||
498 | // @todo: Should we add these to the process cache? |
||
499 | $watchedItems[] = new WatchedItem( |
||
500 | $user, |
||
501 | new TitleValue( (int)$row->wl_namespace, $row->wl_title ), |
||
502 | $row->wl_notificationtimestamp |
||
503 | ); |
||
504 | } |
||
505 | |||
506 | return $watchedItems; |
||
507 | } |
||
508 | |||
509 | /** |
||
510 | * Must be called separately for Subject & Talk namespaces |
||
511 | * |
||
512 | * @param User $user |
||
513 | * @param LinkTarget $target |
||
514 | * |
||
515 | * @return bool |
||
516 | */ |
||
517 | public function isWatched( User $user, LinkTarget $target ) { |
||
518 | return (bool)$this->getWatchedItem( $user, $target ); |
||
519 | } |
||
520 | |||
521 | /** |
||
522 | * @param User $user |
||
523 | * @param LinkTarget[] $targets |
||
524 | * |
||
525 | * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp, |
||
526 | * where $timestamp is: |
||
527 | * - string|null value of wl_notificationtimestamp, |
||
528 | * - false if $target is not watched by $user. |
||
529 | */ |
||
530 | public function getNotificationTimestampsBatch( User $user, array $targets ) { |
||
531 | $timestamps = []; |
||
532 | foreach ( $targets as $target ) { |
||
533 | $timestamps[$target->getNamespace()][$target->getDBkey()] = false; |
||
534 | } |
||
535 | |||
536 | if ( $user->isAnon() ) { |
||
537 | return $timestamps; |
||
538 | } |
||
539 | |||
540 | $targetsToLoad = []; |
||
541 | foreach ( $targets as $target ) { |
||
542 | $cachedItem = $this->getCached( $user, $target ); |
||
543 | if ( $cachedItem ) { |
||
544 | $timestamps[$target->getNamespace()][$target->getDBkey()] = |
||
545 | $cachedItem->getNotificationTimestamp(); |
||
546 | } else { |
||
547 | $targetsToLoad[] = $target; |
||
548 | } |
||
549 | } |
||
550 | |||
551 | if ( !$targetsToLoad ) { |
||
552 | return $timestamps; |
||
553 | } |
||
554 | |||
555 | $dbr = $this->getConnectionRef( DB_REPLICA ); |
||
556 | |||
557 | $lb = new LinkBatch( $targetsToLoad ); |
||
558 | $res = $dbr->select( |
||
559 | 'watchlist', |
||
560 | [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], |
||
561 | [ |
||
562 | $lb->constructSet( 'wl', $dbr ), |
||
563 | 'wl_user' => $user->getId(), |
||
564 | ], |
||
565 | __METHOD__ |
||
566 | ); |
||
567 | |||
568 | 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.
![]() |
|||
569 | $timestamps[$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp; |
||
570 | } |
||
571 | |||
572 | return $timestamps; |
||
573 | } |
||
574 | |||
575 | /** |
||
576 | * Must be called separately for Subject & Talk namespaces |
||
577 | * |
||
578 | * @param User $user |
||
579 | * @param LinkTarget $target |
||
580 | */ |
||
581 | public function addWatch( User $user, LinkTarget $target ) { |
||
582 | $this->addWatchBatchForUser( $user, [ $target ] ); |
||
583 | } |
||
584 | |||
585 | /** |
||
586 | * @param User $user |
||
587 | * @param LinkTarget[] $targets |
||
588 | * |
||
589 | * @return bool success |
||
590 | */ |
||
591 | public function addWatchBatchForUser( User $user, array $targets ) { |
||
592 | if ( $this->loadBalancer->getReadOnlyReason() !== false ) { |
||
593 | return false; |
||
594 | } |
||
595 | // Only loggedin user can have a watchlist |
||
596 | if ( $user->isAnon() ) { |
||
597 | return false; |
||
598 | } |
||
599 | |||
600 | if ( !$targets ) { |
||
601 | return true; |
||
602 | } |
||
603 | |||
604 | $rows = []; |
||
605 | $items = []; |
||
606 | foreach ( $targets as $target ) { |
||
607 | $rows[] = [ |
||
608 | 'wl_user' => $user->getId(), |
||
609 | 'wl_namespace' => $target->getNamespace(), |
||
610 | 'wl_title' => $target->getDBkey(), |
||
611 | 'wl_notificationtimestamp' => null, |
||
612 | ]; |
||
613 | $items[] = new WatchedItem( |
||
614 | $user, |
||
615 | $target, |
||
616 | null |
||
617 | ); |
||
618 | $this->uncache( $user, $target ); |
||
619 | } |
||
620 | |||
621 | $dbw = $this->getConnectionRef( DB_MASTER ); |
||
622 | foreach ( array_chunk( $rows, 100 ) as $toInsert ) { |
||
623 | // Use INSERT IGNORE to avoid overwriting the notification timestamp |
||
624 | // if there's already an entry for this page |
||
625 | $dbw->insert( 'watchlist', $toInsert, __METHOD__, 'IGNORE' ); |
||
626 | } |
||
627 | // Update process cache to ensure skin doesn't claim that the current |
||
628 | // page is unwatched in the response of action=watch itself (T28292). |
||
629 | // This would otherwise be re-queried from a slave by isWatched(). |
||
630 | foreach ( $items as $item ) { |
||
631 | $this->cache( $item ); |
||
632 | } |
||
633 | |||
634 | return true; |
||
635 | } |
||
636 | |||
637 | /** |
||
638 | * Removes the an entry for the User watching the LinkTarget |
||
639 | * Must be called separately for Subject & Talk namespaces |
||
640 | * |
||
641 | * @param User $user |
||
642 | * @param LinkTarget $target |
||
643 | * |
||
644 | * @return bool success |
||
645 | * @throws DBUnexpectedError |
||
646 | * @throws MWException |
||
647 | */ |
||
648 | public function removeWatch( User $user, LinkTarget $target ) { |
||
649 | // Only logged in user can have a watchlist |
||
650 | if ( $this->loadBalancer->getReadOnlyReason() !== false || $user->isAnon() ) { |
||
651 | return false; |
||
652 | } |
||
653 | |||
654 | $this->uncache( $user, $target ); |
||
655 | |||
656 | $dbw = $this->getConnectionRef( DB_MASTER ); |
||
657 | $dbw->delete( 'watchlist', |
||
658 | [ |
||
659 | 'wl_user' => $user->getId(), |
||
660 | 'wl_namespace' => $target->getNamespace(), |
||
661 | 'wl_title' => $target->getDBkey(), |
||
662 | ], __METHOD__ |
||
663 | ); |
||
664 | $success = (bool)$dbw->affectedRows(); |
||
665 | |||
666 | return $success; |
||
667 | } |
||
668 | |||
669 | /** |
||
670 | * @param User $user The user to set the timestamp for |
||
671 | * @param string $timestamp Set the update timestamp to this value |
||
672 | * @param LinkTarget[] $targets List of targets to update. Default to all targets |
||
673 | * |
||
674 | * @return bool success |
||
675 | */ |
||
676 | public function setNotificationTimestampsForUser( User $user, $timestamp, array $targets = [] ) { |
||
677 | // Only loggedin user can have a watchlist |
||
678 | if ( $user->isAnon() ) { |
||
679 | return false; |
||
680 | } |
||
681 | |||
682 | $dbw = $this->getConnectionRef( DB_MASTER ); |
||
683 | |||
684 | $conds = [ 'wl_user' => $user->getId() ]; |
||
685 | if ( $targets ) { |
||
686 | $batch = new LinkBatch( $targets ); |
||
687 | $conds[] = $batch->constructSet( 'wl', $dbw ); |
||
688 | } |
||
689 | |||
690 | $success = $dbw->update( |
||
691 | 'watchlist', |
||
692 | [ 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) ], |
||
693 | $conds, |
||
694 | __METHOD__ |
||
695 | ); |
||
696 | |||
697 | $this->uncacheUser( $user ); |
||
698 | |||
699 | return $success; |
||
700 | } |
||
701 | |||
702 | /** |
||
703 | * @param User $editor The editor that triggered the update. Their notification |
||
704 | * timestamp will not be updated(they have already seen it) |
||
705 | * @param LinkTarget $target The target to update timestamps for |
||
706 | * @param string $timestamp Set the update timestamp to this value |
||
707 | * |
||
708 | * @return int[] Array of user IDs the timestamp has been updated for |
||
709 | */ |
||
710 | public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) { |
||
711 | $dbw = $this->getConnectionRef( DB_MASTER ); |
||
712 | $uids = $dbw->selectFieldValues( |
||
713 | 'watchlist', |
||
714 | 'wl_user', |
||
715 | [ |
||
716 | 'wl_user != ' . intval( $editor->getId() ), |
||
717 | 'wl_namespace' => $target->getNamespace(), |
||
718 | 'wl_title' => $target->getDBkey(), |
||
719 | 'wl_notificationtimestamp IS NULL', |
||
720 | ], |
||
721 | __METHOD__ |
||
722 | ); |
||
723 | |||
724 | $watchers = array_map( 'intval', $uids ); |
||
725 | if ( $watchers ) { |
||
726 | // Update wl_notificationtimestamp for all watching users except the editor |
||
727 | $fname = __METHOD__; |
||
728 | DeferredUpdates::addCallableUpdate( |
||
729 | function () use ( $timestamp, $watchers, $target, $fname ) { |
||
730 | global $wgUpdateRowsPerQuery; |
||
731 | |||
732 | $dbw = $this->getConnectionRef( DB_MASTER ); |
||
733 | $factory = wfGetLBFactory(); |
||
0 ignored issues
–
show
The function
wfGetLBFactory() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead.
This function has been deprecated. The supplier of the file has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead. ![]() |
|||
734 | $ticket = $factory->getEmptyTransactionTicket( __METHOD__ ); |
||
735 | |||
736 | $watchersChunks = array_chunk( $watchers, $wgUpdateRowsPerQuery ); |
||
737 | foreach ( $watchersChunks as $watchersChunk ) { |
||
738 | $dbw->update( 'watchlist', |
||
739 | [ /* SET */ |
||
740 | 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) |
||
741 | ], [ /* WHERE - TODO Use wl_id T130067 */ |
||
742 | 'wl_user' => $watchersChunk, |
||
743 | 'wl_namespace' => $target->getNamespace(), |
||
744 | 'wl_title' => $target->getDBkey(), |
||
745 | ], $fname |
||
746 | ); |
||
747 | if ( count( $watchersChunks ) > 1 ) { |
||
748 | $factory->commitAndWaitForReplication( |
||
749 | __METHOD__, $ticket, [ 'wiki' => $dbw->getWikiID() ] |
||
750 | ); |
||
751 | } |
||
752 | } |
||
753 | $this->uncacheLinkTarget( $target ); |
||
754 | }, |
||
755 | DeferredUpdates::POSTSEND, |
||
756 | $dbw |
||
757 | ); |
||
758 | } |
||
759 | |||
760 | return $watchers; |
||
761 | } |
||
762 | |||
763 | /** |
||
764 | * Reset the notification timestamp of this entry |
||
765 | * |
||
766 | * @param User $user |
||
767 | * @param Title $title |
||
768 | * @param string $force Whether to force the write query to be executed even if the |
||
769 | * page is not watched or the notification timestamp is already NULL. |
||
770 | * 'force' in order to force |
||
771 | * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed. |
||
772 | * |
||
773 | * @return bool success |
||
774 | */ |
||
775 | public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ) { |
||
776 | // Only loggedin user can have a watchlist |
||
777 | if ( $this->loadBalancer->getReadOnlyReason() !== false || $user->isAnon() ) { |
||
778 | return false; |
||
779 | } |
||
780 | |||
781 | $item = null; |
||
782 | if ( $force != 'force' ) { |
||
783 | $item = $this->loadWatchedItem( $user, $title ); |
||
784 | if ( !$item || $item->getNotificationTimestamp() === null ) { |
||
785 | return false; |
||
786 | } |
||
787 | } |
||
788 | |||
789 | // If the page is watched by the user (or may be watched), update the timestamp |
||
790 | $job = new ActivityUpdateJob( |
||
791 | $title, |
||
792 | [ |
||
793 | 'type' => 'updateWatchlistNotification', |
||
794 | 'userid' => $user->getId(), |
||
795 | 'notifTime' => $this->getNotificationTimestamp( $user, $title, $item, $force, $oldid ), |
||
796 | 'curTime' => time() |
||
797 | ] |
||
798 | ); |
||
799 | |||
800 | // Try to run this post-send |
||
801 | // Calls DeferredUpdates::addCallableUpdate in normal operation |
||
802 | call_user_func( |
||
803 | $this->deferredUpdatesAddCallableUpdateCallback, |
||
804 | function() use ( $job ) { |
||
805 | $job->run(); |
||
806 | } |
||
807 | ); |
||
808 | |||
809 | $this->uncache( $user, $title ); |
||
810 | |||
811 | return true; |
||
812 | } |
||
813 | |||
814 | private function getNotificationTimestamp( User $user, Title $title, $item, $force, $oldid ) { |
||
815 | if ( !$oldid ) { |
||
816 | // No oldid given, assuming latest revision; clear the timestamp. |
||
817 | return null; |
||
818 | } |
||
819 | |||
820 | if ( !$title->getNextRevisionID( $oldid ) ) { |
||
0 ignored issues
–
show
The expression
$title->getNextRevisionID($oldid) of type false|integer is loosely compared to false ; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
![]() |
|||
821 | // Oldid given and is the latest revision for this title; clear the timestamp. |
||
822 | return null; |
||
823 | } |
||
824 | |||
825 | if ( $item === null ) { |
||
826 | $item = $this->loadWatchedItem( $user, $title ); |
||
827 | } |
||
828 | |||
829 | if ( !$item ) { |
||
830 | // This can only happen if $force is enabled. |
||
831 | return null; |
||
832 | } |
||
833 | |||
834 | // Oldid given and isn't the latest; update the timestamp. |
||
835 | // This will result in no further notification emails being sent! |
||
836 | // Calls Revision::getTimestampFromId in normal operation |
||
837 | $notificationTimestamp = call_user_func( |
||
838 | $this->revisionGetTimestampFromIdCallback, |
||
839 | $title, |
||
840 | $oldid |
||
841 | ); |
||
842 | |||
843 | // We need to go one second to the future because of various strict comparisons |
||
844 | // throughout the codebase |
||
845 | $ts = new MWTimestamp( $notificationTimestamp ); |
||
846 | $ts->timestamp->add( new DateInterval( 'PT1S' ) ); |
||
847 | $notificationTimestamp = $ts->getTimestamp( TS_MW ); |
||
848 | |||
849 | if ( $notificationTimestamp < $item->getNotificationTimestamp() ) { |
||
850 | if ( $force != 'force' ) { |
||
851 | return false; |
||
852 | } else { |
||
853 | // This is a little silly… |
||
854 | return $item->getNotificationTimestamp(); |
||
855 | } |
||
856 | } |
||
857 | |||
858 | return $notificationTimestamp; |
||
859 | } |
||
860 | |||
861 | /** |
||
862 | * @param User $user |
||
863 | * @param int $unreadLimit |
||
864 | * |
||
865 | * @return int|bool The number of unread notifications |
||
866 | * true if greater than or equal to $unreadLimit |
||
867 | */ |
||
868 | public function countUnreadNotifications( User $user, $unreadLimit = null ) { |
||
869 | $queryOptions = []; |
||
870 | if ( $unreadLimit !== null ) { |
||
871 | $unreadLimit = (int)$unreadLimit; |
||
872 | $queryOptions['LIMIT'] = $unreadLimit; |
||
873 | } |
||
874 | |||
875 | $dbr = $this->getConnectionRef( DB_REPLICA ); |
||
876 | $rowCount = $dbr->selectRowCount( |
||
877 | 'watchlist', |
||
878 | '1', |
||
879 | [ |
||
880 | 'wl_user' => $user->getId(), |
||
881 | 'wl_notificationtimestamp IS NOT NULL', |
||
882 | ], |
||
883 | __METHOD__, |
||
884 | $queryOptions |
||
885 | ); |
||
886 | |||
887 | if ( !isset( $unreadLimit ) ) { |
||
888 | return $rowCount; |
||
889 | } |
||
890 | |||
891 | if ( $rowCount >= $unreadLimit ) { |
||
892 | return true; |
||
893 | } |
||
894 | |||
895 | return $rowCount; |
||
896 | } |
||
897 | |||
898 | /** |
||
899 | * Check if the given title already is watched by the user, and if so |
||
900 | * add a watch for the new title. |
||
901 | * |
||
902 | * To be used for page renames and such. |
||
903 | * |
||
904 | * @param LinkTarget $oldTarget |
||
905 | * @param LinkTarget $newTarget |
||
906 | */ |
||
907 | public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) { |
||
908 | $oldTarget = Title::newFromLinkTarget( $oldTarget ); |
||
909 | $newTarget = Title::newFromLinkTarget( $newTarget ); |
||
910 | |||
911 | $this->duplicateEntry( $oldTarget->getSubjectPage(), $newTarget->getSubjectPage() ); |
||
912 | $this->duplicateEntry( $oldTarget->getTalkPage(), $newTarget->getTalkPage() ); |
||
913 | } |
||
914 | |||
915 | /** |
||
916 | * Check if the given title already is watched by the user, and if so |
||
917 | * add a watch for the new title. |
||
918 | * |
||
919 | * To be used for page renames and such. |
||
920 | * This must be called separately for Subject and Talk pages |
||
921 | * |
||
922 | * @param LinkTarget $oldTarget |
||
923 | * @param LinkTarget $newTarget |
||
924 | */ |
||
925 | public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget ) { |
||
926 | $dbw = $this->getConnectionRef( DB_MASTER ); |
||
927 | |||
928 | $result = $dbw->select( |
||
929 | 'watchlist', |
||
930 | [ 'wl_user', 'wl_notificationtimestamp' ], |
||
931 | [ |
||
932 | 'wl_namespace' => $oldTarget->getNamespace(), |
||
933 | 'wl_title' => $oldTarget->getDBkey(), |
||
934 | ], |
||
935 | __METHOD__, |
||
936 | [ 'FOR UPDATE' ] |
||
937 | ); |
||
938 | |||
939 | $newNamespace = $newTarget->getNamespace(); |
||
940 | $newDBkey = $newTarget->getDBkey(); |
||
941 | |||
942 | # Construct array to replace into the watchlist |
||
943 | $values = []; |
||
944 | foreach ( $result as $row ) { |
||
0 ignored issues
–
show
The expression
$result 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.
![]() |
|||
945 | $values[] = [ |
||
946 | 'wl_user' => $row->wl_user, |
||
947 | 'wl_namespace' => $newNamespace, |
||
948 | 'wl_title' => $newDBkey, |
||
949 | 'wl_notificationtimestamp' => $row->wl_notificationtimestamp, |
||
950 | ]; |
||
951 | } |
||
952 | |||
953 | if ( !empty( $values ) ) { |
||
954 | # Perform replace |
||
955 | # Note that multi-row replace is very efficient for MySQL but may be inefficient for |
||
956 | # some other DBMSes, mostly due to poor simulation by us |
||
957 | $dbw->replace( |
||
958 | 'watchlist', |
||
959 | [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ], |
||
960 | $values, |
||
961 | __METHOD__ |
||
962 | ); |
||
963 | } |
||
964 | } |
||
965 | |||
966 | } |
||
967 |
Let’s assume that you have a directory layout like this:
and let’s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/Foo.php
are loaded in the same runtime, you will see a PHP error such as the following:PHP Fatal error: Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php
However, as
OtherDir/Foo.php
does not necessarily have to be loaded and the error is only triggered if it is loaded beforeOtherDir/Bar.php
, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias: