Skip to content

Commit

Permalink
fixup! Add AbstractCookieDispatchTranscriptor
Browse files Browse the repository at this point in the history
  • Loading branch information
ebln committed Jun 8, 2024
1 parent 1db8dbd commit 6f0ba57
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 57 deletions.
65 changes: 65 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,71 @@
'yoda_style' => ['less_and_greater' => false],
'type_declaration_spaces' => ['elements' => ['function']],
'fully_qualified_strict_types' => false,
'single_space_around_construct' => [
'constructs_followed_by_a_single_space' => [ // default, minus const
'abstract',
'as',
'attribute',
'break',
'case',
'catch',
'class',
'clone',
'comment',
'const_import',
'continue',
'do',
'echo',
'else',
'elseif',
'enum',
'extends',
'final',
'finally',
'for',
'foreach',
'function',
'function_import',
'global',
'goto',
'if',
'implements',
'include',
'include_once',
'instanceof',
'insteadof',
'interface',
'match',
'named_argument',
'namespace',
'new',
'open_tag_with_echo',
'php_doc',
'php_open',
'print',
'private',
'protected',
'public',
'readonly',
'require',
'require_once',
'return',
'static',
'switch',
'throw',
'trait',
'try',
'type_colon',
'use',
'use_lambda',
'use_trait',
'var',
'while',
'yield',
'yield_from',
],

],
]
)
->setFinder($finder)
Expand Down
36 changes: 20 additions & 16 deletions src/Transcriptor/CookieDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

namespace brnc\Symfony1\Message\Transcriptor;

use brnc\Symfony1\Message\Transcriptor\Response\AbstractCookieDispatchTranscriptor;
use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\CookieContainerInterface;
use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\DispatchSubstitutor;

/**
* Wraps sfEventDispatcher to fire PSR7 cookies in sfWebRequest
Expand All @@ -15,29 +15,30 @@
*/
class CookieDispatcher
{
public const APPLICATION_LOG = 'application.log';
public const APPLICATION_LOG = 'application.log';
private const LOGGING_PRIORITY = null; // For debugging: overrides event's priority, @see \sfLogger

private const COOKIE_TRIGGER_HEADER = 'X-cookie-trigger';
private const COOKIE_NAME_PREFIX = '__Host-__';
private ?\sfEventDispatcher $dispatcher;
private bool $logging;

private ?int $headerCountdown = null;

public function __construct(?\sfEventDispatcher $dispatcher, bool $logging)
{
$this->dispatcher = $dispatcher;
$this->logging = $logging;
$this->triggerCookieName = self::COOKIE_NAME_PREFIX . bin2hex(random_bytes(3));
$this->triggerCookieValue = bin2hex(random_bytes(5));
$this->dispatcher = $dispatcher;
$this->logging = $logging;
}

public function __call($name, $arguments)
/**
* @param array{0: \sfEvent} $arguments # |array{0: \sfEvent, 1: mixed}|array{0: string}|array{0: string, 1: callable}
*
* @return mixed # void|false|\sfEvent|bool|callable[]
*/
public function __call(string $name, array $arguments)
{
$this->dispatcher ??= new \sfEventDispatcher();

return call_user_func_array([$this->dispatcher, $name], $arguments);
return call_user_func_array([$this->dispatcher, $name], $arguments); // @phpstan-ignore argument.type
}

public function notify(\sfEvent $event): \sfEvent
Expand All @@ -46,7 +47,7 @@ public function notify(\sfEvent $event): \sfEvent
return $this->passNotify($event);
}
// Override local logging for debug purposes
if (null !== self::LOGGING_PRIORITY) {
if (null !== self::LOGGING_PRIORITY) { // @phpstan-ignore notIdentical.alwaysFalse
$event->offsetSet('priority', self::LOGGING_PRIORITY); // Force logging
}

Expand All @@ -56,20 +57,23 @@ public function notify(\sfEvent $event): \sfEvent
/** @var \sfWebResponse $response */
$response = $event->getSubject();

/** @var string $logMessage */
$logMessage = $event->offsetGet(0);

// There is always at least on header, as Content-Type is forced in sfWebResponseX::sendHttpHeaders
if (str_starts_with($event->offsetGet(0), 'Send header "')) {
if (str_starts_with($logMessage, 'Send header "')) {
// initialize countdown with the number of header lines, after the very first header was sent out…
$this->headerCountdown ??= count($response->getHttpHeaders());
--$this->headerCountdown; // decrease right away…
if (0 === $this->headerCountdown) { // so that we'll reach 0, after the last header was sent
/** @var CookieContainerInterface $container */
$container = $response->getOptions()[AbstractCookieDispatchTranscriptor::PSR_7_COOKIES];
foreach ($container->getCookies() as $cookie) {
/** @var array{__psr7cookies: CookieContainerInterface} $options */
$options = $response->getOptions();
foreach ($options[DispatchSubstitutor::PSR_7_COOKIES]->getCookies() as $cookie) {
$cookie->apply();
if ($this->logging) {
$params = ["Send PSR7 cookie \"{$cookie->getName()}\": \"{$cookie->getValue()}\""];

if (null !== self::LOGGING_PRIORITY) {
if (null !== self::LOGGING_PRIORITY) { // @phpstan-ignore notIdentical.alwaysFalse
$params['priority'] = self::LOGGING_PRIORITY; // Force logging
}
$this->passNotify(
Expand Down
33 changes: 8 additions & 25 deletions src/Transcriptor/Response/AbstractCookieDispatchTranscriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,24 @@

namespace brnc\Symfony1\Message\Transcriptor\Response;

use brnc\Symfony1\Message\Transcriptor\CookieDispatcher;
use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\CookieContainerInterface;
use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\DispatchSubstitutor;
use Psr\Http\Message\ResponseInterface;

abstract class AbstractCookieDispatchTranscriptor implements CookieTranscriptorInterface
{
public const PSR_7_COOKIES = '__psr7cookies';
private DispatchSubstitutor $substitutor;

public function __construct(DispatchSubstitutor $substitutor)
{
$this->substitutor = $substitutor;
}

final public function transcribeCookies(ResponseInterface $psrResponse, \sfWebResponse $sfWebResponse): void
{
$this->wrapDispatcher($sfWebResponse, $this->getCookieContainer($psrResponse));
$this->substitutor->wrapDispatcher($sfWebResponse, $this->getCookieContainer($psrResponse));
}

/** Implement this method to obtain a CookieContainer from your PSR-7 Response! */
abstract protected function getCookieContainer(ResponseInterface $psrResponse): CookieContainerInterface;

private function wrapDispatcher(\sfWebResponse $sfWebResponse, CookieContainerInterface $cookieContainer): void
{
// backup logging-state of the sfWebResponse, to inject that later into the wrapped Dispatcher
$hasLogging = (bool)($sfWebResponse->getOptions()['logging'] ?? false);
// Doing reflection magic… -.-
$reflexiveWebResponse = new \ReflectionObject($sfWebResponse);
// Override enable logging in sfWebResponse!
// Attach PSR7 cookies container to sfWebResponse's `options` property.
$optionsOverride = array_merge($sfWebResponse->getOptions(), ['logging' => true, self::PSR_7_COOKIES => $cookieContainer]);
$reflexOptions = $reflexiveWebResponse->getProperty('options');
$reflexOptions->setAccessible(true);
$reflexOptions->setValue($sfWebResponse, $optionsOverride);
$reflexOptions->setAccessible(false);
// Get the originally attached sfEventDispatcher
$reflexDispatcher = $reflexiveWebResponse->getProperty('dispatcher');
$reflexDispatcher->setAccessible(true);
$dispatcher = $reflexDispatcher->getValue($sfWebResponse);
// Replace the sfEventDispatcher, with the CookieDispatcher with wraps the original one
$reflexDispatcher->setValue($sfWebResponse, new CookieDispatcher($dispatcher, $hasLogging));
$reflexDispatcher->setAccessible(false);
}
}
2 changes: 2 additions & 0 deletions src/Transcriptor/Response/CookieDispatch/AbstractCookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

abstract class AbstractCookie implements CookieInterface
{
/** @var array{expires?: int, path?: string, domain?: string, secure?: bool, httponly?: bool, samesite?: 'Lax'|'None'|'Strict'} */
protected array $options;

private string $name;
private string $value;

/** @param array{expires?: int, path?: string, domain?: string, secure?: bool, httponly?: bool, samesite?: 'Lax'|'None'|'Strict'} $options */
public function __construct(string $name, string $value, array $options)
{
$this->name = $name;
Expand Down
7 changes: 3 additions & 4 deletions src/Transcriptor/Response/CookieDispatch/CookieContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@

namespace brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch;

use Webmozart\Assert\Assert;

class CookieContainer
{
/** @return CookieInterface[] */
/** @var CookieInterface[] */
private array $cookies;

/** @param CookieInterface[] $cookies */
public function __construct(array $cookies)
{
Assert::allImplementsInterface($cookies, CookieInterface::class);
$this->cookies = $cookies;
}

/** @return CookieInterface[] */
public function getCookies(): array
{
return $this->cookies;
Expand Down
37 changes: 37 additions & 0 deletions src/Transcriptor/Response/CookieDispatch/DispatchSubstitutor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch;

use brnc\Symfony1\Message\Transcriptor\CookieDispatcher;

class DispatchSubstitutor
{
public const PSR_7_COOKIES = '__psr7cookies';

public function wrapDispatcher(\sfWebResponse $sfWebResponse, CookieContainerInterface $cookieContainer): void
{
/** @var array{logging: null|bool|int} $responseOptions */
$responseOptions = $sfWebResponse->getOptions();
// backup logging-state of the sfWebResponse, to inject that later into the wrapped Dispatcher
$hasLogging = (bool)($responseOptions['logging'] ?? false);
// Doing reflection magic… -.-
$reflexiveWebResponse = new \ReflectionObject($sfWebResponse);
// Override enable logging in sfWebResponse!
// Attach PSR7 cookies container to sfWebResponse's `options` property.
$optionsOverride = array_merge($responseOptions, ['logging' => true, self::PSR_7_COOKIES => $cookieContainer]);
$reflexOptions = $reflexiveWebResponse->getProperty('options');
$reflexOptions->setAccessible(true);
$reflexOptions->setValue($sfWebResponse, $optionsOverride);
$reflexOptions->setAccessible(false);
// Get the originally attached sfEventDispatcher
$reflexDispatcher = $reflexiveWebResponse->getProperty('dispatcher');
$reflexDispatcher->setAccessible(true);
/** @var null|\sfEventDispatcher $dispatcher */
$dispatcher = $reflexDispatcher->getValue($sfWebResponse);
// Replace the sfEventDispatcher, with the CookieDispatcher with wraps the original one
$reflexDispatcher->setValue($sfWebResponse, new CookieDispatcher($dispatcher, $hasLogging));
$reflexDispatcher->setAccessible(false);
}
}
4 changes: 2 additions & 2 deletions src/Transcriptor/Response/CookieDispatch/TestContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class TestContainer implements CookieContainerInterface
{
/** @var array<int, string> */
public array $reports = [];
/** @return CookieInterface[] */
/** @var CookieInterface[] */
private array $cookies;

/** @param TestCookie[] $cookies */
Expand All @@ -19,7 +19,7 @@ public function __construct(array $cookies)
Assert::allIsInstanceOf($cookies, TestCookie::class);
$this->cookies = $cookies;
foreach ($cookies as $cookie) {
$reporter = fn (string $report) => $this->reports[] = $report;
$reporter = fn (string $report): bool => !($this->reports[] = $report);
$cookie->setReport($reporter);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Transcriptor/Response/CookieDispatch/TestCookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ public function getValue(): string

public function setReport(callable $reportFn): void
{
if ($this->reportFn) {
if (null !== $this->reportFn) {
throw new \RuntimeException('Report already set!');
}
$this->reportFn = $reportFn;
}

public function apply(): bool
{
if (!$this->reportFn) {
if (null === $this->reportFn) {
throw new \RuntimeException('Report not set!');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use brnc\Symfony1\Message\Transcriptor\Response\AbstractCookieDispatchTranscriptor;
use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\CookieContainerInterface;
use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\DispatchSubstitutor;
use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\TestContainer;
use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\TestCookie;
use brnc\Symfony1\Message\Transcriptor\ResponseTranscriptor;
Expand All @@ -21,7 +22,7 @@
final class AbstractCookieDispatchTranscriptorTest extends TestCase
{
/**
* @param array<int,string> $fixture
* @param array<TestCookie> $fixture
* @param array<string,array{name: string, value: string, expire: null|int, path: string, domain: string, secure: bool, httpOnly: bool}> $expectation
*
* @dataProvider provideTranscribeFailCookiesCases
Expand All @@ -31,24 +32,24 @@ public function testTranscribeFailCookies(array $fixture, array $expectation): v
$psr7Response = new Response(203);

$transcriptor = new class($fixture) extends AbstractCookieDispatchTranscriptor {
/** @var TestCookie[] */
private array $cookies;
public ?TestContainer $container;
public TestContainer $container;

/** @param array<TestCookie> $cookies*/
public function __construct(array $cookies)
{
$this->cookies = $cookies;
parent::__construct(new DispatchSubstitutor());

$this->container = new TestContainer($cookies);
}

protected function getCookieContainer(ResponseInterface $psrResponse): CookieContainerInterface
{
$this->container = new TestContainer($this->cookies);

return $this->container;
}
};

$testDispatcher = new class() extends \sfEventDispatcher {
/** @var array<array{event: string, notification: string}> */
public array $notifications = [];

public function notify(\sfEvent $event): \sfEvent
Expand All @@ -74,7 +75,7 @@ public function notify(\sfEvent $event): \sfEvent
self::assertSame($expectation['notifications'], $testDispatcher->notifications);
}

/** @return array<string, array{set-cookie: string[], expectation: array<string,array{name: string, value: string, expire: null|int, path: string, domain: string, secure: bool, httpOnly: bool}>}> */
/** @return array<string, array{cookies: array<TestCookie>, expectation: array{reports: string[], notifications: array<array{event: string, notification: string}>}}> */
public static function provideTranscribeFailCookiesCases(): iterable
{
return [
Expand Down

0 comments on commit 6f0ba57

Please sign in to comment.