-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
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
There are no files selected for viewing
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 GitHub Actions / Build and test on 8.1UndefinedThisPropertyAssignment
|
||
$this->triggerCookieValue = bin2hex(random_bytes(5)); | ||
Check failure on line 33 in src/Transcriptor/CookieDispatcher.php GitHub Actions / Build and test on 8.1UndefinedThisPropertyAssignment
|
||
} | ||
|
||
public function __call($name, $arguments) | ||
Check failure on line 36 in src/Transcriptor/CookieDispatcher.php GitHub Actions / Build and test on 8.1MissingParamType
|
||
{ | ||
$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 GitHub Actions / Build and test on 8.1MixedArgument
|
||
// 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 GitHub Actions / Build and test on 8.1InvalidArrayOffset
|
||
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; | ||
} | ||
} |
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 GitHub Actions / Build and test on 8.1InvalidArrayOffset
|
||
// 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 GitHub Actions / Build and test on 8.1MixedAssignment
|
||
// 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 GitHub Actions / Build and test on 8.1MixedArgument
|
||
$reflexDispatcher->setAccessible(false); | ||
} | ||
} |
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; | ||
} | ||
} |
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; | ||
} |
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; | ||
} |
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(); | ||
} | ||
} |
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); | ||
} | ||
} |