First working version

This commit is contained in:
Ferdinand Kuhl 2021-08-26 15:05:37 +02:00
parent ef6ce83d51
commit a2c7fabfea
9 changed files with 341 additions and 0 deletions

View file

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

View file

@ -0,0 +1,72 @@
<?php
namespace DigiComp\FlowSessionLock\Aspects;
use DigiComp\FlowSessionLock\Http\SessionLockRequestComponent;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface;
use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Http\HttpRequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Lock\Lock;
/**
* @Flow\Aspect
* @Flow\Scope("singleton")
*/
class ReadOnlyAspect
{
/**
* @Flow\Inject
* @var Bootstrap
*/
protected $bootstrap;
/**
* @Flow\Inject
* @var LoggerInterface
*/
protected $logger;
/**
* @var bool
*/
protected bool $readOnly = false;
/**
* @Flow\Around("methodAnnotatedWith(DigiComp\FlowSessionLock\Annotations\ReadOnly) || filter(DigiComp\FlowSessionLock\Aspects\ReadOnlyFilter)")
* @param JoinPointInterface $joinPoint
*
* @return void
*/
public function demoteLockToReadOnly(JoinPointInterface $joinPoint)
{
$handler = $this->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);
}
}

View file

@ -0,0 +1,114 @@
<?php
namespace DigiComp\FlowSessionLock\Aspects;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\Builder\ClassNameIndex;
use Neos\Flow\Aop\Pointcut\PointcutFilterComposite;
use Neos\Flow\Aop\Pointcut\PointcutFilterInterface;
use Neos\Flow\Configuration\ConfigurationManager;
use Neos\Flow\Security\Authorization\Privilege\Method\MethodTargetExpressionParser;
/**
* @Flow\Proxy(false)
* @Flow\Scope("singleton")
*/
class ReadOnlyFilter implements PointcutFilterInterface
{
protected ConfigurationManager $configurationManager;
protected MethodTargetExpressionParser $methodTargetExpressionParser;
/**
* @var PointcutFilterComposite[]
*/
protected ?array $filters = null;
public function injectConfigurationManager(ConfigurationManager $configurationManager): void
{
$this->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 . '"'
);
}
}
}

View file

@ -0,0 +1,61 @@
<?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);
}
}

31
Classes/Package.php Normal file
View file

@ -0,0 +1,31 @@
<?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

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

View file

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

View file

@ -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: []

23
composer.json Normal file
View file

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