diff --git a/CHANGELOG.md b/CHANGELOG.md index f2bbb7bc0..b55f80c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,9 +44,11 @@ ## 03/21/2018 1. [](#new) + * Added `Grav\Framework\Session` class to replace `RocketTheme\Toolbox\Session\Session` * Added new `|nicefilesize` Twig filter for pretty file (auto converts to bytes, kB, MB, GB, etc) * Added new `regex_filter()` Twig function to values in arrays 1. [](#improved) + * Improved session handling, allow all session configuration options in `system.session.options` * Added bosnian to lang codes [#1917](https://github.com/getgrav/grav/issues/1917) * Improved Zip extraction error codes [#1922](https://github.com/getgrav/grav/issues/1922) 1. [](#bugfix) diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index 2cdf6b066..1069fec99 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -107,7 +107,7 @@ class Validation $method = 'filter' . ucfirst(strtr($type, '-', '_')); // If this is a YAML field validate/filter as such - if ($type != 'yaml' && isset($field['yaml']) && $field['yaml'] === true) { + if ($type !== 'yaml' && isset($field['yaml']) && $field['yaml'] === true) { $method = 'filterYaml'; } diff --git a/system/src/Grav/Common/Service/SessionServiceProvider.php b/system/src/Grav/Common/Service/SessionServiceProvider.php index 52e107b7a..9262efa82 100644 --- a/system/src/Grav/Common/Service/SessionServiceProvider.php +++ b/system/src/Grav/Common/Service/SessionServiceProvider.php @@ -29,21 +29,22 @@ class SessionServiceProvider implements ServiceProviderInterface /** @var Uri $uri */ $uri = $c['uri']; - // Get session parameters. - $session_timeout = (int)$config->get('system.session.timeout', 1800); - $session_path = $config->get('system.session.path'); - if (null === $session_path) { - $session_path = '/' . ltrim(Uri::filterPath($uri->rootUrl(false)), '/'); - } - $domain = $uri->host(); - if ($domain === 'localhost') { - $domain = ''; - } - // Get session options. - $secure = (bool)$config->get('system.session.secure', false); - $httponly = (bool)$config->get('system.session.httponly', true); $enabled = (bool)$config->get('system.session.enabled', false); + $cookie_secure = (bool)$config->get('system.session.secure', false); + $cookie_httponly = (bool)$config->get('system.session.httponly', true); + $cookie_lifetime = (int)$config->get('system.session.timeout', 1800); + $cookie_path = $config->get('system.session.path'); + if (null === $cookie_path) { + $cookie_path = '/' . trim(Uri::filterPath($uri->rootUrl(false)), '/'); + } + // Session cookie path requires trailing slash. + $cookie_path = rtrim($cookie_path, '/') . '/'; + + $cookie_domain = $uri->host(); + if ($cookie_domain === 'localhost') { + $cookie_domain = ''; + } // Activate admin if we're inside the admin path. $is_admin = false; @@ -56,14 +57,14 @@ class SessionServiceProvider implements ServiceProviderInterface // Check no language, simple language prefix (en) and region specific language prefix (en-US). $pos = strpos($current_route, $base); if ($pos === 0 || $pos === 3 || $pos === 6) { - $session_timeout = $config->get('plugins.admin.session.timeout', 1800); + $cookie_lifetime = $config->get('plugins.admin.session.timeout', 1800); $enabled = $is_admin = true; } } // Fix for HUGE session timeouts. - if ($session_timeout > 99999999999) { - $session_timeout = 9999999999; + if ($cookie_lifetime > 99999999999) { + $cookie_lifetime = 9999999999; } $inflector = new Inflector(); @@ -73,10 +74,16 @@ class SessionServiceProvider implements ServiceProviderInterface } // Define session service. - $session = new Session($session_timeout, $session_path, $domain); - $session->setName($session_name); - $session->setSecure($secure); - $session->setHttpOnly($httponly); + $options = [ + 'name' => $session_name, + 'cookie_lifetime' => $cookie_lifetime, + 'cookie_path' => $cookie_path, + 'cookie_domain' => $cookie_domain, + 'cookie_secure' => $cookie_secure, + 'cookie_httponly' => $cookie_httponly + ] + (array) $config->get('system.session.options'); + + $session = new Session($options); $session->setAutoStart($enabled); return $session; diff --git a/system/src/Grav/Common/Session.php b/system/src/Grav/Common/Session.php index 7573f0866..f6fa51154 100644 --- a/system/src/Grav/Common/Session.php +++ b/system/src/Grav/Common/Session.php @@ -8,36 +8,11 @@ namespace Grav\Common; -use RocketTheme\Toolbox\Session\Session as BaseSession; - -class Session extends BaseSession +class Session extends \Grav\Framework\Session\Session { /** @var bool */ protected $autoStart = false; - protected $lifetime; - protected $path; - protected $domain; - protected $secure; - protected $httpOnly; - - /** - * @param int $lifetime Defaults to 1800 seconds. - * @param string $path Cookie path. - * @param string $domain Optional, domain for the session - * @throws \RuntimeException - */ - public function __construct($lifetime, $path, $domain = null) - { - $this->lifetime = $lifetime; - $this->path = $path; - $this->domain = $domain; - - if (php_sapi_name() !== 'cli') { - parent::__construct($lifetime, $path, $domain); - } - } - /** * Initialize session. * @@ -48,9 +23,6 @@ class Session extends BaseSession if ($this->autoStart) { $this->start(); - // TODO: This setcookie shouldn't be here, session should by itself be able to update its cookie. - setcookie(session_name(), session_id(), $this->lifetime ? time() + $this->lifetime : 0, $this->path, $this->domain, $this->secure, $this->httpOnly); - $this->autoStart = false; } } @@ -66,30 +38,6 @@ class Session extends BaseSession return $this; } - /** - * @param bool $secure - * @return $this - */ - public function setSecure($secure) - { - $this->secure = $secure; - ini_set('session.cookie_secure', (bool)$secure); - - return $this; - } - - /** - * @param bool $httpOnly - * @return $this - */ - public function setHttpOnly($httpOnly) - { - $this->httpOnly = $httpOnly; - ini_set('session.cookie_httponly', (bool)$httpOnly); - - return $this; - } - /** * Store something in session temporarily. * diff --git a/system/src/Grav/Framework/Session/Session.php b/system/src/Grav/Framework/Session/Session.php new file mode 100644 index 000000000..ac6b96b61 --- /dev/null +++ b/system/src/Grav/Framework/Session/Session.php @@ -0,0 +1,378 @@ +isSessionStarted()) { + session_unset(); + session_destroy(); + } + + // Set default options. + $options += array( + 'cache_limiter' => 'nocache', + 'use_trans_sid' => 0, + 'use_cookies' => 1, + 'lazy_write' => 1, + 'use_strict_mode' => 1 + ); + + $this->setOptions($options); + + session_register_shutdown(); + + self::$instance = $this; + } + + /** + * Sets session.* ini variables. + * + * @param array $options + * + * @see http://php.net/session.configuration + */ + public function setOptions(array $options) + { + if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + return; + } + + $allowedOptions = [ + 'save_path' => true, + 'name' => true, + 'save_handler' => true, + 'gc_probability' => true, + 'gc_divisor' => true, + 'gc_maxlifetime' => true, + 'serialize_handler' => true, + 'cookie_lifetime' => true, + 'cookie_path' => true, + 'cookie_domain' => true, + 'cookie_secure' => true, + 'cookie_httponly' => true, + 'use_strict_mode' => true, + 'use_cookies' => true, + 'use_only_cookies' => true, + 'referer_check' => true, + 'cache_limiter' => true, + 'cache_expire' => true, + 'use_trans_sid' => true, + 'trans_sid_tags' => true, // PHP 7.1 + 'trans_sid_hosts' => true, // PHP 7.1 + 'sid_length' => true, // PHP 7.1 + 'sid_bits_per_character' => true, // PHP 7.1 + 'upload_progress.enabled' => true, + 'upload_progress.cleanup' => true, + 'upload_progress.prefix' => true, + 'upload_progress.name' => true, + 'upload_progress.freq' => true, + 'upload_progress.min-freq' => true, + 'lazy_write' => true, + 'url_rewriter.tags' => true, // Not used in PHP 7.1 + 'hash_function' => true, // Not used in PHP 7.1 + 'hash_bits_per_character' => true, // Not used in PHP 7.1 + 'entropy_file' => true, // Not used in PHP 7.1 + 'entropy_length' => true, // Not used in PHP 7.1 + ]; + + foreach ($options as $key => $value) { + if (is_array($value)) { + // Allow nested options. + foreach ($value as $key2 => $value2) { + $ckey = "{$key}.{$key2}"; + if (isset($value2, $allowedOptions[$ckey])) { + $this->ini_set("session.{$ckey}", $value2); + } + } + } elseif (isset($value, $allowedOptions[$key])) { + $this->ini_set("session.{$key}", $value); + } + } + } + + /** + * Starts the session storage + * + * @return $this + * @throws \RuntimeException + */ + public function start($readonly = false) + { + // Protection against invalid session cookie names throwing exception: http://php.net/manual/en/function.session-id.php#116836 + if (isset($_COOKIE[session_name()]) && !preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[session_name()])) { + unset($_COOKIE[session_name()]); + } + + $options = $readonly ? ['read_and_close' => '1'] : []; + + $success = @session_start($options); + if (!$success) { + $last = error_get_last(); + $error = $last ? $last['message'] : 'Unknown error'; + throw new \RuntimeException('Failed to start session: ' . $error, 500); + } + + $params = session_get_cookie_params(); + + setcookie( + session_name(), + session_id(), + time() + $params['lifetime'], + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); + + $this->started = true; + + return $this; + } + + /** + * Get session ID + * + * @return string|null Session ID + */ + public function getId() + { + return session_id(); + } + + /** + * Set session Id + * + * @param string $id Session ID + * + * @return $this + */ + public function setId($id) + { + session_id($id); + + return $this; + } + + + /** + * Get session name + * + * @return string|null + */ + public function getName() + { + return session_name(); + } + + /** + * Set session name + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + session_name($name); + + return $this; + } + + /** + * Invalidates the current session. + * + * @return $this + */ + public function invalidate() + { + $params = session_get_cookie_params(); + setcookie( + session_name(), + '', + time() - 42000, + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); + + session_unset(); + session_destroy(); + + $this->started = false; + + return $this; + } + + /** + * Force the session to be saved and closed + * + * @return $this + */ + public function close() + { + if ($this->started) { + session_write_close(); + } + + $this->started = false; + + return $this; + } + + /** + * Checks if an attribute is defined. + * + * @param string $name The attribute name + * + * @return bool True if the attribute is defined, false otherwise + */ + public function __isset($name) + { + return isset($_SESSION[$name]); + } + + /** + * Returns an attribute. + * + * @param string $name The attribute name + * + * @return mixed + */ + public function __get($name) + { + return isset($_SESSION[$name]) ? $_SESSION[$name] : null; + } + + /** + * Sets an attribute. + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + $_SESSION[$name] = $value; + } + + /** + * Removes an attribute. + * + * @param string $name + */ + public function __unset($name) + { + unset($_SESSION[$name]); + } + + /** + * Returns attributes. + * + * @return array Attributes + */ + public function all() + { + return $_SESSION; + } + + + /** + * Retrieve an external iterator + * + * @return \ArrayIterator Return an ArrayIterator of $_SESSION + */ + public function getIterator() + { + return new \ArrayIterator($_SESSION); + } + + /** + * Checks if the session was started. + * + * @return Boolean + */ + public function started() + { + return $this->started; + } + + /** + * http://php.net/manual/en/function.session-status.php#113468 + * Check if session is started nicely. + * @return bool + */ + protected function isSessionStarted() + { + return php_sapi_name() !== 'cli' ? \PHP_SESSION_ACTIVE === session_status() : false; + } + + /** + * @param string $key + * @param mixed $value + */ + protected function ini_set($key, $value) + { + if (!is_string($value)) { + if (is_bool($value)) { + $value = $value ? '1' : '0'; + } + $value = (string)$value; + } + + ini_set($key, $value); + } +}