Skip to content

Commit e14e5c8

Browse files
committed
feat: add Twig helper generating hub URLs and setting authorization cookies
1 parent c4bb384 commit e14e5c8

File tree

10 files changed

+156
-17
lines changed

10 files changed

+156
-17
lines changed

.github/workflows/static-analysis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- name: "installing PHP"
1616
uses: "shivammathur/setup-php@v2"
1717
with:
18-
php-version: "7.4"
18+
php-version: "8.0"
1919
ini-values: memory_limit=-1
2020
tools: composer:v2, phpstan, cs2pr
2121

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
/.php_cs.cache
2-
/.php_cs
1+
/.php-cs-fixer.cache
2+
/.php-cs-fixer.php
33
/.phpunit.result.cache
44
/composer.phar
55
/composer.lock
File renamed without changes.

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@
4646
},
4747
"require-dev": {
4848
"lcobucci/jwt": "^3.4|^4.0",
49+
"symfony/event-dispatcher": "^4.4|^5.0|^6.0",
4950
"symfony/phpunit-bridge": "^5.2|^6.0",
50-
"symfony/stopwatch": "^4.4|^5.0|^6.0"
51+
"symfony/stopwatch": "^4.4|^5.0|^6.0",
52+
"twig/twig": "^2.0|^3.0"
5153
},
5254
"minimum-stability": "dev"
5355
}

src/Authorization.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
use Symfony\Component\HttpFoundation\Cookie;
1717
use Symfony\Component\HttpFoundation\Request;
18-
use Symfony\Component\HttpFoundation\Response;
1918
use Symfony\Component\Mercure\Exception\InvalidArgumentException;
2019
use Symfony\Component\Mercure\Exception\RuntimeException;
2120

@@ -36,7 +35,30 @@ public function __construct(HubRegistry $registry, ?int $cookieLifetime = null)
3635
}
3736

3837
/**
39-
* Create Authorization cookie for the given hub.
38+
* Sets mercureAuthorization cookie for the given hub.
39+
*
40+
* @param string[] $subscribe a list of topics that the authorization cookie will allow subscribing to
41+
* @param string[] $publish a list of topics that the authorization cookie will allow publishing to
42+
* @param mixed[] $additionalClaims an array of additional claims for the JWT
43+
* @param string|null $hub the hub to generate the cookie for
44+
*/
45+
public function setCookie(Request $request, array $subscribe = [], array $publish = [], array $additionalClaims = [], ?string $hub = null): void
46+
{
47+
$request->attributes->set('_mercure_authorization_cookie', $this->createCookie($request, $subscribe, $publish, $additionalClaims, $hub));
48+
}
49+
50+
/**
51+
* Clears the mercureAuthorization cookie for the given hub.
52+
*
53+
* @param string|null $hub the hub to clear the cookie for
54+
*/
55+
public function clearCookie(Request $request, ?string $hub = null): void
56+
{
57+
$request->attributes->set('_mercure_authorization_cookie', $this->createClearCookie($request, $hub));
58+
}
59+
60+
/**
61+
* Creates mercureAuthorization cookie for the given hub.
4062
*
4163
* @param string[] $subscribe a list of topics that the authorization cookie will allow subscribing to
4264
* @param string[] $publish a list of topics that the authorization cookie will allow publishing to
@@ -82,18 +104,26 @@ public function createCookie(Request $request, array $subscribe = [], array $pub
82104
);
83105
}
84106

85-
public function clearCookie(Request $request, Response $response, ?string $hub = null): void
107+
/**
108+
* Clears the mercureAuthorization cookie for the given hub.
109+
*
110+
* @param string|null $hub the hub to clear the cookie for
111+
*/
112+
public function createClearCookie(Request $request, ?string $hub = null): Cookie
86113
{
87114
$hubInstance = $this->registry->getHub($hub);
88115
/** @var array $urlComponents */
89116
$urlComponents = parse_url($hubInstance->getPublicUrl());
90117

91-
$response->headers->clearCookie(
118+
return Cookie::create(
92119
self::MERCURE_AUTHORIZATION_COOKIE_NAME,
120+
null,
121+
1,
93122
$urlComponents['path'] ?? '/',
94123
$this->getCookieDomain($request, $urlComponents),
95124
'http' !== strtolower($urlComponents['scheme'] ?? 'https'),
96125
true,
126+
false,
97127
Cookie::SAMESITE_STRICT
98128
);
99129
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Mercure Component project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Mercure\EventSubscriber;
15+
16+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17+
use Symfony\Component\HttpKernel\Event\ResponseEvent;
18+
use Symfony\Component\HttpKernel\KernelEvents;
19+
20+
final class SetCookieSubscriber implements EventSubscriberInterface
21+
{
22+
public function onKernelResponse(ResponseEvent $event)
23+
{
24+
if (
25+
!$event->isMainRequest() ||
26+
null === $cookie = $event->getRequest()->attributes->get('_mercure_authorization_cookie')) {
27+
return;
28+
}
29+
30+
$event->getResponse()->headers->setCookie($cookie);
31+
}
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public static function getSubscribedEvents(): array
37+
{
38+
return [KernelEvents::RESPONSE => 'onKernelResponse'];
39+
}
40+
}

src/Publisher.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ final class Publisher implements PublisherInterface
3737

3838
/**
3939
* @param TokenProviderInterface|callable(Update $update):string $jwtProvider
40+
* @param mixed $jwtProvider
4041
*/
4142
public function __construct(string $hubUrl, $jwtProvider, HttpClientInterface $httpClient = null)
4243
{

src/Twig/MercureExtension.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Mercure Component project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Mercure\Twig;
15+
16+
use Symfony\Component\HttpFoundation\RequestStack;
17+
use Symfony\Component\Mercure\Authorization;
18+
use Symfony\Component\Mercure\HubRegistry;
19+
use Twig\Extension\AbstractExtension;
20+
use Twig\TwigFunction;
21+
22+
final class MercureExtension extends AbstractExtension
23+
{
24+
private HubRegistry $hubRegistry;
25+
private ?Authorization $authorization;
26+
private ?RequestStack $requestStack;
27+
28+
public function __construct(HubRegistry $hubRegistry, ?Authorization $authorization = null, ?RequestStack $requestStack = null)
29+
{
30+
$this->hubRegistry = $hubRegistry;
31+
$this->authorization = $authorization;
32+
$this->requestStack = $requestStack;
33+
}
34+
35+
public function getFunctions()
36+
{
37+
return [new TwigFunction('mercure', [$this, 'mercure'])];
38+
}
39+
40+
/**
41+
* @param string|string[] $topics
42+
*/
43+
public function mercure($topics, array $options = []): string
44+
{
45+
$hub = $options['hub'] ?? null;
46+
$url = $this->hubRegistry->getHub($hub)->getPublicUrl();
47+
// We cannot use http_build_query() because this method doesn't support generating multiple query parameters with the same name without the [] suffix
48+
$separator = '?';
49+
foreach ((array) $topics as $topic) {
50+
$url .= $separator.'topic='.rawurlencode($topic);
51+
if ('?' === $separator) {
52+
$separator = '&';
53+
}
54+
}
55+
56+
if (
57+
null === $this->authorization ||
58+
null === $this->requestStack ||
59+
(!isset($options['subscribe']) && !isset($options['publish']) && !isset($options['additionalClaims'])) ||
60+
null === $request = $this->requestStack->getMainRequest()
61+
) {
62+
return $url;
63+
}
64+
65+
$this->authorization->setCookie($request, $options['subscribe'] ?? [], $options['publish'] ?? [], $options['additionalClaims'] ?? [], $hub);
66+
67+
return $url;
68+
}
69+
}

src/Update.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ final class Update
3333
private $retry;
3434

3535
/**
36-
* @param array|string $topics
36+
* @param string|string[] $topics
3737
*/
3838
public function __construct($topics, string $data = '', bool $private = false, string $id = null, string $type = null, int $retry = null)
3939
{

tests/AuthorizationTest.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use Lcobucci\JWT\Signer\Key\InMemory;
1717
use PHPUnit\Framework\TestCase;
1818
use Symfony\Component\HttpFoundation\Request;
19-
use Symfony\Component\HttpFoundation\Response;
2019
use Symfony\Component\Mercure\Authorization;
2120
use Symfony\Component\Mercure\Exception\RuntimeException;
2221
use Symfony\Component\Mercure\HubRegistry;
@@ -67,15 +66,13 @@ public function create(array $subscribe = [], array $publish = [], array $additi
6766
));
6867

6968
$authorization = new Authorization($registry);
70-
$cookie = $authorization->createCookie($request = Request::create('https://example.com'));
69+
$cookie = $authorization->setCookie($request = Request::create('https://example.com'));
7170

72-
$response = new Response();
73-
$response->headers->setCookie($cookie);
71+
$authorization->clearCookie($request);
7472

75-
$authorization->clearCookie($request, $response);
76-
77-
$this->assertNull($response->headers->getCookies()[0]->getValue());
78-
$this->assertSame(1, $response->headers->getCookies()[0]->getExpiresTime());
73+
$cookie = $request->attributes->get('_mercure_authorization_cookie');
74+
$this->assertNull($cookie->getValue());
75+
$this->assertSame(1, $cookie->getExpiresTime());
7976
}
8077

8178
/**

0 commit comments

Comments
 (0)