Merge "Create and enforce a config setting for max subscriptions per user"

This commit is contained in:
jenkins-bot 2020-08-13 17:03:05 +00:00 committed by Gerrit Code Review
commit aa1f1801e4
9 changed files with 90 additions and 10 deletions

View file

@ -43,7 +43,10 @@ return [
$database $database
); );
return new SubscriptionManager( $dbw, $dbr, $centralIdLookup, $pushProviderStore ); $maxSubscriptionsPerUser = $echoConfig->get( 'EchoPushMaxSubscriptionsPerUser' );
return new SubscriptionManager( $dbw, $dbr, $centralIdLookup, $pushProviderStore,
$maxSubscriptionsPerUser );
} }
]; ];

View file

@ -1013,6 +1013,10 @@
"EchoPushServiceBaseUrl": { "EchoPushServiceBaseUrl": {
"value": false, "value": false,
"description": "Request endpoint URL for the push notification service" "description": "Request endpoint URL for the push notification service"
},
"EchoPushMaxSubscriptionsPerUser": {
"value": 0,
"description": "Maximum number of push subscriptions that may be stored in the DB at any given time for a single central user ID."
} }
}, },
"attributes": { "attributes": {

View file

@ -79,5 +79,6 @@
"apiwarn-echo-deprecation-html": "<kbd>notformat=html</kbd> has been deprecated and will be removed soon. Use <kbd>notformat=special</kbd> instead.", "apiwarn-echo-deprecation-html": "<kbd>notformat=html</kbd> has been deprecated and will be removed soon. Use <kbd>notformat=special</kbd> instead.",
"apierror-echo-event-creation-failed": "Could not create Echo event", "apierror-echo-event-creation-failed": "Could not create Echo event",
"apierror-echo-push-token-exists": "The provided token already exists in the database.", "apierror-echo-push-token-exists": "The provided token already exists in the database.",
"apierror-echo-push-token-not-found": "The provided token was not found in the database." "apierror-echo-push-token-not-found": "The provided token was not found in the database.",
"apierror-echo-push-too-many-subscriptions": "The current user has already registered the maximum allowed number of push subscriptions ($1)."
} }

View file

@ -81,5 +81,6 @@
"apiwarn-echo-deprecation-html": "{{doc-apierror}}", "apiwarn-echo-deprecation-html": "{{doc-apierror}}",
"apierror-echo-event-creation-failed": "{{doc-apierror}}", "apierror-echo-event-creation-failed": "{{doc-apierror}}",
"apierror-echo-push-token-exists": "{{doc-apierror}}", "apierror-echo-push-token-exists": "{{doc-apierror}}",
"apierror-echo-push-token-not-found": "{{doc-apierror}}" "apierror-echo-push-token-not-found": "{{doc-apierror}}",
"apierror-echo-push-too-many-subscriptions": "{{doc-apierror}}"
} }

View file

@ -6,6 +6,7 @@ use CentralIdLookup;
use EchoAbstractMapper; use EchoAbstractMapper;
use IDatabase; use IDatabase;
use MediaWiki\Storage\NameTableStore; use MediaWiki\Storage\NameTableStore;
use OverflowException;
use User; use User;
use Wikimedia\Rdbms\DBError; use Wikimedia\Rdbms\DBError;
@ -23,23 +24,37 @@ class SubscriptionManager extends EchoAbstractMapper {
/** @var NameTableStore */ /** @var NameTableStore */
private $pushProviderStore; private $pushProviderStore;
/** @var int */
private $maxSubscriptionsPerUser;
/** /**
* @param IDatabase $dbw primary DB connection (for writes) * @param IDatabase $dbw primary DB connection (for writes)
* @param IDatabase $dbr replica DB connection (for reads) * @param IDatabase $dbr replica DB connection (for reads)
* @param CentralIdLookup $centralIdLookup * @param CentralIdLookup $centralIdLookup
* @param NameTableStore $pushProviderStore * @param NameTableStore $pushProviderStore
* @param int $maxSubscriptionsPerUser
*/ */
public function __construct( public function __construct(
IDatabase $dbw, IDatabase $dbw,
IDatabase $dbr, IDatabase $dbr,
CentralIdLookup $centralIdLookup, CentralIdLookup $centralIdLookup,
NameTableStore $pushProviderStore NameTableStore $pushProviderStore,
int $maxSubscriptionsPerUser
) { ) {
parent::__construct(); parent::__construct();
$this->dbw = $dbw; $this->dbw = $dbw;
$this->dbr = $dbr; $this->dbr = $dbr;
$this->centralIdLookup = $centralIdLookup; $this->centralIdLookup = $centralIdLookup;
$this->pushProviderStore = $pushProviderStore; $this->pushProviderStore = $pushProviderStore;
$this->maxSubscriptionsPerUser = $maxSubscriptionsPerUser;
}
/**
* Get the configured maximum number of stored subscriptions per user.
* @return int
*/
public function getMaxSubscriptionsPerUser(): int {
return $this->maxSubscriptionsPerUser;
} }
/** /**
@ -48,12 +63,17 @@ class SubscriptionManager extends EchoAbstractMapper {
* @param string $provider Provider name string (validated by presence in the PARAM_TYPE array) * @param string $provider Provider name string (validated by presence in the PARAM_TYPE array)
* @param string $token Subscriber token provided by the push provider * @param string $token Subscriber token provided by the push provider
* @return bool true if the subscription was created; false if the token already exists * @return bool true if the subscription was created; false if the token already exists
* @throws OverflowException if the user already has >= the configured max subscriptions
*/ */
public function create( User $user, string $provider, string $token ): bool { public function create( User $user, string $provider, string $token ): bool {
$centralId = $this->getCentralId( $user );
if ( $this->userHasMaxAllowedSubscriptions( $centralId ) ) {
throw new OverflowException( 'Max subscriptions exceeded' );
}
$this->dbw->insert( $this->dbw->insert(
'echo_push_subscription', 'echo_push_subscription',
[ [
'eps_user' => $this->getCentralId( $user ), 'eps_user' => $centralId,
'eps_provider' => $this->pushProviderStore->acquireId( $provider ), 'eps_provider' => $this->pushProviderStore->acquireId( $provider ),
'eps_token' => $token, 'eps_token' => $token,
'eps_token_sha256' => hash( 'sha256', $token ), 'eps_token_sha256' => hash( 'sha256', $token ),
@ -66,7 +86,7 @@ class SubscriptionManager extends EchoAbstractMapper {
} }
/** /**
* Get all registered subscriptions for a user (by central ID). * Get full data for all registered subscriptions for a user (by central ID).
* @param int $centralId * @param int $centralId
* @return array array of Subscription objects * @return array array of Subscription objects
*/ */
@ -108,6 +128,29 @@ class SubscriptionManager extends EchoAbstractMapper {
return $this->dbw->affectedRows(); return $this->dbw->affectedRows();
} }
/**
* Get count of all registered subscriptions for a user (by central ID).
* @param int $centralId
* @return int
*/
private function getSubscriptionCountForUser( int $centralId ) {
return $this->dbr->selectRowCount(
'echo_push_subscription',
'eps_id',
[ 'eps_user' => $centralId ],
__METHOD__
);
}
/**
* Returns true if the central user has >= the configured maximum push subscriptions in the DB
* @param int $centralId
* @return bool
*/
private function userHasMaxAllowedSubscriptions( int $centralId ): bool {
return $this->getSubscriptionCountForUser( $centralId ) >= $this->maxSubscriptionsPerUser;
}
/** /**
* Get the user's central ID. * Get the user's central ID.
* @param User $user * @param User $user

View file

@ -6,6 +6,7 @@ use ApiBase;
use ApiMain; use ApiMain;
use EchoPush\SubscriptionManager; use EchoPush\SubscriptionManager;
use EchoServices; use EchoServices;
use OverflowException;
use Wikimedia\ParamValidator\ParamValidator; use Wikimedia\ParamValidator\ParamValidator;
class ApiEchoPushSubscriptionsCreate extends ApiBase { class ApiEchoPushSubscriptionsCreate extends ApiBase {
@ -58,7 +59,13 @@ class ApiEchoPushSubscriptionsCreate extends ApiBase {
public function execute(): void { public function execute(): void {
$provider = $this->getParameter( 'provider' ); $provider = $this->getParameter( 'provider' );
$token = $this->getParameter( 'providertoken' ); $token = $this->getParameter( 'providertoken' );
$success = $this->subscriptionManager->create( $this->getUser(), $provider, $token ); try {
$success = $this->subscriptionManager->create( $this->getUser(), $provider, $token );
} catch ( OverflowException $e ) {
$maxSubscriptionsPerUser = $this->subscriptionManager->getMaxSubscriptionsPerUser();
$this->dieWithError( [ 'apierror-echo-push-too-many-subscriptions',
$maxSubscriptionsPerUser ] );
}
if ( !$success ) { if ( !$success ) {
$this->dieWithError( 'apierror-echo-push-token-exists' ); $this->dieWithError( 'apierror-echo-push-token-exists' );
} }

View file

@ -13,7 +13,10 @@ class ApiEchoPushSubscriptionsCreateTest extends ApiTestCase {
public function setUp(): void { public function setUp(): void {
parent::setUp(); parent::setUp();
$this->setMwGlobals( 'wgEchoEnablePush', true ); $this->setMwGlobals( [
'wgEchoEnablePush' => true,
'wgEchoPushMaxSubscriptionsPerUser' => 2
] );
$this->tablesUsed[] = 'echo_push_subscription'; $this->tablesUsed[] = 'echo_push_subscription';
$this->tablesUsed[] = 'echo_push_provider'; $this->tablesUsed[] = 'echo_push_provider';
$this->user = $this->getTestUser()->getUser(); $this->user = $this->getTestUser()->getUser();
@ -21,6 +24,7 @@ class ApiEchoPushSubscriptionsCreateTest extends ApiTestCase {
} }
public function testApiCreateSubscription(): void { public function testApiCreateSubscription(): void {
// Before max subscriptions reached
$params = [ $params = [
'action' => 'echopushsubscriptions', 'action' => 'echopushsubscriptions',
'command' => 'create', 'command' => 'create',
@ -29,6 +33,11 @@ class ApiEchoPushSubscriptionsCreateTest extends ApiTestCase {
]; ];
$result = $this->doApiRequestWithToken( $params, null, $this->user ); $result = $this->doApiRequestWithToken( $params, null, $this->user );
$this->assertEquals( 'Success', $result[0]['create']['result'] ); $this->assertEquals( 'Success', $result[0]['create']['result'] );
// After max subscriptions reached
$params['providertoken'] = 'DEF456';
$this->expectException( ApiUsageException::class );
$this->doApiRequestWithToken( $params, null, $this->user );
} }
public function testApiCreateSubscriptionTokenExists(): void { public function testApiCreateSubscriptionTokenExists(): void {

View file

@ -13,7 +13,10 @@ class ApiEchoPushSubscriptionsDeleteTest extends ApiTestCase {
public function setUp(): void { public function setUp(): void {
parent::setUp(); parent::setUp();
$this->setMwGlobals( 'wgEchoEnablePush', true ); $this->setMwGlobals( [
'wgEchoEnablePush' => true,
'wgEchoPushMaxSubscriptionsPerUser' => 1
] );
$this->tablesUsed[] = 'echo_push_subscription'; $this->tablesUsed[] = 'echo_push_subscription';
$this->tablesUsed[] = 'echo_push_provider'; $this->tablesUsed[] = 'echo_push_provider';
$this->user = $this->getTestUser()->getUser(); $this->user = $this->getTestUser()->getUser();

View file

@ -1,5 +1,7 @@
<?php <?php
use Wikimedia\TestingAccessWrapper;
/** /**
* @group Database * @group Database
* @covers \EchoPush\SubscriptionManager * @covers \EchoPush\SubscriptionManager
@ -10,18 +12,25 @@ class SubscriptionManagerTest extends MediaWikiIntegrationTestCase {
parent::setUp(); parent::setUp();
$this->tablesUsed[] = 'echo_push_subscription'; $this->tablesUsed[] = 'echo_push_subscription';
$this->tablesUsed[] = 'echo_push_provider'; $this->tablesUsed[] = 'echo_push_provider';
$this->setMwGlobals( 'wgEchoPushMaxSubscriptionsPerUser', 1 );
} }
public function testManagePushSubscriptions(): void { public function testManagePushSubscriptions(): void {
$subscriptionManager = EchoServices::getInstance()->getPushSubscriptionManager(); $subscriptionManagerBase = EchoServices::getInstance()->getPushSubscriptionManager();
$subscriptionManager = TestingAccessWrapper::newFromObject( $subscriptionManagerBase );
$user = $this->getTestUser()->getUser(); $user = $this->getTestUser()->getUser();
$centralId = CentralIdLookup::factory()->centralIdFromLocalUser( $user ); $centralId = CentralIdLookup::factory()->centralIdFromLocalUser( $user );
$subscriptionManager->create( $user, 'test', 'ABC123' ); $subscriptionManager->create( $user, 'test', 'ABC123' );
$subscriptions = $subscriptionManager->getSubscriptionsForUser( $centralId ); $subscriptions = $subscriptionManager->getSubscriptionsForUser( $centralId );
$this->assertCount( 1, $subscriptions ); $this->assertCount( 1, $subscriptions );
$this->assertTrue( $subscriptionManager->userHasMaxAllowedSubscriptions( $centralId ) );
$subscriptionManager->delete( $user, 'ABC123' ); $subscriptionManager->delete( $user, 'ABC123' );
$subscriptions = $subscriptionManager->getSubscriptionsForUser( $centralId ); $subscriptions = $subscriptionManager->getSubscriptionsForUser( $centralId );
$this->assertCount( 0, $subscriptions ); $this->assertCount( 0, $subscriptions );
$this->assertFalse( $subscriptionManager->userHasMaxAllowedSubscriptions( $centralId ) );
} }
} }