diff --git a/application/config/session.php b/application/config/session.php index acb0436e..ed7f3825 100644 --- a/application/config/session.php +++ b/application/config/session.php @@ -31,6 +31,22 @@ 'table' => 'sessions', + /* + |-------------------------------------------------------------------------- + | Session Garbage Collection Probability + |-------------------------------------------------------------------------- + | + | Some session drivers require the manual clean-up of expired sessions. + | This option specifies the probability of session garbage collection + | occuring for any given request. + | + | For example, the default value states that garbage collection has about + | a 2% (2 / 100) chance of occuring for any given request. + | + */ + + 'sweepage' => array(2, 100), + /* |-------------------------------------------------------------------------- | Session Lifetime diff --git a/laravel/config/container.php b/laravel/config/container.php index 5d91f395..ef347996 100644 --- a/laravel/config/container.php +++ b/laravel/config/container.php @@ -168,15 +168,19 @@ }), - 'laravel.session' => array('singleton' => true, 'resolver' => function($container) + 'laravel.session.manager' => array('singleton' => true, 'resolver' => function($c) { - return $container->resolve('laravel.session.manager')->driver($container->resolve('laravel.config')->get('session.driver')); + $config = $c->resolve('laravel.config'); + + $driver = $c->resolve('laravel.session.'.$config->get('session.driver')); + + return new Session\Manager($driver, $c->resolve('laravel.session.transporter'), $config); }), - 'laravel.session.manager' => array('singleton' => true, 'resolver' => function($container) + 'laravel.session.transporter' => array('resolver' => function($c) { - return new Session\Manager($container); + return new Session\Transporters\Cookie($c->resolve('laravel.cookie')); }), diff --git a/laravel/database/query.php b/laravel/database/query.php index 085e39f2..6b850e4e 100644 --- a/laravel/database/query.php +++ b/laravel/database/query.php @@ -430,7 +430,7 @@ public function take($value) /** * Set the query limit and offset for a given page and item per page count. * - * If the given page is not an integer or is less than zero, one will be used. + * If the given page is not an integer or is less than one, one will be used. * * @param int $page * @param int $per_page diff --git a/laravel/laravel.php b/laravel/laravel.php index 8ec5bf07..a3252b99 100644 --- a/laravel/laravel.php +++ b/laravel/laravel.php @@ -49,9 +49,9 @@ // -------------------------------------------------------------- if ($config->get('session.driver') !== '') { - $id = $container->resolve('laravel.session.id'); + $session = $container->resolve('laravel.session.manager'); - $container->resolve('laravel.session')->start($container->resolve('laravel.config'), $id); + $container->instance('laravel.session', $session->payload()); } // -------------------------------------------------------------- @@ -76,13 +76,9 @@ // -------------------------------------------------------------- // Close the session and write the session cookie. // -------------------------------------------------------------- -if ($config->get('session.driver') !== '') +if (isset($session)) { - $session = $container->resolve('laravel.session'); - - $session->close($container->resolve('laravel.input'), time()); - - $session->cookie($container->resolve('laravel.cookie')); + $session->close($container->resolve('laravel.session')); } // -------------------------------------------------------------- diff --git a/laravel/session/drivers/apc.php b/laravel/session/drivers/apc.php index c1792127..975cb228 100644 --- a/laravel/session/drivers/apc.php +++ b/laravel/session/drivers/apc.php @@ -1,6 +1,6 @@ apc->get($id); } /** - * Save the session to persistant storage. + * Save a given session to storage. * * @param array $session + * @param array $config * @return void */ - protected function save($session) + public function save($session, $config) { - $this->apc->put($session['id'], $session, $this->config->get('session.lifetime')); + $this->apc->put($session['id'], $session, $config['lifetime']); } /** - * Delete the session from persistant storage. + * Delete a session from storage by a given ID. * * @param string $id * @return void */ - protected function delete($id) + public function delete($id) { $this->apc->forget($id); } diff --git a/laravel/session/drivers/cookie.php b/laravel/session/drivers/cookie.php index 274ccf52..36e7e6f5 100644 --- a/laravel/session/drivers/cookie.php +++ b/laravel/session/drivers/cookie.php @@ -2,7 +2,7 @@ use Laravel\Security\Crypter; -class Cookie extends Driver { +class Cookie implements Driver { /** * The cookie manager instance. @@ -36,16 +36,14 @@ public function __construct(Crypter $crypter, \Laravel\Cookie $cookie) } /** - * Load a session by ID. + * Load a session from storage by a given ID. * - * This method is responsible for retrieving the session from persistant storage. If the - * session does not exist in storage, nothing should be returned from the method, in which - * case a new session will be created by the base driver. + * If no session is found for the ID, null will be returned. * * @param string $id * @return array */ - protected function load($id) + public function load($id) { if ($this->cookie->has('session_payload')) { @@ -54,17 +52,16 @@ protected function load($id) } /** - * Save the session to persistant storage. + * Save a given session to storage. * * @param array $session + * @param array $config * @return void */ - protected function save($session) + public function save($session, $config) { if ( ! headers_sent()) { - $config = $this->config->get('session'); - extract($config); $payload = $this->crypter->encrypt(serialize($session)); @@ -74,12 +71,12 @@ protected function save($session) } /** - * Delete the session from persistant storage. + * Delete a session from storage by a given ID. * * @param string $id * @return void */ - protected function delete($id) + public function delete($id) { $this->cookie->forget('session_payload'); } diff --git a/laravel/session/drivers/database.php b/laravel/session/drivers/database.php index fb0b85cd..ec6b2ae9 100644 --- a/laravel/session/drivers/database.php +++ b/laravel/session/drivers/database.php @@ -2,14 +2,14 @@ use Laravel\Database\Connection; -class Database extends Driver implements Sweeper { +class Database implements Driver, Sweeper { /** * The database connection. * * @var Connection */ - protected $connection; + private $connection; /** * Create a new database session driver. @@ -23,16 +23,14 @@ public function __construct(Connection $connection) } /** - * Load a session by ID. + * Load a session from storage by a given ID. * - * This method is responsible for retrieving the session from persistant storage. If the - * session does not exist in storage, nothing should be returned from the method, in which - * case a new session will be created by the base driver. + * If no session is found for the ID, null will be returned. * * @param string $id * @return array */ - protected function load($id) + public function load($id) { $session = $this->table()->find($id); @@ -47,12 +45,13 @@ protected function load($id) } /** - * Save the session to persistant storage. + * Save a given session to storage. * * @param array $session + * @param array $config * @return void */ - protected function save($session) + public function save($session, $config) { $this->delete($session['id']); @@ -64,12 +63,12 @@ protected function save($session) } /** - * Delete the session from persistant storage. + * Delete a session from storage by a given ID. * * @param string $id * @return void */ - protected function delete($id) + public function delete($id) { $this->table()->delete($id); } @@ -90,7 +89,7 @@ public function sweep($expiration) * * @return Query */ - protected function table() + private function table() { return $this->connection->table($this->config->get('session.table')); } diff --git a/laravel/session/drivers/driver.php b/laravel/session/drivers/driver.php index 7d1dc7ed..b767bf94 100644 --- a/laravel/session/drivers/driver.php +++ b/laravel/session/drivers/driver.php @@ -1,314 +1,32 @@ config = $config; - - $this->session = ( ! is_null($id)) ? $this->load($id) : null; - - // If the session is expired, a new session will be generated and all of the data from - // the previous session will be lost. The new session will be assigned a random, long - // string ID to uniquely identify it among the application's current users. - if (is_null($this->session) or $this->expired()) - { - $this->session = array('id' => Str::random(40), 'last_activity' => time(), 'data' => array()); - } - - // If a CSRF token is not present in the session, we will generate one. These tokens - // are generated per session to protect against Cross-Site Request Forgery attacks on - // the application. It is up to the developer to take advantage of them using the token - // methods on the Form class and the "csrf" route filter. - if ( ! $this->has('csrf_token')) - { - $this->put('csrf_token', Str::random(16)); - } - } - - /** - * Deteremine if the session is expired based on the last activity timestamp - * and the session lifetime set in the configuration file. - * - * @return bool - */ - private function expired() - { - return (time() - $this->session['last_activity']) > ($this->config->get('session.lifetime') * 60); - } - - /** - * Load a session by ID. - * - * This method is responsible for retrieving the session from persistant storage. If the - * session does not exist in storage, nothing should be returned from the method, in which - * case a new session will be created by the base driver. + * If no session is found for the ID, null will be returned. * * @param string $id * @return array */ - abstract protected function load($id); + public function load($id); /** - * Delete the session from persistant storage. + * Save a given session to storage. + * + * @param array $session + * @param array $config + * @return void + */ + public function save($session, $config); + + /** + * Delete a session from storage by a given ID. * * @param string $id * @return void */ - abstract protected function delete($id); - - /** - * Save the session to persistant storage. - * - * @param array $session - * @return void - */ - abstract protected function save($session); - - /** - * Determine if the session or flash data contains an item. - * - * @param string $key - * @return bool - */ - public function has($key) - { - return ( ! is_null($this->get($key))); - } - - /** - * Get an item from the session. - * - * A default value may also be specified, and will be returned in the item doesn't exist. - * - * @param string $key - * @param mixed $default - * @return mixed - */ - public final function get($key, $default = null) - { - foreach (array($key, ':old:'.$key, ':new:'.$key) as $possibility) - { - if (array_key_exists($possibility, $this->session['data'])) return $this->session['data'][$possibility]; - } - - return ($default instanceof Closure) ? call_user_func($default) : $default; - } - - /** - * Write an item to the session. - * - * @param string $key - * @param mixed $value - * @return Driver - */ - public final function put($key, $value) - { - $this->session['data'][$key] = $value; - - return $this; - } - - /** - * Write an item to the session flash data. - * - * Flash data only exists for the next request. After that, it will be removed from - * the session. Flash data is useful for temporary status or welcome messages. - * - * @param string $key - * @param mixed $value - * @return Driver - */ - public final function flash($key, $value) - { - $this->put(':new:'.$key, $value); - - return $this; - } - - /** - * Keep all of the session flash data from expiring at the end of the request. - * - * @return void - */ - public final function reflash() - { - $this->readdress(':old:', ':new:', array_keys($this->session['data'])); - } - - /** - * Keep a session flash item from expiring at the end of the request. - * - * If a string is passed to the method, only that item will be kept. An array may also - * be passed to the method, in which case all items in the array will be kept. - * - * @param string|array $key - * @return void - */ - public final function keep($key) - { - if (is_array($key)) return array_map(array($this, 'keep'), $key); - - $this->flash($key, $this->get($key)); - - $this->forget(':old:'.$key); - } - - /** - * Remove an item from the session. - * - * @param string $key - * @return Driver - */ - public final function forget($key) - { - unset($this->session['data'][$key]); - } - - /** - * Remove all items from the session. - * - * @return void - */ - public final function flush() - { - $this->session['data'] = array(); - } - - /** - * Regenerate the session ID. - * - * @return void - */ - public final function regenerate() - { - $this->delete($this->session['id']); - - $this->session['id'] = Str::random(40); - } - - /** - * Readdress the session data by performing a string replacement on the keys. - * - * @param string $search - * @param string $replace - * @param array $keys - * @return void - */ - private function readdress($search, $replace, $keys) - { - $this->session['data'] = array_combine(str_replace($search, $replace, $keys), array_values($this->session['data'])); - } - - /** - * Close the session and store the session payload in persistant storage. - * - * @param Laravel\Input $input - * @param int $time - * @return void - */ - public final function close(Input $input, $time) - { - // The input for the current request will be flashed to the session for - // convenient access through the "old" method of the input class. This - // allows the easy repopulation of forms. - $this->flash('laravel_old_input', $input->get())->age(); - - $this->session['last_activity'] = $time; - - $this->save($this->session); - - // Some session drivers implement the "Sweeper" interface, which specifies - // that the driver needs to manually clean up its expired sessions. If the - // driver does in fact implement this interface, we will randomly call the - // sweep method on the driver. - if ($this instanceof Sweeper and mt_rand(1, 100) <= 2) - { - $this->sweep($time - ($this->config->get('session.lifetime') * 60)); - } - } - - /** - * Write the session cookie. - * - * @param Laravel\Cookie $cookie - * @param array $config - * @return void - */ - public final function cookie(Cookie $cookies) - { - if ( ! headers_sent()) - { - $config = $this->config->get('session'); - - extract($config); - - $minutes = ($expire_on_close) ? 0 : $lifetime; - - $cookies->put('laravel_session', $this->session['id'], $minutes, $path, $domain); - } - } - - /** - * Age the session flash data. - * - * @return void - */ - private function age() - { - // To age the data, we will forget all of the old keys and then rewrite the newly - // flashed items to have old keys, which will be available for the next request. - foreach ($this->session['data'] as $key => $value) - { - if (strpos($key, ':old:') === 0) $this->forget($key); - } - - $this->readdress(':new:', ':old:', array_keys($this->session['data'])); - } - - /** - * Magic Method for retrieving items from the session. - */ - public function __get($key) - { - return $this->get($key); - } - - /** - * Magic Method for writings items to the session. - */ - public function __set($key, $value) - { - $this->put($key, $value); - } + public function delete($id); } \ No newline at end of file diff --git a/laravel/session/drivers/file.php b/laravel/session/drivers/file.php index a3d01e3f..dbf73a67 100644 --- a/laravel/session/drivers/file.php +++ b/laravel/session/drivers/file.php @@ -1,6 +1,6 @@ file->exists($path = $this->path.$id)) return unserialize($this->file->get($path)); } /** - * Save the session to persistant storage. + * Save a given session to storage. * * @param array $session + * @param array $config * @return void */ - protected function save($session) + public function save($session, $config) { $this->file->put($this->path.$session['id'], serialize($session), LOCK_EX); } /** - * Delete the session from persistant storage. + * Delete a session from storage by a given ID. * * @param string $id * @return void */ - protected function delete($id) + public function delete($id) { $this->file->delete($this->path.$id); } diff --git a/laravel/session/drivers/memcached.php b/laravel/session/drivers/memcached.php index 0400f661..ae500e65 100644 --- a/laravel/session/drivers/memcached.php +++ b/laravel/session/drivers/memcached.php @@ -1,6 +1,6 @@ memcached->get($id); } /** - * Save the session to persistant storage. + * Save a given session to storage. * * @param array $session + * @param array $config * @return void */ - protected function save($session) + public function save($session, $config) { - $this->memcached->put($session['id'], $session, $this->config->get('session.lifetime')); + $this->memcached->put($session['id'], $session, $config['lifetime']); } /** - * Delete the session from persistant storage. + * Delete a session from storage by a given ID. * * @param string $id * @return void */ - protected function delete($id) + public function delete($id) { $this->memcached->forget($id); } diff --git a/laravel/session/manager.php b/laravel/session/manager.php index 619f59e2..ad6e63a7 100644 --- a/laravel/session/manager.php +++ b/laravel/session/manager.php @@ -1,45 +1,129 @@ container = $container; + $this->driver = $driver; + $this->config = $config; + $this->transporter = $transporter; } /** - * Get the session driver. + * Get the session payload for the request. * - * The session driver returned will be the driver specified in the session configuration - * file. Only one session driver may be active for a given request, so the driver will - * be managed as a singleton. - * - * @param string $driver - * @return Session\Driver + * @return Payload */ - public function driver($driver) + public function payload() { - if ($this->container->registered('laravel.session.'.$driver)) + $session = $this->driver->load($this->transporter->get()); + + // If the session is expired, a new session will be generated and all of the data from + // the previous session will be lost. The new session will be assigned a random, long + // string ID to uniquely identify it among the application's current users. + if (is_null($session) or $this->expired($session)) { - return $this->container->resolve('laravel.session.'.$driver); + $session = array('id' => Str::random(40), 'data' => array()); } - throw new \Exception("Session driver [$driver] is not supported."); + $payload = new Payload($session); + + // If a CSRF token is not present in the session, we will generate one. These tokens + // are generated per session to protect against Cross-Site Request Forgery attacks on + // the application. It is up to the developer to take advantage of them using the token + // methods on the Form class and the "csrf" route filter. + if ( ! $payload->has('csrf_token')) $payload->put('csrf_token', Str::random(16)); + + return $payload; + } + + /** + * Deteremine if the session is expired based on the last activity timestamp + * and the session lifetime set in the configuration file. + * + * @param array $payload + * @return bool + */ + private function expired($payload) + { + return (time() - $payload['last_activity']) > ($this->config->get('session.lifetime') * 60); + } + + /** + * Close the session handling for the request. + * + * @param Payload $payload + * @return void + */ + public function close(Payload $payload) + { + $config = $this->config->get('session'); + + $this->driver->save($payload->age(), $config); + + $this->transporter->put($payload->session['id'], $config); + + // Some session drivers implement the Sweeper interface, which specified that the driver + // must do its garbage collection manually. Alternatively, some drivers such as APC and + // Memcached are not required to manually clean up their sessions. + if (mt_rand(1, $config['sweepage'][1]) <= $config['sweepage'][0] and $this->driver instanceof Sweeper) + { + $this->driver->sweep(time() - ($config['lifetime'] * 60)); + } + } + + /** + * Magic Method for calling methods on the session payload instance. + */ + public function __call($method, $parameters) + { + if (method_exists($this->payload, $method)) + { + return call_user_func_array(array($this->payload, $method), $parameters); + } + + throw new \Exception("Attempting to call undefined method [$method] on session manager."); } } \ No newline at end of file diff --git a/laravel/session/payload.php b/laravel/session/payload.php new file mode 100644 index 00000000..1b3c10cb --- /dev/null +++ b/laravel/session/payload.php @@ -0,0 +1,186 @@ +session = $session; + } + + /** + * Determine if the session or flash data contains an item. + * + * @param string $key + * @return bool + */ + public function has($key) + { + return ( ! is_null($this->get($key))); + } + + /** + * Get an item from the session. + * + * A default value may also be specified, and will be returned in the item doesn't exist. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get($key, $default = null) + { + foreach (array($key, ':old:'.$key, ':new:'.$key) as $possibility) + { + if (array_key_exists($possibility, $this->session['data'])) + { + return $this->session['data'][$possibility]; + } + } + + return ($default instanceof Closure) ? call_user_func($default) : $default; + } + + /** + * Write an item to the session. + * + * @param string $key + * @param mixed $value + * @return Driver + */ + public function put($key, $value) + { + $this->session['data'][$key] = $value; + + return $this; + } + + /** + * Write an item to the session flash data. + * + * Flash data only exists for the next request. After that, it will be removed from + * the session. Flash data is useful for temporary status or welcome messages. + * + * @param string $key + * @param mixed $value + * @return Driver + */ + public function flash($key, $value) + { + $this->put(':new:'.$key, $value); + + return $this; + } + + /** + * Keep all of the session flash data from expiring at the end of the request. + * + * @return void + */ + public function reflash() + { + $this->readdress(':old:', ':new:', array_keys($this->session['data'])); + } + + /** + * Keep a session flash item from expiring at the end of the request. + * + * If a string is passed to the method, only that item will be kept. An array may also + * be passed to the method, in which case all items in the array will be kept. + * + * @param string|array $key + * @return void + */ + public function keep($key) + { + if (is_array($key)) return array_map(array($this, 'keep'), $key); + + $this->flash($key, $this->get($key)); + + $this->forget(':old:'.$key); + } + + /** + * Remove an item from the session. + * + * @param string $key + * @return Driver + */ + public function forget($key) + { + unset($this->session['data'][$key]); + } + + /** + * Remove all items from the session. + * + * @return void + */ + public function flush() + { + $this->session['data'] = array(); + } + + /** + * Regenerate the session ID. + * + * @return void + */ + public function regenerate() + { + $this->session['id'] = Str::random(40); + } + + /** + * Age the session payload, preparing it for storage after a request. + * + * The session flash data will be aged and the last activity timestamp will be updated. + * The aged session array will be returned by the method. + * + * @return array + */ + public function age() + { + $this->session['last_activity'] = time(); + + // To age the data, we will forget all of the old keys and then rewrite the newly + // flashed items to have old keys, which will be available for the next request. + foreach ($this->session['data'] as $key => $value) + { + if (strpos($key, ':old:') === 0) $this->forget($key); + } + + $this->readdress(':new:', ':old:', array_keys($this->session['data'])); + + return $this->session; + } + + /** + * Readdress the session data by performing a string replacement on the keys. + * + * @param string $search + * @param string $replace + * @param array $keys + * @return void + */ + private function readdress($search, $replace, $keys) + { + $this->session['data'] = array_combine(str_replace($search, $replace, $keys), array_values($this->session['data'])); + } + +} \ No newline at end of file diff --git a/laravel/session/transporters/cookie.php b/laravel/session/transporters/cookie.php new file mode 100644 index 00000000..61fba531 --- /dev/null +++ b/laravel/session/transporters/cookie.php @@ -0,0 +1,43 @@ +cookie = $cookie; + } + + /** + * Get the session identifier for the request. + * + * @return string + */ + public function get() + { + return $this->cookie->get('laravel_session'); + } + + /** + * Store the session identifier for the request. + * + * @param string $id + * @param array $config + * @return void + */ + public function put($id, $config) + { + if ( ! headers_sent()) + { + $minutes = ($config['expire_on_close']) ? 0 : $config['lifetime']; + + $this->cookie->put('laravel_session', $id, $minutes, $config['path'], $config['domain']); + } + } + +} \ No newline at end of file diff --git a/laravel/session/transporters/transporter.php b/laravel/session/transporters/transporter.php new file mode 100644 index 00000000..afc1fbc6 --- /dev/null +++ b/laravel/session/transporters/transporter.php @@ -0,0 +1,21 @@ +