diff --git a/Classes/Annotations/ReadOnly.php b/Classes/Annotations/ReadOnly.php new file mode 100644 index 0000000..ce2e966 --- /dev/null +++ b/Classes/Annotations/ReadOnly.php @@ -0,0 +1,11 @@ +bootstrap->getActiveRequestHandler(); + if (! $handler instanceof HttpRequestHandlerInterface) { + $this->logger->debug(\get_class($handler)); + 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'); + $lock->release(); + } + return $joinPoint->getAdviceChain()->proceed($joinPoint); + } + + /** + * @Flow\Around("method(Neos\Flow\Session\Session->shutdownObject())") + * + * @param JoinPointInterface $joinPoint + */ + public function doNotSaveSession(JoinPointInterface $joinPoint) + { + if ($this->readOnly) { + return; + } + $joinPoint->getAdviceChain()->proceed($joinPoint); + } +} diff --git a/Classes/Aspects/ReadOnlyFilter.php b/Classes/Aspects/ReadOnlyFilter.php new file mode 100644 index 0000000..f7d7d04 --- /dev/null +++ b/Classes/Aspects/ReadOnlyFilter.php @@ -0,0 +1,114 @@ +configurationManager = $configurationManager; + } + + public function injectMethodTargetExpressionParser(MethodTargetExpressionParser $methodTargetExpressionParser): void + { + $this->methodTargetExpressionParser = $methodTargetExpressionParser; + } + + /** + * @inheritDoc + */ + public function matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier): bool + { + if ($this->filters === null) { + $this->buildPointcutFilters(); + } + + $matchingFilters = \array_filter( + $this->filters, + function (PointcutFilterInterface $filter) use ( + $className, + $methodName, + $methodDeclaringClassName, + $pointcutQueryIdentifier + ): bool { + return $filter->matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier); + } + ); + + if ($matchingFilters === []) { + return false; + } + + return true; + } + + /** + * @inheritDoc + */ + public function hasRuntimeEvaluationsDefinition(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function getRuntimeEvaluationsDefinition(): array + { + return []; + } + + /** + * @inheritDoc + */ + 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)); + } + return $result; + } + + protected function buildPointcutFilters(): void + { + $this->filters = []; + $readOnlyExpressions = $this->configurationManager->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'DigiComp.FlowSessionLock.readOnlyExpressions' + ) ?? []; + foreach ($readOnlyExpressions as $key => $pointcut) { + if ($pointcut === null) { + continue; + } + $this->filters[] = $this->methodTargetExpressionParser->parse( + $pointcut, + 'Settings.yaml at "DigiComp.FlowSessionLock.readOnlyExpressions", key: "' . $key . '"' + ); + } + } +} diff --git a/Classes/Http/SessionLockRequestComponent.php b/Classes/Http/SessionLockRequestComponent.php new file mode 100644 index 0000000..6a76898 --- /dev/null +++ b/Classes/Http/SessionLockRequestComponent.php @@ -0,0 +1,61 @@ +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); + } +} diff --git a/Classes/Package.php b/Classes/Package.php new file mode 100644 index 0000000..66e97be --- /dev/null +++ b/Classes/Package.php @@ -0,0 +1,31 @@ +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); + } + } + ); + } +} diff --git a/Configuration/Development/Settings.yaml b/Configuration/Development/Settings.yaml new file mode 100644 index 0000000..0760c4d --- /dev/null +++ b/Configuration/Development/Settings.yaml @@ -0,0 +1,3 @@ +DigiComp: + FlowSessionLock: + lockStoreDir: '%FLOW_PATH_DATA%Temporary/Development/SessionLocks' diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml new file mode 100644 index 0000000..6523a12 --- /dev/null +++ b/Configuration/Objects.yaml @@ -0,0 +1,12 @@ +DigiComp.FlowSessionLock:LockFactory: + className: 'Symfony\Component\Lock\LockFactory' + arguments: + 1: + object: 'DigiComp.FlowSessionLock:LockStore' + +DigiComp.FlowSessionLock:LockStore: + className: 'Symfony\Component\Lock\Store\FlockStore' + scope: 'singleton' + arguments: + 1: + setting: 'DigiComp.FlowSessionLock.lockStoreDir' diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml new file mode 100644 index 0000000..b55eb19 --- /dev/null +++ b/Configuration/Settings.yaml @@ -0,0 +1,14 @@ +Neos: + Flow: + http: + chain: + preprocess: + chain: + lockSession: + position: 'before getSessionCookieFromRequest' + component: 'DigiComp\FlowSessionLock\Http\SessionLockRequestComponent' + +DigiComp: + FlowSessionLock: + lockStoreDir: '%FLOW_PATH_DATA%Temporary/Production/SessionLocks' + readOnlyExpressions: [] diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..308915f --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "digicomp/flowsessionlock", + "description": "Sesion locking for Neos Flow", + "type": "neos-package", + "require": { + "neos/flow": "^6.2", + "php": "^7.4", + "symfony/lock": "^5.2" + }, + "autoload": { + "psr-4": { + "DigiComp\\FlowSessionLock\\": "Classes/" + } + }, + "extra": { + "neos": { + "package-key": "DigiComp.FlowSessionLock" + }, + "branch-alias": { + "dev-develop": "1.0.x-dev" + } + } +}