diff --git a/composer.json b/composer.json index 0897e66..b1618d2 100644 --- a/composer.json +++ b/composer.json @@ -22,5 +22,6 @@ , "require": { "php": ">=5.3.2" , "guzzle/guzzle": "v2.0.2" + , "symfony/http-foundation": "dev-master" } } \ No newline at end of file diff --git a/composer.lock b/composer.lock index 4018db3..9d06440 100644 --- a/composer.lock +++ b/composer.lock @@ -1,5 +1,5 @@ { - "hash": "bd52a853cdf4e34ae75e805f32ed97ae", + "hash": "d7129e7aaad8e0eb4a90bca173a7cfe2", "packages": [ { "package": "doctrine/common", @@ -15,6 +15,11 @@ "version": "dev-master", "source-reference": "b98d68d3b8513c62d35504570f09e9d3dc33d083" }, + { + "package": "symfony/http-foundation", + "version": "dev-master", + "source-reference": "c42a11f51217244a1b57fa45bb2da5f1edcee010" + }, { "package": "symfony/validator", "version": "dev-master", diff --git a/src/Ratchet/Component/Session/Serialize/HandlerInterface.php b/src/Ratchet/Component/Session/Serialize/HandlerInterface.php new file mode 100644 index 0000000..42774b1 --- /dev/null +++ b/src/Ratchet/Component/Session/Serialize/HandlerInterface.php @@ -0,0 +1,16 @@ +_app = $app; + $this->_handler = $handler; + $this->_null = new NullSessionHandler; + + ini_set('session.auto_start', 0); + ini_set('session.cache_limiter', ''); + ini_set('session.use_cookies', 0); + + $this->setOptions($options); + + if (null === $serializer) { + $serialClass = __NAMESPACE__ . "\\Serialize\\{$this->toClassCase(ini_get('session.serialize_handler'))}Handler"; // awesome/terrible hack, eh? + if (!class_exists($serialClass)) { + throw new \RuntimeExcpetion('Unable to parse session serialize handler'); + } + + $serializer = new $serialClass; + } + + $this->_serializer = $serializer; + } + + /** + * {@inheritdoc} + */ + function onOpen(ConnectionInterface $conn) { + if (null === ($id = $conn->WebSocket->headers->getCookie(ini_get('session.name')))) { + $saveHandler = $this->_null; + $id = ''; + } else { + $saveHandler = $this->_handler; + } + + $conn->Session = new Session(new VirtualSessionStorage($saveHandler, $id, $this->_serializer)); + + if (ini_get('session.auto_start')) { + $conn->Session->start(); + } + + return $this->_app->onOpen($conn); + } + + /** + * {@inheritdoc} + */ + function onMessage(ConnectionInterface $from, $msg) { + return $this->_app->onMessage($from, $msg); + } + + /** + * {@inheritdoc} + */ + function onClose(ConnectionInterface $conn) { + // "close" session for Connection + + return $this->_app->onClose($conn); + } + + /** + * {@inheritdoc} + */ + function onError(ConnectionInterface $conn, \Exception $e) { + return $this->_app->onError($conn, $e); + } + + /** + * Set all the php session. ini options + * © Symfony + * @param array + * @return array + */ + protected function setOptions(array $options) { + $all = array( + 'auto_start', 'cache_limiter', 'cookie_domain', 'cookie_httponly', + 'cookie_lifetime', 'cookie_path', 'cookie_secure', + 'entropy_file', 'entropy_length', 'gc_divisor', + 'gc_maxlifetime', 'gc_probability', 'hash_bits_per_character', + 'hash_function', 'name', 'referer_check', + 'serialize_handler', 'use_cookies', + 'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled', + 'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name', + 'upload_progress.freq', 'upload_progress.min-freq', 'url_rewriter.tags' + ); + + foreach ($all as $key) { + if (!array_key_exists($key, $options)) { + $options[$key] = ini_get("session.{$key}"); + } else { + ini_set("session.{$key}", $options[$key]); + } + } + + return $options; + } + + /** + * @param string Input to convert + * @return string + */ + protected function toClassCase($langDef) { + return str_replace(' ', '', ucwords(str_replace('_', ' ', $langDef))); + } +} \ No newline at end of file diff --git a/src/Ratchet/Component/Session/Storage/Proxy/VirtualProxy.php b/src/Ratchet/Component/Session/Storage/Proxy/VirtualProxy.php new file mode 100644 index 0000000..25ce486 --- /dev/null +++ b/src/Ratchet/Component/Session/Storage/Proxy/VirtualProxy.php @@ -0,0 +1,55 @@ +saveHandlerName = 'user'; + $this->_sessionName = ini_get('session.name'); + } + + /** + * {@inheritdoc} + */ + public function getId() { + return $this->_sessionId; + } + + /** + * {@inheritdoc} + */ + public function setId($id) { + $this->_sessionId = $id; + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->_sessionName; + } + + /** + * DO NOT CALL THIS METHOD + * @param string + * @throws RuntimeException + */ + public function setName($name) { + throw new \RuntimeException("Can not change session name in VirtualProxy"); + } +} \ No newline at end of file diff --git a/src/Ratchet/Component/Session/Storage/VirtualSessionStorage.php b/src/Ratchet/Component/Session/Storage/VirtualSessionStorage.php new file mode 100644 index 0000000..795e4f4 --- /dev/null +++ b/src/Ratchet/Component/Session/Storage/VirtualSessionStorage.php @@ -0,0 +1,81 @@ +setSaveHandler($handler); + $this->saveHandler->setId($sessionId); + $this->_serializer = $serializer; + } + + /** + * {@inheritdoc} + */ + public function start() { + if ($this->started && !$this->closed) { + return true; + } + + $rawData = $this->saveHandler->read($this->saveHandler->getId()); + $sessionData = $this->_serializer->unserialize($rawData); + + $this->loadSession($sessionData); + + if (!$this->saveHandler->isWrapper() && !$this->saveHandler->isSessionHandlerInterface()) { + $this->saveHandler->setActive(false); + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function regenerate($destroy = false) { + // .. ? + } + + /** + * {@inheritdoc} + */ + public function save() { + // get the data from the bags? + // serialize the data + // save the data using the saveHandler +// $this->saveHandler->write($this->saveHandler->getId(), + + if (!$this->saveHandler->isWrapper() && !$this->getSaveHandler()->isSessionHandlerInterface()) { + $this->saveHandler->setActive(false); + } + + $this->closed = true; + } + + /** + * {@inheritdoc} + */ + public function setSaveHandler($saveHandler = null) { + if (!($saveHandler instanceof \SessionHandlerInterface)) { + throw new \InvalidArgumentException('Handler must be instance of SessionHandlerInterface'); + } + + if (!($saveHandler instanceof \VirtualProxy)) { + $saveHandler = new VirtualProxy($saveHandler); + } + + $this->saveHandler = $saveHandler; + } +} \ No newline at end of file diff --git a/tests/Ratchet/Tests/Component/Session/Serialize/PhpHandlerTest.php b/tests/Ratchet/Tests/Component/Session/Serialize/PhpHandlerTest.php new file mode 100644 index 0000000..0528817 --- /dev/null +++ b/tests/Ratchet/Tests/Component/Session/Serialize/PhpHandlerTest.php @@ -0,0 +1,36 @@ +_handler = new PhpHandler; + } + + public function serializedProvider() { + return array( + array( + '_sf2_attributes|a:2:{s:5:"hello";s:5:"world";s:4:"last";i:1332872102;}_sf2_flashes|a:0:{}' + , array( + '_sf2_attributes' => array( + 'hello' => 'world' + , 'last' => 1332872102 + ) + , '_sf2_flashes' => array() + ) + ) + ); + } + + /** + * @dataProvider serializedProvider + */ + public function testUnserialize($in, $expected) { + $this->assertEquals($expected, $this->_handler->unserialize($in)); + } +} \ No newline at end of file diff --git a/tests/Ratchet/Tests/Component/Session/SessionComponentTest.php b/tests/Ratchet/Tests/Component/Session/SessionComponentTest.php new file mode 100644 index 0000000..f335e3e --- /dev/null +++ b/tests/Ratchet/Tests/Component/Session/SessionComponentTest.php @@ -0,0 +1,79 @@ +markTestSkipped('SessionHandlerInterface not defined. Requires PHP 5.4 or Symfony HttpFoundation'); + } + + $ref = new \ReflectionClass('\\Ratchet\\Component\\Session\\SessionComponent'); + $method = $ref->getMethod('toClassCase'); + $method->setAccessible(true); + + $component = new SessionComponent(new NullMessageComponent, new MemorySessionHandler); + $this->assertEquals($out, $method->invokeArgs($component, array($in))); + } + + /** + * I think I have severly butchered this test...it's not so much of a unit test as it is a full-fledged component test + */ + public function testConnectionValueFromPdo() { + if (false === $this->checkSymfonyPresent()) { + return $this->markTestSkipped('Dependency of Symfony HttpFoundation failed'); + } + + $sessionId = md5('testSession'); + + $dbOptions = array( + 'db_table' => 'sessions' + , 'db_id_col' => 'sess_id' + , 'db_data_col' => 'sess_data' + , 'db_time_col' => 'sess_time' + ); + + $pdo = new \PDO("sqlite::memory:"); + $pdo->exec(vsprintf("CREATE TABLE %s (%s VARCHAR(255) PRIMARY KEY, %s TEXT, %s INTEGER)", $dbOptions)); + $pdo->prepare(vsprintf("INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)", $dbOptions))->execute(array($sessionId, base64_encode('_sf2_attributes|a:2:{s:5:"hello";s:5:"world";s:4:"last";i:1332872102;}_sf2_flashes|a:0:{}'), time())); + + $component = new SessionComponent(new NullMessageComponent, new PdoSessionHandler($pdo, $dbOptions), array('auto_start' => 1)); + $connection = new Connection(new FakeSocket); + + $headers = $this->getMock('Guzzle\\Http\\Message\\Request', array('getCookie'), array('POST', '/', array())); + $headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue($sessionId)); + + $connection->WebSocket = new \StdClass; + $connection->WebSocket->headers = $headers; + + $component->onOpen($connection); + + $this->assertEquals('world', $connection->Session->get('hello')); + } +} \ No newline at end of file diff --git a/tests/Ratchet/Tests/Mock/MemorySessionHandler.php b/tests/Ratchet/Tests/Mock/MemorySessionHandler.php new file mode 100644 index 0000000..87e9953 --- /dev/null +++ b/tests/Ratchet/Tests/Mock/MemorySessionHandler.php @@ -0,0 +1,39 @@ +_sessions[$session_id])) { + unset($this->_sessions[$session_id]); + } + + return true; + } + + public function gc($maxlifetime) { + return true; + } + + public function open($save_path, $session_id) { + if (!isset($this->_sessions[$session_id])) { + $this->_sessions[$session_id] = ''; + } + + return true; + } + + public function read($session_id) { + return $this->_sessions[$session_id]; + } + + public function write($session_id, $session_data) { + $this->_sessions[$session_id] = $session_data; + + return true; + } +} \ No newline at end of file diff --git a/tests/Ratchet/Tests/Mock/NullMessageComponent.php b/tests/Ratchet/Tests/Mock/NullMessageComponent.php new file mode 100644 index 0000000..0f93fde --- /dev/null +++ b/tests/Ratchet/Tests/Mock/NullMessageComponent.php @@ -0,0 +1,55 @@ +connections = new \SplObjectStorage; + $this->messageHistory = new \SplQueue; + $this->errorHistory = new \SplQueue; + } + + /** + * {@inheritdoc} + */ + function onOpen(ConnectionInterface $conn) { + $this->connections->attach($conn); + } + + /** + * {@inheritdoc} + */ + function onMessage(ConnectionInterface $from, $msg) { + $this->messageHistory->enqueue(array('from' => $from, 'msg' => $msg)); + } + + /** + * {@inheritdoc} + */ + function onClose(ConnectionInterface $conn) { + $this->connections->detach($conn); + } + + /** + * {@inheritdoc} + */ + function onError(ConnectionInterface $conn, \Exception $e) { + $this->errorHistory->enqueue(array('conn' => $conn, 'exception' => $e)); + } +} \ No newline at end of file