From d5f93e52fb34b415bfe811ddcda81e8f1b45451b Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Sat, 21 Dec 2013 01:13:26 +0100 Subject: [PATCH] Session management is now part of Phraseanet configuration --- CHANGELOG.md | 1 + config/configuration.sample.yml | 4 + lib/Alchemy/Phrasea/Application.php | 7 ++ .../Configuration/SessionHandlerFactory.php | 76 +++++++++++++ .../SessionHandlerServiceProvider.php | 37 ++++++ .../Phrasea/Utilities/RedisSessionHandler.php | 101 +++++++++++++++++ lib/conf.d/configuration.yml | 4 + .../Core/Configuration/PropertyAccessTest.php | 77 +------------ .../SessionHandlerServiceProviderTest.php | 105 ++++++++++++++++++ tests/Alchemy/Tests/Phrasea/MockArrayConf.php | 70 ++++++++++++ .../TaskManager/TaskManagerStatusTest.php | 75 +------------ 11 files changed, 416 insertions(+), 141 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Core/Configuration/SessionHandlerFactory.php create mode 100644 lib/Alchemy/Phrasea/Core/Provider/SessionHandlerServiceProvider.php create mode 100644 lib/Alchemy/Phrasea/Utilities/RedisSessionHandler.php create mode 100644 tests/Alchemy/Tests/Phrasea/Core/Provider/SessionHandlerServiceProviderTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/MockArrayConf.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 852b34c1ec..dd5c1867ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Convert Users custom adapter to Doctrine entity. - Convert Ftp Export custom adapter to Doctrine entity. - Convert Ftp Export custom adapter to Doctrine entity. + - Session management is now part of Phraseanet configuration. * 3.8.2 (2013-11-15) diff --git a/config/configuration.sample.yml b/config/configuration.sample.yml index a643d72f75..9151ee0dc0 100644 --- a/config/configuration.sample.yml +++ b/config/configuration.sample.yml @@ -28,6 +28,10 @@ main: search-engine: type: Alchemy\Phrasea\SearchEngine\Phrasea\PhraseaEngine options: [] + session: + type: 'file' + options: [] + ttl: 86400 binaries: ghostscript_binary: null php_binary: null diff --git a/lib/Alchemy/Phrasea/Application.php b/lib/Alchemy/Phrasea/Application.php index 5652658598..7a26cece82 100644 --- a/lib/Alchemy/Phrasea/Application.php +++ b/lib/Alchemy/Phrasea/Application.php @@ -102,6 +102,7 @@ use Alchemy\Phrasea\Core\Provider\PluginServiceProvider; use Alchemy\Phrasea\Core\Provider\PhraseaVersionServiceProvider; use Alchemy\Phrasea\Core\Provider\RegistrationServiceProvider; use Alchemy\Phrasea\Core\Provider\SearchEngineServiceProvider; +use Alchemy\Phrasea\Core\Provider\SessionHandlerServiceProvider; use Alchemy\Phrasea\Core\Provider\SubdefServiceProvider; use Alchemy\Phrasea\Core\Provider\TasksServiceProvider; use Alchemy\Phrasea\Core\Provider\TemporaryFilesystemServiceProvider; @@ -295,9 +296,15 @@ class Application extends SilexApplication }); $this->register(new SearchEngineServiceProvider()); + + $this->register(new SessionHandlerServiceProvider()); $this->register(new SessionServiceProvider(), [ 'session.test' => $this->getEnvironment() === static::ENV_TEST ]); + $this['session.storage.handler'] = $this->share(function ($app) { + return $this['session.storage.handler.factory']->create($app['conf']); + }); + $this->register(new ServiceControllerServiceProvider()); $this->register(new SwiftmailerServiceProvider()); $this->register(new TasksServiceProvider()); diff --git a/lib/Alchemy/Phrasea/Core/Configuration/SessionHandlerFactory.php b/lib/Alchemy/Phrasea/Core/Configuration/SessionHandlerFactory.php new file mode 100644 index 0000000000..98a42cb63f --- /dev/null +++ b/lib/Alchemy/Phrasea/Core/Configuration/SessionHandlerFactory.php @@ -0,0 +1,76 @@ +connectionFactory = $connectionFactory; + $this->root = __DIR__ . '/../../../../..'; + } + + /** + * Creates a SessionHandlerInterface given a conf. + * + * @param PropertyAccess $conf + * + * @return \SessionHandlerInterface + * + * @throws \Alchemy\Phrasea\Exception\RuntimeException + */ + public function create(PropertyAccess $conf) + { + $type = $conf->get(['main', 'session', 'type'], 'file'); + $options = $conf->get(['main', 'session', 'options'], []); + $serverOpts = [ + 'expiretime' => $conf->get(['main', 'session', 'ttl'], 86400), + 'prefix' => $conf->get(['main', 'key']), + ]; + + switch (strtolower($type)) { + case 'memcache': + return new WriteCheckSessionHandler( + new MemcacheSessionHandler( + $this->connectionFactory->getMemcacheConnection($options, $serverOpts) + ) + ); + case 'memcached': + return new WriteCheckSessionHandler( + new MemcachedSessionHandler( + $this->connectionFactory->getMemcachedConnection($options, $serverOpts) + ) + ); + case 'file': + return new NativeFileSessionHandler($this->root.'/tmp/sessions'); + case 'redis': + return new WriteCheckSessionHandler( + new RedisSessionHandler( + $this->connectionFactory->getRedisConnection($options, $serverOpts) + ) + ); + } + + throw new RuntimeException(sprintf('Unable to create the specified session handler "%s"', $type)); + } +} diff --git a/lib/Alchemy/Phrasea/Core/Provider/SessionHandlerServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/SessionHandlerServiceProvider.php new file mode 100644 index 0000000000..4bec2de142 --- /dev/null +++ b/lib/Alchemy/Phrasea/Core/Provider/SessionHandlerServiceProvider.php @@ -0,0 +1,37 @@ +share(function (Application $app) { + return new SessionHandlerFactory($app['cache.connection-factory'], $app['root.path']); + }); + } + + /** + * {@inheritdoc} + */ + public function boot(Application $app) + { + } +} diff --git a/lib/Alchemy/Phrasea/Utilities/RedisSessionHandler.php b/lib/Alchemy/Phrasea/Utilities/RedisSessionHandler.php new file mode 100644 index 0000000000..b8118da568 --- /dev/null +++ b/lib/Alchemy/Phrasea/Utilities/RedisSessionHandler.php @@ -0,0 +1,101 @@ + + */ +class RedisSessionHandler implements \SessionHandlerInterface +{ + /** + * @var \Redis + */ + private $redis; + + /** + * @var integer + */ + private $lifetime; + + /** + * @var string Key prefix for shared environments. + */ + private $prefix; + + /** + * Constructor + * + * @param \Redis $redis The redis instance + * @param array $options An associative array of Memcached options + * + * @throws \InvalidArgumentException When unsupported options are passed + */ + public function __construct(\Redis $redis, array $options = array()) + { + $this->redis = $redis; + + if ($diff = array_diff(array_keys($options), array('prefix', 'expiretime'))) { + throw new \InvalidArgumentException(sprintf( + 'The following options are not supported "%s"', implode(', ', $diff) + )); + } + + $this->lifetime = isset($options['expiretime']) ? (int) $options['expiretime'] : 86400; + $this->prefix = isset($options['prefix']) ? $options['prefix'] : 'sf2s'; + } + + /** + * {@inheritDoc} + */ + public function open($savePath, $sessionName) + { + return true; + } + + /** + * {@inheritDoc} + */ + public function read($sessionId) + { + return $this->redis->get($this->prefix.$sessionId) ?: ''; + } + + /** + * {@inheritDoc} + */ + public function write($sessionId, $data) + { + return $this->redis->setex($this->prefix.$sessionId, $this->lifetime, $data); + } + + /** + * {@inheritDoc} + */ + public function destroy($sessionId) + { + return 1 === $this->redis->delete($this->prefix.$sessionId); + } + + /** + * {@inheritDoc} + */ + public function gc($lifetime) + { + return true; + } + + /** + * {@inheritDoc} + */ + public function close() + { + return true; + } +} diff --git a/lib/conf.d/configuration.yml b/lib/conf.d/configuration.yml index fc9f62929c..3cafee1617 100644 --- a/lib/conf.d/configuration.yml +++ b/lib/conf.d/configuration.yml @@ -28,6 +28,10 @@ main: search-engine: type: Alchemy\Phrasea\SearchEngine\Phrasea\PhraseaEngine options: [] + session: + type: 'file' + options: [] + ttl: 86400 binaries: ghostscript_binary: null php_binary: null diff --git a/tests/Alchemy/Tests/Phrasea/Core/Configuration/PropertyAccessTest.php b/tests/Alchemy/Tests/Phrasea/Core/Configuration/PropertyAccessTest.php index 42252da4da..40fc50cef9 100644 --- a/tests/Alchemy/Tests/Phrasea/Core/Configuration/PropertyAccessTest.php +++ b/tests/Alchemy/Tests/Phrasea/Core/Configuration/PropertyAccessTest.php @@ -2,7 +2,7 @@ namespace Alchemy\Tests\Phrasea\Core\Configuration; -use Alchemy\Phrasea\Core\Configuration\ConfigurationInterface; +use Alchemy\Tests\Phrasea\MockArrayConf; use Alchemy\Phrasea\Core\Configuration\PropertyAccess; class PropertyAccessTest extends \PhraseanetTestCase @@ -12,7 +12,7 @@ class PropertyAccessTest extends \PhraseanetTestCase */ public function testGet($conf, $props, $expected, $default) { - $propAccess = new PropertyAccess(new ArrayConf($conf)); + $propAccess = new PropertyAccess(new MockArrayConf($conf)); $this->assertSame($expected, $propAccess->get($props, $default)); } @@ -21,7 +21,7 @@ class PropertyAccessTest extends \PhraseanetTestCase */ public function testHas($conf, $props, $expected) { - $propAccess = new PropertyAccess(new ArrayConf($conf)); + $propAccess = new PropertyAccess(new MockArrayConf($conf)); $this->assertSame($expected, $propAccess->has($props)); } @@ -30,7 +30,7 @@ class PropertyAccessTest extends \PhraseanetTestCase */ public function testSet($conf, $props, $value, $expectedConf) { - $conf = new ArrayConf($conf); + $conf = new MockArrayConf($conf); $propAccess = new PropertyAccess($conf); $this->assertSame($value, $propAccess->set($props, $value)); $this->assertSame($expectedConf, $conf->getConfig()); @@ -41,7 +41,7 @@ class PropertyAccessTest extends \PhraseanetTestCase */ public function testRemove($conf, $props, $expectedReturnValue, $expectedConf) { - $conf = new ArrayConf($conf); + $conf = new MockArrayConf($conf); $propAccess = new PropertyAccess($conf); $this->assertSame($expectedReturnValue, $propAccess->remove($props)); $this->assertSame($expectedConf, $conf->getConfig()); @@ -52,7 +52,7 @@ class PropertyAccessTest extends \PhraseanetTestCase */ public function testMerge($conf, $props, $value, $expectedReturnValue, $expectedConf) { - $conf = new ArrayConf($conf); + $conf = new MockArrayConf($conf); $propAccess = new PropertyAccess($conf); $this->assertSame($expectedReturnValue, $propAccess->merge($props, $value)); $this->assertSame($expectedConf, $conf->getConfig()); @@ -143,68 +143,3 @@ class PropertyAccessTest extends \PhraseanetTestCase ]; } } - -class ArrayConf implements ConfigurationInterface -{ - private $conf; - - public function __construct(array $conf) - { - $this->conf = $conf; - } - - public function getConfig() - { - return $this->conf; - } - - public function setConfig(array $config) - { - $this->conf = $config; - } - - public function offsetGet($offset) - { - throw new \Exception('not implemented'); - } - - public function offsetSet($offset, $value) - { - throw new \Exception('not implemented'); - } - - public function offsetUnset($offset) - { - throw new \Exception('not implemented'); - } - - public function offsetExists($offset) - { - throw new \Exception('not implemented'); - } - - public function setDefault($name) - { - throw new \Exception('not implemented'); - } - - public function initialize() - { - throw new \Exception('not implemented'); - } - - public function delete() - { - throw new \Exception('not implemented'); - } - - public function isSetup() - { - throw new \Exception('not implemented'); - } - - public function compileAndWrite() - { - throw new \Exception('not implemented'); - } -} diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/SessionHandlerServiceProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/SessionHandlerServiceProviderTest.php new file mode 100644 index 0000000000..456d3727c8 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/SessionHandlerServiceProviderTest.php @@ -0,0 +1,105 @@ +register(new SessionServiceProvider()); + $app->register(new SessionHandlerServiceProvider()); + + $app['conf'] = new PropertyAccess(new MockArrayConf(['main' => ['session' => $sessionConf ]])); + $app['cache.connection-factory'] = $this->getMockBuilder('Alchemy\Phrasea\Cache\ConnectionFactory') + ->disableOriginalConstructor() + ->getMock(); + if ($method) { + $app['cache.connection-factory']->expects($this->once()) + ->method($method) + ->with($options) + ->will($this->returnValue($mock)); + } + + $handler = $app['session.storage.handler.factory']->create($app['conf']); + $this->assertInstanceOf($expectedInstance, $handler); + } + + public function provideVariousConfs() + { + $memcache = $this->getMockBuilder('Memcache') + ->disableOriginalConstructor() + ->getMock(); + + $memcached = $this->getMockBuilder('Memcached') + ->disableOriginalConstructor() + ->getMock(); + + $redis = $this->getMockBuilder('Redis') + ->disableOriginalConstructor() + ->getMock(); + + return [ + [ + [ + 'type' => 'memcache', + 'options' => [ + 'host' => 'localhost', + 'port' => '11211', + ] + ], + 'Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler', + 'getMemcacheConnection', + ['host' => 'localhost', 'port' => 11211], + $memcache + ], + [ + [ + 'type' => 'memcached', + 'options' => [ + 'host' => 'localhost', + 'port' => '11211', + ] + ], + 'Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler', + 'getMemcachedConnection', + ['host' => 'localhost', 'port' => 11211], + $memcached + ], + [ + [ + 'type' => 'redis', + 'options' => [ + 'host' => '127.0.0.1', + 'port' => '6379', + ] + ], + 'Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler', + 'getRedisConnection', + ['host' => '127.0.0.1', 'port' => 6379], + $redis + ], + [ + [ + 'main' => [ + 'session' => [ + 'type' => 'file', + ] + ] + ], + 'Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler' + ] + ]; + } +} diff --git a/tests/Alchemy/Tests/Phrasea/MockArrayConf.php b/tests/Alchemy/Tests/Phrasea/MockArrayConf.php new file mode 100644 index 0000000000..bf5c9e5aae --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/MockArrayConf.php @@ -0,0 +1,70 @@ +conf = $conf; + } + + public function getConfig() + { + return $this->conf; + } + + public function setConfig(array $config) + { + $this->conf = $config; + } + + public function offsetGet($key) + { + return $this->conf[$key]; + } + + public function offsetSet($key, $value) + { + $this->conf[$key] = $value; + } + + public function offsetExists($key) + { + return isset($this->conf[$key]); + } + + public function offsetUnset($key) + { + unset($this->conf[$key]); + } + + public function setDefault($name) + { + throw new \Exception('not implemented'); + } + + public function initialize() + { + throw new \Exception('not implemented'); + } + + public function delete() + { + throw new \Exception('not implemented'); + } + + public function isSetup() + { + throw new \Exception('not implemented'); + } + + public function compileAndWrite() + { + throw new \Exception('not implemented'); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/TaskManager/TaskManagerStatusTest.php b/tests/Alchemy/Tests/Phrasea/TaskManager/TaskManagerStatusTest.php index 80fa434cee..ecd7c53e1a 100644 --- a/tests/Alchemy/Tests/Phrasea/TaskManager/TaskManagerStatusTest.php +++ b/tests/Alchemy/Tests/Phrasea/TaskManager/TaskManagerStatusTest.php @@ -3,7 +3,7 @@ namespace Alchemy\Tests\Phrasea\TaskManager; use Alchemy\Phrasea\TaskManager\TaskManagerStatus; -use Alchemy\Phrasea\Core\Configuration\ConfigurationInterface; +use Alchemy\Tests\Phrasea\MockArrayConf; class TaskManagerStatusTest extends \PhraseanetTestCase { @@ -12,7 +12,7 @@ class TaskManagerStatusTest extends \PhraseanetTestCase */ public function testStart($initialData) { - $conf = new ConfigurationTest($initialData); + $conf = new MockArrayConf($initialData); $expected = $conf->getConfig(); $expected['main']['task-manager']['status'] = TaskManagerStatus::STATUS_STARTED; @@ -41,7 +41,7 @@ class TaskManagerStatusTest extends \PhraseanetTestCase */ public function testStop($initialData) { - $conf = new ConfigurationTest($initialData); + $conf = new MockArrayConf($initialData); $expected = $conf->getConfig(); $expected['main']['task-manager']['status'] = TaskManagerStatus::STATUS_STOPPED; @@ -56,7 +56,7 @@ class TaskManagerStatusTest extends \PhraseanetTestCase */ public function testIsRunning($data, $expectedStatus, $isRunning) { - $conf = new ConfigurationTest($data); + $conf = new MockArrayConf($data); $status = new TaskManagerStatus($conf); $this->assertEquals($isRunning, $status->isRunning()); } @@ -77,73 +77,8 @@ class TaskManagerStatusTest extends \PhraseanetTestCase */ public function testGetStatus($data, $expectedStatus, $isRunning) { - $conf = new ConfigurationTest($data); + $conf = new MockArrayConf($data); $status = new TaskManagerStatus($conf); $this->assertEquals($expectedStatus, $status->getStatus()); } } - -class ConfigurationTest implements ConfigurationInterface -{ - private $data = []; - - public function __construct(array $data) - { - $this->data = $data; - } - - public function offsetGet($key) - { - return $this->data[$key]; - } - - public function offsetSet($key, $value) - { - $this->data[$key] = $value; - } - - public function offsetExists($key) - { - return isset($this->data[$key]); - } - - public function offsetUnset($key) - { - unset($this->data[$key]); - } - - public function getConfig() - { - return $this->data; - } - - public function initialize() - { - throw new \RuntimeException('This method should not be used here'); - } - - public function delete() - { - throw new \RuntimeException('This method should not be used here'); - } - - public function isSetup() - { - throw new \RuntimeException('This method should not be used here'); - } - - public function setDefault($name) - { - throw new \RuntimeException('This method should not be used here'); - } - - public function setConfig(array $config) - { - throw new \RuntimeException('This method should not be used here'); - } - - public function compileAndWrite() - { - throw new \RuntimeException('This method should not be used here'); - } -}