Merge remote-tracking branch 'composer/develop' into version/2.x-dev
This commit is contained in:
commit
069217a8a8
13 changed files with 320 additions and 145 deletions
|
@ -1,10 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace DigiComp\FlowSessionLock\Annotations;
|
namespace DigiComp\FlowSessionLock\Annotations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Annotation
|
* @Annotation
|
||||||
* @Target("METHOD")
|
* @Target({"METHOD"})
|
||||||
*/
|
*/
|
||||||
final class ReadOnly
|
final class ReadOnly
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace DigiComp\FlowSessionLock\Aspects;
|
namespace DigiComp\FlowSessionLock\Aspects;
|
||||||
|
|
||||||
use DigiComp\FlowSessionLock\Http\SessionLockRequestComponent;
|
use DigiComp\FlowSessionLock\Http\SessionLockRequestComponent;
|
||||||
|
@ -36,37 +38,43 @@ class ReadOnlyAspect
|
||||||
/**
|
/**
|
||||||
* @Flow\Around("methodAnnotatedWith(DigiComp\FlowSessionLock\Annotations\ReadOnly) || filter(DigiComp\FlowSessionLock\Aspects\ReadOnlyFilter)")
|
* @Flow\Around("methodAnnotatedWith(DigiComp\FlowSessionLock\Annotations\ReadOnly) || filter(DigiComp\FlowSessionLock\Aspects\ReadOnlyFilter)")
|
||||||
* @param JoinPointInterface $joinPoint
|
* @param JoinPointInterface $joinPoint
|
||||||
*
|
* @return mixed
|
||||||
* @return void
|
|
||||||
*/
|
*/
|
||||||
public function demoteLockToReadOnly(JoinPointInterface $joinPoint)
|
public function demoteLockToReadOnly(JoinPointInterface $joinPoint)
|
||||||
{
|
{
|
||||||
$handler = $this->bootstrap->getActiveRequestHandler();
|
$activeRequestHandler = $this->bootstrap->getActiveRequestHandler();
|
||||||
if (! $handler instanceof HttpRequestHandlerInterface) {
|
if (!$activeRequestHandler instanceof HttpRequestHandlerInterface) {
|
||||||
$this->logger->debug(\get_class($handler));
|
$this->logger->debug('SessionLock: ' . \get_class($activeRequestHandler));
|
||||||
|
|
||||||
return $joinPoint->getAdviceChain()->proceed($joinPoint);
|
return $joinPoint->getAdviceChain()->proceed($joinPoint);
|
||||||
}
|
}
|
||||||
$componentContext = $handler->getComponentContext();
|
|
||||||
/** @var Lock $lock */
|
|
||||||
$lock = $componentContext->getParameter(SessionLockRequestComponent::class, 'sessionLock');
|
|
||||||
$this->readOnly = true;
|
$this->readOnly = true;
|
||||||
if ($lock) {
|
|
||||||
$this->logger->debug('SessionLock: Release, as this is marked read only');
|
/** @var Lock|null $lock */
|
||||||
|
$lock = $activeRequestHandler->getComponentContext()->getParameter(
|
||||||
|
SessionLockRequestComponent::class,
|
||||||
|
SessionLockRequestComponent::PARAMETER_NAME
|
||||||
|
);
|
||||||
|
if ($lock !== null) {
|
||||||
|
$this->logger->debug('SessionLock: Release, as this is marked read only.');
|
||||||
$lock->release();
|
$lock->release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $joinPoint->getAdviceChain()->proceed($joinPoint);
|
return $joinPoint->getAdviceChain()->proceed($joinPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Flow\Around("method(Neos\Flow\Session\Session->shutdownObject())")
|
* @Flow\Around("method(Neos\Flow\Session\Session->shutdownObject())")
|
||||||
*
|
|
||||||
* @param JoinPointInterface $joinPoint
|
* @param JoinPointInterface $joinPoint
|
||||||
|
* @return mixed|void
|
||||||
*/
|
*/
|
||||||
public function doNotSaveSession(JoinPointInterface $joinPoint)
|
public function doNotSaveSession(JoinPointInterface $joinPoint)
|
||||||
{
|
{
|
||||||
if ($this->readOnly) {
|
if ($this->readOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$joinPoint->getAdviceChain()->proceed($joinPoint);
|
|
||||||
|
return $joinPoint->getAdviceChain()->proceed($joinPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace DigiComp\FlowSessionLock\Aspects;
|
namespace DigiComp\FlowSessionLock\Aspects;
|
||||||
|
|
||||||
use Neos\Flow\Annotations as Flow;
|
use Neos\Flow\Annotations as Flow;
|
||||||
use Neos\Flow\Aop\Builder\ClassNameIndex;
|
use Neos\Flow\Aop\Builder\ClassNameIndex;
|
||||||
|
use Neos\Flow\Aop\Exception as NeosFlowAopException;
|
||||||
|
use Neos\Flow\Aop\Exception\InvalidPointcutExpressionException;
|
||||||
use Neos\Flow\Aop\Pointcut\PointcutFilterComposite;
|
use Neos\Flow\Aop\Pointcut\PointcutFilterComposite;
|
||||||
use Neos\Flow\Aop\Pointcut\PointcutFilterInterface;
|
use Neos\Flow\Aop\Pointcut\PointcutFilterInterface;
|
||||||
use Neos\Flow\Configuration\ConfigurationManager;
|
use Neos\Flow\Configuration\ConfigurationManager;
|
||||||
|
use Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException;
|
||||||
use Neos\Flow\Security\Authorization\Privilege\Method\MethodTargetExpressionParser;
|
use Neos\Flow\Security\Authorization\Privilege\Method\MethodTargetExpressionParser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,20 +20,32 @@ use Neos\Flow\Security\Authorization\Privilege\Method\MethodTargetExpressionPars
|
||||||
*/
|
*/
|
||||||
class ReadOnlyFilter implements PointcutFilterInterface
|
class ReadOnlyFilter implements PointcutFilterInterface
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var ConfigurationManager
|
||||||
|
*/
|
||||||
protected ConfigurationManager $configurationManager;
|
protected ConfigurationManager $configurationManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var MethodTargetExpressionParser
|
||||||
|
*/
|
||||||
protected MethodTargetExpressionParser $methodTargetExpressionParser;
|
protected MethodTargetExpressionParser $methodTargetExpressionParser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var PointcutFilterComposite[]
|
* @var PointcutFilterComposite[]
|
||||||
*/
|
*/
|
||||||
protected ?array $filters = null;
|
protected ?array $pointcutFilterComposites = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConfigurationManager $configurationManager
|
||||||
|
*/
|
||||||
public function injectConfigurationManager(ConfigurationManager $configurationManager): void
|
public function injectConfigurationManager(ConfigurationManager $configurationManager): void
|
||||||
{
|
{
|
||||||
$this->configurationManager = $configurationManager;
|
$this->configurationManager = $configurationManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param MethodTargetExpressionParser $methodTargetExpressionParser
|
||||||
|
*/
|
||||||
public function injectMethodTargetExpressionParser(MethodTargetExpressionParser $methodTargetExpressionParser): void
|
public function injectMethodTargetExpressionParser(MethodTargetExpressionParser $methodTargetExpressionParser): void
|
||||||
{
|
{
|
||||||
$this->methodTargetExpressionParser = $methodTargetExpressionParser;
|
$this->methodTargetExpressionParser = $methodTargetExpressionParser;
|
||||||
|
@ -36,31 +53,29 @@ class ReadOnlyFilter implements PointcutFilterInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
|
* @throws InvalidConfigurationTypeException
|
||||||
|
* @throws InvalidPointcutExpressionException
|
||||||
|
* @throws NeosFlowAopException
|
||||||
*/
|
*/
|
||||||
public function matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier): bool
|
public function matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier): bool
|
||||||
{
|
{
|
||||||
if ($this->filters === null) {
|
|
||||||
$this->buildPointcutFilters();
|
$this->buildPointcutFilters();
|
||||||
}
|
|
||||||
|
|
||||||
$matchingFilters = \array_filter(
|
foreach ($this->pointcutFilterComposites as $pointcutFilterComposite) {
|
||||||
$this->filters,
|
if (
|
||||||
function (PointcutFilterInterface $filter) use (
|
$pointcutFilterComposite->matches(
|
||||||
$className,
|
$className,
|
||||||
$methodName,
|
$methodName,
|
||||||
$methodDeclaringClassName,
|
$methodDeclaringClassName,
|
||||||
$pointcutQueryIdentifier
|
$pointcutQueryIdentifier
|
||||||
): bool {
|
)
|
||||||
return $filter->matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier);
|
) {
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($matchingFilters === []) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
|
@ -80,33 +95,47 @@ class ReadOnlyFilter implements PointcutFilterInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
|
* @throws InvalidConfigurationTypeException
|
||||||
|
* @throws InvalidPointcutExpressionException
|
||||||
|
* @throws NeosFlowAopException
|
||||||
*/
|
*/
|
||||||
public function reduceTargetClassNames(ClassNameIndex $classNameIndex): ClassNameIndex
|
public function reduceTargetClassNames(ClassNameIndex $classNameIndex): ClassNameIndex
|
||||||
{
|
{
|
||||||
if ($this->filters === null) {
|
|
||||||
$this->buildPointcutFilters();
|
$this->buildPointcutFilters();
|
||||||
}
|
|
||||||
|
|
||||||
$result = new ClassNameIndex();
|
$result = new ClassNameIndex();
|
||||||
foreach ($this->filters as $filter) {
|
|
||||||
$result->applyUnion($filter->reduceTargetClassNames($classNameIndex));
|
foreach ($this->pointcutFilterComposites as $pointcutFilterComposite) {
|
||||||
|
$result->applyUnion($pointcutFilterComposite->reduceTargetClassNames($classNameIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidConfigurationTypeException
|
||||||
|
* @throws InvalidPointcutExpressionException
|
||||||
|
* @throws NeosFlowAopException
|
||||||
|
*/
|
||||||
protected function buildPointcutFilters(): void
|
protected function buildPointcutFilters(): void
|
||||||
{
|
{
|
||||||
$this->filters = [];
|
if ($this->pointcutFilterComposites !== null) {
|
||||||
$readOnlyExpressions = $this->configurationManager->getConfiguration(
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pointcutFilterComposites = [];
|
||||||
|
foreach (
|
||||||
|
$this->configurationManager->getConfiguration(
|
||||||
ConfigurationManager::CONFIGURATION_TYPE_SETTINGS,
|
ConfigurationManager::CONFIGURATION_TYPE_SETTINGS,
|
||||||
'DigiComp.FlowSessionLock.readOnlyExpressions'
|
'DigiComp.FlowSessionLock.readOnlyExpressions'
|
||||||
) ?? [];
|
) as $key => $pointcutExpression
|
||||||
foreach ($readOnlyExpressions as $key => $pointcut) {
|
) {
|
||||||
if ($pointcut === null) {
|
if ($pointcutExpression === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$this->filters[] = $this->methodTargetExpressionParser->parse(
|
|
||||||
$pointcut,
|
$this->pointcutFilterComposites[] = $this->methodTargetExpressionParser->parse(
|
||||||
|
$pointcutExpression,
|
||||||
'Settings.yaml at "DigiComp.FlowSessionLock.readOnlyExpressions", key: "' . $key . '"'
|
'Settings.yaml at "DigiComp.FlowSessionLock.readOnlyExpressions", key: "' . $key . '"'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,23 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace DigiComp\FlowSessionLock\Http;
|
namespace DigiComp\FlowSessionLock\Http;
|
||||||
|
|
||||||
use Neos\Flow\Annotations as Flow;
|
use Neos\Flow\Annotations as Flow;
|
||||||
use Neos\Flow\Http\Component\ComponentContext;
|
use Neos\Flow\Http\Component\ComponentContext;
|
||||||
use Neos\Flow\Http\Component\ComponentInterface;
|
use Neos\Flow\Http\Component\ComponentInterface;
|
||||||
use Neos\Flow\Utility\Environment;
|
|
||||||
use Neos\Utility\Files;
|
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Lock\Exception\LockAcquiringException;
|
||||||
use Symfony\Component\Lock\Key;
|
use Symfony\Component\Lock\Key;
|
||||||
use Symfony\Component\Lock\LockFactory;
|
use Symfony\Component\Lock\LockFactory;
|
||||||
|
|
||||||
class SessionLockRequestComponent implements ComponentInterface
|
class SessionLockRequestComponent implements ComponentInterface
|
||||||
{
|
{
|
||||||
/**
|
public const PARAMETER_NAME = 'sessionLock';
|
||||||
* @Flow\InjectConfiguration(package="Neos.Flow", path="session")
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $sessionSettings;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Flow\Inject(lazy=false)
|
* @Flow\Inject
|
||||||
* @var LoggerInterface
|
* @var LoggerInterface
|
||||||
*/
|
*/
|
||||||
protected $logger;
|
protected $logger;
|
||||||
|
@ -31,6 +28,18 @@ class SessionLockRequestComponent implements ComponentInterface
|
||||||
*/
|
*/
|
||||||
protected $lockFactory;
|
protected $lockFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Flow\InjectConfiguration(package="Neos.Flow", path="session")
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected array $sessionSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="timeToLive")
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
protected float $timeToLive;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="autoRelease")
|
* @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="autoRelease")
|
||||||
* @var bool
|
* @var bool
|
||||||
|
@ -38,36 +47,41 @@ class SessionLockRequestComponent implements ComponentInterface
|
||||||
protected bool $autoRelease;
|
protected bool $autoRelease;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="timeToLive")
|
* @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="secondsToWait")
|
||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
protected int $timeToLive;
|
protected int $secondsToWait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function handle(ComponentContext $componentContext)
|
public function handle(ComponentContext $componentContext): void
|
||||||
{
|
{
|
||||||
$sessionCookieName = $this->sessionSettings['name'];
|
$sessionCookieName = $this->sessionSettings['name'];
|
||||||
$request = $componentContext->getHttpRequest();
|
|
||||||
$cookies = $request->getCookieParams();
|
|
||||||
|
|
||||||
|
$cookies = $componentContext->getHttpRequest()->getCookieParams();
|
||||||
if (!isset($cookies[$sessionCookieName])) {
|
if (!isset($cookies[$sessionCookieName])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sessionIdentifier = $cookies[$sessionCookieName];
|
// TODO: sessionIdentifier might be wrong, probably it should probably be storage identifier
|
||||||
|
$key = new Key('session-' . $cookies[$sessionCookieName]);
|
||||||
$key = new Key(
|
|
||||||
'session-' . $sessionIdentifier
|
|
||||||
); //TODO: sessionIdentifier might be wrong, probably it should probably be storage identifier
|
|
||||||
|
|
||||||
$lock = $this->lockFactory->createLockFromKey($key, $this->timeToLive, $this->autoRelease);
|
$lock = $this->lockFactory->createLockFromKey($key, $this->timeToLive, $this->autoRelease);
|
||||||
|
|
||||||
$componentContext->setParameter(SessionLockRequestComponent::class, 'sessionLock', $lock);
|
$componentContext->setParameter(SessionLockRequestComponent::class, static::PARAMETER_NAME, $lock);
|
||||||
|
|
||||||
$this->logger->debug('SessionLock: Get ' . $key);
|
$this->logger->debug('SessionLock: Try to get "' . $key . '"');
|
||||||
$lock->acquire(true);
|
$timedOut = \time() + $this->secondsToWait;
|
||||||
$this->logger->debug('SessionLock: Acquired ' . $key);
|
while (!$lock->acquire()) {
|
||||||
|
if (\time() >= $timedOut) {
|
||||||
|
throw new LockAcquiringException(
|
||||||
|
'Could not acquire the lock for "' . $key . '" in ' . $this->secondsToWait . ' seconds.',
|
||||||
|
1652687960
|
||||||
|
);
|
||||||
|
}
|
||||||
|
\usleep(100000);
|
||||||
|
}
|
||||||
|
$this->logger->debug('SessionLock: Acquired "' . $key . '"');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace DigiComp\FlowSessionLock;
|
|
||||||
|
|
||||||
use Neos\Flow\Configuration\ConfigurationManager;
|
|
||||||
use Neos\Flow\Core\Bootstrap;
|
|
||||||
use Neos\Flow\Package\Package as BasePackage;
|
|
||||||
use Neos\Utility\Files;
|
|
||||||
|
|
||||||
class Package extends BasePackage
|
|
||||||
{
|
|
||||||
public function boot(Bootstrap $bootstrap)
|
|
||||||
{
|
|
||||||
parent::boot($bootstrap);
|
|
||||||
$dispatcher = $bootstrap->getSignalSlotDispatcher();
|
|
||||||
|
|
||||||
$dispatcher->connect(
|
|
||||||
ConfigurationManager::class,
|
|
||||||
'configurationManagerReady',
|
|
||||||
function (ConfigurationManager $configurationManager) {
|
|
||||||
$lockStoreDir = $configurationManager->getConfiguration(
|
|
||||||
ConfigurationManager::CONFIGURATION_TYPE_SETTINGS,
|
|
||||||
'DigiComp.FlowSessionLock.lockStoreDir'
|
|
||||||
);
|
|
||||||
if (is_string($lockStoreDir)) {
|
|
||||||
Files::createDirectoryRecursively($lockStoreDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +1,3 @@
|
||||||
DigiComp:
|
DigiComp:
|
||||||
FlowSessionLock:
|
FlowSessionLock:
|
||||||
lockStoreDir: '%FLOW_PATH_DATA%Temporary/Development/SessionLocks'
|
lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Development/SessionLocks/"
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
DigiComp.FlowSessionLock:LockFactory:
|
DigiComp.FlowSessionLock:LockFactory:
|
||||||
className: 'Symfony\Component\Lock\LockFactory'
|
className: "Symfony\\Component\\Lock\\LockFactory"
|
||||||
arguments:
|
arguments:
|
||||||
1:
|
1:
|
||||||
object: 'DigiComp.FlowSessionLock:LockStore'
|
object:
|
||||||
|
factoryObjectName: "Symfony\\Component\\Lock\\Store\\StoreFactory"
|
||||||
DigiComp.FlowSessionLock:LockStore:
|
factoryMethodName: "createStore"
|
||||||
className: 'Symfony\Component\Lock\Store\FlockStore'
|
|
||||||
scope: 'singleton'
|
|
||||||
arguments:
|
arguments:
|
||||||
1:
|
1:
|
||||||
setting: 'DigiComp.FlowSessionLock.lockStoreDir'
|
setting: "DigiComp.FlowSessionLock.lockStoreConnection"
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
DigiComp:
|
||||||
|
FlowSessionLock:
|
||||||
|
lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Production/SessionLocks/"
|
||||||
|
timeToLive: 300.0
|
||||||
|
autoRelease: true
|
||||||
|
secondsToWait: 30
|
||||||
|
readOnlyExpressions: {}
|
||||||
|
|
||||||
Neos:
|
Neos:
|
||||||
Flow:
|
Flow:
|
||||||
http:
|
http:
|
||||||
|
@ -5,12 +13,5 @@ Neos:
|
||||||
preprocess:
|
preprocess:
|
||||||
chain:
|
chain:
|
||||||
lockSession:
|
lockSession:
|
||||||
position: 'before getSessionCookieFromRequest'
|
position: "before getSessionCookieFromRequest"
|
||||||
component: 'DigiComp\FlowSessionLock\Http\SessionLockRequestComponent'
|
component: "DigiComp\\FlowSessionLock\\Http\\SessionLockRequestComponent"
|
||||||
|
|
||||||
DigiComp:
|
|
||||||
FlowSessionLock:
|
|
||||||
lockStoreDir: '%FLOW_PATH_DATA%Temporary/Production/SessionLocks'
|
|
||||||
readOnlyExpressions: []
|
|
||||||
autoRelease: true
|
|
||||||
timeToLive: 300
|
|
||||||
|
|
5
Configuration/Testing/Settings.yaml
Normal file
5
Configuration/Testing/Settings.yaml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
DigiComp:
|
||||||
|
FlowSessionLock:
|
||||||
|
lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Testing/SessionLocks/"
|
||||||
|
readOnlyExpressions:
|
||||||
|
TestUnprotected: "method(DigiComp\\FlowSessionLock\\Tests\\Functional\\Fixtures\\Controller\\ExampleController->unprotectedByConfigurationAction())"
|
13
README.md
13
README.md
|
@ -1,13 +1,16 @@
|
||||||
DigiComp.FlowSessionLock
|
DigiComp.FlowSessionLock
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
By default the session established by Flow is not "protected" in any way.
|
By default, the session established by Flow is not "protected" in any way. This package restricts every request to load
|
||||||
This package restricts every request to load the session only, if there are no other requests having it in access currently.
|
the session only, if there are no other requests having it in access currently. It allows to set custom pointcut which
|
||||||
It allows to set custom pointcut which will set the session in "ReadOnly" mode, which allows concurrent requests to read, but disallows the current request to write the session.
|
will set the session in "ReadOnly" mode, which allows concurrent requests to read, but disallows the current request to
|
||||||
|
write the session.
|
||||||
|
|
||||||
If you want to allow concurrent access somewhere, you can add your trigger pointcut in Settings.yaml like such:
|
If you want to allow concurrent access somewhere, you can add your trigger pointcut in `Settings.yaml` like such:
|
||||||
|
|
||||||
|
```yaml
|
||||||
DigiComp:
|
DigiComp:
|
||||||
FlowSessionLock:
|
FlowSessionLock:
|
||||||
readOnlyExpressions:
|
readOnlyExpressions:
|
||||||
'AcmeLock': 'method(Acme/SuperPackage/Controller/ConcurrentController->concurrentAction())'
|
MyLock: "method(My\\Package\\Controller\\MyController->myAction())"
|
||||||
|
```
|
||||||
|
|
43
Tests/Functional/Fixtures/Controller/ExampleController.php
Normal file
43
Tests/Functional/Fixtures/Controller/ExampleController.php
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DigiComp\FlowSessionLock\Tests\Functional\Fixtures\Controller;
|
||||||
|
|
||||||
|
use DigiComp\FlowSessionLock\Annotations as FlowSessionLock;
|
||||||
|
use Neos\Flow\Annotations as Flow;
|
||||||
|
use Neos\Flow\Mvc\Controller\ActionController;
|
||||||
|
|
||||||
|
class ExampleController extends ActionController
|
||||||
|
{
|
||||||
|
public const CONTROLLER_TIME = 200;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Flow\Session(autoStart=true);
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function protectedAction()
|
||||||
|
{
|
||||||
|
\usleep(static::CONTROLLER_TIME * 1000);
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Flow\Session(autoStart=true);
|
||||||
|
* @FlowSessionLock\ReadOnly
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function unprotectedByAnnotationAction()
|
||||||
|
{
|
||||||
|
\usleep(static::CONTROLLER_TIME * 1000);
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Flow\Session(autoStart=true);
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function unprotectedByConfigurationAction()
|
||||||
|
{
|
||||||
|
\usleep(static::CONTROLLER_TIME * 1000);
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
100
Tests/Functional/SessionLockRequestComponentTest.php
Normal file
100
Tests/Functional/SessionLockRequestComponentTest.php
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DigiComp\FlowSessionLock\Tests\Functional;
|
||||||
|
|
||||||
|
use DigiComp\FlowSessionLock\Tests\Functional\Fixtures\Controller\ExampleController;
|
||||||
|
use GuzzleHttp\Psr7\Uri;
|
||||||
|
use Neos\Flow\Http\Cookie;
|
||||||
|
use Neos\Flow\Mvc\Routing\Route;
|
||||||
|
use Neos\Flow\Tests\FunctionalTestCase;
|
||||||
|
use Psr\Http\Message\ServerRequestFactoryInterface;
|
||||||
|
|
||||||
|
class SessionLockRequestComponentTest extends FunctionalTestCase
|
||||||
|
{
|
||||||
|
protected ServerRequestFactoryInterface $serverRequestFactory;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->serverRequestFactory = $this->objectManager->get(ServerRequestFactoryInterface::class);
|
||||||
|
$route = new Route();
|
||||||
|
$route->setName('Functional Test - SessionRequestComponent::Restricted');
|
||||||
|
$route->setUriPattern('test/sessionlock/{@action}');
|
||||||
|
$route->setDefaults([
|
||||||
|
'@package' => 'DigiComp.FlowSessionLock',
|
||||||
|
'@subpackage' => 'Tests\Functional\Fixtures',
|
||||||
|
'@controller' => 'Example',
|
||||||
|
'@action' => 'protected',
|
||||||
|
'@format' => 'html',
|
||||||
|
]);
|
||||||
|
$route->setAppendExceedingArguments(true);
|
||||||
|
$this->router->addRoute($route);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function expectedDuration(): array
|
||||||
|
{
|
||||||
|
$parallelChecker = function ($allRequests, $oneRequest) {
|
||||||
|
self::assertGreaterThan(ExampleController::CONTROLLER_TIME, $oneRequest * 1000);
|
||||||
|
self::assertLessThan(ExampleController::CONTROLLER_TIME * 4, $allRequests * 1000);
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'http://localhost/test/sessionlock/protected',
|
||||||
|
function ($allRequests, $oneRequest) {
|
||||||
|
self::assertGreaterThan(ExampleController::CONTROLLER_TIME, $oneRequest * 1000);
|
||||||
|
self::assertGreaterThan(ExampleController::CONTROLLER_TIME * 4, $allRequests * 1000);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'http://localhost/test/sessionlock/unprotectedbyannotation',
|
||||||
|
$parallelChecker
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'http://localhost/test/sessionlock/unprotectedbyconfiguration',
|
||||||
|
$parallelChecker
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider expectedDuration
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function itDoesNotAllowToEnterMoreThanOneWithTheSameSession(string $url, \Closure $checker): void
|
||||||
|
{
|
||||||
|
$request = $this->serverRequestFactory
|
||||||
|
->createServerRequest('GET', new Uri($url));
|
||||||
|
$start = microtime(true);
|
||||||
|
$response = $this->browser->sendRequest($request);
|
||||||
|
$neededForOne = microtime(true) - $start;
|
||||||
|
|
||||||
|
$sessionCookies = array_map(static function ($cookie) {
|
||||||
|
return Cookie::createFromRawSetCookieHeader($cookie);
|
||||||
|
}, $response->getHeader('Set-Cookie'));
|
||||||
|
self::assertNotEmpty($sessionCookies);
|
||||||
|
|
||||||
|
$cookies = array_reduce($sessionCookies, static function ($out, $cookie) {
|
||||||
|
$out[$cookie->getName()] = $cookie->getValue();
|
||||||
|
return $out;
|
||||||
|
}, []);
|
||||||
|
$nextRequest = $this->serverRequestFactory
|
||||||
|
->createServerRequest('GET', new Uri($url))
|
||||||
|
->withCookieParams($cookies);
|
||||||
|
$childs = [];
|
||||||
|
$start = microtime(true);
|
||||||
|
for ($i = 0; $i < 4; $i++) {
|
||||||
|
$child = \pcntl_fork();
|
||||||
|
if ($child === 0) {
|
||||||
|
$this->browser->sendRequest($nextRequest);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
$childs[] = $child;
|
||||||
|
}
|
||||||
|
foreach ($childs as $child) {
|
||||||
|
\pcntl_waitpid($child, $status);
|
||||||
|
}
|
||||||
|
$neededForAll = microtime(true) - $start;
|
||||||
|
|
||||||
|
$checker($neededForAll, $neededForOne);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,25 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "digicomp/flowsessionlock",
|
"name": "digicomp/flowsessionlock",
|
||||||
|
"description": "Session locking for Neos Flow - it secures the session becoming corrupted by concurrent access to the same session by different requests",
|
||||||
"type": "neos-package",
|
"type": "neos-package",
|
||||||
"description": "Sesion locking for Neos Flow - it secures the session becoming corrupted by concurrent access to the same session by different requests",
|
|
||||||
"keywords": [
|
|
||||||
"flow",
|
|
||||||
"neos"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Ferdinand Kuhl",
|
|
||||||
"email": "f.kuhl@digital-competence.de",
|
|
||||||
"homepage": "http://www.digital-competence.de",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"homepage": "https://github.com/digicomp/DigiComp.FlowSessionLock",
|
|
||||||
"require": {
|
"require": {
|
||||||
"neos/flow": "^6.2",
|
"neos/flow": "^6.3.0",
|
||||||
"php": "^7.4",
|
"php": ">=7.4",
|
||||||
"symfony/lock": "^5.2"
|
"symfony/lock": "^5.2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-pcntl": "*"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
@ -34,5 +23,19 @@
|
||||||
"dev-develop": "3.0.x-dev",
|
"dev-develop": "3.0.x-dev",
|
||||||
"dev-version/2.x-dev": "2.1.x-dev"
|
"dev-version/2.x-dev": "2.1.x-dev"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Ferdinand Kuhl",
|
||||||
|
"email": "f.kuhl@digital-competence.de",
|
||||||
|
"homepage": "https://www.digital-competence.de",
|
||||||
|
"role": "Developer"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://github.com/digital-competence/FlowSessionLock",
|
||||||
|
"keywords": [
|
||||||
|
"Neos",
|
||||||
|
"Flow"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue