mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Echo
synced 2024-11-30 18:45:07 +00:00
Merge "Notification agent whitelist and blacklist"
This commit is contained in:
commit
3b5a2f0d14
18
Echo.php
18
Echo.php
|
@ -86,6 +86,13 @@ $wgAutoloadClasses['MWEchoBackend'] = $dir . 'includes/EchoBackend.php';
|
||||||
$wgAutoloadClasses['MWDbEchoBackend'] = $dir . 'includes/DbEchoBackend.php';
|
$wgAutoloadClasses['MWDbEchoBackend'] = $dir . 'includes/DbEchoBackend.php';
|
||||||
$wgAutoloadClasses['MWEchoDbFactory'] = $dir . 'includes/EchoDbFactory.php';
|
$wgAutoloadClasses['MWEchoDbFactory'] = $dir . 'includes/EchoDbFactory.php';
|
||||||
|
|
||||||
|
// Whitelist and Blacklist
|
||||||
|
$wgAutoloadClasses['EchoContainmentList'] = $dir . 'includes/ContainmentSet.php';
|
||||||
|
$wgAutoloadClasses['EchoContainmentSet'] = $dir . 'includes/ContainmentSet.php';
|
||||||
|
$wgAutoloadClasses['EchoArrayList'] = $dir . 'includes/ContainmentSet.php';
|
||||||
|
$wgAutoloadClasses['EchoOnWikiList'] = $dir . 'includes/ContainmentSet.php';
|
||||||
|
$wgAutoloadClasses['EchoCachedList'] = $dir . 'includes/ContainmentSet.php';
|
||||||
|
|
||||||
// Housekeeping hooks
|
// Housekeeping hooks
|
||||||
$wgHooks['LoadExtensionSchemaUpdates'][] = 'EchoHooks::getSchemaUpdates';
|
$wgHooks['LoadExtensionSchemaUpdates'][] = 'EchoHooks::getSchemaUpdates';
|
||||||
$wgHooks['GetPreferences'][] = 'EchoHooks::getPreferences';
|
$wgHooks['GetPreferences'][] = 'EchoHooks::getPreferences';
|
||||||
|
@ -274,6 +281,17 @@ $wgEchoNotifiers = array(
|
||||||
'email' => array( 'EchoNotifier', 'notifyWithEmail' ),
|
'email' => array( 'EchoNotifier', 'notifyWithEmail' ),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// List of usernames that will not trigger notification creation. This is initially
|
||||||
|
// for bots that perform automated edits that are not important enough to regularly
|
||||||
|
// spam people with notifications. Set to empty array when not in use.
|
||||||
|
$wgEchoAgentBlacklist = array();
|
||||||
|
|
||||||
|
// Page location of community maintained blacklist within NS_MEDIAWIKI. Set to null to disable.
|
||||||
|
$wgEchoOnWikiBlacklist = 'Echo-blacklist';
|
||||||
|
|
||||||
|
// sprintf format of per-user notification agent whitelists. Set to null to disable.
|
||||||
|
$wgEchoPerUserWhitelistFormat = '%s/Echo-whitelist';
|
||||||
|
|
||||||
// Define the categories that notifications can belong to. Categories can be
|
// Define the categories that notifications can belong to. Categories can be
|
||||||
// assigned the following parameters: priority, nodismiss, and usergroups. All
|
// assigned the following parameters: priority, nodismiss, and usergroups. All
|
||||||
// parameters are optional.
|
// parameters are optional.
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
class EchoNotificationController {
|
class EchoNotificationController {
|
||||||
|
static protected $blacklist;
|
||||||
|
static protected $userWhitelist;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves number of unread notifications that a user has, would return
|
* Retrieves number of unread notifications that a user has, would return
|
||||||
* $wgEchoMaxNotificationCount + 1 at most
|
* $wgEchoMaxNotificationCount + 1 at most
|
||||||
|
@ -253,11 +256,15 @@ class EchoNotificationController {
|
||||||
|
|
||||||
$users = self::getUsersToNotifyForEvent( $event );
|
$users = self::getUsersToNotifyForEvent( $event );
|
||||||
|
|
||||||
|
$blacklisted = self::isBlacklisted( $event );
|
||||||
foreach ( $users as $user ) {
|
foreach ( $users as $user ) {
|
||||||
// Notification should not be sent to anonymous user
|
// Notification should not be sent to anonymous user
|
||||||
if ( $user->isAnon() ) {
|
if ( $user->isAnon() ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if ( $blacklisted && !self::isWhitelistedByUser( $event, $user ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
wfRunHooks( 'EchoGetNotificationTypes', array( $user, $event, &$notifyTypes ) );
|
wfRunHooks( 'EchoGetNotificationTypes', array( $user, $event, &$notifyTypes ) );
|
||||||
|
|
||||||
|
@ -268,6 +275,71 @@ class EchoNotificationController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements blacklist per active wiki expected to be initialized
|
||||||
|
* from InitializeSettings.php
|
||||||
|
*
|
||||||
|
* @param $event EchoEvent The event to test for exclusion via global blacklist
|
||||||
|
* @return boolean True when the event agent is in the global blacklist
|
||||||
|
*/
|
||||||
|
protected static function isBlacklisted( EchoEvent $event ) {
|
||||||
|
if ( !$event->getAgent() ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( self::$blacklist === null ) {
|
||||||
|
global $wgEchoAgentBlacklist, $wgEchoOnWikiBlacklist,
|
||||||
|
$wgMemc;
|
||||||
|
|
||||||
|
self::$blacklist = new EchoContainmentSet;
|
||||||
|
self::$blacklist->addArray( $wgEchoAgentBlacklist );
|
||||||
|
if ( $wgEchoOnWikiBlacklist !== null ) {
|
||||||
|
self::$blacklist->addOnWiki(
|
||||||
|
NS_MEDIAWIKI,
|
||||||
|
$wgEchoOnWikiBlacklist,
|
||||||
|
$wgMemc,
|
||||||
|
wfMemcKey( "echo_on_wiki_blacklist")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$blacklist->contains( $event->getAgent()->getName() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements per-user whitelist sourced from a user wiki page
|
||||||
|
*
|
||||||
|
* @param $event EchoEvent The event to test for inclusion in whitelist
|
||||||
|
* @param $user User The user that owns the whitelist
|
||||||
|
* @return boolean True when the event agent is in the user whitelist
|
||||||
|
*/
|
||||||
|
protected static function isWhitelistedByUser( EchoEvent $event, User $user ) {
|
||||||
|
global $wgEchoPerUserWhitelistFormat, $wgMemc;
|
||||||
|
|
||||||
|
|
||||||
|
if ( $wgEchoPerUserWhitelistFormat === null || !$event->getAgent() ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = $user->getID();
|
||||||
|
if ( $userId === 0 ) {
|
||||||
|
return false; // anonymous user
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !isset( self::$userWhitelist[$userId] ) ) {
|
||||||
|
self::$userWhitelist[$userId] = new EchoContainmentSet;
|
||||||
|
self::$userWhitelist[$userId]->addOnWiki(
|
||||||
|
NS_USER,
|
||||||
|
sprintf( $wgEchoPerUserWhitelistFormat, $user->getName() ),
|
||||||
|
$wgMemc,
|
||||||
|
wfMemcKey( "echo_on_wiki_whitelist_" . $userId )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$userWhitelist[$userId]
|
||||||
|
->contains( $event->getAgent()->getName() );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes a single notification for an EchoEvent
|
* Processes a single notification for an EchoEvent
|
||||||
*
|
*
|
||||||
|
|
240
includes/ContainmentSet.php
Normal file
240
includes/ContainmentSet.php
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface providing list of contained values and an optional cache key to go along with it.
|
||||||
|
*/
|
||||||
|
interface EchoContainmentList {
|
||||||
|
/**
|
||||||
|
* @return array The values contained within this list.
|
||||||
|
*/
|
||||||
|
public function getValues();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string A string suitable for appending to the cache key prefix to facilitate
|
||||||
|
* cache busting when the underlying data changes, or a blank string if
|
||||||
|
* not relevant.
|
||||||
|
*/
|
||||||
|
public function getCacheKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilizes EchoContainmentList interface to provide a fluent interface to whitelist/blacklist
|
||||||
|
* from multiple sources like global variables, wiki pages, etc.
|
||||||
|
*
|
||||||
|
* Initialize:
|
||||||
|
* $set = new EchoContainmentSet;
|
||||||
|
* $set->addArray( $wgSomeGlobalParameter );
|
||||||
|
* $set->addOnWiki( NS_USER, 'Foo/bar-baz', $wgMemc, 'some_user_specific_cache_key' );
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* if ( $set->contains( 'SomeUser' ) ) {
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
class EchoContainmentSet {
|
||||||
|
/**
|
||||||
|
* @var $lists array of EchoContainmentList objects
|
||||||
|
*/
|
||||||
|
protected $lists = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an EchoContainmentList to the set of lists checked by self::contains()
|
||||||
|
*
|
||||||
|
* @param $list EchoContainmentList
|
||||||
|
*/
|
||||||
|
public function add( EchoContainmentList $list ) {
|
||||||
|
$this->lists[] = $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a php array to the set of lists checked by self::contains()
|
||||||
|
*
|
||||||
|
* @param $list array
|
||||||
|
*/
|
||||||
|
public function addArray( array $list ) {
|
||||||
|
$this->add( new EchoArrayList( $list ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a list from a wiki page to the set of lists checked by self::contains(). Data
|
||||||
|
* from wiki pages is cached via the BagOStuff. Caching is disabled when passing a null
|
||||||
|
* $cache object.
|
||||||
|
*
|
||||||
|
* @param $namespace integer An NS_* constant representing the mediawiki namespace of the page containing the list.
|
||||||
|
* @param $title string The title of the page containing the list.
|
||||||
|
* @param $cache BagOStuff An object to cache the page with or null for no cache.
|
||||||
|
* @param $cacheKeyPrefix string A prefix to be combined with the pages latest revision id and used as a cache key.
|
||||||
|
*/
|
||||||
|
public function addOnWiki( $namespace, $title, BagOStuff $cache = null, $cacheKeyPrefix = '' ) {
|
||||||
|
$list = new EchoOnWikiList( $namespace, $title );
|
||||||
|
if ( $cache ) {
|
||||||
|
if ( $cacheKeyPrefix === '' ) {
|
||||||
|
throw new MWException( 'Cache requires providing a cache key prefix.' );
|
||||||
|
}
|
||||||
|
$list = new EchoCachedList( $cache, $cacheKeyPrefix, $list );
|
||||||
|
}
|
||||||
|
$this->add( $list );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the wrapped lists for existence of $value
|
||||||
|
*
|
||||||
|
* @param $value mixed The value to look for
|
||||||
|
* @return boolean True when the set contains the provided value
|
||||||
|
*/
|
||||||
|
public function contains( $value ) {
|
||||||
|
foreach ( $this->lists as $list ) {
|
||||||
|
if ( array_search( $value, $list->getValues() ) !== false ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the EchoContainmentList interface for php arrays. Possible source
|
||||||
|
* of arrays includes $wg* global variables initialized from extensions or global
|
||||||
|
* wiki config.
|
||||||
|
*/
|
||||||
|
class EchoArrayList implements EchoContainmentList {
|
||||||
|
/**
|
||||||
|
* @param $list array
|
||||||
|
*/
|
||||||
|
protected $list;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $list array
|
||||||
|
*/
|
||||||
|
public function __construct( array $list ) {
|
||||||
|
$this->list = $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getValues() {
|
||||||
|
return $this->list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getCacheKey() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements EchoContainmentList interface for sourcing a list of items from a wiki
|
||||||
|
* page. Uses the pages latest revision ID as cache key.
|
||||||
|
*/
|
||||||
|
class EchoOnWikiList implements EchoContainmentList {
|
||||||
|
/**
|
||||||
|
* @var $title Title|null A title object representing the page to source the list from,
|
||||||
|
* or null if the page does not exist.
|
||||||
|
*/
|
||||||
|
protected $title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $titleNs integer An NS_* constant representing the mediawiki namespace of the page
|
||||||
|
* @param $titleString string String portion of the wiki page title
|
||||||
|
*/
|
||||||
|
public function __construct( $titleNs, $titleString ) {
|
||||||
|
$title = Title::newFromText( $titleString, $titleNs );
|
||||||
|
if ( $title !== null && $title->getArticleId() ) {
|
||||||
|
$this->title = $title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getValues() {
|
||||||
|
if ( !$this->title ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$article = WikiPage::newFromID( $this->title->getArticleId() );
|
||||||
|
if ( $article === null || !$article->exists() ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter( array_map( 'trim', explode( "\n", $article->getText() ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getCacheKey() {
|
||||||
|
if ( !$this->title ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->title->getLatestRevID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches an EchoContainmentList within a BagOStuff(memcache, etc) to prevent needing
|
||||||
|
* to load the nested list from a potentially slow source (mysql, etc).
|
||||||
|
*/
|
||||||
|
class EchoCachedList implements EchoContainmentList {
|
||||||
|
const ONE_WEEK = 4233600;
|
||||||
|
const ONE_DAY = 86400;
|
||||||
|
|
||||||
|
protected $cache;
|
||||||
|
protected $partialCacheKey;
|
||||||
|
protected $nestedList;
|
||||||
|
protected $timeout;
|
||||||
|
private $result;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $cache BagOStuff Bag to stored cached data in.
|
||||||
|
* @param $partialCacheKey string Partial cache key, $nestedList->getCacheKey() will be appended to this
|
||||||
|
* to construct the cache key used.
|
||||||
|
* @param $nestedList EchoContainmentList The nested EchoContainmentList to cache the result of.
|
||||||
|
* @param $timeout integer How long in seconds to cache the nested list, defaults to 1 week.
|
||||||
|
*/
|
||||||
|
public function __construct( BagOStuff $cache, $partialCacheKey, EchoContainmentList $nestedList, $timeout = self::ONE_WEEK ) {
|
||||||
|
$this->cache = $cache;
|
||||||
|
$this->partialCacheKey = $partialCacheKey;
|
||||||
|
$this->nestedList = $nestedList;
|
||||||
|
$this->timeout = $timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getValues() {
|
||||||
|
if ( $this->result ) {
|
||||||
|
return $this->result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheKey = $this->getCacheKey();
|
||||||
|
$fetched = $this->cache->get( $cacheKey );
|
||||||
|
if ( is_array( $fetched ) ) {
|
||||||
|
return $this->result = $fetched;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->nestedList->getValues();
|
||||||
|
if ( !is_array( $result ) ) {
|
||||||
|
throw new MWException( sprintf(
|
||||||
|
"Expected array but received '%s' from '%s::getValues'",
|
||||||
|
is_object( $result ) ? get_class( $result ) : gettype( $result ),
|
||||||
|
get_class( $this->nestedList )
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
$this->cache->set( $cacheKey, $result, $this->timeout );
|
||||||
|
|
||||||
|
return $this->result = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public function getCacheKey() {
|
||||||
|
return $this->partialCacheKey . '_' . $this->nestedList->getCacheKey();
|
||||||
|
}
|
||||||
|
}
|
70
tests/ContainmentSetTest.php
Normal file
70
tests/ContainmentSetTest.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class ContainmentSetTest extends MediaWikiTestCase {
|
||||||
|
|
||||||
|
public function testGenericContains() {
|
||||||
|
$list = new EchoContainmentSet;
|
||||||
|
|
||||||
|
$list->addArray( array( 'foo', 'bar' ) );
|
||||||
|
$this->assertTrue( $list->contains( 'foo' ) );
|
||||||
|
$this->assertTrue( $list->contains( 'bar' ) );
|
||||||
|
$this->assertFalse( $list->contains( 'whammo' ) );
|
||||||
|
|
||||||
|
$list->addArray( array( 'whammo' ) );
|
||||||
|
$this->assertTrue( $list->contains( 'whammo' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCachedListInnerListIsOnlyCalledOnce() {
|
||||||
|
|
||||||
|
// the global $wgMemc during tests is an EmptyBagOStuff, so it
|
||||||
|
// wont do anything. We use a HashBagOStuff to get more like a real
|
||||||
|
// client
|
||||||
|
$innerCache = new HashBagOStuff;
|
||||||
|
|
||||||
|
$inner = array( 'bing', 'bang' );
|
||||||
|
// We use a mock instead of the real thing for the $this->once() assertion
|
||||||
|
// verifying that the cache doesn't just keep asking the inner object
|
||||||
|
$list = $this->getMockBuilder('EchoArrayList')
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock();
|
||||||
|
$list->expects( $this->once() )
|
||||||
|
->method( 'getValues' )
|
||||||
|
->will( $this->returnValue( $inner ) );
|
||||||
|
|
||||||
|
$cached = new EchoCachedList( $innerCache, 'test_key', $list );
|
||||||
|
|
||||||
|
// First run through should hit the main list, and save to innerCache
|
||||||
|
$this->assertEquals( $inner, $cached->getValues() );
|
||||||
|
$this->assertEquals( $inner, $cached->getValues() );
|
||||||
|
|
||||||
|
// Reinitialize to get a fresh instance that will pull directly from
|
||||||
|
// innerCache without hitting the $list
|
||||||
|
$freshCached = new EchoCachedList( $innerCache, 'test_key', $list );
|
||||||
|
$this->assertEquals( $inner, $freshCached->getValues() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Database
|
||||||
|
*/
|
||||||
|
public function testOnWikiList() {
|
||||||
|
$this->editPage( 'User:Foo/Bar-baz', "abc\ndef\r\nghi\n\n\n" );
|
||||||
|
|
||||||
|
$list = new EchoOnWikiList( NS_USER, "Foo/Bar-baz" );
|
||||||
|
$this->assertEquals(
|
||||||
|
array( 'abc', 'def', 'ghi' ),
|
||||||
|
$list->getValues()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOnWikiListNonExistant() {
|
||||||
|
$list = new EchoOnWikiList( NS_USER, "Some_Non_Existant_Page" );
|
||||||
|
$this->assertEquals( array(), $list->getValues() );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) {
|
||||||
|
$title = Title::newFromText( $pageName, $defaultNs );
|
||||||
|
$page = WikiPage::factory( $title );
|
||||||
|
|
||||||
|
return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary );
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue