- <?php
- /**
- * Zend Framework (http://framework.zend.com/)
- *
- * @link http://github.com/zendframework/zf2 for the canonical source repository
- * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
- * @license http://framework.zend.com/license/new-bsd New BSD License
- */
-
- namespace Zend\Session;
-
- use ArrayIterator;
- use Iterator;
- use Traversable;
- use Zend\Session\ManagerInterface as Manager;
- use Zend\Session\Storage\StorageInterface as Storage;
- use Zend\Stdlib\ArrayObject;
-
- /**
- * Session storage container
- *
- * Allows for interacting with session storage in isolated containers, which
- * may have their own expiries, or even expiries per key in the container.
- * Additionally, expiries may be absolute TTLs or measured in "hops", which
- * are based on how many times the key or container were accessed.
- */
- abstract class AbstractContainer extends ArrayObject
- {
- /**
- * Container name
- *
- * @var string
- */
- protected $name;
-
- /**
- * @var Manager
- */
- protected $manager;
-
- /**
- * Default manager class to use if no manager has been provided
- *
- * @var string
- */
- protected static $managerDefaultClass = 'Zend\\Session\\SessionManager';
-
- /**
- * Default manager to use when instantiating a container without providing a ManagerInterface
- *
- * @var Manager
- */
- protected static $defaultManager;
-
- /**
- * Constructor
- *
- * Provide a name ('Default' if none provided) and a ManagerInterface instance.
- *
- * @param null|string $name
- * @param Manager $manager
- * @throws Exception\InvalidArgumentException
- */
- public function __construct($name = 'Default', Manager $manager = null)
- {
- if (!preg_match('/^[a-z][a-z0-9_\\\]+$/i', $name)) {
- throw new Exception\InvalidArgumentException(
- 'Name passed to container is invalid; must consist of alphanumerics, backslashes and underscores only'
- );
- }
- $this->name = $name;
- $this->setManager($manager);
-
- // Create namespace
- parent::__construct(array(), ArrayObject::ARRAY_AS_PROPS);
-
- // Start session
- $this->getManager()->start();
- }
-
- /**
- * Set the default ManagerInterface instance to use when none provided to constructor
- *
- * @param Manager $manager
- * @return void
- */
- public static function setDefaultManager(Manager $manager = null)
- {
- static::$defaultManager = $manager;
- }
-
- /**
- * Get the default ManagerInterface instance
- *
- * If none provided, instantiates one of type {@link $managerDefaultClass}
- *
- * @return Manager
- * @throws Exception\InvalidArgumentException if invalid manager default class provided
- */
- public static function getDefaultManager()
- {
- if (null === static::$defaultManager) {
- $manager = new static::$managerDefaultClass();
- if (!$manager instanceof Manager) {
- throw new Exception\InvalidArgumentException(
- 'Invalid default manager type provided; must implement ManagerInterface'
- );
- }
- static::$defaultManager = $manager;
- }
-
- return static::$defaultManager;
- }
-
- /**
- * Get container name
- *
- * @return string
- */
- public function getName()
- {
- return $this->name;
- }
-
- /**
- * Set session manager
- *
- * @param null|Manager $manager
- * @return Container
- * @throws Exception\InvalidArgumentException
- */
- protected function setManager(Manager $manager = null)
- {
- if (null === $manager) {
- $manager = static::getDefaultManager();
- if (!$manager instanceof Manager) {
- throw new Exception\InvalidArgumentException(
- 'Manager provided is invalid; must implement ManagerInterface'
- );
- }
- }
- $this->manager = $manager;
-
- return $this;
- }
-
- /**
- * Get manager instance
- *
- * @return Manager
- */
- public function getManager()
- {
- return $this->manager;
- }
-
- /**
- * Get session storage object
- *
- * Proxies to ManagerInterface::getStorage()
- *
- * @return Storage
- */
- protected function getStorage()
- {
- return $this->getManager()->getStorage();
- }
-
- /**
- * Create a new container object on which to act
- *
- * @return ArrayObject
- */
- protected function createContainer()
- {
- return new ArrayObject(array(), ArrayObject::ARRAY_AS_PROPS);
- }
-
- /**
- * Verify container namespace
- *
- * Checks to see if a container exists within the Storage object already.
- * If not, one is created; if so, checks to see if it's an ArrayObject.
- * If not, it raises an exception; otherwise, it returns the Storage
- * object.
- *
- * @param bool $createContainer Whether or not to create the container for the namespace
- * @return Storage|null Returns null only if $createContainer is false
- * @throws Exception\RuntimeException
- */
- protected function verifyNamespace($createContainer = true)
- {
- $storage = $this->getStorage();
- $name = $this->getName();
- if (!isset($storage[$name])) {
- if (!$createContainer) {
- return;
- }
- $storage[$name] = $this->createContainer();
- }
- if (!is_array($storage[$name]) && !$storage[$name] instanceof Traversable) {
- throw new Exception\RuntimeException('Container cannot write to storage due to type mismatch');
- }
-
- return $storage;
- }
-
- /**
- * Determine whether a given key needs to be expired
- *
- * Returns true if the key has expired, false otherwise.
- *
- * @param null|string $key
- * @return bool
- */
- protected function expireKeys($key = null)
- {
- $storage = $this->verifyNamespace();
- $name = $this->getName();
-
- // Return early if key not found
- if ((null !== $key) && !isset($storage[$name][$key])) {
- return true;
- }
-
- if ($this->expireByExpiryTime($storage, $name, $key)) {
- return true;
- }
-
- if ($this->expireByHops($storage, $name, $key)) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Expire a key by expiry time
- *
- * Checks to see if the entire container has expired based on TTL setting,
- * or the individual key.
- *
- * @param Storage $storage
- * @param string $name Container name
- * @param string $key Key in container to check
- * @return bool
- */
- protected function expireByExpiryTime(Storage $storage, $name, $key)
- {
- $metadata = $storage->getMetadata($name);
-
- // Global container expiry
- if (is_array($metadata)
- && isset($metadata['EXPIRE'])
- && ($_SERVER['REQUEST_TIME'] > $metadata['EXPIRE'])
- ) {
- unset($metadata['EXPIRE']);
- $storage->setMetadata($name, $metadata, true);
- $storage[$name] = $this->createContainer();
-
- return true;
- }
-
- // Expire individual key
- if ((null !== $key)
- && is_array($metadata)
- && isset($metadata['EXPIRE_KEYS'])
- && isset($metadata['EXPIRE_KEYS'][$key])
- && ($_SERVER['REQUEST_TIME'] > $metadata['EXPIRE_KEYS'][$key])
- ) {
- unset($metadata['EXPIRE_KEYS'][$key]);
- $storage->setMetadata($name, $metadata, true);
- unset($storage[$name][$key]);
-
- return true;
- }
-
- // Find any keys that have expired
- if ((null === $key)
- && is_array($metadata)
- && isset($metadata['EXPIRE_KEYS'])
- ) {
- foreach (array_keys($metadata['EXPIRE_KEYS']) as $key) {
- if ($_SERVER['REQUEST_TIME'] > $metadata['EXPIRE_KEYS'][$key]) {
- unset($metadata['EXPIRE_KEYS'][$key]);
- if (isset($storage[$name][$key])) {
- unset($storage[$name][$key]);
- }
- }
- }
- $storage->setMetadata($name, $metadata, true);
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Expire key by session hops
- *
- * Determines whether the container or an individual key within it has
- * expired based on session hops
- *
- * @param Storage $storage
- * @param string $name
- * @param string $key
- * @return bool
- */
- protected function expireByHops(Storage $storage, $name, $key)
- {
- $ts = $storage->getRequestAccessTime();
- $metadata = $storage->getMetadata($name);
-
- // Global container expiry
- if (is_array($metadata)
- && isset($metadata['EXPIRE_HOPS'])
- && ($ts > $metadata['EXPIRE_HOPS']['ts'])
- ) {
- $metadata['EXPIRE_HOPS']['hops']--;
- if (-1 === $metadata['EXPIRE_HOPS']['hops']) {
- unset($metadata['EXPIRE_HOPS']);
- $storage->setMetadata($name, $metadata, true);
- $storage[$name] = $this->createContainer();
-
- return true;
- }
- $metadata['EXPIRE_HOPS']['ts'] = $ts;
- $storage->setMetadata($name, $metadata, true);
-
- return false;
- }
-
- // Single key expiry
- if ((null !== $key)
- && is_array($metadata)
- && isset($metadata['EXPIRE_HOPS_KEYS'])
- && isset($metadata['EXPIRE_HOPS_KEYS'][$key])
- && ($ts > $metadata['EXPIRE_HOPS_KEYS'][$key]['ts'])
- ) {
- $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']--;
- if (-1 === $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']) {
- unset($metadata['EXPIRE_HOPS_KEYS'][$key]);
- $storage->setMetadata($name, $metadata, true);
- unset($storage[$name][$key]);
-
- return true;
- }
- $metadata['EXPIRE_HOPS_KEYS'][$key]['ts'] = $ts;
- $storage->setMetadata($name, $metadata, true);
-
- return false;
- }
-
- // Find all expired keys
- if ((null === $key)
- && is_array($metadata)
- && isset($metadata['EXPIRE_HOPS_KEYS'])
- ) {
- foreach (array_keys($metadata['EXPIRE_HOPS_KEYS']) as $key) {
- if ($ts > $metadata['EXPIRE_HOPS_KEYS'][$key]['ts']) {
- $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']--;
- if (-1 === $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']) {
- unset($metadata['EXPIRE_HOPS_KEYS'][$key]);
- $storage->setMetadata($name, $metadata, true);
- unset($storage[$name][$key]);
- continue;
- }
- $metadata['EXPIRE_HOPS_KEYS'][$key]['ts'] = $ts;
- }
- }
- $storage->setMetadata($name, $metadata, true);
-
- return false;
- }
-
- return false;
- }
-
- /**
- * Store a value within the container
- *
- * @param string $key
- * @param mixed $value
- * @return void
- */
- public function offsetSet($key, $value)
- {
- $this->expireKeys($key);
- $storage = $this->verifyNamespace();
- $name = $this->getName();
- $storage[$name][$key] = $value;
- }
-
- /**
- * Determine if the key exists
- *
- * @param string $key
- * @return bool
- */
- public function offsetExists($key)
- {
- // If no container exists, we can't inspect it
- if (null === ($storage = $this->verifyNamespace(false))) {
- return false;
- }
- $name = $this->getName();
-
- // Return early if the key isn't set
- if (!isset($storage[$name][$key])) {
- return false;
- }
-
- $expired = $this->expireKeys($key);
-
- return !$expired;
- }
-
- /**
- * Retrieve a specific key in the container
- *
- * @param string $key
- * @return mixed
- */
- public function offsetGet($key)
- {
- if (!$this->offsetExists($key)) {
- return null;
- }
- $storage = $this->getStorage();
- $name = $this->getName();
-
- return $storage[$name][$key];
- }
-
- /**
- * Unset a single key in the container
- *
- * @param string $key
- * @return void
- */
- public function offsetUnset($key)
- {
- if (!$this->offsetExists($key)) {
- return;
- }
- $storage = $this->getStorage();
- $name = $this->getName();
- unset($storage[$name][$key]);
- }
-
- /**
- * Exchange the current array with another array or object.
- *
- * @param array|object $input
- * @return array Returns the old array
- * @see ArrayObject::exchangeArray()
- */
- public function exchangeArray($input)
- {
- // handle arrayobject, iterators and the like:
- if (is_object($input) && ($input instanceof ArrayObject || $input instanceof \ArrayObject)) {
- $input = $input->getArrayCopy();
- }
- if (!is_array($input)) {
- $input = (array) $input;
- }
-
- $storage = $this->verifyNamespace();
- $name = $this->getName();
-
- $old = $storage[$name];
- $storage[$name] = $input;
- if ($old instanceof ArrayObject) {
- return $old->getArrayCopy();
- }
-
- return $old;
- }
-
- /**
- * Iterate over session container
- *
- * @return Iterator
- */
- public function getIterator()
- {
- $this->expireKeys();
- $storage = $this->getStorage();
- $container = $storage[$this->getName()];
-
- if ($container instanceof Traversable) {
- return $container;
- }
-
- return new ArrayIterator($container);
- }
-
- /**
- * Set expiration TTL
- *
- * Set the TTL for the entire container, a single key, or a set of keys.
- *
- * @param int $ttl TTL in seconds
- * @param string|array|null $vars
- * @return Container
- * @throws Exception\InvalidArgumentException
- */
- public function setExpirationSeconds($ttl, $vars = null)
- {
- $storage = $this->getStorage();
- $ts = $_SERVER['REQUEST_TIME'] + $ttl;
- if (is_scalar($vars) && null !== $vars) {
- $vars = (array) $vars;
- }
-
- if (null === $vars) {
- $this->expireKeys(); // first we need to expire global key, since it can already be expired
- $data = array('EXPIRE' => $ts);
- } elseif (is_array($vars)) {
- // Cannot pass "$this" to a lambda
- $container = $this;
-
- // Filter out any items not in our container
- $expires = array_filter($vars, function ($value) use ($container) {
- return $container->offsetExists($value);
- });
-
- // Map item keys => timestamp
- $expires = array_flip($expires);
- $expires = array_map(function ($value) use ($ts) {
- return $ts;
- }, $expires);
-
- // Create metadata array to merge in
- $data = array('EXPIRE_KEYS' => $expires);
- } else {
- throw new Exception\InvalidArgumentException(
- 'Unknown data provided as second argument to ' . __METHOD__
- );
- }
-
- $storage->setMetadata(
- $this->getName(),
- $data
- );
-
- return $this;
- }
-
- /**
- * Set expiration hops for the container, a single key, or set of keys
- *
- * @param int $hops
- * @param null|string|array $vars
- * @throws Exception\InvalidArgumentException
- * @return Container
- */
- public function setExpirationHops($hops, $vars = null)
- {
- $storage = $this->getStorage();
- $ts = $storage->getRequestAccessTime();
-
- if (is_scalar($vars) && (null !== $vars)) {
- $vars = (array) $vars;
- }
-
- if (null === $vars) {
- $this->expireKeys(); // first we need to expire global key, since it can already be expired
- $data = array('EXPIRE_HOPS' => array('hops' => $hops, 'ts' => $ts));
- } elseif (is_array($vars)) {
- // Cannot pass "$this" to a lambda
- $container = $this;
-
- // FilterInterface out any items not in our container
- $expires = array_filter($vars, function ($value) use ($container) {
- return $container->offsetExists($value);
- });
-
- // Map item keys => timestamp
- $expires = array_flip($expires);
- $expires = array_map(function ($value) use ($hops, $ts) {
- return array('hops' => $hops, 'ts' => $ts);
- }, $expires);
-
- // Create metadata array to merge in
- $data = array('EXPIRE_HOPS_KEYS' => $expires);
- } else {
- throw new Exception\InvalidArgumentException(
- 'Unknown data provided as second argument to ' . __METHOD__
- );
- }
-
- $storage->setMetadata(
- $this->getName(),
- $data
- );
-
- return $this;
- }
-
- /**
- * Creates a copy of the specific container name
- *
- * @return array
- */
- public function getArrayCopy()
- {
- $storage = $this->verifyNamespace();
- $container = $storage[$this->getName()];
-
- return $container->getArrayCopy();
- }
- }