Merge branch 'feature/flow-7.3' into develop
Some checks failed
ci/woodpecker/push/code-style Pipeline failed

This commit is contained in:
Ferdinand Kuhl 2022-08-01 00:23:32 +02:00
commit 9e3d1fd2ac
15 changed files with 399 additions and 202 deletions

View file

@ -0,0 +1,7 @@
pipeline:
code-style:
image: composer
commands:
- composer global config repositories.repo-name vcs https://git.digital-competence.de/Packages/php-codesniffer
- composer global require digicomp/php-codesniffer:@dev
- composer global exec -- phpcs --runtime-set ignore_warnings_on_exit 1 --standard=DigiComp Classes/ Tests/

View file

@ -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
{ {

View file

@ -1,8 +1,10 @@
<?php <?php
declare(strict_types=1);
namespace DigiComp\FlowSessionLock\Aspects; namespace DigiComp\FlowSessionLock\Aspects;
use DigiComp\FlowSessionLock\Http\SessionLockRequestComponent; use DigiComp\FlowSessionLock\Http\SessionLockRequestMiddleware;
use Neos\Flow\Annotations as Flow; use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface; use Neos\Flow\Aop\JoinPointInterface;
use Neos\Flow\Core\Bootstrap; use Neos\Flow\Core\Bootstrap;
@ -36,37 +38,42 @@ 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->getHttpRequest()->getAttribute(
SessionLockRequestMiddleware::class . '.' . SessionLockRequestMiddleware::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);
} }
} }

View file

@ -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 . '"'
); );
} }

View file

@ -1,73 +0,0 @@
<?php
namespace DigiComp\FlowSessionLock\Http;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Http\Component\ComponentContext;
use Neos\Flow\Http\Component\ComponentInterface;
use Neos\Flow\Utility\Environment;
use Neos\Utility\Files;
use Psr\Log\LoggerInterface;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\LockFactory;
class SessionLockRequestComponent implements ComponentInterface
{
/**
* @Flow\InjectConfiguration(package="Neos.Flow", path="session")
* @var array
*/
protected $sessionSettings;
/**
* @Flow\Inject(lazy=false)
* @var LoggerInterface
*/
protected $logger;
/**
* @Flow\Inject(name="DigiComp.FlowSessionLock:LockFactory")
* @var LockFactory
*/
protected $lockFactory;
/**
* @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="autoRelease")
* @var bool
*/
protected bool $autoRelease;
/**
* @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="timeToLive")
* @var int
*/
protected int $timeToLive;
/**
* @inheritDoc
*/
public function handle(ComponentContext $componentContext)
{
$sessionCookieName = $this->sessionSettings['name'];
$request = $componentContext->getHttpRequest();
$cookies = $request->getCookieParams();
if (!isset($cookies[$sessionCookieName])) {
return;
}
$sessionIdentifier = $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);
$componentContext->setParameter(SessionLockRequestComponent::class, 'sessionLock', $lock);
$this->logger->debug('SessionLock: Get ' . $key);
$lock->acquire(true);
$this->logger->debug('SessionLock: Acquired ' . $key);
}
}

View file

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace DigiComp\FlowSessionLock\Http;
use Neos\Flow\Annotations as Flow;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\LockFactory;
class SessionLockRequestMiddleware implements MiddlewareInterface
{
public const PARAMETER_NAME = 'sessionLock';
/**
* @Flow\Inject
* @var LoggerInterface
*/
protected $logger;
/**
* @Flow\Inject(name="DigiComp.FlowSessionLock:LockFactory")
* @var 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")
* @var bool
*/
protected bool $autoRelease;
/**
* @Flow\InjectConfiguration(package="DigiComp.FlowSessionLock", path="secondsToWait")
* @var int
*/
protected int $secondsToWait;
/**
* @inheritDoc
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$sessionCookieName = $this->sessionSettings['name'];
$cookies = $request->getCookieParams();
if (!isset($cookies[$sessionCookieName])) {
return $handler->handle($request);
}
// TODO: sessionIdentifier might be wrong, probably it should probably be storage identifier
$key = new Key('session-' . $cookies[$sessionCookieName]);
$lock = $this->lockFactory->createLockFromKey($key, $this->timeToLive, $this->autoRelease);
$request = $request->withAttribute(SessionLockRequestMiddleware::class . '.' . static::PARAMETER_NAME, $lock);
$this->logger->debug('SessionLock: Try to get "' . $key . '"');
$timedOut = \time() + $this->secondsToWait;
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 . '"');
return $handler->handle($request);
}
}

View file

@ -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);
}
}
);
}
}

View file

@ -1,3 +1,3 @@
DigiComp: DigiComp:
FlowSessionLock: FlowSessionLock:
lockStoreDir: '%FLOW_PATH_DATA%Temporary/Development/SessionLocks' lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Development/SessionLocks/"

View file

@ -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"

View file

@ -1,16 +1,15 @@
DigiComp:
FlowSessionLock:
lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Production/SessionLocks/"
timeToLive: 300.0
autoRelease: true
secondsToWait: 30
readOnlyExpressions: {}
Neos: Neos:
Flow: Flow:
http: http:
chain: middlewares:
preprocess:
chain:
lockSession: lockSession:
position: 'before getSessionCookieFromRequest' position: "before session"
component: 'DigiComp\FlowSessionLock\Http\SessionLockRequestComponent' middleware: "DigiComp\\FlowSessionLock\\Http\\SessionLockRequestMiddleware"
DigiComp:
FlowSessionLock:
lockStoreDir: '%FLOW_PATH_DATA%Temporary/Production/SessionLocks'
readOnlyExpressions: []
autoRelease: true
timeToLive: 300

View 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())"

View file

@ -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())"
```

View 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!';
}
}

View file

@ -0,0 +1,104 @@
<?php
namespace DigiComp\FlowSessionLock\Tests\Functional;
use DigiComp\FlowSessionLock\Tests\Functional\Fixtures\Controller\ExampleController;
use GuzzleHttp\Psr7\Uri;
use Neos\Flow\Composer\ComposerUtility;
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
{
// Functional tests are currently broken, until a version containing
// https://github.com/neos/flow-development-collection/commit/bebfc4e6566bc4ba2ba28330344105adb2d6ada0
// gets released
$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);
}
}

View file

@ -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": "^7.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": {
@ -31,7 +20,32 @@
"package-key": "DigiComp.FlowSessionLock" "package-key": "DigiComp.FlowSessionLock"
}, },
"branch-alias": { "branch-alias": {
"dev-develop": "1.0.x-dev" "dev-develop": "3.0.x-dev"
} },
"applied-flow-migrations": [
"Neos.SwiftMailer-20161130105617",
"Neos.Flow-20180415105700",
"Neos.Flow-20190425144900",
"Neos.Flow-20190515215000",
"Neos.Flow-20200813181400",
"Neos.Flow-20201003165200",
"Neos.Flow-20201109224100",
"Neos.Flow-20201205172733",
"Neos.Flow-20201207104500"
]
},
"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"
]
} }