Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
21.32% |
58 / 272 |
|
14.81% |
4 / 27 |
CRAP | |
0.00% |
0 / 1 |
NotifUser | |
21.32% |
58 / 272 |
|
14.81% |
4 / 27 |
2814.41 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
newFromUser | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
2 | |||
clearUserTalkNotifications | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getMessageCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAlertCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLocalNotificationCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getNotificationCount | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
56 | |||
getLastUnreadAlertTime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLastUnreadMessageTime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLastUnreadNotificationTime | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
72 | |||
markRead | |
43.48% |
10 / 23 |
|
0.00% |
0 / 1 |
12.50 | |||
markUnRead | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
42 | |||
markAllRead | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
5.04 | |||
markReadForeign | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getForeignNotificationInfo | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
resetNotificationCount | |
18.18% |
4 / 22 |
|
0.00% |
0 / 1 |
33.84 | |||
getGlobalUpdateTime | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getCountsAndTimestamps | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
30 | |||
computeLocalCountsAndTimestamps | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
12 | |||
computeGlobalCountsAndTimestamps | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
getEmailFormat | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getMemcKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getGlobalMemcKey | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getForeignNotifications | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getForeignCount | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getForeignTimestamp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
capNotificationCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Notifications; |
4 | |
5 | use InvalidArgumentException; |
6 | use MediaWiki\Extension\Notifications\Gateway\UserNotificationGateway; |
7 | use MediaWiki\Extension\Notifications\Mapper\NotificationMapper; |
8 | use MediaWiki\Extension\Notifications\Mapper\TargetPageMapper; |
9 | use MediaWiki\Extension\Notifications\Model\Notification; |
10 | use MediaWiki\MediaWikiServices; |
11 | use MediaWiki\Request\WebRequest; |
12 | use MediaWiki\User\CentralId\CentralIdLookup; |
13 | use MediaWiki\User\Options\UserOptionsLookup; |
14 | use MediaWiki\User\UserFactory; |
15 | use MediaWiki\User\UserIdentity; |
16 | use MediaWiki\Utils\MWTimestamp; |
17 | use MediaWiki\WikiMap\WikiMap; |
18 | use WANObjectCache; |
19 | use Wikimedia\Rdbms\Database; |
20 | use Wikimedia\Rdbms\ReadOnlyMode; |
21 | |
22 | /** |
23 | * Entity that represents a notification target user |
24 | */ |
25 | class NotifUser { |
26 | |
27 | /** |
28 | * Notification target user |
29 | * @var UserIdentity |
30 | */ |
31 | private $mUser; |
32 | |
33 | /** |
34 | * Object cache |
35 | * @var WANObjectCache |
36 | */ |
37 | private $cache; |
38 | |
39 | /** |
40 | * Database access gateway |
41 | * @var UserNotificationGateway |
42 | */ |
43 | private $userNotifGateway; |
44 | |
45 | /** |
46 | * Notification mapper |
47 | * @var NotificationMapper |
48 | */ |
49 | private $notifMapper; |
50 | |
51 | /** |
52 | * Target page mapper |
53 | * @var TargetPageMapper |
54 | */ |
55 | private $targetPageMapper; |
56 | |
57 | /** |
58 | * @var ForeignNotifications|null |
59 | */ |
60 | private $foreignNotifications; |
61 | |
62 | /** |
63 | * @var array[]|null |
64 | */ |
65 | private $localCountsAndTimestamps; |
66 | |
67 | /** |
68 | * @var array[]|null |
69 | */ |
70 | private $globalCountsAndTimestamps; |
71 | |
72 | /** |
73 | * @var UserOptionsLookup |
74 | */ |
75 | private $userOptionsLookup; |
76 | |
77 | /** |
78 | * @var UserFactory |
79 | */ |
80 | private $userFactory; |
81 | |
82 | /** |
83 | * @var ReadOnlyMode |
84 | */ |
85 | private $readOnlyMode; |
86 | |
87 | // The max notification count shown in badge |
88 | |
89 | // The max number shown in bundled message, eg, <user> and 99+ others <action>. |
90 | // This is really a totally separate thing, and could be its own constant. |
91 | |
92 | // WARNING: If you change this, you should also change all references in the |
93 | // i18n messages (100 and 99) in all repositories using Echo. |
94 | public const MAX_BADGE_COUNT = 99; |
95 | |
96 | private const CACHE_TTL = 86400; |
97 | private const CACHE_KEY = 'echo-notification-counts'; |
98 | private const CHECK_KEY = 'echo-notification-updated'; |
99 | |
100 | /** |
101 | * Usually client code doesn't need to initialize the object directly |
102 | * because it could be obtained from factory method newFromUser() |
103 | * @param UserIdentity $user |
104 | * @param WANObjectCache $cache |
105 | * @param UserNotificationGateway $userNotifGateway |
106 | * @param NotificationMapper $notifMapper |
107 | * @param TargetPageMapper $targetPageMapper |
108 | * @param UserOptionsLookup $userOptionsLookup |
109 | * @param UserFactory $userFactory |
110 | * @param ReadOnlyMode $readOnlyMode |
111 | */ |
112 | public function __construct( |
113 | UserIdentity $user, |
114 | WANObjectCache $cache, |
115 | UserNotificationGateway $userNotifGateway, |
116 | NotificationMapper $notifMapper, |
117 | TargetPageMapper $targetPageMapper, |
118 | UserOptionsLookup $userOptionsLookup, |
119 | UserFactory $userFactory, |
120 | ReadOnlyMode $readOnlyMode |
121 | ) { |
122 | $this->mUser = $user; |
123 | $this->userNotifGateway = $userNotifGateway; |
124 | $this->cache = $cache; |
125 | $this->notifMapper = $notifMapper; |
126 | $this->targetPageMapper = $targetPageMapper; |
127 | $this->userOptionsLookup = $userOptionsLookup; |
128 | $this->userFactory = $userFactory; |
129 | $this->readOnlyMode = $readOnlyMode; |
130 | } |
131 | |
132 | /** |
133 | * Factory method. The caller should make sure that the user is registered. |
134 | * @param UserIdentity $user |
135 | * @return NotifUser |
136 | */ |
137 | public static function newFromUser( UserIdentity $user ) { |
138 | if ( !$user->isRegistered() ) { |
139 | throw new InvalidArgumentException( 'User must be logged in to view notification!' ); |
140 | } |
141 | $services = MediaWikiServices::getInstance(); |
142 | return new NotifUser( |
143 | $user, |
144 | $services->getMainWANObjectCache(), |
145 | new UserNotificationGateway( |
146 | $user, |
147 | DbFactory::newFromDefault(), |
148 | $services->getMainConfig() |
149 | ), |
150 | new NotificationMapper(), |
151 | new TargetPageMapper(), |
152 | $services->getUserOptionsLookup(), |
153 | $services->getUserFactory(), |
154 | $services->getReadOnlyMode() |
155 | ); |
156 | } |
157 | |
158 | /** |
159 | * Mark all edit-user-talk notifications as read. This is called when a user visits their user talk page. |
160 | */ |
161 | public function clearUserTalkNotifications() { |
162 | $this->markRead( |
163 | $this->userNotifGateway->getUnreadNotifications( |
164 | 'edit-user-talk' |
165 | ) |
166 | ); |
167 | } |
168 | |
169 | /** |
170 | * Get message count for this user. |
171 | * |
172 | * @return int |
173 | */ |
174 | public function getMessageCount() { |
175 | return $this->getNotificationCount( AttributeManager::MESSAGE ); |
176 | } |
177 | |
178 | /** |
179 | * Get alert count for this user. |
180 | * |
181 | * @return int |
182 | */ |
183 | public function getAlertCount() { |
184 | return $this->getNotificationCount( AttributeManager::ALERT ); |
185 | } |
186 | |
187 | /** |
188 | * Get the number of unread local notifications in a given section. This does not include |
189 | * foreign notifications, even if the user has cross-wiki notifications enabled. |
190 | * |
191 | * @param string $section Notification section |
192 | * @return int |
193 | */ |
194 | public function getLocalNotificationCount( $section = AttributeManager::ALL ) { |
195 | return $this->getNotificationCount( $section, false ); |
196 | } |
197 | |
198 | /** |
199 | * Retrieves number of unread notifications that a user has, would return |
200 | * NotifUser::MAX_BADGE_COUNT + 1 at most. |
201 | * |
202 | * If $wgEchoCrossWikiNotifications is disabled, the $global parameter is ignored. |
203 | * |
204 | * @param string $section Notification section |
205 | * @param bool|string $global Whether to include foreign notifications. |
206 | * If set to 'preference', uses the user's preference. |
207 | * @return int |
208 | */ |
209 | public function getNotificationCount( $section = AttributeManager::ALL, $global = 'preference' ) { |
210 | if ( !$this->mUser->isRegistered() ) { |
211 | return 0; |
212 | } |
213 | |
214 | global $wgEchoCrossWikiNotifications; |
215 | if ( !$wgEchoCrossWikiNotifications ) { |
216 | // Ignore the $global parameter |
217 | $global = false; |
218 | } |
219 | |
220 | if ( $global === 'preference' ) { |
221 | $global = $this->getForeignNotifications()->isEnabledByUser(); |
222 | } |
223 | |
224 | $data = $this->getCountsAndTimestamps( $global ); |
225 | if ( $global && $data['global'] === null ) { |
226 | // No global user exists, no data. Use only local count |
227 | $global = false; |
228 | } |
229 | $count = $data[$global ? 'global' : 'local'][$section]['count']; |
230 | return (int)$count; |
231 | } |
232 | |
233 | /** |
234 | * Get the timestamp of the latest unread alert |
235 | * |
236 | * @return bool|MWTimestamp Timestamp of latest unread alert, or false if there are no unread alerts. |
237 | */ |
238 | public function getLastUnreadAlertTime() { |
239 | return $this->getLastUnreadNotificationTime( AttributeManager::ALERT ); |
240 | } |
241 | |
242 | /** |
243 | * Get the timestamp of the latest unread message |
244 | * |
245 | * @return bool|MWTimestamp |
246 | */ |
247 | public function getLastUnreadMessageTime() { |
248 | return $this->getLastUnreadNotificationTime( AttributeManager::MESSAGE ); |
249 | } |
250 | |
251 | /** |
252 | * Returns the timestamp of the last unread notification. |
253 | * |
254 | * If $wgEchoCrossWikiNotifications is disabled, the $global parameter is ignored. |
255 | * |
256 | * @param string $section Notification section |
257 | * @param bool|string $global Whether to include foreign notifications. |
258 | * If set to 'preference', uses the user's preference. |
259 | * @return bool|MWTimestamp Timestamp of latest unread message, or false if there are no unread messages. |
260 | */ |
261 | public function getLastUnreadNotificationTime( $section = AttributeManager::ALL, $global = 'preference' ) { |
262 | if ( !$this->mUser->isRegistered() ) { |
263 | return false; |
264 | } |
265 | |
266 | global $wgEchoCrossWikiNotifications; |
267 | if ( !$wgEchoCrossWikiNotifications ) { |
268 | // Ignore the $global parameter |
269 | $global = false; |
270 | } |
271 | |
272 | if ( $global === 'preference' ) { |
273 | $global = $this->getForeignNotifications()->isEnabledByUser(); |
274 | } |
275 | |
276 | $data = $this->getCountsAndTimestamps( $global ); |
277 | if ( $global && $data['global'] === null ) { |
278 | // No global user exists, no data. Use only local count |
279 | $global = false; |
280 | } |
281 | $timestamp = $data[$global ? 'global' : 'local'][$section]['timestamp']; |
282 | return $timestamp === -1 ? false : new MWTimestamp( $timestamp ); |
283 | } |
284 | |
285 | /** |
286 | * Mark one or more notifications read for a user. |
287 | * @param array $eventIds Array of event IDs to mark read |
288 | * @return bool Returns true when data has been updated in DB, false on |
289 | * failure, or when there was nothing to update |
290 | */ |
291 | public function markRead( $eventIds ) { |
292 | $eventIds = array_filter( (array)$eventIds, 'is_numeric' ); |
293 | if ( !$eventIds || $this->readOnlyMode->isReadOnly() ) { |
294 | return false; |
295 | } |
296 | |
297 | $updated = $this->userNotifGateway->markRead( $eventIds ); |
298 | if ( $updated ) { |
299 | // Update notification count in cache |
300 | $this->resetNotificationCount(); |
301 | |
302 | // After this 'mark read', is there any unread edit-user-talk |
303 | // remaining? If not, we should clear the newtalk flag. |
304 | $talkPageNotificationManager = MediaWikiServices::getInstance() |
305 | ->getTalkPageNotificationManager(); |
306 | if ( $talkPageNotificationManager->userHasNewMessages( $this->mUser ) ) { |
307 | $attributeManager = Services::getInstance()->getAttributeManager(); |
308 | $categoryMap = $attributeManager->getEventsByCategory(); |
309 | $usertalkTypes = $categoryMap['edit-user-talk']; |
310 | $unreadEditUserTalk = $this->notifMapper->fetchUnreadByUser( |
311 | $this->mUser, |
312 | 1, |
313 | null, |
314 | $usertalkTypes, |
315 | null, |
316 | DB_PRIMARY |
317 | ); |
318 | if ( $unreadEditUserTalk === [] ) { |
319 | $talkPageNotificationManager->removeUserHasNewMessages( $this->mUser ); |
320 | } |
321 | } |
322 | } |
323 | |
324 | return $updated; |
325 | } |
326 | |
327 | /** |
328 | * Mark one or more notifications unread for a user. |
329 | * @param array $eventIds Array of event IDs to mark unread |
330 | * @return bool Returns true when data has been updated in DB, false on |
331 | * failure, or when there was nothing to update |
332 | */ |
333 | public function markUnRead( $eventIds ) { |
334 | $eventIds = array_filter( (array)$eventIds, 'is_numeric' ); |
335 | if ( !$eventIds || $this->readOnlyMode->isReadOnly() ) { |
336 | return false; |
337 | } |
338 | |
339 | $updated = $this->userNotifGateway->markUnRead( $eventIds ); |
340 | if ( $updated ) { |
341 | // Update notification count in cache |
342 | $this->resetNotificationCount(); |
343 | |
344 | // After this 'mark unread', is there any unread edit-user-talk? |
345 | // If so, we should add the edit-user-talk flag |
346 | $talkPageNotificationManager = MediaWikiServices::getInstance() |
347 | ->getTalkPageNotificationManager(); |
348 | if ( !$talkPageNotificationManager->userHasNewMessages( $this->mUser ) ) { |
349 | $attributeManager = Services::getInstance()->getAttributeManager(); |
350 | $categoryMap = $attributeManager->getEventsByCategory(); |
351 | $usertalkTypes = $categoryMap['edit-user-talk']; |
352 | $unreadEditUserTalk = $this->notifMapper->fetchUnreadByUser( |
353 | $this->mUser, |
354 | 1, |
355 | null, |
356 | $usertalkTypes, |
357 | null, |
358 | DB_PRIMARY |
359 | ); |
360 | if ( $unreadEditUserTalk !== [] ) { |
361 | $talkPageNotificationManager->setUserHasNewMessages( $this->mUser ); |
362 | } |
363 | } |
364 | } |
365 | |
366 | return $updated; |
367 | } |
368 | |
369 | /** |
370 | * Attempt to mark all or sections of notifications as read, this only |
371 | * updates up to $wgEchoMaxUpdateCount records per request, see more |
372 | * detail about this in Echo.php, the other reason is that mediawiki |
373 | * database interface doesn't support updateJoin() that would update |
374 | * across multiple tables, we would visit this later |
375 | * |
376 | * @param string[] $sections |
377 | * @return bool |
378 | */ |
379 | public function markAllRead( array $sections = [ AttributeManager::ALL ] ) { |
380 | if ( $this->readOnlyMode->isReadOnly() ) { |
381 | return false; |
382 | } |
383 | |
384 | global $wgEchoMaxUpdateCount; |
385 | |
386 | // Mark all sections as read if this is the case |
387 | if ( in_array( AttributeManager::ALL, $sections ) ) { |
388 | $sections = AttributeManager::$sections; |
389 | } |
390 | |
391 | $attributeManager = Services::getInstance()->getAttributeManager(); |
392 | $eventTypes = $attributeManager->getUserEnabledEventsBySections( $this->mUser, 'web', $sections ); |
393 | |
394 | $notifs = $this->notifMapper->fetchUnreadByUser( $this->mUser, $wgEchoMaxUpdateCount, null, $eventTypes ); |
395 | |
396 | $eventIds = array_filter( |
397 | array_map( static function ( Notification $notif ) { |
398 | // This should not happen at all, but use 0 in |
399 | // such case so to keep the code running |
400 | if ( $notif->getEvent() ) { |
401 | return $notif->getEvent()->getId(); |
402 | } |
403 | return 0; |
404 | }, $notifs ) |
405 | ); |
406 | |
407 | $updated = $this->markRead( $eventIds ); |
408 | if ( $updated ) { |
409 | // Delete records from echo_target_page |
410 | /** |
411 | * Keep the 'echo_target_page' records so they can be used for moderation. |
412 | */ |
413 | // $this->targetPageMapper->deleteByUserEvents( $this->mUser, $eventIds ); |
414 | } |
415 | |
416 | return $updated; |
417 | } |
418 | |
419 | /** |
420 | * Mark one of more notifications as read on a foreign wiki. |
421 | * |
422 | * @param int[] $eventIds Event IDs to mark as read |
423 | * @param string $wiki Wiki name |
424 | * @param WebRequest|null $originalRequest Original request data to be sent with these requests |
425 | */ |
426 | public function markReadForeign( array $eventIds, $wiki, ?WebRequest $originalRequest = null ) { |
427 | $foreignReq = new ForeignWikiRequest( |
428 | $this->userFactory->newFromUserIdentity( $this->mUser ), |
429 | [ |
430 | 'action' => 'echomarkread', |
431 | 'list' => implode( '|', $eventIds ), |
432 | ], |
433 | [ $wiki ], |
434 | 'wikis', |
435 | 'csrf' |
436 | ); |
437 | $foreignReq->execute( $originalRequest ); |
438 | } |
439 | |
440 | /** |
441 | * Get information about a set of unread notifications on a foreign wiki. |
442 | * |
443 | * @param int[] $eventIds Event IDs to look up. Only unread notifications can be found. |
444 | * @param string $wiki Wiki name |
445 | * @param WebRequest|null $originalRequest Original request data to be sent with these requests |
446 | * @return array[] Array of notification data as returned by api.php, keyed by event ID |
447 | */ |
448 | public function getForeignNotificationInfo( array $eventIds, $wiki, ?WebRequest $originalRequest = null ) { |
449 | $foreignReq = new ForeignWikiRequest( |
450 | $this->userFactory->newFromUserIdentity( $this->mUser ), |
451 | [ |
452 | 'action' => 'query', |
453 | 'meta' => 'notifications', |
454 | 'notprop' => 'list', |
455 | 'notfilter' => '!read', |
456 | 'notlimit' => 'max' |
457 | ], |
458 | [ $wiki ], |
459 | 'notwikis' |
460 | ); |
461 | $foreignResults = $foreignReq->execute( $originalRequest ); |
462 | $list = $foreignResults[$wiki]['query']['notifications']['list'] ?? []; |
463 | |
464 | $result = []; |
465 | foreach ( $list as $notif ) { |
466 | if ( in_array( $notif['id'], $eventIds ) ) { |
467 | $result[$notif['id']] = $notif; |
468 | } |
469 | } |
470 | return $result; |
471 | } |
472 | |
473 | /** |
474 | * Invalidate cache and update echo_unread_wikis if x-wiki notifications is enabled. |
475 | * |
476 | * This updates the user's touched timestamp, as well as the value returned by getGlobalUpdateTime(). |
477 | * |
478 | * NOTE: Consider calling this function from a deferred update, since it will read from and write to |
479 | * the primary DB if cross-wiki notifications are enabled. |
480 | */ |
481 | public function resetNotificationCount() { |
482 | global $wgEchoCrossWikiNotifications; |
483 | |
484 | // Delete cached local counts and timestamps |
485 | $localMemcKey = $this->getMemcKey( self::CACHE_KEY ); |
486 | $this->cache->delete( $localMemcKey ); |
487 | |
488 | // Update the user touched timestamp for the local user |
489 | $this->userFactory->newFromUserIdentity( $this->mUser )->invalidateCache(); |
490 | |
491 | if ( $wgEchoCrossWikiNotifications ) { |
492 | // Delete cached global counts and timestamps |
493 | $globalMemcKey = $this->getGlobalMemcKey( self::CACHE_KEY ); |
494 | if ( $globalMemcKey !== false ) { |
495 | $this->cache->delete( $globalMemcKey ); |
496 | } |
497 | |
498 | $uw = UnreadWikis::newFromUser( $this->mUser ); |
499 | if ( $uw ) { |
500 | // Immediately compute new local counts and timestamps |
501 | $newLocalData = $this->computeLocalCountsAndTimestamps( DB_PRIMARY ); |
502 | // Write the new values to the echo_unread_wikis table |
503 | $alertTs = $newLocalData[AttributeManager::ALERT]['timestamp']; |
504 | $messageTs = $newLocalData[AttributeManager::MESSAGE]['timestamp']; |
505 | $uw->updateCount( |
506 | WikiMap::getCurrentWikiId(), |
507 | $newLocalData[AttributeManager::ALERT]['count'], |
508 | $alertTs === -1 ? false : new MWTimestamp( $alertTs ), |
509 | $newLocalData[AttributeManager::MESSAGE]['count'], |
510 | $messageTs === -1 ? false : new MWTimestamp( $messageTs ) |
511 | ); |
512 | // We could set() $newLocalData into the cache here, but we don't because that seems risky; |
513 | // instead we let it be recomputed on demand |
514 | } |
515 | |
516 | // Update the global touched timestamp |
517 | $checkKey = $this->getGlobalMemcKey( self::CHECK_KEY ); |
518 | if ( $checkKey ) { |
519 | $this->cache->touchCheckKey( $checkKey ); |
520 | } |
521 | } |
522 | } |
523 | |
524 | /** |
525 | * Get the timestamp of the last time the global notification counts/timestamps were updated, if available. |
526 | * |
527 | * If the timestamp of the last update is not known, this will return the current timestamp. |
528 | * If the user is not attached, this will return false. |
529 | * |
530 | * @return string|false MW timestamp of the last update, or false if the user is not attached |
531 | */ |
532 | public function getGlobalUpdateTime() { |
533 | $key = $this->getGlobalMemcKey( self::CHECK_KEY ); |
534 | if ( $key === false ) { |
535 | return false; |
536 | } |
537 | return wfTimestamp( TS_MW, $this->cache->getCheckKeyTime( $key ) ); |
538 | } |
539 | |
540 | /** |
541 | * Get the number of notifications in each section, and the timestamp of the latest notification in |
542 | * each section. This returns the raw data structure that is stored in the cache; unless you want |
543 | * all of this information, you're probably looking for getNotificationCount(), |
544 | * getLastUnreadNotificationTime() or one of its wrappers. |
545 | * |
546 | * The returned data structure looks like: |
547 | * [ |
548 | * 'local' => [ |
549 | * 'alert' => [ 'count' => N, 'timestamp' => TS ], |
550 | * 'message' => [ 'count' = N, 'timestamp' => TS ], |
551 | * 'all' => [ 'count' => N, 'timestamp' => TS ], |
552 | * ], |
553 | * 'global' => [ |
554 | * 'alert' => [ 'count' => N, 'timestamp' => TS ], |
555 | * 'message' => [ 'count' = N, 'timestamp' => TS ], |
556 | * 'all' => [ 'count' => N, 'timestamp' => TS ], |
557 | * ], |
558 | * ] |
559 | * Where N is a number and TS is a timestamp in TS_MW format or -1. If $includeGlobal is false, |
560 | * the 'global' key will not be present. It could be null, if no global user exists. |
561 | * |
562 | * @param bool $includeGlobal Whether to include cross-wiki notifications as well |
563 | * @return array |
564 | */ |
565 | public function getCountsAndTimestamps( $includeGlobal = false ) { |
566 | if ( $this->localCountsAndTimestamps === null ) { |
567 | $this->localCountsAndTimestamps = $this->cache->getWithSetCallback( |
568 | $this->getMemcKey( self::CACHE_KEY ), |
569 | self::CACHE_TTL, |
570 | function ( $oldValue, &$ttl, array &$setOpts ) { |
571 | $dbr = $this->userNotifGateway->getDB( DB_REPLICA ); |
572 | $setOpts += Database::getCacheSetOptions( $dbr ); |
573 | return $this->computeLocalCountsAndTimestamps(); |
574 | } |
575 | ); |
576 | } |
577 | $result = [ 'local' => $this->localCountsAndTimestamps ]; |
578 | |
579 | if ( $includeGlobal ) { |
580 | if ( $this->globalCountsAndTimestamps === null ) { |
581 | $memcKey = $this->getGlobalMemcKey( self::CACHE_KEY ); |
582 | // If getGlobalMemcKey returns false, we don't have a global user ID |
583 | // In that case, don't compute data that we can't cache or store |
584 | if ( $memcKey !== false ) { |
585 | $this->globalCountsAndTimestamps = $this->cache->getWithSetCallback( |
586 | $memcKey, |
587 | self::CACHE_TTL, |
588 | function ( $oldValue, &$ttl, array &$setOpts ) { |
589 | $dbr = $this->userNotifGateway->getDB( DB_REPLICA ); |
590 | $setOpts += Database::getCacheSetOptions( $dbr ); |
591 | return $this->computeGlobalCountsAndTimestamps(); |
592 | } |
593 | ); |
594 | } |
595 | } |
596 | $result['global'] = $this->globalCountsAndTimestamps; |
597 | } |
598 | return $result; |
599 | } |
600 | |
601 | /** |
602 | * Compute the counts and timestamps for the local notifications in each section. |
603 | * @param int $dbSource DB_REPLICA or DB_PRIMARY |
604 | * @return array[] [ 'alert' => [ 'count' => N, 'timestamp' => TS ], ... ] |
605 | */ |
606 | protected function computeLocalCountsAndTimestamps( $dbSource = DB_REPLICA ) { |
607 | $attributeManager = Services::getInstance()->getAttributeManager(); |
608 | $result = []; |
609 | $totals = [ 'count' => 0, 'timestamp' => -1 ]; |
610 | |
611 | foreach ( AttributeManager::$sections as $section ) { |
612 | $eventTypesToLoad = $attributeManager->getUserEnabledEventsBySections( |
613 | $this->mUser, |
614 | 'web', |
615 | [ $section ] |
616 | ); |
617 | |
618 | $count = $this->userNotifGateway->getCappedNotificationCount( |
619 | $dbSource, |
620 | $eventTypesToLoad, |
621 | self::MAX_BADGE_COUNT + 1 |
622 | ); |
623 | $result[$section]['count'] = $count; |
624 | $totals['count'] += $count; |
625 | |
626 | $notifications = $this->notifMapper->fetchUnreadByUser( |
627 | $this->mUser, |
628 | 1, |
629 | null, |
630 | $eventTypesToLoad, |
631 | null, |
632 | $dbSource |
633 | ); |
634 | if ( $notifications ) { |
635 | $notification = reset( $notifications ); |
636 | $timestamp = $notification->getTimestamp(); |
637 | } else { |
638 | $timestamp = -1; |
639 | } |
640 | $result[$section]['timestamp'] = $timestamp; |
641 | $totals['timestamp'] = max( $totals['timestamp'], $timestamp ); |
642 | } |
643 | $totals['count'] = self::capNotificationCount( $totals['count'] ); |
644 | $result[AttributeManager::ALL] = $totals; |
645 | return $result; |
646 | } |
647 | |
648 | /** |
649 | * Compute the global counts and timestamps for each section. |
650 | * |
651 | * This calls getCountsAndTimestamps() to get data about local notifications, which may end up |
652 | * calling computeLocalCountsAndTimestamps() if there's a cache miss. |
653 | * @return array[] [ 'alert' => [ 'count' => N, 'timestamp' => TS ], ... ] |
654 | */ |
655 | protected function computeGlobalCountsAndTimestamps() { |
656 | $localData = $this->getCountsAndTimestamps()['local']; |
657 | $result = []; |
658 | $totals = [ 'count' => 0, 'timestamp' => -1 ]; |
659 | foreach ( AttributeManager::$sections as $section ) { |
660 | $localCount = $localData[$section]['count']; |
661 | $globalCount = self::capNotificationCount( $localCount + $this->getForeignCount( $section ) ); |
662 | $result[$section]['count'] = $globalCount; |
663 | $totals['count'] += $globalCount; |
664 | |
665 | $localTimestamp = $localData[$section]['timestamp']; |
666 | $foreignTimestamp = $this->getForeignTimestamp( $section ); |
667 | $globalTimestamp = max( |
668 | $localTimestamp, |
669 | $foreignTimestamp ? $foreignTimestamp->getTimestamp( TS_MW ) : -1 |
670 | ); |
671 | $result[$section]['timestamp'] = $globalTimestamp; |
672 | $totals['timestamp'] = max( $totals['timestamp'], $globalTimestamp ); |
673 | } |
674 | $totals['count'] = self::capNotificationCount( $totals['count'] ); |
675 | $result[AttributeManager::ALL] = $totals; |
676 | return $result; |
677 | } |
678 | |
679 | /** |
680 | * Get the user's email notification format |
681 | * @return string |
682 | */ |
683 | public function getEmailFormat() { |
684 | global $wgAllowHTMLEmail; |
685 | |
686 | if ( $wgAllowHTMLEmail ) { |
687 | return $this->userOptionsLookup->getOption( $this->mUser, 'echo-email-format' ); |
688 | } |
689 | |
690 | return EmailFormat::PLAIN_TEXT; |
691 | } |
692 | |
693 | /** |
694 | * Build a cache key for local use (local to this wiki) |
695 | * |
696 | * @param string $key Key, typically prefixed with echo-notification- |
697 | * @return string Cache key |
698 | */ |
699 | protected function getMemcKey( $key ) { |
700 | global $wgEchoCacheVersion; |
701 | return $this->cache->makeKey( $key, $this->mUser->getId(), $wgEchoCacheVersion ); |
702 | } |
703 | |
704 | /** |
705 | * Build a cache key for global use |
706 | * |
707 | * @param string $key Key, typically prefixed with echo-notification- |
708 | * @return string|false Memcached key, or false if one could not be generated |
709 | */ |
710 | protected function getGlobalMemcKey( $key ) { |
711 | global $wgEchoCacheVersion; |
712 | $globalId = MediaWikiServices::getInstance() |
713 | ->getCentralIdLookup() |
714 | ->centralIdFromLocalUser( $this->mUser, CentralIdLookup::AUDIENCE_RAW ); |
715 | if ( !$globalId ) { |
716 | return false; |
717 | } |
718 | return $this->cache->makeGlobalKey( $key, $globalId, $wgEchoCacheVersion ); |
719 | } |
720 | |
721 | /** |
722 | * Lazy-construct an ForeignNotifications instance. This instance is force-enabled, so it |
723 | * returns information about cross-wiki notifications even if the user has them disabled. |
724 | * @return ForeignNotifications |
725 | */ |
726 | protected function getForeignNotifications() { |
727 | if ( !$this->foreignNotifications ) { |
728 | $this->foreignNotifications = new ForeignNotifications( $this->mUser, true ); |
729 | } |
730 | return $this->foreignNotifications; |
731 | } |
732 | |
733 | /** |
734 | * Get the number of foreign notifications in a given section. |
735 | * @param string $section One of AttributeManager::$sections |
736 | * @return int Number of foreign notifications |
737 | */ |
738 | protected function getForeignCount( $section = AttributeManager::ALL ) { |
739 | return self::capNotificationCount( |
740 | $this->getForeignNotifications()->getCount( $section ) |
741 | ); |
742 | } |
743 | |
744 | /** |
745 | * Get the timestamp of the most recent foreign notification in a given section. |
746 | * @param string $section One of AttributeManager::$sections |
747 | * @return MWTimestamp|false Timestamp of the most recent foreign notification, or false if |
748 | * there aren't any |
749 | */ |
750 | protected function getForeignTimestamp( $section = AttributeManager::ALL ) { |
751 | return $this->getForeignNotifications()->getTimestamp( $section ); |
752 | } |
753 | |
754 | /** |
755 | * Helper function to produce the capped number of notifications |
756 | * based on the value of NotifUser::MAX_BADGE_COUNT |
757 | * |
758 | * @param int $number Raw notification count to cap |
759 | * @return int Capped notification count |
760 | */ |
761 | public static function capNotificationCount( $number ) { |
762 | return min( $number, self::MAX_BADGE_COUNT + 1 ); |
763 | } |
764 | } |