Session management is now part of Phraseanet configuration

This commit is contained in:
Romain Neutron
2013-12-21 01:13:26 +01:00
parent a5d815b667
commit d5f93e52fb
11 changed files with 416 additions and 141 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,76 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Configuration;
use Alchemy\Phrasea\Cache\ConnectionFactory;
use Alchemy\Phrasea\Exception\RuntimeException;
use Alchemy\Phrasea\Utilities\RedisSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcachedSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcacheSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler;
class SessionHandlerFactory
{
private $connectionFactory;
private $root;
public function __construct(ConnectionFactory $connectionFactory, $root = null)
{
$this->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));
}
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Provider;
use Alchemy\Phrasea\Core\Configuration\SessionHandlerFactory;
use Silex\Application;
use Silex\ServiceProviderInterface;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler;
class SessionHandlerServiceProvider implements ServiceProviderInterface
{
/**
* {@inheritdoc}
*/
public function register(Application $app)
{
$app['session.storage.handler.factory'] = $app->share(function (Application $app) {
return new SessionHandlerFactory($app['cache.connection-factory'], $app['root.path']);
});
}
/**
* {@inheritdoc}
*/
public function boot(Application $app)
{
}
}

View File

@@ -0,0 +1,101 @@
<?php
/*
* This file has been written by Markus Bachmann and released under the MIT
* license.
*/
namespace Alchemy\Phrasea\Utilities;
/**
* RedisSessionHandler
*
* @author Markus Bachmann <markus.bachmann@bachi.biz>
*/
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;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,105 @@
<?php
namespace Alchemy\Tests\Phrasea\Core\Provider;
use Alchemy\Phrasea\Core\Configuration\PropertyAccess;
use Alchemy\Phrasea\Core\Provider\SessionHandlerServiceProvider;
use Alchemy\Tests\Tools\TranslatorMockTrait;
use Alchemy\Tests\Phrasea\MockArrayConf;
use Silex\Application;
use Silex\Provider\SessionServiceProvider;
class SessionHandlerServiceProviderTest extends \PhraseanetTestCase
{
/**
* @dataProvider provideVariousConfs
*/
public function testWithVariousConf($sessionConf, $expectedInstance, $method = null, $options = null, $mock = null)
{
$app = new Application();
$app['root.path'] = __DIR__ . '/../../../../../..';
$app->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'
]
];
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Alchemy\Tests\Phrasea;
use Alchemy\Phrasea\Core\Configuration\ConfigurationInterface;
class MockArrayConf 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($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');
}
}

View File

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