From c0b83886fb611a567e3a13bbcdb41cf4857b50b1 Mon Sep 17 00:00:00 2001 From: "Bruno O. Notario" Date: Fri, 23 Feb 2024 23:02:36 -0300 Subject: [PATCH] Session handlers --- system/Exception/BadFunctionCallException.php | 2 + system/Exception/BadMethodCallException.php | 2 + system/Exception/DomainException.php | 2 + system/Exception/InvalidArgumentException.php | 3 + system/Exception/LengthException.php | 2 + system/Exception/LogicException.php | 2 + system/Exception/NotFoundException.php | 21 ++ system/Exception/OutOfBoundsException.php | 2 + system/Exception/OutOfRangeException.php | 2 + system/Exception/OverflowException.php | 2 + system/Exception/RESTException.php | 3 + system/Exception/RangeException.php | 2 + system/Exception/ReflectionException.php | 2 + system/Exception/RuntimeException.php | 2 + system/Exception/Throwable.php | 2 + system/Exception/UnderflowException.php | 2 + system/Exception/UnexpectedValueException.php | 2 + system/Exception/WebApiException.php | 3 + system/session/Handlers/Database.php | 4 + system/session/Handlers/File.php | 201 ++++++++++++++++++ system/session/Handlers/Redis.php | 7 + system/session/autoload.php | 54 ++++- 22 files changed, 315 insertions(+), 9 deletions(-) create mode 100644 system/Exception/NotFoundException.php create mode 100644 system/session/Handlers/File.php diff --git a/system/Exception/BadFunctionCallException.php b/system/Exception/BadFunctionCallException.php index 40dc997..210140d 100644 --- a/system/Exception/BadFunctionCallException.php +++ b/system/Exception/BadFunctionCallException.php @@ -12,6 +12,8 @@ namespace Phacil\Framework\Exception; /** * Exception thrown if a callback refers to an undefined function or if some arguments are missing. * + * @since 2.0.0 + * @api * @package Phacil\Framework\Exception */ class BadFunctionCallException extends \Phacil\Framework\Exception\LogicException { diff --git a/system/Exception/BadMethodCallException.php b/system/Exception/BadMethodCallException.php index 84af70e..f56c725 100644 --- a/system/Exception/BadMethodCallException.php +++ b/system/Exception/BadMethodCallException.php @@ -12,6 +12,8 @@ namespace Phacil\Framework\Exception; /** * Exception thrown if a callback refers to an undefined method or if some arguments are missing. * + * @api + * @since 2.0.0 * @package Phacil\Framework\Exception */ class BadMethodCallException extends \Phacil\Framework\Exception\BadFunctionCallException { diff --git a/system/Exception/DomainException.php b/system/Exception/DomainException.php index b981d96..cc85759 100644 --- a/system/Exception/DomainException.php +++ b/system/Exception/DomainException.php @@ -12,6 +12,8 @@ namespace Phacil\Framework\Exception; /** * Exception thrown if a value does not adhere to a defined valid data domain. * + * @api + * @since 2.0.0 * @package Phacil\Framework\Exception */ class DomainException extends \Phacil\Framework\Exception\LogicException { diff --git a/system/Exception/InvalidArgumentException.php b/system/Exception/InvalidArgumentException.php index 4188439..9dbb068 100644 --- a/system/Exception/InvalidArgumentException.php +++ b/system/Exception/InvalidArgumentException.php @@ -11,6 +11,9 @@ namespace Phacil\Framework\Exception; /** * Exception thrown if an argument is not of the expected type. + * + * @since 2.0.0 + * @api * @package Phacil\Framework\Exception */ class InvalidArgumentException extends \Phacil\Framework\Exception\LogicException { diff --git a/system/Exception/LengthException.php b/system/Exception/LengthException.php index 79fd0da..b5fc22f 100644 --- a/system/Exception/LengthException.php +++ b/system/Exception/LengthException.php @@ -12,6 +12,8 @@ namespace Phacil\Framework\Exception; /** * Exception thrown if a length is invalid. * + * @since 2.0.0 + * @api * @package Phacil\Framework\Exception */ class LengthException extends \Phacil\Framework\Exception\LogicException { diff --git a/system/Exception/LogicException.php b/system/Exception/LogicException.php index 156a7c7..ed039c9 100644 --- a/system/Exception/LogicException.php +++ b/system/Exception/LogicException.php @@ -11,6 +11,8 @@ namespace Phacil\Framework\Exception; /** * Exception that represents error in the program logic. This kind of exception should lead directly to a fix in your code. * + * @since 2.0.0 + * @api * @package Phacil\Framework\Exception */ class LogicException extends \Phacil\Framework\Exception { diff --git a/system/Exception/NotFoundException.php b/system/Exception/NotFoundException.php new file mode 100644 index 0000000..3e78378 --- /dev/null +++ b/system/Exception/NotFoundException.php @@ -0,0 +1,21 @@ +config = $config; + $this->encryptor = $encryption; + $this->json = $json; + if(!\Phacil\Framework\Config::DIR_SESSION()) + \Phacil\Framework\Config::DIR_SESSION(\Phacil\Framework\Config::DIR_CACHE()."sessions/"); + + $this->checkDir(); + } + + /** + * @return void + * @throws \Phacil\Framework\Exception + */ + protected function checkDir() + { + if (!is_dir(\Phacil\Framework\Config::DIR_SESSION())) { + mkdir(\Phacil\Framework\Config::DIR_SESSION(), 0764, true); + } + if (!is_writable(\Phacil\Framework\Config::DIR_SESSION())) { + throw new \Phacil\Framework\Exception( + "The session dir storage doesn't exist or isn't writable. Verify the permissions and try again." + ); + } + } + + /** {@inheritdoc} */ + public function getFailedLockAttempts() { } + + /** {@inheritdoc} */ + public function setName($name) { } + + /** @return int */ + protected function getLifetime() + { + return (int)$this->config->get('session_expire') ?: self::DEFAULT_SESSION_LIFETIME; + } + + /** @return int */ + protected function getFirstLifetime() + { + return (int)$this->config->get('session_first_lifetime') ?: self::DEFAULT_SESSION_FIRST_LIFETIME; + } + + /** + * @param string $hash + * @return string|false + */ + private function hashed($hash) + { + //$this->encryptor->setHashAlgo('sha256'); + return $this->encryptor->hash($hash) ?: $hash; + //return $hash; + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function close() + { + return true; + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function open($savePath, $sessionName) + { + return true; + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function read($session_id) + { + $file = \Phacil\Framework\Config::DIR_SESSION() . 'sess_' . $this->hashed(basename($session_id)); + + if (is_file($file)) { + $data = $this->json->decode(file_get_contents($file)); + if($data && $data[self::KEY_EXPIRES] >= time()){ + $this->isFirstSession = false; + return $data['data']; + } else { + $this->destroy($session_id); + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function write($session_id, $data) + { + $data = [ + 'data' => $data, + self::KEY_EXPIRES => time() + ($this->isFirstSession ? $this->getFirstLifetime() : $this->getLifetime()) + ]; + file_put_contents(\Phacil\Framework\Config::DIR_SESSION() . 'sess_' . $this->hashed(basename($session_id)), $this->json->encode($data)); + + return true; + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function destroy($session_id) + { + $file = \Phacil\Framework\Config::DIR_SESSION() . 'sess_' . $this->hashed(basename($session_id)); + + if (is_file($file) && is_writable($file)) { + return unlink($file); + } + + return false; + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function gc($maxLifeTime) + { + if (round(mt_rand(1, $this->config->get('session_divisor') / $this->config->get('session_probability'))) == 1) { + $expire = time() - $this->config->get('session_expire'); + + $files = scandir(\Phacil\Framework\Config::DIR_SESSION()); + + foreach ($files as $file) { + if (is_file($file) && filemtime($file) < $expire && is_writable($file)) { + unlink($file); + } + } + } + + return true; + } +} diff --git a/system/session/Handlers/Redis.php b/system/session/Handlers/Redis.php index 465c96e..83546cc 100644 --- a/system/session/Handlers/Redis.php +++ b/system/session/Handlers/Redis.php @@ -14,7 +14,14 @@ use Cm\RedisSession\ConnectionFailedException; use Cm\RedisSession\ConcurrentConnectionsExceededException; use Phacil\Framework\Exception; +/** + * Redis handler for PHP Session + * + * @since 2.0.0 + * @package Phacil\Framework\Session\Handlers + */ class Redis implements \Phacil\Framework\Session\Api\HandlerInterface { + const SHORT_NAME = 'redis'; private $savePath; diff --git a/system/session/autoload.php b/system/session/autoload.php index b8a5438..4fa7642 100644 --- a/system/session/autoload.php +++ b/system/session/autoload.php @@ -66,6 +66,12 @@ class Session */ private $cookieConfig; + /** + * + * @var \Phacil\Framework\Config + */ + private $config; + /** * * @param \Phacil\Framework\Registry $registry @@ -83,11 +89,12 @@ class Session $this->cookieConfig = $cookieConfig; - $this->name = (Config::SESSION_PREFIX() ?: 'SESS') . (isset($_SERVER['REMOTE_ADDR']) ? md5($_SERVER['REMOTE_ADDR']) : md5(date("dmY"))); + $this->config = $config; - //define('SESSION_PREFIX_INTERNAL_REDIS', Config::REDIS_SESSION_PREFIX() ?: 'phacil_'); + $this->name = (Config::SESSION_PREFIX() ?: 'SESS') . (isset($_SERVER['REMOTE_ADDR']) ? md5($_SERVER['REMOTE_ADDR']) : md5(date("dmY"))); - $this->redis($config->get('session_redis')); + if($config->get('session_handler')) + $this->selectHandler(); if (!session_id()) { $this->openSession(); @@ -108,7 +115,6 @@ class Session */ private function openSession() { - $this->closeSession(); ini_set('session.use_cookies', 'On'); @@ -133,15 +139,45 @@ class Session session_start(); } + /** + * Check and iniciate a session handler + * @return bool + * @throws \Phacil\Framework\Exception + * @throws \Phacil\Framework\Exception\NotFoundException + */ + private function selectHandler() { + if($this->config->get("session_handler") && !\Phacil\Framework\Registry::checkPreferenceExist(\Phacil\Framework\Session\Api\HandlerInterface::class)){ + switch($this->config->get("session_handler")){ + case \Phacil\Framework\Session\Handlers\Redis::SHORT_NAME: + \Phacil\Framework\Registry::addDIPreference(\Phacil\Framework\Session\Api\HandlerInterface::class, \Phacil\Framework\Session\Handlers\Redis::class); + break; + case \Phacil\Framework\Session\Handlers\Database::SHORT_NAME: + \Phacil\Framework\Registry::addDIPreference(\Phacil\Framework\Session\Api\HandlerInterface::class, \Phacil\Framework\Session\Handlers\Database::class); + break; + case \Phacil\Framework\Session\Handlers\File::SHORT_NAME: + \Phacil\Framework\Registry::addDIPreference(\Phacil\Framework\Session\Api\HandlerInterface::class, \Phacil\Framework\Session\Handlers\File::class); + break; + default: + if(!class_exists($this->config->get("session_handler"))) throw new \Phacil\Framework\Exception\NotFoundException("Session Handler class not found."); + + \Phacil\Framework\Registry::addDIPreference(\Phacil\Framework\Session\Api\HandlerInterface::class, $this->config->get("session_handler")); + break; + } + + $this->saveHandler = $this->registry->getInstance(\Phacil\Framework\Session\Api\HandlerInterface::class); + + $this->saveHandler->setName($this->name); + + return $this->registerSaveHandler(); + } + + return false; + } + /** * Check and iniciate the Redis connection * * @param bool $redis - * @param string|null $redisDSN - * @param string|null $redisPort - * @param string|null $redisPass - * @param int|null $redis_expire - * @param string $redis_prefix * * @since 2.0.0 * @return bool