Compare commits

...

62 commits

Author SHA1 Message Date
22f19da3e5 Merge branch 'release/4.0.2'
All checks were successful
ci/woodpecker/manual/code-style Pipeline was successful
ci/woodpecker/manual/functional-tests Pipeline was successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
2023-01-26 13:04:13 +01:00
c29106fb9f trying to understand matrix building
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
2023-01-26 13:02:02 +01:00
0a24e50d08 Allow Flow 8.2
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
2023-01-26 12:43:03 +01:00
978200451a Merge tag '4.0.1' into develop
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
Tagging 4.0.1
2023-01-10 13:37:28 +01:00
1ccc533984 Merge branch 'release/4.0.1'
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
2023-01-10 13:37:24 +01:00
872f799ec9 Allow symfony/lock 6.2.0
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
2023-01-10 13:25:31 +01:00
c09d5e0d1b Merge tag '4.0.0' into develop
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
Tagging 4.0.0
2023-01-10 12:03:39 +01:00
e541b6f0e5 Merge branch 'release/4.0.0'
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
2023-01-10 12:03:35 +01:00
afc89b5cbd Merge branch 'feature/php8.1' into develop 2023-01-10 12:03:18 +01:00
719de0e53f - Allow PHP 7.4 again, as no PHP 8.1 features are necessary to work
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
2023-01-10 12:00:53 +01:00
12d048e0ca - try to fix CI trouble without error message
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
2023-01-10 11:59:18 +01:00
6e1079a81c Allow PHP 8.1
Some checks failed
ci/woodpecker/push/functional-tests Pipeline failed
ci/woodpecker/push/code-style Pipeline was successful
- for that resolve a keyword conflict with the ReadOnly class
2023-01-10 11:55:58 +01:00
190db833fc Merge tag '3.0.0' into develop
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
Tagging 3.0.0
2022-08-30 14:46:14 +02:00
edf3c15935 Merge branch 'release/3.0.0'
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
2022-08-30 14:46:10 +02:00
a9487d312d Adding build status to README.md
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
2022-08-30 14:46:03 +02:00
deddac89ad Adding functional tests
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
ci/woodpecker/push/functional-tests Pipeline was successful
2022-08-30 14:43:13 +02:00
1c3b713c2d Removing obsolete class, slipped in during experimenting with automatic skipping
All checks were successful
ci/woodpecker/push/code-style Pipeline was successful
2022-08-01 01:05:38 +02:00
84e52eaa68 Allow phpcodesniffer composer installer scripts in CI
Some checks failed
ci/woodpecker/push/code-style Pipeline failed
2022-08-01 01:02:58 +02:00
6c42398c72 Merge branch 'version/2.x-dev' into develop
Some checks failed
ci/woodpecker/push/code-style Pipeline failed
2022-08-01 00:48:10 +02:00
9f7dab7513 Allow phpcodesniffer composer installer scripts in CI
Some checks failed
ci/woodpecker/push/code-style Pipeline failed
2022-08-01 00:37:15 +02:00
069217a8a8 Merge remote-tracking branch 'composer/develop' into version/2.x-dev 2022-08-01 00:33:50 +02:00
a1b08d7adc Merge branch 'version/2.x-dev' into develop
Some checks failed
ci/woodpecker/push/code-style Pipeline failed
2022-08-01 00:31:07 +02:00
e7c8492b74 Updating branch aliases 2022-08-01 00:29:51 +02:00
9e3d1fd2ac Merge branch 'feature/flow-7.3' into develop
Some checks failed
ci/woodpecker/push/code-style Pipeline failed
2022-08-01 00:23:32 +02:00
aa150cea9f Adding code-style build pipeline
Some checks failed
ci/woodpecker/push/code-style Pipeline failed
2022-08-01 00:18:22 +02:00
287ef82621 note about broken functional tests 2022-08-01 00:17:55 +02:00
9f30b80d39 Marking breaking change 2022-07-31 23:52:38 +02:00
7c13ff8e71 PHPCBF fixes 2022-07-31 23:51:35 +02:00
0ae4ab9269 do not use static class name as key in proxied classes (changes after compile run to "_Original") 2022-06-01 00:25:02 +02:00
28c6c8ef59 [WIP] migrates SessionLockRequestComponent to new MiddlewareInterface 2022-05-19 11:53:26 +02:00
d7dbff9fc2 TASK: Apply migration Neos.Flow-20201207104500
Adjust code to deprecation of ComponentContext/ComponentParameters

Note: This migration did not produce any changes, so the commit simply
marks the migration as applied. This makes sure it will not be applied
again.
2022-05-19 11:51:43 +02:00
e58a157037 TASK: Apply migration Neos.Flow-20201205172733
This migration does not actually change any code. It just displays a
warning if a PHP file still refers to the no longer existing
ComponentInterface

Note: This migration did not produce any changes, so the commit simply
marks the migration as applied. This makes sure it will not be applied
again.
2022-05-19 11:51:43 +02:00
5d8e93084b TASK: Apply migration Neos.Flow-20201109224100
Adjust DB migrations to Doctrine Migrations 3.0

Note: This migration did not produce any changes, so the commit simply
marks the migration as applied. This makes sure it will not be applied
again.
2022-05-19 11:51:43 +02:00
4dfb3b8568 TASK: Apply migration Neos.Flow-20201003165200
Make default ValueObjects embedded=false

Note: This migration did not produce any changes, so the commit simply
marks the migration as applied. This makes sure it will not be applied
again.
2022-05-19 11:51:43 +02:00
7306320814 TASK: Apply migration Neos.Flow-20200813181400
Adjust "Settings.yaml" to new naming of settings (see
https://github.com/neos/flow-development-collection/pull/2051)

Note: This migration did not produce any changes, so the commit simply
marks the migration as applied. This makes sure it will not be applied
again.
2022-05-19 11:51:43 +02:00
5ed22d20f0 TASK: Apply migration Neos.Flow-20190515215000
Adjust "Settings.yaml" to new PSR-3 logging settings (see
https://github.com/neos/flow-development-collection/pull/1574)

Note: This migration did not produce any changes, so the commit simply
marks the migration as applied. This makes sure it will not be applied
again.
2022-05-19 11:51:43 +02:00
d3714249e5 TASK: Apply migration Neos.Flow-20190425144900
Adjusts code to FlashMessageContainer renaming from "\Neos\Flow\Mvc" to
"\Neos\Flow\Mvc\FlashMessage".

Note: This migration did not produce any changes, so the commit simply
marks the migration as applied. This makes sure it will not be applied
again.
2022-05-19 11:51:43 +02:00
abfb8332b3 TASK: Apply migration Neos.Flow-20180415105700
Add scalar type hint to CacheAwareInterface implementations.

Note: This migration did not produce any changes, so the commit simply
marks the migration as applied. This makes sure it will not be applied
again.
2022-05-19 11:51:43 +02:00
f7914f7a4b TASK: Apply migration Neos.SwiftMailer-20161130105617
Adjusts code to package renaming from "TYPO3.SwiftMailer" to
"Neos.SwiftMailer".

Note: This migration did not produce any changes, so the commit simply
marks the migration as applied. This makes sure it will not be applied
again.
2022-05-19 11:51:43 +02:00
aaacd95a06 Merge pull request 'Adding a functional test which covers all functions' (#1) from feature/tests into develop
Reviewed-on: #1
2022-05-19 11:01:33 +02:00
b9ed514ef2 Adding a functional test which covers all functions 2022-05-17 09:54:09 +02:00
3cde6b589a Merge tag '2.1.2' into develop
Tagging 2.1.2
2022-05-16 12:08:57 +02:00
5e62c2b02d Merge branch 'release/2.1.2' 2022-05-16 12:08:53 +02:00
24283b795f Check first, then wait 2022-05-16 12:08:39 +02:00
d294df5079 Merge tag '2.1.1' into develop
Tagging 2.1.1
2022-05-16 12:04:43 +02:00
f0880cdf13 Merge branch 'release/2.1.1' 2022-05-16 12:04:40 +02:00
1452d19892 Safe one unneccessary check against redis lock 2022-05-16 12:04:25 +02:00
ac56fee761 Merge tag '2.1.0' into develop
Tagging 2.1.0
2022-05-16 11:30:32 +02:00
da747f2af8 Merge branch 'release/2.1.0' 2022-05-16 11:30:28 +02:00
ec13bc4960 Do not wait until Lock acquiry but a configurable amount of time (30s per default) 2022-05-16 11:28:39 +02:00
Robin Krahnen
2e45f615f8 Merge branch 'release/2.0.0' 2022-05-04 23:27:23 +02:00
Robin Krahnen
a5ce21d554 Merge tag '2.0.0' into develop
2.0.0
2022-05-04 23:27:23 +02:00
Robin Krahnen
8a94d4ec8c Merge branch 'feature/revision' into develop 2022-05-04 23:26:53 +02:00
Robin Krahnen
2879b0730e optimized versions in composer.json 2022-05-04 19:01:15 +02:00
Robin Krahnen
f93dfd0df6 add "declare(strict_types=1);" 2022-05-02 09:56:10 +02:00
Robin Krahnen
f784fb3d3f revised code 2022-04-20 17:36:30 +02:00
Robin Krahnen
e7ca78f855 code style 2022-04-05 15:19:26 +02:00
Robin Krahnen
737047d856 do load lazy if possible 2022-04-05 12:31:33 +02:00
Robin Krahnen
7420419663 change dependency to neos/flow 2022-04-04 22:41:28 +02:00
Robin Krahnen
3b3566bb73 revised code 2022-03-15 09:32:42 +01:00
759b50d2b0 makes timeToLive and autoRelease configurable
* for example redis store as lock storage needs the autoRelease to be true
2022-03-14 08:53:06 +01:00
42bd77abb4 Merge tag '1.0.0' into develop
Tagging 1.0.0
2021-08-26 15:19:48 +02:00
17 changed files with 445 additions and 199 deletions

View file

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

View file

@ -0,0 +1,31 @@
workspace:
base: /woodpecker
path: package
matrix:
include:
- FLOW_VERSION: 7.3
PHP_VERSION: 7.4
- FLOW_VERSION: 7.3
PHP_VERSION: 8.1
- FLOW_VERSION: 8.2
PHP_VERSION: 8.1
pipeline:
functional-tests:
image: "thecodingmachine/php:${PHP_VERSION}-v4-cli"
environment:
# Enable the PDO_SQLITE extension
- "PHP_EXTENSION_PDO_SQLITE=1"
- "FLOW_VERSION=${FLOW_VERSION}"
- "NEOS_BUILD_DIR=/woodpecker/Build-${FLOW_VERSION}"
commands:
- "sudo mkdir $NEOS_BUILD_DIR"
- "sudo chown -R docker:docker $NEOS_BUILD_DIR"
- "cd $NEOS_BUILD_DIR"
- "composer create-project --no-install neos/flow-base-distribution:^$FLOW_VERSION ."
- "composer config repositories.repo-name path /woodpecker/package"
- "composer config --no-plugins allow-plugins.neos/composer-plugin true"
- "composer require digicomp/flowsessionlock:@dev"
- "bin/phpunit --configuration Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/DigiComp.FlowSessionLock/Tests/Functional"

View file

@ -1,11 +0,0 @@
<?php
namespace DigiComp\FlowSessionLock\Annotations;
/**
* @Annotation
* @Target("METHOD")
*/
final class ReadOnly
{
}

View file

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace DigiComp\FlowSessionLock\Annotations;
/**
* @Annotation
* @Target({"METHOD"})
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
final class Unlock
{
}

View file

@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
namespace DigiComp\FlowSessionLock\Aspects;
use DigiComp\FlowSessionLock\Http\SessionLockRequestComponent;
use DigiComp\FlowSessionLock\Http\SessionLockRequestMiddleware;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface;
use Neos\Flow\Core\Bootstrap;
@ -34,39 +36,44 @@ class ReadOnlyAspect
protected bool $readOnly = false;
/**
* @Flow\Around("methodAnnotatedWith(DigiComp\FlowSessionLock\Annotations\ReadOnly) || filter(DigiComp\FlowSessionLock\Aspects\ReadOnlyFilter)")
* @Flow\Around("methodAnnotatedWith(DigiComp\FlowSessionLock\Annotations\Unlock) || filter(DigiComp\FlowSessionLock\Aspects\ReadOnlyFilter)")
* @param JoinPointInterface $joinPoint
*
* @return void
* @return mixed
*/
public function demoteLockToReadOnly(JoinPointInterface $joinPoint)
{
$handler = $this->bootstrap->getActiveRequestHandler();
if (! $handler instanceof HttpRequestHandlerInterface) {
$this->logger->debug(\get_class($handler));
$activeRequestHandler = $this->bootstrap->getActiveRequestHandler();
if (!$activeRequestHandler instanceof HttpRequestHandlerInterface) {
$this->logger->debug('SessionLock: ' . \get_class($activeRequestHandler));
return $joinPoint->getAdviceChain()->proceed($joinPoint);
}
$componentContext = $handler->getComponentContext();
/** @var Lock $lock */
$lock = $componentContext->getParameter(SessionLockRequestComponent::class, 'sessionLock');
$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();
}
return $joinPoint->getAdviceChain()->proceed($joinPoint);
}
/**
* @Flow\Around("method(Neos\Flow\Session\Session->shutdownObject())")
*
* @param JoinPointInterface $joinPoint
* @return mixed|void
*/
public function doNotSaveSession(JoinPointInterface $joinPoint)
{
if ($this->readOnly) {
return;
}
$joinPoint->getAdviceChain()->proceed($joinPoint);
return $joinPoint->getAdviceChain()->proceed($joinPoint);
}
}

View file

@ -1,12 +1,17 @@
<?php
declare(strict_types=1);
namespace DigiComp\FlowSessionLock\Aspects;
use Neos\Flow\Annotations as Flow;
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\PointcutFilterInterface;
use Neos\Flow\Configuration\ConfigurationManager;
use Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException;
use Neos\Flow\Security\Authorization\Privilege\Method\MethodTargetExpressionParser;
/**
@ -15,20 +20,32 @@ use Neos\Flow\Security\Authorization\Privilege\Method\MethodTargetExpressionPars
*/
class ReadOnlyFilter implements PointcutFilterInterface
{
/**
* @var ConfigurationManager
*/
protected ConfigurationManager $configurationManager;
/**
* @var MethodTargetExpressionParser
*/
protected MethodTargetExpressionParser $methodTargetExpressionParser;
/**
* @var PointcutFilterComposite[]
*/
protected ?array $filters = null;
protected ?array $pointcutFilterComposites = null;
/**
* @param ConfigurationManager $configurationManager
*/
public function injectConfigurationManager(ConfigurationManager $configurationManager): void
{
$this->configurationManager = $configurationManager;
}
/**
* @param MethodTargetExpressionParser $methodTargetExpressionParser
*/
public function injectMethodTargetExpressionParser(MethodTargetExpressionParser $methodTargetExpressionParser): void
{
$this->methodTargetExpressionParser = $methodTargetExpressionParser;
@ -36,31 +53,29 @@ class ReadOnlyFilter implements PointcutFilterInterface
/**
* @inheritDoc
* @throws InvalidConfigurationTypeException
* @throws InvalidPointcutExpressionException
* @throws NeosFlowAopException
*/
public function matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier): bool
{
if ($this->filters === null) {
$this->buildPointcutFilters();
}
$matchingFilters = \array_filter(
$this->filters,
function (PointcutFilterInterface $filter) use (
foreach ($this->pointcutFilterComposites as $pointcutFilterComposite) {
if (
$pointcutFilterComposite->matches(
$className,
$methodName,
$methodDeclaringClassName,
$pointcutQueryIdentifier
): bool {
return $filter->matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier);
}
);
if ($matchingFilters === []) {
return false;
}
)
) {
return true;
}
}
return false;
}
/**
* @inheritDoc
@ -80,33 +95,47 @@ class ReadOnlyFilter implements PointcutFilterInterface
/**
* @inheritDoc
* @throws InvalidConfigurationTypeException
* @throws InvalidPointcutExpressionException
* @throws NeosFlowAopException
*/
public function reduceTargetClassNames(ClassNameIndex $classNameIndex): ClassNameIndex
{
if ($this->filters === null) {
$this->buildPointcutFilters();
}
$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;
}
/**
* @throws InvalidConfigurationTypeException
* @throws InvalidPointcutExpressionException
* @throws NeosFlowAopException
*/
protected function buildPointcutFilters(): void
{
$this->filters = [];
$readOnlyExpressions = $this->configurationManager->getConfiguration(
if ($this->pointcutFilterComposites !== null) {
return;
}
$this->pointcutFilterComposites = [];
foreach (
$this->configurationManager->getConfiguration(
ConfigurationManager::CONFIGURATION_TYPE_SETTINGS,
'DigiComp.FlowSessionLock.readOnlyExpressions'
) ?? [];
foreach ($readOnlyExpressions as $key => $pointcut) {
if ($pointcut === null) {
) as $key => $pointcutExpression
) {
if ($pointcutExpression === null) {
continue;
}
$this->filters[] = $this->methodTargetExpressionParser->parse(
$pointcut,
$this->pointcutFilterComposites[] = $this->methodTargetExpressionParser->parse(
$pointcutExpression,
'Settings.yaml at "DigiComp.FlowSessionLock.readOnlyExpressions", key: "' . $key . '"'
);
}

View file

@ -1,61 +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;
/**
* @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, 300, false);
$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:
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:
className: 'Symfony\Component\Lock\LockFactory'
className: "Symfony\\Component\\Lock\\LockFactory"
arguments:
1:
object: 'DigiComp.FlowSessionLock:LockStore'
DigiComp.FlowSessionLock:LockStore:
className: 'Symfony\Component\Lock\Store\FlockStore'
scope: 'singleton'
object:
factoryObjectName: "Symfony\\Component\\Lock\\Store\\StoreFactory"
factoryMethodName: "createStore"
arguments:
1:
setting: 'DigiComp.FlowSessionLock.lockStoreDir'
setting: "DigiComp.FlowSessionLock.lockStoreConnection"

View file

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

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,18 @@
DigiComp.FlowSessionLock
------------------------
By default the session established by Flow is not "protected" in any way.
This package restricts every request to load the session only, if there are no other requests having it in access currently.
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.
![Build status](https://ci.digital-competence.de/api/badges/Packages/DigiComp.FlowSessionLock/status.svg)
If you want to allow concurrent access somewhere, you can add your trigger pointcut in Settings.yaml like such:
By default, the session established by Flow is not "protected" in any way. This package restricts every request to load
the session only, if there are no other requests having it in access currently. 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.
If you want to allow concurrent access somewhere, you can add your trigger pointcut in `Settings.yaml` like such:
```yaml
DigiComp:
FlowSessionLock:
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\Unlock
* @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,103 @@
<?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
{
// 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",
"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",
"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": {
"neos/flow": "^6.2",
"php": "^7.4",
"symfony/lock": "^5.2"
"neos/flow": "^7.3.0 | ^8.2",
"php": "^7.4 | ^8.1",
"symfony/lock": "^5.2.0 | ^6.2.0"
},
"require-dev": {
"ext-pcntl": "*"
},
"autoload": {
"psr-4": {
@ -31,7 +20,33 @@
"package-key": "DigiComp.FlowSessionLock"
},
"branch-alias": {
"dev-develop": "1.0.x-dev"
}
"dev-develop": "4.0.x-dev",
"dev-version/2.x-dev": "2.1.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"
]
}