2012-04-27 15:14:24 +00:00
|
|
|
<?php
|
2019-10-23 10:31:35 +00:00
|
|
|
|
2018-10-04 21:26:03 +00:00
|
|
|
use MediaWiki\Logger\LoggerFactory;
|
|
|
|
use MediaWiki\MediaWikiServices;
|
2018-10-12 18:14:14 +00:00
|
|
|
use MediaWiki\Revision\RevisionStore;
|
2015-08-10 18:27:52 +00:00
|
|
|
|
2013-06-12 23:18:26 +00:00
|
|
|
/**
|
2014-08-05 00:15:14 +00:00
|
|
|
* This class represents the controller for notifications
|
2013-06-12 23:18:26 +00:00
|
|
|
*/
|
2012-04-27 15:14:24 +00:00
|
|
|
class EchoNotificationController {
|
2014-08-05 00:15:14 +00:00
|
|
|
|
2016-11-10 04:37:10 +00:00
|
|
|
/**
|
|
|
|
* Echo maximum number of users to cache
|
|
|
|
*
|
|
|
|
* @var int $maxRecipientCacheSize
|
|
|
|
*/
|
2019-02-06 13:01:32 +00:00
|
|
|
protected static $maxRecipientCacheSize = 200;
|
2016-11-10 04:37:10 +00:00
|
|
|
|
2020-04-20 19:40:07 +00:00
|
|
|
/**
|
|
|
|
* Max number of users for which we in-process cache titles.
|
|
|
|
*
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
protected static $maxUsersTitleCacheSize = 200;
|
|
|
|
|
2016-11-10 04:37:10 +00:00
|
|
|
/**
|
|
|
|
* Echo event agent per user blacklist
|
|
|
|
*
|
|
|
|
* @var MapCacheLRU
|
|
|
|
*/
|
2019-02-06 13:01:32 +00:00
|
|
|
protected static $blacklistByUser;
|
2016-11-10 04:37:10 +00:00
|
|
|
|
2020-04-20 19:40:07 +00:00
|
|
|
/**
|
|
|
|
* Echo event agent per page linked event title mute list.
|
|
|
|
*
|
|
|
|
* @var MapCacheLRU
|
|
|
|
*/
|
|
|
|
protected static $mutedPageLinkedTitlesCache;
|
|
|
|
|
2014-08-05 00:15:14 +00:00
|
|
|
/**
|
|
|
|
* Echo event agent per wiki blacklist
|
|
|
|
*
|
2016-11-10 04:37:10 +00:00
|
|
|
* @var EchoContainmentList|null
|
2014-08-05 00:15:14 +00:00
|
|
|
*/
|
2019-02-06 13:01:32 +00:00
|
|
|
protected static $wikiBlacklist;
|
2014-08-05 00:15:14 +00:00
|
|
|
|
|
|
|
/**
|
2016-11-10 04:37:10 +00:00
|
|
|
* Echo event agent per user whitelist, this overwrites $blacklistByUser
|
2014-08-05 00:15:14 +00:00
|
|
|
*
|
2016-11-10 04:37:10 +00:00
|
|
|
* @var MapCacheLRU
|
2014-08-05 00:15:14 +00:00
|
|
|
*/
|
2019-02-06 13:01:32 +00:00
|
|
|
protected static $whitelistByUser;
|
2013-05-06 22:34:50 +00:00
|
|
|
|
2012-11-13 23:06:11 +00:00
|
|
|
/**
|
2016-03-09 04:50:31 +00:00
|
|
|
* Returns the count passed in, or MWEchoNotifUser::MAX_BADGE_COUNT + 1,
|
|
|
|
* whichever is less.
|
2014-08-05 00:15:14 +00:00
|
|
|
*
|
2014-08-26 18:58:34 +00:00
|
|
|
* @param int $count
|
2016-03-09 04:50:31 +00:00
|
|
|
* @return int Notification count, with ceiling applied
|
2012-11-13 23:06:11 +00:00
|
|
|
*/
|
2016-03-09 04:50:31 +00:00
|
|
|
public static function getCappedNotificationCount( $count ) {
|
|
|
|
if ( $count <= MWEchoNotifUser::MAX_BADGE_COUNT ) {
|
|
|
|
return $count;
|
2012-11-13 23:06:11 +00:00
|
|
|
} else {
|
2016-03-09 04:50:31 +00:00
|
|
|
return MWEchoNotifUser::MAX_BADGE_COUNT + 1;
|
2012-11-13 23:06:11 +00:00
|
|
|
}
|
2016-03-09 04:50:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-10-14 13:45:13 +00:00
|
|
|
* Format the notification count as a string. This should only be used for an
|
|
|
|
* isolated string count, e.g. as displayed in personal tools or returned by the API.
|
|
|
|
*
|
|
|
|
* If using it in sentence context, pass the value from getCappedNotificationCount
|
|
|
|
* into a message and use PLURAL. Example: notification-bundle-header-page-linked
|
|
|
|
*
|
|
|
|
* @param int $count Notification count
|
|
|
|
* @return string Formatted count, after applying cap then formatting to string
|
|
|
|
*/
|
2016-03-09 04:50:31 +00:00
|
|
|
public static function formatNotificationCount( $count ) {
|
|
|
|
$cappedCount = self::getCappedNotificationCount( $count );
|
2012-11-13 23:06:11 +00:00
|
|
|
|
2016-03-09 04:50:31 +00:00
|
|
|
return wfMessage( 'echo-badge-count' )->numParams( $cappedCount )->text();
|
2012-11-13 23:06:11 +00:00
|
|
|
}
|
|
|
|
|
2012-04-27 15:14:24 +00:00
|
|
|
/**
|
|
|
|
* Processes notifications for a newly-created EchoEvent
|
2012-08-31 21:50:46 +00:00
|
|
|
*
|
2014-08-05 00:15:14 +00:00
|
|
|
* @param EchoEvent $event
|
2017-08-09 15:20:55 +00:00
|
|
|
* @param bool $defer Defer to job queue or not
|
2012-04-27 15:14:24 +00:00
|
|
|
*/
|
2012-06-06 07:04:28 +00:00
|
|
|
public static function notify( $event, $defer = true ) {
|
2014-08-27 18:39:53 +00:00
|
|
|
// Defer to job queue if defer to job queue is requested and
|
|
|
|
// this event should use job queue
|
|
|
|
if ( $defer && $event->getUseJobQueue() ) {
|
2013-08-30 02:05:29 +00:00
|
|
|
// defer job insertion till end of request when all primary db transactions
|
|
|
|
// have been committed
|
2015-10-01 13:48:52 +00:00
|
|
|
DeferredUpdates::addCallableUpdate( function () use ( $event ) {
|
2018-11-14 09:51:29 +00:00
|
|
|
self::enqueueEvent( $event );
|
2014-08-02 06:41:19 +00:00
|
|
|
} );
|
2015-10-01 13:48:52 +00:00
|
|
|
|
2012-06-06 07:04:28 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-03-01 01:09:40 +00:00
|
|
|
// Check if the event object has valid event type. Events with invalid
|
|
|
|
// event types left in the job queue should not be processed
|
2012-11-13 23:06:11 +00:00
|
|
|
if ( !$event->isEnabledEvent() ) {
|
2013-03-01 01:09:40 +00:00
|
|
|
return;
|
2012-11-13 23:06:11 +00:00
|
|
|
}
|
|
|
|
|
2014-08-02 06:52:16 +00:00
|
|
|
$type = $event->getType();
|
|
|
|
$notifyTypes = self::getEventNotifyTypes( $type );
|
2016-12-05 18:51:07 +00:00
|
|
|
$userIds = [];
|
2014-09-09 22:11:53 +00:00
|
|
|
$userIdsCount = 0;
|
2014-08-02 06:52:16 +00:00
|
|
|
foreach ( self::getUsersToNotifyForEvent( $event ) as $user ) {
|
2014-09-09 22:11:53 +00:00
|
|
|
$userIds[$user->getId()] = $user->getId();
|
2014-08-02 06:52:16 +00:00
|
|
|
$userNotifyTypes = $notifyTypes;
|
2015-06-16 00:40:14 +00:00
|
|
|
// Respect the enotifminoredits preference
|
|
|
|
// @todo should this be checked somewhere else?
|
2018-10-04 21:26:03 +00:00
|
|
|
if (
|
|
|
|
!$user->getOption( 'enotifminoredits' ) &&
|
|
|
|
self::hasMinorRevision( $event )
|
|
|
|
) {
|
|
|
|
$notifyTypes = array_diff( $notifyTypes, [ 'email' ] );
|
2015-06-16 00:40:14 +00:00
|
|
|
}
|
2016-12-05 18:51:07 +00:00
|
|
|
Hooks::run( 'EchoGetNotificationTypes', [ $user, $event, &$userNotifyTypes ] );
|
2013-03-12 22:52:00 +00:00
|
|
|
|
2014-08-02 06:52:16 +00:00
|
|
|
// types such as web, email, etc
|
|
|
|
foreach ( $userNotifyTypes as $type ) {
|
|
|
|
self::doNotification( $event, $user, $type );
|
|
|
|
}
|
2014-09-09 22:11:53 +00:00
|
|
|
|
|
|
|
$userIdsCount++;
|
|
|
|
// Process 1000 users per NotificationDeleteJob
|
|
|
|
if ( $userIdsCount > 1000 ) {
|
|
|
|
self::enqueueDeleteJob( $userIds, $event );
|
2016-12-05 18:51:07 +00:00
|
|
|
$userIds = [];
|
2014-09-09 22:11:53 +00:00
|
|
|
$userIdsCount = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// process the userIds left in the array
|
|
|
|
if ( $userIds ) {
|
|
|
|
self::enqueueDeleteJob( $userIds, $event );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-04 21:26:03 +00:00
|
|
|
/**
|
|
|
|
* Check if an event is associated with a minor revision.
|
|
|
|
*
|
|
|
|
* @param EchoEvent $event
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
private static function hasMinorRevision( EchoEvent $event ) {
|
|
|
|
$revId = $event->getExtraParam( 'revid' );
|
|
|
|
if ( !$revId ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
|
|
|
|
$rev = $revisionStore->getRevisionById( $revId, RevisionStore::READ_LATEST );
|
|
|
|
if ( !$rev ) {
|
|
|
|
$logger = LoggerFactory::getInstance( 'Echo' );
|
|
|
|
$logger->debug(
|
|
|
|
'Notifying for event {eventId}. Revision \'{revId}\' not found.',
|
|
|
|
[
|
|
|
|
'eventId' => $event->getId(),
|
|
|
|
'revId' => $revId,
|
|
|
|
]
|
|
|
|
);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $rev->isMinor();
|
|
|
|
}
|
|
|
|
|
2014-09-09 22:11:53 +00:00
|
|
|
/**
|
|
|
|
* Schedule a job to check and delete older notifications
|
|
|
|
*
|
2017-11-07 16:06:17 +00:00
|
|
|
* @param int[] $userIds
|
2014-09-09 22:11:53 +00:00
|
|
|
* @param EchoEvent $event
|
|
|
|
*/
|
|
|
|
public static function enqueueDeleteJob( array $userIds, EchoEvent $event ) {
|
|
|
|
// Do nothing if there is no user
|
|
|
|
if ( !$userIds ) {
|
|
|
|
return;
|
2014-08-02 06:52:16 +00:00
|
|
|
}
|
2014-09-09 22:11:53 +00:00
|
|
|
|
|
|
|
$job = new EchoNotificationDeleteJob(
|
|
|
|
$event->getTitle() ?: Title::newMainPage(),
|
2016-12-05 18:51:07 +00:00
|
|
|
[
|
2014-09-09 22:11:53 +00:00
|
|
|
'userIds' => $userIds
|
2016-12-05 18:51:07 +00:00
|
|
|
]
|
2014-09-09 22:11:53 +00:00
|
|
|
);
|
|
|
|
JobQueueGroup::singleton()->push( $job );
|
2014-08-02 06:52:16 +00:00
|
|
|
}
|
2013-03-12 22:52:00 +00:00
|
|
|
|
2014-08-02 06:52:16 +00:00
|
|
|
/**
|
BREAKING CHANGE: Change $wgEchoDefaultNotificationTypes to be logical
Merge and deploy at the *same time* as:
* BounceHandler - I3c669945080d8e1f67880bd8a31af7f88a70904d
* mediawiki-config - I13817c139967ed9e230cfb0c87c5de66da793c96
Despite claiming to be about categories, $wgEchoDefaultNotificationTypes
was actually configuring both categories and types (which go inside
categories).
For example, 'thank-you-edit' is a type, but 'emailuser' is both
a category and a type (when used as a category, this has special
effects at Special:Preferences).
Since types and categories can and sometimes do have the same names,
this leaves no way to properly and clearly configure them. It also
makes it difficult to document what is going on (as required by
T132127).
Split into three variables:
$wgDefaultNotifyTypeAvailability - Applies unless overriden
$wgNotifyTypeAvailabilityByCategory - By category; this can be and is
displayed at Special:Preferences
$wgNotifyTypeAvailabilityByNotificationType - By type; this cannot
be displayed at Special:Preferences. To avoid confusing the user,
we introduce a restriction (which was previously followed in practice,
AFAICT) that types can only be overridden if the category is not
displayed in preferences.
Otherwise, it can look to the user like a category is on/off, but the
types within might have the opposite state.
Due to this configuration change, this is a breaking change, and needs
coordinated deployments.
This also lays the groundwork for T132127
Also change terminology to consistently use "notify type" for web/email.
It was mixing between that and output format (which unfortunately
sounds like the API format, e.g. 'model').
Bug: T132820
Bug: T132127
Change-Id: I09f39f5fc5f13f3253af9f7819bca81f1601da93
2016-04-19 02:54:15 +00:00
|
|
|
* Get the notify types for this event, eg, web/email
|
|
|
|
*
|
|
|
|
* @param string $eventType Event type
|
|
|
|
* @return string[] List of notify types that apply for
|
2014-08-02 06:52:16 +00:00
|
|
|
* this event type
|
|
|
|
*/
|
BREAKING CHANGE: Change $wgEchoDefaultNotificationTypes to be logical
Merge and deploy at the *same time* as:
* BounceHandler - I3c669945080d8e1f67880bd8a31af7f88a70904d
* mediawiki-config - I13817c139967ed9e230cfb0c87c5de66da793c96
Despite claiming to be about categories, $wgEchoDefaultNotificationTypes
was actually configuring both categories and types (which go inside
categories).
For example, 'thank-you-edit' is a type, but 'emailuser' is both
a category and a type (when used as a category, this has special
effects at Special:Preferences).
Since types and categories can and sometimes do have the same names,
this leaves no way to properly and clearly configure them. It also
makes it difficult to document what is going on (as required by
T132127).
Split into three variables:
$wgDefaultNotifyTypeAvailability - Applies unless overriden
$wgNotifyTypeAvailabilityByCategory - By category; this can be and is
displayed at Special:Preferences
$wgNotifyTypeAvailabilityByNotificationType - By type; this cannot
be displayed at Special:Preferences. To avoid confusing the user,
we introduce a restriction (which was previously followed in practice,
AFAICT) that types can only be overridden if the category is not
displayed in preferences.
Otherwise, it can look to the user like a category is on/off, but the
types within might have the opposite state.
Due to this configuration change, this is a breaking change, and needs
coordinated deployments.
This also lays the groundwork for T132127
Also change terminology to consistently use "notify type" for web/email.
It was mixing between that and output format (which unfortunately
sounds like the API format, e.g. 'model').
Bug: T132820
Bug: T132127
Change-Id: I09f39f5fc5f13f3253af9f7819bca81f1601da93
2016-04-19 02:54:15 +00:00
|
|
|
public static function getEventNotifyTypes( $eventType ) {
|
|
|
|
$attributeManager = EchoAttributeManager::newFromGlobalVars();
|
|
|
|
|
|
|
|
$category = $attributeManager->getNotificationCategory( $eventType );
|
|
|
|
|
2019-04-22 22:35:31 +00:00
|
|
|
return array_keys( array_filter(
|
|
|
|
$attributeManager->getNotifyTypeAvailabilityForCategory( $category )
|
|
|
|
) );
|
2012-04-27 15:14:24 +00:00
|
|
|
}
|
|
|
|
|
2014-08-02 06:41:19 +00:00
|
|
|
/**
|
|
|
|
* Push $event onto the mediawiki job queue
|
|
|
|
*
|
|
|
|
* @param EchoEvent $event
|
|
|
|
*/
|
|
|
|
public static function enqueueEvent( EchoEvent $event ) {
|
|
|
|
$job = new EchoNotificationJob(
|
|
|
|
$event->getTitle() ?: Title::newMainPage(),
|
2016-12-05 18:51:07 +00:00
|
|
|
[
|
2018-05-08 19:52:55 +00:00
|
|
|
'eventId' => $event->getId(),
|
2016-12-05 18:51:07 +00:00
|
|
|
]
|
2014-08-02 06:41:19 +00:00
|
|
|
);
|
|
|
|
JobQueueGroup::singleton()->push( $job );
|
|
|
|
}
|
|
|
|
|
2013-05-06 22:34:50 +00:00
|
|
|
/**
|
|
|
|
* Implements blacklist per active wiki expected to be initialized
|
|
|
|
* from InitializeSettings.php
|
|
|
|
*
|
2016-11-10 04:37:10 +00:00
|
|
|
* @param EchoEvent $event The event to test for exclusion
|
|
|
|
* @param User $user recipient of the notification for per-user blacklists
|
2017-07-26 19:34:44 +00:00
|
|
|
* @return bool True when the event agent is blacklisted
|
2013-05-06 22:34:50 +00:00
|
|
|
*/
|
2016-11-10 04:37:10 +00:00
|
|
|
public static function isBlacklistedByUser( EchoEvent $event, User $user ) {
|
|
|
|
global $wgEchoAgentBlacklist, $wgEchoPerUserBlacklist;
|
|
|
|
|
2013-05-06 22:34:50 +00:00
|
|
|
if ( !$event->getAgent() ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-11-10 04:37:10 +00:00
|
|
|
// Ensure we have a list of blacklists
|
|
|
|
if ( self::$blacklistByUser === null ) {
|
|
|
|
self::$blacklistByUser = new MapCacheLRU( self::$maxRecipientCacheSize );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure we have a blacklist for the user
|
2019-12-21 05:45:14 +00:00
|
|
|
if ( !self::$blacklistByUser->has( (string)$user->getId() ) ) {
|
2016-11-10 04:37:10 +00:00
|
|
|
$blacklist = new EchoContainmentSet( $user );
|
|
|
|
|
|
|
|
// Add the config setting
|
|
|
|
$blacklist->addArray( $wgEchoAgentBlacklist );
|
|
|
|
|
|
|
|
// Add wiki-wide blacklist
|
|
|
|
$wikiBlacklist = self::getWikiBlacklist();
|
|
|
|
if ( $wikiBlacklist !== null ) {
|
|
|
|
$blacklist->add( $wikiBlacklist );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add to blacklist from user preference
|
|
|
|
if ( $wgEchoPerUserBlacklist ) {
|
|
|
|
$blacklist->addFromUserOption( 'echo-notifications-blacklist' );
|
2013-05-06 22:34:50 +00:00
|
|
|
}
|
2016-11-10 04:37:10 +00:00
|
|
|
|
|
|
|
// Add user's blacklist to dictionary if user wasn't already there
|
2019-12-21 05:45:14 +00:00
|
|
|
self::$blacklistByUser->set( (string)$user->getId(), $blacklist );
|
2016-11-10 04:37:10 +00:00
|
|
|
} else {
|
|
|
|
// Just get the user's blacklist if it's already there
|
2019-12-21 05:45:14 +00:00
|
|
|
$blacklist = self::$blacklistByUser->get( (string)$user->getId() );
|
2013-05-06 22:34:50 +00:00
|
|
|
}
|
2020-04-20 19:40:07 +00:00
|
|
|
return $blacklist->contains( $event->getAgent()->getName() ) ||
|
|
|
|
( $wgEchoPerUserBlacklist &&
|
|
|
|
$event->getType() === 'page-linked' &&
|
2020-04-28 03:39:40 +00:00
|
|
|
self::isPageLinkedTitleMutedByUser( $event->getTitle(), $user ) );
|
2020-04-20 19:40:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-04-28 03:39:40 +00:00
|
|
|
* Check if a title is in the user's page-linked event blacklist.
|
2020-04-20 19:40:07 +00:00
|
|
|
*
|
2020-04-28 03:39:40 +00:00
|
|
|
* @param Title $title
|
2020-04-20 19:40:07 +00:00
|
|
|
* @param User $user
|
|
|
|
* @return bool
|
|
|
|
*/
|
2020-04-28 03:39:40 +00:00
|
|
|
public static function isPageLinkedTitleMutedByUser( Title $title, User $user ) {
|
|
|
|
if ( self::$mutedPageLinkedTitlesCache === null ) {
|
|
|
|
self::$mutedPageLinkedTitlesCache = new MapCacheLRU( self::$maxUsersTitleCacheSize );
|
|
|
|
}
|
2020-04-20 19:40:07 +00:00
|
|
|
if ( !self::$mutedPageLinkedTitlesCache->has( (string)$user->getId() ) ) {
|
|
|
|
$pageLinkedTitleMutedList = new EchoContainmentSet( $user );
|
|
|
|
$pageLinkedTitleMutedList->addTitleIDsFromUserOption(
|
|
|
|
'echo-notifications-page-linked-title-muted-list'
|
|
|
|
);
|
|
|
|
self::$mutedPageLinkedTitlesCache->set( (string)$user->getId(), $pageLinkedTitleMutedList );
|
|
|
|
} else {
|
|
|
|
$pageLinkedTitleMutedList = self::$mutedPageLinkedTitlesCache->get( (string)$user->getId() );
|
|
|
|
}
|
2020-04-28 03:39:40 +00:00
|
|
|
return $pageLinkedTitleMutedList->contains( (string)$title->getArticleID() );
|
2016-11-10 04:37:10 +00:00
|
|
|
}
|
2013-05-06 22:34:50 +00:00
|
|
|
|
2016-11-10 04:37:10 +00:00
|
|
|
/**
|
|
|
|
* @return EchoContainmentList|null
|
|
|
|
*/
|
|
|
|
protected static function getWikiBlacklist() {
|
|
|
|
global $wgEchoOnWikiBlacklist;
|
|
|
|
if ( !$wgEchoOnWikiBlacklist ) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if ( self::$wikiBlacklist === null ) {
|
2020-03-08 10:00:04 +00:00
|
|
|
$clusterCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
|
2016-11-10 04:37:10 +00:00
|
|
|
self::$wikiBlacklist = new EchoCachedList(
|
|
|
|
$clusterCache,
|
|
|
|
$clusterCache->makeKey( "echo_on_wiki_blacklist" ),
|
|
|
|
new EchoOnWikiList( NS_MEDIAWIKI, $wgEchoOnWikiBlacklist )
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return self::$wikiBlacklist;
|
2013-05-06 22:34:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements per-user whitelist sourced from a user wiki page
|
|
|
|
*
|
2014-08-05 00:15:14 +00:00
|
|
|
* @param EchoEvent $event The event to test for inclusion in whitelist
|
|
|
|
* @param User $user The user that owns the whitelist
|
2017-07-26 19:34:44 +00:00
|
|
|
* @return bool True when the event agent is in the user whitelist
|
2013-05-06 22:34:50 +00:00
|
|
|
*/
|
2014-08-26 18:58:34 +00:00
|
|
|
public static function isWhitelistedByUser( EchoEvent $event, User $user ) {
|
2019-02-28 21:17:15 +00:00
|
|
|
$clusterCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
|
2016-04-22 18:58:03 +00:00
|
|
|
global $wgEchoPerUserWhitelistFormat;
|
2013-05-06 22:34:50 +00:00
|
|
|
|
|
|
|
if ( $wgEchoPerUserWhitelistFormat === null || !$event->getAgent() ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-06-11 21:43:05 +00:00
|
|
|
$userId = $user->getId();
|
2013-05-06 22:34:50 +00:00
|
|
|
if ( $userId === 0 ) {
|
|
|
|
return false; // anonymous user
|
|
|
|
}
|
|
|
|
|
2016-11-10 04:37:10 +00:00
|
|
|
// Ensure we have a list of whitelists
|
|
|
|
if ( self::$whitelistByUser === null ) {
|
|
|
|
self::$whitelistByUser = new MapCacheLRU( self::$maxRecipientCacheSize );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure we have a whitelist for the user
|
2019-12-21 05:45:14 +00:00
|
|
|
if ( !self::$whitelistByUser->has( (string)$userId ) ) {
|
2016-11-10 04:37:10 +00:00
|
|
|
$whitelist = new EchoContainmentSet( $user );
|
2019-12-21 05:45:14 +00:00
|
|
|
self::$whitelistByUser->set( (string)$userId, $whitelist );
|
2016-11-10 04:37:10 +00:00
|
|
|
$whitelist->addOnWiki(
|
2013-05-06 22:34:50 +00:00
|
|
|
NS_USER,
|
|
|
|
sprintf( $wgEchoPerUserWhitelistFormat, $user->getName() ),
|
2016-11-10 04:37:10 +00:00
|
|
|
$clusterCache,
|
|
|
|
$clusterCache->makeKey( "echo_on_wiki_whitelist_" . $userId )
|
2013-05-06 22:34:50 +00:00
|
|
|
);
|
2016-11-10 04:37:10 +00:00
|
|
|
} else {
|
|
|
|
// Just get the user's whitelist
|
2019-12-21 05:45:14 +00:00
|
|
|
$whitelist = self::$whitelistByUser->get( (string)$userId );
|
2013-05-06 22:34:50 +00:00
|
|
|
}
|
2016-11-10 04:37:10 +00:00
|
|
|
return $whitelist->contains( $event->getAgent()->getName() );
|
2013-05-06 22:34:50 +00:00
|
|
|
}
|
|
|
|
|
2012-04-27 15:14:24 +00:00
|
|
|
/**
|
|
|
|
* Processes a single notification for an EchoEvent
|
|
|
|
*
|
2014-08-05 00:15:14 +00:00
|
|
|
* @param EchoEvent $event
|
|
|
|
* @param User $user The user to be notified.
|
|
|
|
* @param string $type The type of notification delivery to process, e.g. 'email'.
|
2012-05-17 00:29:37 +00:00
|
|
|
* @throws MWException
|
2012-04-27 15:14:24 +00:00
|
|
|
*/
|
|
|
|
public static function doNotification( $event, $user, $type ) {
|
|
|
|
global $wgEchoNotifiers;
|
|
|
|
|
2012-08-31 21:50:46 +00:00
|
|
|
if ( !isset( $wgEchoNotifiers[$type] ) ) {
|
2012-04-27 15:14:24 +00:00
|
|
|
throw new MWException( "Invalid notification type $type" );
|
|
|
|
}
|
|
|
|
|
2015-06-01 23:22:48 +00:00
|
|
|
// Don't send any notifications to anonymous users
|
|
|
|
if ( $user->isAnon() ) {
|
|
|
|
throw new MWException( "Cannot notify anonymous user: {$user->getName()}" );
|
2013-06-08 01:05:35 +00:00
|
|
|
}
|
|
|
|
|
2018-06-08 08:02:35 +00:00
|
|
|
( $wgEchoNotifiers[$type] )( $user, $event );
|
2012-04-27 15:14:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2014-07-30 03:18:48 +00:00
|
|
|
* Returns an array each element of which is the result of a
|
Allow certain users to be excluded
Right now, if certain users should be excluded, that would have
to be part of the user-locators already. This is annoying because
it's hard to write "generic" user locators when you want to exclude
just a couple of people in certain cases.
In Flow, for example, we have user-locators for users watching a
board or topic. We don't want to send the notification to people
that have also been mentioned in that post (they'll get a separate
notification). We could build that exception into those
user-locators, but then we couldn't re-use them in other places...
This basically means we couldn't use EchoUserLocator::locateUsersWatchingTitle,
we would have to roll our own that also excludes mentioned users.
Instead, this lets you add 'user-filters' (that functionality
actually exists already, but is not currently exposed), which
lists users to not send the notification to, even though they could
be in a user-locator.
Bug: T125428
Change-Id: Ifa0e2d3283f57624af4c5ec264f9f66223508e83
2016-02-02 13:16:39 +00:00
|
|
|
* user-locator|user-filters attached to the event type.
|
2012-08-31 21:50:46 +00:00
|
|
|
*
|
2014-07-29 23:54:00 +00:00
|
|
|
* @param EchoEvent $event
|
Allow certain users to be excluded
Right now, if certain users should be excluded, that would have
to be part of the user-locators already. This is annoying because
it's hard to write "generic" user locators when you want to exclude
just a couple of people in certain cases.
In Flow, for example, we have user-locators for users watching a
board or topic. We don't want to send the notification to people
that have also been mentioned in that post (they'll get a separate
notification). We could build that exception into those
user-locators, but then we couldn't re-use them in other places...
This basically means we couldn't use EchoUserLocator::locateUsersWatchingTitle,
we would have to roll our own that also excludes mentioned users.
Instead, this lets you add 'user-filters' (that functionality
actually exists already, but is not currently exposed), which
lists users to not send the notification to, even though they could
be in a user-locator.
Bug: T125428
Change-Id: Ifa0e2d3283f57624af4c5ec264f9f66223508e83
2016-02-02 13:16:39 +00:00
|
|
|
* @param string $locator Either EchoAttributeManager::ATTR_LOCATORS or EchoAttributeManager::ATTR_FILTERS
|
2014-07-30 03:18:48 +00:00
|
|
|
* @return array
|
2012-04-27 15:14:24 +00:00
|
|
|
*/
|
Allow certain users to be excluded
Right now, if certain users should be excluded, that would have
to be part of the user-locators already. This is annoying because
it's hard to write "generic" user locators when you want to exclude
just a couple of people in certain cases.
In Flow, for example, we have user-locators for users watching a
board or topic. We don't want to send the notification to people
that have also been mentioned in that post (they'll get a separate
notification). We could build that exception into those
user-locators, but then we couldn't re-use them in other places...
This basically means we couldn't use EchoUserLocator::locateUsersWatchingTitle,
we would have to roll our own that also excludes mentioned users.
Instead, this lets you add 'user-filters' (that functionality
actually exists already, but is not currently exposed), which
lists users to not send the notification to, even though they could
be in a user-locator.
Bug: T125428
Change-Id: Ifa0e2d3283f57624af4c5ec264f9f66223508e83
2016-02-02 13:16:39 +00:00
|
|
|
public static function evaluateUserCallable( EchoEvent $event, $locator = EchoAttributeManager::ATTR_LOCATORS ) {
|
2014-07-29 23:54:00 +00:00
|
|
|
$attributeManager = EchoAttributeManager::newFromGlobalVars();
|
2014-07-30 03:18:48 +00:00
|
|
|
$type = $event->getType();
|
2016-12-05 18:51:07 +00:00
|
|
|
$result = [];
|
Allow certain users to be excluded
Right now, if certain users should be excluded, that would have
to be part of the user-locators already. This is annoying because
it's hard to write "generic" user locators when you want to exclude
just a couple of people in certain cases.
In Flow, for example, we have user-locators for users watching a
board or topic. We don't want to send the notification to people
that have also been mentioned in that post (they'll get a separate
notification). We could build that exception into those
user-locators, but then we couldn't re-use them in other places...
This basically means we couldn't use EchoUserLocator::locateUsersWatchingTitle,
we would have to roll our own that also excludes mentioned users.
Instead, this lets you add 'user-filters' (that functionality
actually exists already, but is not currently exposed), which
lists users to not send the notification to, even though they could
be in a user-locator.
Bug: T125428
Change-Id: Ifa0e2d3283f57624af4c5ec264f9f66223508e83
2016-02-02 13:16:39 +00:00
|
|
|
foreach ( $attributeManager->getUserCallable( $type, $locator ) as $callable ) {
|
2014-07-31 02:31:47 +00:00
|
|
|
// locator options can be set per-event by using an array with
|
2014-07-30 03:18:48 +00:00
|
|
|
// name as first parameter.
|
2014-07-31 02:31:47 +00:00
|
|
|
if ( is_array( $callable ) ) {
|
|
|
|
$options = $callable;
|
2016-12-05 18:51:07 +00:00
|
|
|
$spliced = array_splice( $options, 0, 1, [ $event ] );
|
2014-07-30 03:18:48 +00:00
|
|
|
$callable = reset( $spliced );
|
2014-07-31 02:31:47 +00:00
|
|
|
} else {
|
2016-12-05 18:51:07 +00:00
|
|
|
$options = [ $event ];
|
2014-07-31 02:31:47 +00:00
|
|
|
}
|
2014-07-29 23:54:00 +00:00
|
|
|
if ( is_callable( $callable ) ) {
|
2018-06-08 08:02:35 +00:00
|
|
|
$result[] = $callable( ...$options );
|
2014-07-29 23:54:00 +00:00
|
|
|
} else {
|
Allow certain users to be excluded
Right now, if certain users should be excluded, that would have
to be part of the user-locators already. This is annoying because
it's hard to write "generic" user locators when you want to exclude
just a couple of people in certain cases.
In Flow, for example, we have user-locators for users watching a
board or topic. We don't want to send the notification to people
that have also been mentioned in that post (they'll get a separate
notification). We could build that exception into those
user-locators, but then we couldn't re-use them in other places...
This basically means we couldn't use EchoUserLocator::locateUsersWatchingTitle,
we would have to roll our own that also excludes mentioned users.
Instead, this lets you add 'user-filters' (that functionality
actually exists already, but is not currently exposed), which
lists users to not send the notification to, even though they could
be in a user-locator.
Bug: T125428
Change-Id: Ifa0e2d3283f57624af4c5ec264f9f66223508e83
2016-02-02 13:16:39 +00:00
|
|
|
wfDebugLog( __CLASS__, __FUNCTION__ . ": Invalid $locator returned for $type" );
|
2014-07-29 23:54:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-30 03:18:48 +00:00
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieves an array of User objects to be notified for an EchoEvent.
|
|
|
|
*
|
|
|
|
* @param EchoEvent $event
|
|
|
|
* @return Iterator values are User objects
|
|
|
|
*/
|
|
|
|
public static function getUsersToNotifyForEvent( EchoEvent $event ) {
|
|
|
|
$notify = new EchoFilteredSequentialIterator;
|
Allow certain users to be excluded
Right now, if certain users should be excluded, that would have
to be part of the user-locators already. This is annoying because
it's hard to write "generic" user locators when you want to exclude
just a couple of people in certain cases.
In Flow, for example, we have user-locators for users watching a
board or topic. We don't want to send the notification to people
that have also been mentioned in that post (they'll get a separate
notification). We could build that exception into those
user-locators, but then we couldn't re-use them in other places...
This basically means we couldn't use EchoUserLocator::locateUsersWatchingTitle,
we would have to roll our own that also excludes mentioned users.
Instead, this lets you add 'user-filters' (that functionality
actually exists already, but is not currently exposed), which
lists users to not send the notification to, even though they could
be in a user-locator.
Bug: T125428
Change-Id: Ifa0e2d3283f57624af4c5ec264f9f66223508e83
2016-02-02 13:16:39 +00:00
|
|
|
foreach ( self::evaluateUserCallable( $event, EchoAttributeManager::ATTR_LOCATORS ) as $users ) {
|
2014-07-30 03:18:48 +00:00
|
|
|
$notify->add( $users );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hook for injecting more users.
|
2014-07-29 23:54:00 +00:00
|
|
|
// @deprecated
|
2016-12-05 18:51:07 +00:00
|
|
|
$users = [];
|
|
|
|
Hooks::run( 'EchoGetDefaultNotifiedUsers', [ $event, &$users ] );
|
2019-11-07 19:35:03 +00:00
|
|
|
// @phan-suppress-next-line PhanImpossibleCondition May be set by hook
|
2014-07-30 03:18:48 +00:00
|
|
|
if ( $users ) {
|
|
|
|
$notify->add( $users );
|
2012-04-27 15:14:24 +00:00
|
|
|
}
|
|
|
|
|
Allow certain users to be excluded
Right now, if certain users should be excluded, that would have
to be part of the user-locators already. This is annoying because
it's hard to write "generic" user locators when you want to exclude
just a couple of people in certain cases.
In Flow, for example, we have user-locators for users watching a
board or topic. We don't want to send the notification to people
that have also been mentioned in that post (they'll get a separate
notification). We could build that exception into those
user-locators, but then we couldn't re-use them in other places...
This basically means we couldn't use EchoUserLocator::locateUsersWatchingTitle,
we would have to roll our own that also excludes mentioned users.
Instead, this lets you add 'user-filters' (that functionality
actually exists already, but is not currently exposed), which
lists users to not send the notification to, even though they could
be in a user-locator.
Bug: T125428
Change-Id: Ifa0e2d3283f57624af4c5ec264f9f66223508e83
2016-02-02 13:16:39 +00:00
|
|
|
// Exclude certain users
|
|
|
|
foreach ( self::evaluateUserCallable( $event, EchoAttributeManager::ATTR_FILTERS ) as $users ) {
|
|
|
|
// the result of the callback can be both an iterator or array
|
|
|
|
$users = is_array( $users ) ? $users : iterator_to_array( $users );
|
2016-02-11 13:46:18 +00:00
|
|
|
$notify->addFilter( function ( User $user ) use ( $users ) {
|
|
|
|
// we need to check if $user is in $users, but they're not
|
|
|
|
// guaranteed to be the same object, so I'll compare ids.
|
|
|
|
$userId = $user->getId();
|
|
|
|
$userIds = array_map( function ( User $user ) {
|
|
|
|
return $user->getId();
|
|
|
|
}, $users );
|
|
|
|
return !in_array( $userId, $userIds );
|
Allow certain users to be excluded
Right now, if certain users should be excluded, that would have
to be part of the user-locators already. This is annoying because
it's hard to write "generic" user locators when you want to exclude
just a couple of people in certain cases.
In Flow, for example, we have user-locators for users watching a
board or topic. We don't want to send the notification to people
that have also been mentioned in that post (they'll get a separate
notification). We could build that exception into those
user-locators, but then we couldn't re-use them in other places...
This basically means we couldn't use EchoUserLocator::locateUsersWatchingTitle,
we would have to roll our own that also excludes mentioned users.
Instead, this lets you add 'user-filters' (that functionality
actually exists already, but is not currently exposed), which
lists users to not send the notification to, even though they could
be in a user-locator.
Bug: T125428
Change-Id: Ifa0e2d3283f57624af4c5ec264f9f66223508e83
2016-02-02 13:16:39 +00:00
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2014-07-30 03:18:48 +00:00
|
|
|
// Filter non-User, anon and duplicate users
|
2016-12-05 18:51:07 +00:00
|
|
|
$seen = [];
|
2018-09-30 12:22:35 +00:00
|
|
|
$fname = __METHOD__;
|
|
|
|
$notify->addFilter( function ( $user ) use ( &$seen, $fname ) {
|
2014-07-30 03:18:48 +00:00
|
|
|
if ( !$user instanceof User ) {
|
2018-09-30 12:22:35 +00:00
|
|
|
wfDebugLog( $fname, 'Expected all User instances, received:' .
|
2014-07-30 03:18:48 +00:00
|
|
|
( is_object( $user ) ? get_class( $user ) : gettype( $user ) )
|
|
|
|
);
|
2015-10-01 13:48:52 +00:00
|
|
|
|
2014-07-30 03:18:48 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if ( $user->isAnon() || isset( $seen[$user->getId()] ) ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
$seen[$user->getId()] = true;
|
2015-10-01 13:48:52 +00:00
|
|
|
|
2014-07-30 03:18:48 +00:00
|
|
|
return true;
|
|
|
|
} );
|
|
|
|
|
2018-10-26 23:46:16 +00:00
|
|
|
// Don't notify the person who initiated the event unless the event allows it
|
|
|
|
if ( !$event->canNotifyAgent() && $event->getAgent() ) {
|
2014-07-30 03:18:48 +00:00
|
|
|
$agentId = $event->getAgent()->getId();
|
2015-10-01 13:48:52 +00:00
|
|
|
$notify->addFilter( function ( $user ) use ( $agentId ) {
|
2014-07-30 03:18:48 +00:00
|
|
|
return $user->getId() != $agentId;
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2016-11-10 04:37:10 +00:00
|
|
|
// Apply blacklists and whitelists.
|
|
|
|
$notify->addFilter( function ( $user ) use ( $event ) {
|
2017-05-30 23:42:20 +00:00
|
|
|
$title = $event->getTitle();
|
|
|
|
|
|
|
|
if ( self::isBlacklistedByUser( $event, $user ) &&
|
|
|
|
(
|
|
|
|
$title === null ||
|
|
|
|
!(
|
|
|
|
// Still notify for posts anywhere in
|
|
|
|
// user's talk space
|
|
|
|
$title->getRootText() === $user->getName() &&
|
|
|
|
$title->getNamespace() === NS_USER_TALK
|
|
|
|
)
|
|
|
|
)
|
|
|
|
) {
|
2016-11-10 04:37:10 +00:00
|
|
|
return self::isWhitelistedByUser( $event, $user );
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
} );
|
2012-04-27 15:14:24 +00:00
|
|
|
|
2014-07-30 03:18:48 +00:00
|
|
|
return $notify->getIterator();
|
2012-04-27 15:14:24 +00:00
|
|
|
}
|
|
|
|
|
2014-02-06 23:06:54 +00:00
|
|
|
/**
|
|
|
|
* INTERNAL. Must be public to be callable by the php error handling methods.
|
|
|
|
*
|
2014-07-29 23:54:00 +00:00
|
|
|
* Converts E_RECOVERABLE_ERROR, such as passing null to a method expecting
|
2014-02-06 23:06:54 +00:00
|
|
|
* a non-null object, into exceptions.
|
2017-08-09 15:20:55 +00:00
|
|
|
* @param int $errno
|
|
|
|
* @param string $errstr
|
|
|
|
* @param string $errfile
|
|
|
|
* @param int $errline
|
|
|
|
* @return bool
|
2017-09-24 05:23:47 +00:00
|
|
|
* @throws EchoCatchableFatalErrorException
|
2014-02-06 23:06:54 +00:00
|
|
|
*/
|
|
|
|
public static function formatterErrorHandler( $errno, $errstr, $errfile, $errline ) {
|
|
|
|
if ( $errno !== E_RECOVERABLE_ERROR ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2014-08-05 00:15:14 +00:00
|
|
|
throw new EchoCatchableFatalErrorException( $errno, $errstr, $errfile, $errline );
|
2012-04-27 15:14:24 +00:00
|
|
|
}
|
2012-06-08 05:33:25 +00:00
|
|
|
}
|