Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
40.28% |
87 / 216 |
|
26.67% |
4 / 15 |
CRAP | |
0.00% |
0 / 1 |
NotificationController | |
40.28% |
87 / 216 |
|
26.67% |
4 / 15 |
1144.80 | |
0.00% |
0 / 1 |
getCappedNotificationCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
formatNotificationCount | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
notify | |
8.33% |
3 / 36 |
|
0.00% |
0 / 1 |
104.20 | |||
hasMinorRevision | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
enqueueDeleteJob | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getEventNotifyTypes | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getEventParams | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
enqueueEvent | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
4 | |||
isBlacklistedByUser | |
11.11% |
2 / 18 |
|
0.00% |
0 / 1 |
65.89 | |||
isPageLinkedTitleMutedByUser | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
3.07 | |||
getWikiBlacklist | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
isWhitelistedByUser | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
42 | |||
doNotification | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
evaluateUserCallable | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
getUsersToNotifyForEvent | |
70.21% |
33 / 47 |
|
0.00% |
0 / 1 |
20.95 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Notifications\Controller; |
4 | |
5 | use IDBAccessObject; |
6 | use InvalidArgumentException; |
7 | use Iterator; |
8 | use MapCacheLRU; |
9 | use MediaWiki\Deferred\DeferredUpdates; |
10 | use MediaWiki\Extension\Notifications\AttributeManager; |
11 | use MediaWiki\Extension\Notifications\CachedList; |
12 | use MediaWiki\Extension\Notifications\ContainmentList; |
13 | use MediaWiki\Extension\Notifications\ContainmentSet; |
14 | use MediaWiki\Extension\Notifications\Hooks\HookRunner; |
15 | use MediaWiki\Extension\Notifications\Iterator\FilteredSequentialIterator; |
16 | use MediaWiki\Extension\Notifications\Jobs\NotificationDeleteJob; |
17 | use MediaWiki\Extension\Notifications\Jobs\NotificationJob; |
18 | use MediaWiki\Extension\Notifications\Model\Event; |
19 | use MediaWiki\Extension\Notifications\NotifUser; |
20 | use MediaWiki\Extension\Notifications\OnWikiList; |
21 | use MediaWiki\Extension\Notifications\Services; |
22 | use MediaWiki\Logger\LoggerFactory; |
23 | use MediaWiki\MediaWikiServices; |
24 | use MediaWiki\Title\Title; |
25 | use MediaWiki\User\User; |
26 | use MediaWiki\User\UserIdentity; |
27 | |
28 | /** |
29 | * This class represents the controller for notifications |
30 | */ |
31 | class NotificationController { |
32 | |
33 | /** |
34 | * Echo maximum number of users to cache |
35 | * |
36 | * @var int |
37 | */ |
38 | protected static $maxRecipientCacheSize = 200; |
39 | |
40 | /** |
41 | * Max number of users for which we in-process cache titles. |
42 | * |
43 | * @var int |
44 | */ |
45 | protected static $maxUsersTitleCacheSize = 200; |
46 | |
47 | /** |
48 | * Echo event agent per user blacklist |
49 | * |
50 | * @var MapCacheLRU |
51 | */ |
52 | protected static $blacklistByUser; |
53 | |
54 | /** |
55 | * Echo event agent per page linked event title mute list. |
56 | * |
57 | * @var MapCacheLRU |
58 | */ |
59 | protected static $mutedPageLinkedTitlesCache; |
60 | |
61 | /** |
62 | * Echo event agent per wiki blacklist |
63 | * |
64 | * @var ContainmentList|null |
65 | */ |
66 | protected static $wikiBlacklist; |
67 | |
68 | /** |
69 | * Echo event agent per user whitelist, this overwrites $blacklistByUser |
70 | * |
71 | * @var MapCacheLRU |
72 | */ |
73 | protected static $whitelistByUser; |
74 | |
75 | /** |
76 | * Returns the count passed in, or NotifUser::MAX_BADGE_COUNT + 1, |
77 | * whichever is less. |
78 | * |
79 | * @param int $count |
80 | * @return int Notification count, with ceiling applied |
81 | */ |
82 | public static function getCappedNotificationCount( int $count ): int { |
83 | return min( $count, NotifUser::MAX_BADGE_COUNT + 1 ); |
84 | } |
85 | |
86 | /** |
87 | * Format the notification count as a string. This should only be used for an |
88 | * isolated string count, e.g. as displayed in personal tools or returned by the API. |
89 | * |
90 | * If using it in sentence context, pass the value from getCappedNotificationCount |
91 | * into a message and use PLURAL. Example: notification-bundle-header-page-linked |
92 | * |
93 | * @param int $count Notification count |
94 | * @return string Formatted count, after applying cap then formatting to string |
95 | */ |
96 | public static function formatNotificationCount( $count ) { |
97 | $cappedCount = self::getCappedNotificationCount( $count ); |
98 | |
99 | return wfMessage( 'echo-badge-count' )->numParams( $cappedCount )->text(); |
100 | } |
101 | |
102 | /** |
103 | * Processes notifications for a newly-created Event |
104 | * |
105 | * @param Event $event |
106 | * @param bool $defer Defer to job queue or not |
107 | */ |
108 | public static function notify( $event, $defer = true ) { |
109 | // Defer to job queue if defer to job queue is requested and |
110 | // this event should use job queue |
111 | if ( $defer && $event->getUseJobQueue() ) { |
112 | // defer job insertion till end of request when all primary db transactions |
113 | // have been committed |
114 | DeferredUpdates::addCallableUpdate( function () use ( $event ) { |
115 | self::enqueueEvent( $event ); |
116 | } ); |
117 | |
118 | return; |
119 | } |
120 | |
121 | // Check if the event object has valid event type. Events with invalid |
122 | // event types left in the job queue should not be processed |
123 | if ( !$event->isEnabledEvent() ) { |
124 | return; |
125 | } |
126 | |
127 | $type = $event->getType(); |
128 | $notifyTypes = self::getEventNotifyTypes( $type ); |
129 | $userIds = []; |
130 | $userIdsCount = 0; |
131 | $services = MediaWikiServices::getInstance(); |
132 | $hookRunner = new HookRunner( $services->getHookContainer() ); |
133 | $userOptionsLookup = $services->getUserOptionsLookup(); |
134 | /** @var bool|null $hasMinorRevision */ |
135 | $hasMinorRevision = null; |
136 | /** @var User $user */ |
137 | foreach ( self::getUsersToNotifyForEvent( $event ) as $user ) { |
138 | $userIds[$user->getId()] = $user->getId(); |
139 | $userNotifyTypes = $notifyTypes; |
140 | // Respect the enotifminoredits preference |
141 | // @todo should this be checked somewhere else? |
142 | if ( !$userOptionsLookup->getOption( $user, 'enotifminoredits' ) ) { |
143 | if ( $hasMinorRevision === null ) { |
144 | // Do this only once per event |
145 | $hasMinorRevision = self::hasMinorRevision( $event ); |
146 | } |
147 | if ( $hasMinorRevision ) { |
148 | $userNotifyTypes = array_diff( $userNotifyTypes, [ 'email' ] ); |
149 | } |
150 | } |
151 | $hookRunner->onEchoGetNotificationTypes( $user, $event, $userNotifyTypes ); |
152 | |
153 | // types such as web, email, etc |
154 | foreach ( $userNotifyTypes as $type ) { |
155 | self::doNotification( $event, $user, $type ); |
156 | } |
157 | |
158 | $userIdsCount++; |
159 | // Process 1000 users per NotificationDeleteJob |
160 | if ( $userIdsCount > 1000 ) { |
161 | self::enqueueDeleteJob( $userIds, $event ); |
162 | $userIds = []; |
163 | $userIdsCount = 0; |
164 | } |
165 | |
166 | $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); |
167 | $stats->increment( 'echo.notification.all' ); |
168 | $stats->increment( "echo.notification.$type" ); |
169 | } |
170 | |
171 | // process the userIds left in the array |
172 | if ( $userIds ) { |
173 | self::enqueueDeleteJob( $userIds, $event ); |
174 | } |
175 | } |
176 | |
177 | /** |
178 | * Check if an event is associated with a minor revision. |
179 | * |
180 | * @param Event $event |
181 | * @return bool |
182 | */ |
183 | private static function hasMinorRevision( Event $event ) { |
184 | $revId = $event->getExtraParam( 'revid' ); |
185 | if ( !$revId ) { |
186 | return false; |
187 | } |
188 | |
189 | $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); |
190 | $rev = $revisionStore->getRevisionById( $revId, IDBAccessObject::READ_LATEST ); |
191 | if ( !$rev ) { |
192 | $logger = LoggerFactory::getInstance( 'Echo' ); |
193 | $logger->debug( |
194 | 'Notifying for event {eventId}. Revision \'{revId}\' not found.', |
195 | [ |
196 | 'eventId' => $event->getId(), |
197 | 'revId' => $revId, |
198 | ] |
199 | ); |
200 | return false; |
201 | } |
202 | |
203 | return $rev->isMinor(); |
204 | } |
205 | |
206 | /** |
207 | * Schedule a job to check and delete older notifications |
208 | * |
209 | * @param int[] $userIds |
210 | * @param Event $event |
211 | */ |
212 | public static function enqueueDeleteJob( array $userIds, Event $event ) { |
213 | // Do nothing if there is no user |
214 | if ( !$userIds ) { |
215 | return; |
216 | } |
217 | |
218 | $jobQueueGroup = MediaWikiServices::getInstance()->getJobQueueGroup(); |
219 | $job = new NotificationDeleteJob( |
220 | $event->getTitle() ?: Title::newMainPage(), |
221 | [ |
222 | 'userIds' => $userIds |
223 | ], |
224 | $jobQueueGroup |
225 | ); |
226 | $jobQueueGroup->push( $job ); |
227 | } |
228 | |
229 | /** |
230 | * Get the notify types for this event, eg, web/email |
231 | * |
232 | * @param string $eventType Event type |
233 | * @return string[] List of notify types that apply for |
234 | * this event type |
235 | */ |
236 | public static function getEventNotifyTypes( $eventType ) { |
237 | $attributeManager = Services::getInstance()->getAttributeManager(); |
238 | |
239 | $category = $attributeManager->getNotificationCategory( $eventType ); |
240 | |
241 | return array_keys( array_filter( |
242 | $attributeManager->getNotifyTypeAvailabilityForCategory( $category ) |
243 | ) ); |
244 | } |
245 | |
246 | /** |
247 | * Helper function to extract event task params |
248 | * @param Event $event |
249 | * @return array Event params |
250 | */ |
251 | public static function getEventParams( Event $event ) { |
252 | $delay = $event->getExtraParam( 'delay' ); |
253 | $rootJobSignature = $event->getExtraParam( 'rootJobSignature' ); |
254 | $rootJobTimestamp = $event->getExtraParam( 'rootJobTimestamp' ); |
255 | |
256 | return [ 'eventId' => $event->getId() ] |
257 | + ( $delay ? [ 'jobReleaseTimestamp' => (int)wfTimestamp() + $delay ] : [] ) |
258 | + ( $rootJobSignature ? [ 'rootJobSignature' => $rootJobSignature ] : [] ) |
259 | + ( $rootJobTimestamp ? [ 'rootJobTimestamp' => $rootJobTimestamp ] : [] ); |
260 | } |
261 | |
262 | /** |
263 | * Push $event onto the mediawiki job queue |
264 | * |
265 | * @param Event $event |
266 | */ |
267 | public static function enqueueEvent( Event $event ) { |
268 | $queue = MediaWikiServices::getInstance()->getJobQueueGroup(); |
269 | $params = self::getEventParams( $event ); |
270 | |
271 | $job = new NotificationJob( |
272 | $event->getTitle() ?: Title::newMainPage(), $params |
273 | ); |
274 | |
275 | if ( isset( $params[ 'jobReleaseTimestamp' ] ) && !$queue->get( $job->getType() )->delayedJobsEnabled() ) { |
276 | $logger = LoggerFactory::getInstance( 'Echo' ); |
277 | $logger->warning( |
278 | 'Delayed jobs are not enabled. Skipping enqueuing event {id} of type {type}.', |
279 | [ |
280 | 'id' => $event->getId(), |
281 | 'type' => $event->getType() |
282 | ] |
283 | ); |
284 | return; |
285 | } |
286 | |
287 | $queue->push( $job ); |
288 | } |
289 | |
290 | /** |
291 | * Implements blacklist per active wiki expected to be initialized |
292 | * from InitializeSettings.php |
293 | * |
294 | * @param Event $event The event to test for exclusion |
295 | * @param User $user recipient of the notification for per-user blacklists |
296 | * @return bool True when the event agent is blacklisted |
297 | */ |
298 | public static function isBlacklistedByUser( Event $event, User $user ) { |
299 | global $wgEchoAgentBlacklist, $wgEchoPerUserBlacklist; |
300 | |
301 | if ( !$event->getAgent() ) { |
302 | return false; |
303 | } |
304 | |
305 | // Ensure we have a list of blacklists |
306 | if ( self::$blacklistByUser === null ) { |
307 | self::$blacklistByUser = new MapCacheLRU( self::$maxRecipientCacheSize ); |
308 | } |
309 | |
310 | // Ensure we have a blacklist for the user |
311 | if ( !self::$blacklistByUser->has( (string)$user->getId() ) ) { |
312 | $blacklist = new ContainmentSet( $user ); |
313 | |
314 | // Add the config setting |
315 | $blacklist->addArray( $wgEchoAgentBlacklist ); |
316 | |
317 | // Add wiki-wide blacklist |
318 | $wikiBlacklist = self::getWikiBlacklist(); |
319 | if ( $wikiBlacklist !== null ) { |
320 | $blacklist->add( $wikiBlacklist ); |
321 | } |
322 | |
323 | // Add to blacklist from user preference |
324 | if ( $wgEchoPerUserBlacklist ) { |
325 | $blacklist->addFromUserOption( 'echo-notifications-blacklist' ); |
326 | } |
327 | |
328 | // Add user's blacklist to dictionary if user wasn't already there |
329 | self::$blacklistByUser->set( (string)$user->getId(), $blacklist ); |
330 | } else { |
331 | // Just get the user's blacklist if it's already there |
332 | $blacklist = self::$blacklistByUser->get( (string)$user->getId() ); |
333 | } |
334 | return $blacklist->contains( $event->getAgent()->getName() ) || |
335 | ( $wgEchoPerUserBlacklist && |
336 | $event->getType() === 'page-linked' && |
337 | self::isPageLinkedTitleMutedByUser( $event->getTitle(), $user ) ); |
338 | } |
339 | |
340 | /** |
341 | * Check if a title is in the user's page-linked event blacklist. |
342 | * |
343 | * @param Title $title |
344 | * @param User $user |
345 | * @return bool |
346 | */ |
347 | public static function isPageLinkedTitleMutedByUser( Title $title, User $user ) { |
348 | if ( self::$mutedPageLinkedTitlesCache === null ) { |
349 | self::$mutedPageLinkedTitlesCache = new MapCacheLRU( self::$maxUsersTitleCacheSize ); |
350 | } |
351 | if ( !self::$mutedPageLinkedTitlesCache->has( (string)$user->getId() ) ) { |
352 | $pageLinkedTitleMutedList = new ContainmentSet( $user ); |
353 | $pageLinkedTitleMutedList->addTitleIDsFromUserOption( |
354 | 'echo-notifications-page-linked-title-muted-list' |
355 | ); |
356 | self::$mutedPageLinkedTitlesCache->set( (string)$user->getId(), $pageLinkedTitleMutedList ); |
357 | } else { |
358 | $pageLinkedTitleMutedList = self::$mutedPageLinkedTitlesCache->get( (string)$user->getId() ); |
359 | } |
360 | return $pageLinkedTitleMutedList->contains( (string)$title->getArticleID() ); |
361 | } |
362 | |
363 | /** |
364 | * @return ContainmentList|null |
365 | */ |
366 | protected static function getWikiBlacklist() { |
367 | global $wgEchoOnWikiBlacklist; |
368 | if ( !$wgEchoOnWikiBlacklist ) { |
369 | return null; |
370 | } |
371 | if ( self::$wikiBlacklist === null ) { |
372 | $clusterCache = MediaWikiServices::getInstance()->getMainWANObjectCache(); |
373 | self::$wikiBlacklist = new CachedList( |
374 | $clusterCache, |
375 | $clusterCache->makeKey( "echo_on_wiki_blacklist" ), |
376 | new OnWikiList( NS_MEDIAWIKI, $wgEchoOnWikiBlacklist ) |
377 | ); |
378 | } |
379 | |
380 | return self::$wikiBlacklist; |
381 | } |
382 | |
383 | /** |
384 | * Implements per-user whitelist sourced from a user wiki page |
385 | * |
386 | * @param Event $event The event to test for inclusion in whitelist |
387 | * @param User $user The user that owns the whitelist |
388 | * @return bool True when the event agent is in the user whitelist |
389 | */ |
390 | public static function isWhitelistedByUser( Event $event, User $user ) { |
391 | $clusterCache = MediaWikiServices::getInstance()->getMainWANObjectCache(); |
392 | global $wgEchoPerUserWhitelistFormat; |
393 | |
394 | if ( $wgEchoPerUserWhitelistFormat === null || !$event->getAgent() ) { |
395 | return false; |
396 | } |
397 | |
398 | $userId = $user->getId(); |
399 | if ( $userId === 0 ) { |
400 | // anonymous user |
401 | return false; |
402 | } |
403 | |
404 | // Ensure we have a list of whitelists |
405 | if ( self::$whitelistByUser === null ) { |
406 | self::$whitelistByUser = new MapCacheLRU( self::$maxRecipientCacheSize ); |
407 | } |
408 | |
409 | // Ensure we have a whitelist for the user |
410 | if ( !self::$whitelistByUser->has( (string)$userId ) ) { |
411 | $whitelist = new ContainmentSet( $user ); |
412 | self::$whitelistByUser->set( (string)$userId, $whitelist ); |
413 | $whitelist->addOnWiki( |
414 | NS_USER, |
415 | sprintf( $wgEchoPerUserWhitelistFormat, $user->getName() ), |
416 | $clusterCache, |
417 | $clusterCache->makeKey( "echo_on_wiki_whitelist_" . $userId ) |
418 | ); |
419 | } else { |
420 | // Just get the user's whitelist |
421 | $whitelist = self::$whitelistByUser->get( (string)$userId ); |
422 | } |
423 | return $whitelist->contains( $event->getAgent()->getName() ); |
424 | } |
425 | |
426 | /** |
427 | * Processes a single notification for an Event |
428 | * |
429 | * @param Event $event |
430 | * @param User $user The user to be notified. |
431 | * @param string $type The type of notification delivery to process, e.g. 'email'. |
432 | */ |
433 | public static function doNotification( $event, $user, $type ) { |
434 | global $wgEchoNotifiers; |
435 | |
436 | if ( !isset( $wgEchoNotifiers[$type] ) ) { |
437 | throw new InvalidArgumentException( "Invalid notification type $type" ); |
438 | } |
439 | |
440 | // Don't send any notifications to anonymous users |
441 | if ( !$user->isRegistered() ) { |
442 | throw new InvalidArgumentException( "Cannot notify anonymous user: {$user->getName()}" ); |
443 | } |
444 | |
445 | ( $wgEchoNotifiers[$type] )( $user, $event ); |
446 | } |
447 | |
448 | /** |
449 | * Returns an array each element of which is the result of a |
450 | * user-locator|user-filters attached to the event type. |
451 | * |
452 | * @param Event $event |
453 | * @param string $locator Either AttributeManager::ATTR_LOCATORS or AttributeManager::ATTR_FILTERS |
454 | * @return array |
455 | */ |
456 | public static function evaluateUserCallable( Event $event, $locator = AttributeManager::ATTR_LOCATORS ) { |
457 | $attributeManager = Services::getInstance()->getAttributeManager(); |
458 | $type = $event->getType(); |
459 | $result = []; |
460 | foreach ( $attributeManager->getUserCallable( $type, $locator ) as $callable ) { |
461 | // locator options can be set per-event by using an array with |
462 | // name as first parameter. |
463 | if ( is_array( $callable ) ) { |
464 | $options = $callable; |
465 | $spliced = array_splice( $options, 0, 1, [ $event ] ); |
466 | $callable = reset( $spliced ); |
467 | } else { |
468 | $options = [ $event ]; |
469 | } |
470 | if ( is_callable( $callable ) ) { |
471 | $result[] = $callable( ...$options ); |
472 | } else { |
473 | wfDebugLog( __CLASS__, __FUNCTION__ . ": Invalid $locator returned for $type" ); |
474 | } |
475 | } |
476 | |
477 | return $result; |
478 | } |
479 | |
480 | /** |
481 | * Retrieves an array of User objects to be notified for an Event. |
482 | * |
483 | * @param Event $event |
484 | * @return Iterator values are User objects |
485 | */ |
486 | public static function getUsersToNotifyForEvent( Event $event ) { |
487 | $notify = new FilteredSequentialIterator; |
488 | foreach ( self::evaluateUserCallable( $event, AttributeManager::ATTR_LOCATORS ) as $users ) { |
489 | $notify->add( $users ); |
490 | } |
491 | |
492 | // Hook for injecting more users. |
493 | // @deprecated |
494 | $users = []; |
495 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
496 | ->onEchoGetDefaultNotifiedUsers( $event, $users ); |
497 | if ( $users ) { |
498 | $notify->add( $users ); |
499 | } |
500 | |
501 | // Exclude certain users |
502 | foreach ( self::evaluateUserCallable( $event, AttributeManager::ATTR_FILTERS ) as $users ) { |
503 | // the result of the callback can be both an iterator or array |
504 | $users = is_array( $users ) ? $users : iterator_to_array( $users ); |
505 | $notify->addFilter( static function ( UserIdentity $user ) use ( $users ) { |
506 | // we need to check if $user is in $users, but they're not |
507 | // guaranteed to be the same object, so I'll compare ids. |
508 | $userId = $user->getId(); |
509 | $userIds = array_map( static function ( UserIdentity $user ) { |
510 | return $user->getId(); |
511 | }, $users ); |
512 | return !in_array( $userId, $userIds ); |
513 | } ); |
514 | } |
515 | |
516 | // Filter non-User, anon and duplicate users |
517 | $seen = []; |
518 | $fname = __METHOD__; |
519 | $notify->addFilter( static function ( $user ) use ( &$seen, $fname ) { |
520 | if ( !$user instanceof User ) { |
521 | wfDebugLog( $fname, 'Expected all User instances, received: ' . |
522 | ( is_object( $user ) ? get_class( $user ) : gettype( $user ) ) |
523 | ); |
524 | |
525 | return false; |
526 | } |
527 | if ( !$user->isRegistered() || isset( $seen[$user->getId()] ) ) { |
528 | return false; |
529 | } |
530 | $seen[$user->getId()] = true; |
531 | |
532 | return true; |
533 | } ); |
534 | |
535 | // Don't notify the person who initiated the event unless the event allows it |
536 | if ( !$event->canNotifyAgent() && $event->getAgent() ) { |
537 | $agentId = $event->getAgent()->getId(); |
538 | $notify->addFilter( static function ( $user ) use ( $agentId ) { |
539 | return $user->getId() != $agentId; |
540 | } ); |
541 | } |
542 | |
543 | // Apply blacklists and whitelists. |
544 | $notify->addFilter( function ( $user ) use ( $event ) { |
545 | $title = $event->getTitle(); |
546 | |
547 | if ( self::isBlacklistedByUser( $event, $user ) && |
548 | ( |
549 | $title === null || |
550 | !( |
551 | // Still notify for posts on the user's talk page |
552 | // (but not subpages, T288112) |
553 | $title->getText() === $user->getName() && |
554 | $title->getNamespace() === NS_USER_TALK |
555 | ) |
556 | ) |
557 | ) { |
558 | return self::isWhitelistedByUser( $event, $user ); |
559 | } |
560 | |
561 | return true; |
562 | } ); |
563 | |
564 | return $notify->getIterator(); |
565 | } |
566 | } |