Skip to content

Commit 94a30e3

Browse files
committed
add anonymous users support
1 parent eed7f16 commit 94a30e3

13 files changed

+202
-29
lines changed

src/Action/RegisterSubscriptionAction.php

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use BenTools\WebPushBundle\Model\Subscription\UserSubscriptionManagerRegistry;
66
use Symfony\Component\HttpFoundation\Request;
77
use Symfony\Component\HttpFoundation\Response;
8-
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
98
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
109
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
1110
use Symfony\Component\Security\Core\User\UserInterface;
@@ -16,13 +15,17 @@ final class RegisterSubscriptionAction
1615
* @var UserSubscriptionManagerRegistry
1716
*/
1817
private $registry;
18+
private $deleteAnonymousSubscriptionWithTheSameEndpoint;
1919

2020
/**
2121
* RegisterSubscriptionAction constructor.
22+
* @param UserSubscriptionManagerRegistry $registry
23+
* @param $deleteAnonymousSubscriptionWithTheSameEndpoint
2224
*/
23-
public function __construct(UserSubscriptionManagerRegistry $registry)
25+
public function __construct(UserSubscriptionManagerRegistry $registry, $deleteAnonymousSubscriptionWithTheSameEndpoint)
2426
{
2527
$this->registry = $registry;
28+
$this->deleteAnonymousSubscriptionWithTheSameEndpoint = $deleteAnonymousSubscriptionWithTheSameEndpoint;
2629
}
2730

2831
/**
@@ -31,7 +34,7 @@ public function __construct(UserSubscriptionManagerRegistry $registry)
3134
* @throws \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException
3235
* @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
3336
*/
34-
private function subscribe(UserInterface $user, string $subscriptionHash, array $subscription, array $options = [])
37+
private function subscribe(?UserInterface $user, string $subscriptionHash, array $subscription, array $options = [])
3538
{
3639
$manager = $this->registry->getManager($user);
3740
$userSubscription = $manager->getUserSubscription($user, $subscriptionHash)
@@ -43,7 +46,7 @@ private function subscribe(UserInterface $user, string $subscriptionHash, array
4346
* @throws BadRequestHttpException
4447
* @throws \RuntimeException
4548
*/
46-
private function unsubscribe(UserInterface $user, string $subscriptionHash)
49+
private function unsubscribe(?UserInterface $user, string $subscriptionHash)
4750
{
4851
$manager = $this->registry->getManager($user);
4952
$subscription = $manager->getUserSubscription($user, $subscriptionHash);
@@ -53,12 +56,8 @@ private function unsubscribe(UserInterface $user, string $subscriptionHash)
5356
$manager->delete($subscription);
5457
}
5558

56-
public function __invoke(Request $request, UserInterface $user = null): Response
59+
public function __invoke(Request $request, ?UserInterface $user = null): Response
5760
{
58-
if (null === $user) {
59-
throw new AccessDeniedHttpException('Not authenticated.');
60-
}
61-
6261
if (!in_array($request->getMethod(), ['POST', 'DELETE'])) {
6362
throw new MethodNotAllowedHttpException(['POST', 'DELETE']);
6463
}
@@ -75,6 +74,14 @@ public function __invoke(Request $request, UserInterface $user = null): Response
7574
throw new BadRequestHttpException('Invalid subscription object.');
7675
}
7776

77+
if (Request::METHOD_POST === $request->getMethod() && null !== $user && true === $this->deleteAnonymousSubscriptionWithTheSameEndpoint) {
78+
$manager = $this->registry->getManager(null);
79+
$anonymousSubscriptionHash = $manager->hash($subscription['endpoint'], null);
80+
$anonymousSubscriptions = $manager->findByHash($anonymousSubscriptionHash);
81+
foreach ($anonymousSubscriptions as $anonymousSubscription) {
82+
$manager->delete($anonymousSubscription);
83+
}
84+
}
7885
$manager = $this->registry->getManager($user);
7986
$subscriptionHash = $manager->hash($subscription['endpoint'], $user);
8087

src/DependencyInjection/Configuration.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public function getConfigTreeBuilder()
2020

2121
$rootNode
2222
->children()
23-
23+
->scalarNode('delete_anonymous_subscription_with_the_same_endpoint')
24+
->end()
2425
->arrayNode('settings')
2526
->children()
2627
->scalarNode('subject')

src/DependencyInjection/WebPushCompilerPass.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ public function process(ContainerBuilder $container)
1414
$registry = $container->getDefinition(UserSubscriptionManagerRegistry::class);
1515
$taggedSubscriptionManagers = $container->findTaggedServiceIds('bentools_webpush.subscription_manager');
1616
foreach ($taggedSubscriptionManagers as $id => $tag) {
17-
if (!isset($tag[0]['user_class'])) {
18-
throw new \InvalidArgumentException(sprintf('Missing user_class attribute in tag for service %s', $id));
17+
if (!isset($tag[0]['user_class']) && !isset($tag[0][UserSubscriptionManagerRegistry::ANONYMOUS])) {
18+
throw new \InvalidArgumentException(sprintf('Missing user_class & anonymous attributes in tag for service %s', $id));
1919
}
20-
$registry->addMethodCall('register', [$tag[0]['user_class'], new Reference($id)]);
20+
$registry->addMethodCall('register', [$tag[0]['user_class'] ?? UserSubscriptionManagerRegistry::ANONYMOUS, new Reference($id)]);
2121
}
2222
}
2323
}

src/DependencyInjection/WebPushExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public function load(array $configs, ContainerBuilder $container)
2222
$configuration = new Configuration();
2323

2424
$config = $this->processConfiguration($configuration, $configs);
25+
$container->setParameter('bentools_webpush.delete_anonymous_subscription_with_the_same_endpoint', $config['delete_anonymous_subscription_with_the_same_endpoint'] ?? false);
2526
$container->setParameter('bentools_webpush.vapid_subject', $config['settings']['subject'] ?? $container->getParameter('router.request_context.host'));
2627
$container->setParameter('bentools_webpush.vapid_public_key', $config['settings']['public_key'] ?? null);
2728
$container->setParameter('bentools_webpush.vapid_private_key', $config['settings']['private_key'] ?? null);

src/Model/Subscription/UserSubscriptionInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ interface UserSubscriptionInterface
99
/**
1010
* Return the user associated to this subscription.
1111
*/
12-
public function getUser(): UserInterface;
12+
public function getUser(): ?UserInterface;
1313

1414
/**
1515
* Return the hash of this subscription. Can be a fingerprint or a cookie.

src/Model/Subscription/UserSubscriptionManagerInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ interface UserSubscriptionManagerInterface
99
/**
1010
* Create a user/subscription association.
1111
*/
12-
public function factory(UserInterface $user, string $subscriptionHash, array $subscription, array $options = []): UserSubscriptionInterface;
12+
public function factory(?UserInterface $user, string $subscriptionHash, array $subscription, array $options = []): UserSubscriptionInterface;
1313

1414
/**
1515
* Return a string representation of the subscription's endpoint.
1616
* Example: md5($endpoint).
1717
*/
18-
public function hash(string $endpoint, UserInterface $user): string;
18+
public function hash(string $endpoint, ?UserInterface $user): string;
1919

2020
/**
2121
* Return the subscription attached to this user.

src/Model/Subscription/UserSubscriptionManagerRegistry.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
final class UserSubscriptionManagerRegistry implements UserSubscriptionManagerInterface
1111
{
12+
const ANONYMOUS = 'anonymous';
1213
/**
1314
* @var UserSubscriptionManagerInterface[]
1415
*/
@@ -19,12 +20,16 @@ final class UserSubscriptionManagerRegistry implements UserSubscriptionManagerIn
1920
*/
2021
public function register(string $userClass, UserSubscriptionManagerInterface $userSubscriptionManager)
2122
{
22-
if (!is_a($userClass, UserInterface::class, true)) {
23+
if ($userClass !== UserSubscriptionManagerRegistry::ANONYMOUS && !is_a($userClass, UserInterface::class, true)) {
2324
throw new \InvalidArgumentException(sprintf('Expected class implementing %s, %s given', UserInterface::class, $userClass));
2425
}
2526

2627
if (array_key_exists($userClass, $this->registry)) {
27-
throw new \InvalidArgumentException(sprintf('User class %s is already registered.', $userClass));
28+
if ($userClass !== UserSubscriptionManagerRegistry::ANONYMOUS) {
29+
throw new \InvalidArgumentException(sprintf('User class %s is already registered.', $userClass));
30+
} else {
31+
throw new \InvalidArgumentException(sprintf('%s user is already registered.', UserSubscriptionManagerRegistry::ANONYMOUS));
32+
}
2833
}
2934

3035
if (self::class === get_class($userSubscriptionManager)) {
@@ -44,7 +49,7 @@ public function register(string $userClass, UserSubscriptionManagerInterface $us
4449
*/
4550
public function getManager($userClass): UserSubscriptionManagerInterface
4651
{
47-
if (!is_a($userClass, UserInterface::class, true)) {
52+
if ($userClass !== null && !is_a($userClass, UserInterface::class, true)) {
4853
throw new \InvalidArgumentException(sprintf('Expected class or object that implements %s, %s given', UserInterface::class, is_object($userClass) ? get_class($userClass) : gettype($userClass)));
4954
}
5055

@@ -57,33 +62,33 @@ public function getManager($userClass): UserSubscriptionManagerInterface
5762
return $this->getManager(ClassUtils::getRealClass($userClass));
5863
}
5964

60-
if (!isset($this->registry[$userClass])) {
65+
if (!isset($this->registry[$userClass ?: UserSubscriptionManagerRegistry::ANONYMOUS])) {
6166
throw new \InvalidArgumentException(sprintf('There is no user subscription manager configured for class %s.', $userClass));
6267
}
6368

64-
return $this->registry[$userClass];
69+
return $this->registry[$userClass ?: UserSubscriptionManagerRegistry::ANONYMOUS];
6570
}
6671

6772
/**
6873
* {@inheritdoc}
6974
*/
70-
public function factory(UserInterface $user, string $subscriptionHash, array $subscription, array $options = []): UserSubscriptionInterface
75+
public function factory(?UserInterface $user, string $subscriptionHash, array $subscription, array $options = []): UserSubscriptionInterface
7176
{
7277
return $this->getManager($user)->factory($user, $subscriptionHash, $subscription, $options);
7378
}
7479

7580
/**
7681
* {@inheritdoc}
7782
*/
78-
public function hash(string $endpoint, UserInterface $user): string
83+
public function hash(string $endpoint, ?UserInterface $user): string
7984
{
8085
return $this->getManager($user)->hash($endpoint, $user);
8186
}
8287

8388
/**
8489
* {@inheritdoc}
8590
*/
86-
public function getUserSubscription(UserInterface $user, string $subscriptionHash): ?UserSubscriptionInterface
91+
public function getUserSubscription(?UserInterface $user, string $subscriptionHash): ?UserSubscriptionInterface
8792
{
8893
return $this->getManager($user)->getUserSubscription($user, $subscriptionHash);
8994
}

src/Resources/config/services.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
<service class="BenTools\WebPushBundle\Action\RegisterSubscriptionAction" id="BenTools\WebPushBundle\Action\RegisterSubscriptionAction" public="true">
2222
<argument id="BenTools\WebPushBundle\Model\Subscription\UserSubscriptionManagerRegistry" type="service"/>
23+
<argument>%bentools_webpush.delete_anonymous_subscription_with_the_same_endpoint%</argument>
2324
<tag name="controller.service_arguments"/>
2425
</service>
2526

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
namespace BenTools\WebPushBundle\Tests\Classes;
4+
5+
use BenTools\WebPushBundle\Model\Subscription\UserSubscriptionInterface;
6+
use BenTools\WebPushBundle\Model\Subscription\UserSubscriptionManagerInterface;
7+
use Doctrine\Common\Persistence\ManagerRegistry;
8+
use Symfony\Component\Security\Core\User\UserInterface;
9+
10+
final class TestAnonymousSubscriptionManager implements UserSubscriptionManagerInterface
11+
{
12+
/**
13+
* @var ManagerRegistry
14+
*/
15+
private $doctrine;
16+
17+
/**
18+
* UserSubscriptionManager constructor.
19+
* @param ManagerRegistry $doctrine
20+
*/
21+
public function __construct(ManagerRegistry $doctrine)
22+
{
23+
$this->doctrine = $doctrine;
24+
}
25+
26+
/**
27+
* @inheritDoc
28+
*/
29+
public function factory(?UserInterface $user, string $subscriptionHash, array $subscription, array $options = []): UserSubscriptionInterface
30+
{
31+
return new TestUserSubscription(
32+
null,
33+
$subscription['endpoint'],
34+
$subscription['keys']['p256dh'],
35+
$subscription['keys']['auth'],
36+
$subscriptionHash
37+
);
38+
}
39+
40+
/**
41+
* @inheritDoc
42+
*/
43+
public function hash(string $endpoint, ?UserInterface $user): string
44+
{
45+
return md5($endpoint);
46+
}
47+
48+
/**
49+
* @inheritDoc
50+
*/
51+
public function getUserSubscription(?UserInterface $user, string $subscriptionHash): ?UserSubscriptionInterface
52+
{
53+
return $this->doctrine->getManagerForClass(TestUserSubscription::class)->getRepository(TestUserSubscription::class)->findOneBy([
54+
'user' => $user,
55+
'subscriptionHash' => $subscriptionHash,
56+
]);
57+
}
58+
59+
/**
60+
* @inheritDoc
61+
*/
62+
public function findByUser(?UserInterface $user): iterable
63+
{
64+
return null;
65+
}
66+
67+
/**
68+
* Return the list of all known subscriptions.
69+
* A user can have several subscriptions (on chrome, firefox, etc.).
70+
*
71+
* @return iterable|UserSubscriptionInterface[]
72+
*/
73+
public function findAll(): iterable
74+
{
75+
return $this->doctrine->getManagerForClass(TestUserSubscription::class)->getRepository(TestUserSubscription::class)->findAll();
76+
}
77+
78+
/**
79+
* @inheritDoc
80+
*/
81+
public function findByHash(string $subscriptionHash): iterable
82+
{
83+
return $this->doctrine->getManagerForClass(TestUserSubscription::class)->getRepository(TestUserSubscription::class)->findBy([
84+
'subscriptionHash' => $subscriptionHash,
85+
]);
86+
}
87+
88+
/**
89+
* @inheritDoc
90+
*/
91+
public function save(UserSubscriptionInterface $userSubscription): void
92+
{
93+
$this->doctrine->getManagerForClass(TestUserSubscription::class)->persist($userSubscription);
94+
$this->doctrine->getManagerForClass(TestUserSubscription::class)->flush();
95+
}
96+
97+
/**
98+
* @inheritDoc
99+
*/
100+
public function delete(UserSubscriptionInterface $userSubscription): void
101+
{
102+
$this->doctrine->getManagerForClass(TestUserSubscription::class)->remove($userSubscription);
103+
$this->doctrine->getManagerForClass(TestUserSubscription::class)->flush();
104+
}
105+
}

tests/Classes/TestUserSubscription.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,21 @@ final class TestUserSubscription implements UserSubscriptionInterface
3636
private $subscriptionHash;
3737

3838
public function __construct(
39-
UserInterface $user,
39+
?UserInterface $user,
4040
string $endpoint,
4141
string $publicKey,
4242
string $authtoken,
4343
string $subscriptionHash
4444
) {
45-
$this->id = $user->getUsername();
45+
$this->id = $user !== null ? $user->getUsername() : uniqid();
4646
$this->user = $user;
4747
$this->endpoint = $endpoint;
4848
$this->publicKey = $publicKey;
4949
$this->authtoken = $authtoken;
5050
$this->subscriptionHash = $subscriptionHash;
5151
}
5252

53-
public function getUser(): UserInterface
53+
public function getUser(): ?UserInterface
5454
{
5555
return $this->user;
5656
}

0 commit comments

Comments
 (0)