Skip to content

Commit

Permalink
Add AbstractCookieDispatchTranscriptor
Browse files Browse the repository at this point in the history
It uses black magic excessively:

- wraps & replaces the EventDispatcher of the Symfony Response
- high-jacks logging-events to unwind present day cookies
  • Loading branch information
ebln committed Jun 8, 2024
1 parent 6d95de9 commit 1db8dbd
Show file tree
Hide file tree
Showing 15 changed files with 535 additions and 13 deletions.
30 changes: 26 additions & 4 deletions mock/sfEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
class sfEvent
{
/** @var mixed */
protected $value = null;
/** @var string */
protected $name = '';
protected $value = null;
protected string $name = '';
protected object $subject;
protected array $parameters;

public function getName(): string
{
Expand All @@ -19,7 +20,9 @@ public function getName(): string

public function __construct($subject, string $name, array $parameters = [])
{
$this->name = $name;
$this->name = $name;
$this->subject = $subject;
$this->parameters = $parameters;
}

public function setReturnValue($value)
Expand All @@ -31,4 +34,23 @@ public function getReturnValue()
{
return $this->value;
}

public function getSubject(): object
{
return $this->subject;
}

public function offsetGet($name)
{
if (!array_key_exists($name, $this->parameters)) {
throw new \InvalidArgumentException();
}

return $this->parameters[$name];
}

public function offsetSet($name, $value)
{
$this->parameters[$name] = $value;
}
}
9 changes: 9 additions & 0 deletions mock/sfEventDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,13 @@ public function filter(sfEvent $event, $value): sfEvent

return $event;
}

public function notify(sfEvent $event): sfEvent
{
foreach ($this->listeners[$event->getName()] ?? [] as $listener) {
call_user_func($listener, $event);
}

return $event;
}
}
33 changes: 24 additions & 9 deletions mock/sfWebResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,13 @@ public function getCookies()
}

/**
* @param string $name
* @param string $value
* @param null $expire
* @param string $path
* @param string $domain
* @param bool $secure
* @param bool $httpOnly
* @param string $name
* @param string $value
* @param null|int|string $expire
* @param string $path
* @param string $domain
* @param bool $secure
* @param bool $httpOnly
*/
public function setCookie(
$name,
Expand Down Expand Up @@ -235,9 +235,8 @@ public function getContent()
}

/**
* @deprecated Only for testing! Original methods echos instead of returning
*
* @return null|string
* @deprecated Only for testing! Original methods echos instead of returning
*/
public function sendContent()
{
Expand All @@ -248,4 +247,20 @@ public function sendContent()

return $event->getReturnValue();
}

/**
* @deprecated Only for testing! Original methods uses `header` & `setrawcookie` instead of returning
*/
public function sendHttpHeaders(): void
{
if ($this->options['logging']) {
$this->dispatcher->notify(new sfEvent($this, 'application.log', ['Send status "???"']));
}
$this->setHttpHeader('Content-Type', 'example');
foreach ($this->headers as $name => $value) {
if (null !== $this->dispatcher && $this->options['logging'] && !empty($value)) {
$this->dispatcher->notify(new sfEvent($this, 'application.log', ["Send header \"{$name}: {$value}\""]));
}
}
}
}
94 changes: 94 additions & 0 deletions src/Transcriptor/CookieDispatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

namespace brnc\Symfony1\Message\Transcriptor;

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

/**
* Wraps sfEventDispatcher to fire PSR7 cookies in sfWebRequest
*
* There is no interface to implement and this class deliberately doesn't extend the sfEventDispatcher.
* As there is to type coercion in the Sf1 code, that should be fine.
*/
class CookieDispatcher
{
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));

Check failure on line 32 in src/Transcriptor/CookieDispatcher.php

View workflow job for this annotation

GitHub Actions / Build and test on 8.1

UndefinedThisPropertyAssignment

src/Transcriptor/CookieDispatcher.php:32:9: UndefinedThisPropertyAssignment: Instance property brnc\Symfony1\Message\Transcriptor\CookieDispatcher::$triggerCookieName is not defined (see https://psalm.dev/040)
$this->triggerCookieValue = bin2hex(random_bytes(5));

Check failure on line 33 in src/Transcriptor/CookieDispatcher.php

View workflow job for this annotation

GitHub Actions / Build and test on 8.1

UndefinedThisPropertyAssignment

src/Transcriptor/CookieDispatcher.php:33:9: UndefinedThisPropertyAssignment: Instance property brnc\Symfony1\Message\Transcriptor\CookieDispatcher::$triggerCookieValue is not defined (see https://psalm.dev/040)
}

public function __call($name, $arguments)

Check failure on line 36 in src/Transcriptor/CookieDispatcher.php

View workflow job for this annotation

GitHub Actions / Build and test on 8.1

MissingParamType

src/Transcriptor/CookieDispatcher.php:36:28: MissingParamType: Parameter $name has no provided type (see https://psalm.dev/154)

Check failure on line 36 in src/Transcriptor/CookieDispatcher.php

View workflow job for this annotation

GitHub Actions / Build and test on 8.1

MissingParamType

src/Transcriptor/CookieDispatcher.php:36:35: MissingParamType: Parameter $arguments has no provided type (see https://psalm.dev/154)
{
$this->dispatcher ??= new \sfEventDispatcher();

return call_user_func_array([$this->dispatcher, $name], $arguments);
}

public function notify(\sfEvent $event): \sfEvent
{ // We are only interested in logging events from the response, and pass-through everything else.
if (self::APPLICATION_LOG !== $event->getName() || !$event->getSubject() instanceof \sfWebResponse) {
return $this->passNotify($event);
}
// Override local logging for debug purposes
if (null !== self::LOGGING_PRIORITY) {
$event->offsetSet('priority', self::LOGGING_PRIORITY); // Force logging
}

if ($this->logging) {
$this->passNotify($event); // Notify is not expected to change the event; Sticking to in-order logging, over preserving possible return-event.
}
/** @var \sfWebResponse $response */
$response = $event->getSubject();

// There is always at least on header, as Content-Type is forced in sfWebResponseX::sendHttpHeaders
if (str_starts_with($event->offsetGet(0), 'Send header "')) {

Check failure on line 60 in src/Transcriptor/CookieDispatcher.php

View workflow job for this annotation

GitHub Actions / Build and test on 8.1

MixedArgument

src/Transcriptor/CookieDispatcher.php:60:29: MixedArgument: Argument 1 of str_starts_with cannot be mixed, expecting string (see https://psalm.dev/030)
// 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];

Check failure on line 66 in src/Transcriptor/CookieDispatcher.php

View workflow job for this annotation

GitHub Actions / Build and test on 8.1

InvalidArrayOffset

src/Transcriptor/CookieDispatcher.php:66:30: InvalidArrayOffset: Cannot access value on variable using offset value of '__psr7cookies', expecting 'http_protocol' or '__brncBodyStreamHook' (see https://psalm.dev/115)
foreach ($container->getCookies() as $cookie) {
$cookie->apply();
if ($this->logging) {
$params = ["Send PSR7 cookie \"{$cookie->getName()}\": \"{$cookie->getValue()}\""];

if (null !== self::LOGGING_PRIORITY) {
$params['priority'] = self::LOGGING_PRIORITY; // Force logging
}
$this->passNotify(
new \sfEvent($this, self::APPLICATION_LOG, $params)
);
}
}
}
}

return $event;
}

private function passNotify(\sfEvent $event): \sfEvent
{
if ($this->dispatcher) {
return $this->dispatcher->notify($event);
}

return $event;
}
}
44 changes: 44 additions & 0 deletions src/Transcriptor/Response/AbstractCookieDispatchTranscriptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace brnc\Symfony1\Message\Transcriptor\Response;

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

abstract class AbstractCookieDispatchTranscriptor implements CookieTranscriptorInterface
{
public const PSR_7_COOKIES = '__psr7cookies';

final public function transcribeCookies(ResponseInterface $psrResponse, \sfWebResponse $sfWebResponse): void
{
$this->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);

Check failure on line 26 in src/Transcriptor/Response/AbstractCookieDispatchTranscriptor.php

View workflow job for this annotation

GitHub Actions / Build and test on 8.1

InvalidArrayOffset

src/Transcriptor/Response/AbstractCookieDispatchTranscriptor.php:26:30: InvalidArrayOffset: Cannot access value on variable using offset value of 'logging', expecting 'http_protocol' or '__brncBodyStreamHook' (see https://psalm.dev/115)
// 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);

Check failure on line 39 in src/Transcriptor/Response/AbstractCookieDispatchTranscriptor.php

View workflow job for this annotation

GitHub Actions / Build and test on 8.1

MixedAssignment

src/Transcriptor/Response/AbstractCookieDispatchTranscriptor.php:39:9: MixedAssignment: Unable to determine the type that $dispatcher is being assigned to (see https://psalm.dev/032)
// Replace the sfEventDispatcher, with the CookieDispatcher with wraps the original one
$reflexDispatcher->setValue($sfWebResponse, new CookieDispatcher($dispatcher, $hasLogging));

Check failure on line 41 in src/Transcriptor/Response/AbstractCookieDispatchTranscriptor.php

View workflow job for this annotation

GitHub Actions / Build and test on 8.1

MixedArgument

src/Transcriptor/Response/AbstractCookieDispatchTranscriptor.php:41:74: MixedArgument: Argument 1 of brnc\Symfony1\Message\Transcriptor\CookieDispatcher::__construct cannot be mixed, expecting null|sfEventDispatcher (see https://psalm.dev/030)
$reflexDispatcher->setAccessible(false);
}
}
30 changes: 30 additions & 0 deletions src/Transcriptor/Response/CookieDispatch/AbstractCookie.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

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

abstract class AbstractCookie implements CookieInterface
{
protected array $options;

private string $name;
private string $value;

public function __construct(string $name, string $value, array $options)
{
$this->name = $name;
$this->value = $value;
$this->options = $options;
}

public function getName(): string
{
return $this->name;
}

public function getValue(): string
{
return $this->value;
}
}
24 changes: 24 additions & 0 deletions src/Transcriptor/Response/CookieDispatch/CookieContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

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

use Webmozart\Assert\Assert;

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

public function __construct(array $cookies)
{
Assert::allImplementsInterface($cookies, CookieInterface::class);
$this->cookies = $cookies;
}

public function getCookies(): array
{
return $this->cookies;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

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

interface CookieContainerInterface
{
/** @return CookieInterface[] */
public function getCookies(): array;
}
14 changes: 14 additions & 0 deletions src/Transcriptor/Response/CookieDispatch/CookieInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

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

interface CookieInterface
{
public function getName(): string;

public function getValue(): string;

public function apply(): bool;
}
36 changes: 36 additions & 0 deletions src/Transcriptor/Response/CookieDispatch/HeaderCookie.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

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

class HeaderCookie implements CookieInterface
{
private string $name;
private string $value;
private string $completeHeader;

public function __construct(string $name, string $value, string $completeHeader)
{
$this->name = $name;
$this->value = $value;
$this->completeHeader = $completeHeader;
}

public function getName(): string
{
return $this->name;
}

public function getValue(): string
{
return $this->value;
}

public function apply(): bool
{
header($this->completeHeader, false);

return !headers_sent();
}
}
13 changes: 13 additions & 0 deletions src/Transcriptor/Response/CookieDispatch/SetCookie.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

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

class SetCookie extends AbstractCookie
{
public function apply(): bool
{
return setcookie($this->getName(), $this->getValue(), $this->options);
}
}
Loading

0 comments on commit 1db8dbd

Please sign in to comment.