setPhpSaveHandler($value); return $this; default: return parent::setOption($option, $value); } } /** * Set storage option in backend configuration store * * @param string $storageName * @param mixed $storageValue * @return SessionConfig * @throws Exception\InvalidArgumentException */ public function setStorageOption($storageName, $storageValue) { switch ($storageName) { case 'remember_me_seconds': // do nothing; not an INI option return; case 'url_rewriter_tags': $key = 'url_rewriter.tags'; break; case 'save_handler': // Save handlers must be treated differently due to changes // introduced in PHP 7.2. Do not alter running INI setting. return $this; default: $key = 'session.' . $storageName; break; } $iniGet = ini_get($key); $storageValue = (string) $storageValue; if (false !== $iniGet && (string) $iniGet === $storageValue) { return $this; } $sessionRequiresRestart = false; if (session_status() === PHP_SESSION_ACTIVE) { session_write_close(); $sessionRequiresRestart = true; } $result = ini_set($key, $storageValue); if ($sessionRequiresRestart) { session_start(); } if (false === $result) { throw new Exception\InvalidArgumentException( "'{$key}' is not a valid sessions-related ini setting." ); } return $this; } /** * Retrieve a storage option from a backend configuration store * * Used to retrieve default values from a backend configuration store. * * @param string $storageOption * @return mixed */ public function getStorageOption($storageOption) { return match ($storageOption) { // No remote storage option; just return the current value 'remember_me_seconds' => $this->rememberMeSeconds, 'url_rewriter_tags' => ini_get('url_rewriter.tags'), // The following all need a transformation on the retrieved value; // however they use the same key naming scheme 'use_cookies', 'use_only_cookies', 'use_trans_sid', 'cookie_httponly' => (bool) ini_get('session.' . $storageOption), 'save_handler' => $this->saveHandler ?? $this->sessionModuleName(), default => ini_get('session.' . $storageOption), }; } /** * Proxy to setPhpSaveHandler() * * Prevents calls to `setSaveHandler()` from hitting `setOption()` instead, * and thus bypassing the logic of `setPhpSaveHandler()`. * * @param string $phpSaveHandler * @return SessionConfig * @throws Exception\InvalidArgumentException */ public function setSaveHandler($phpSaveHandler) { return $this->setPhpSaveHandler($phpSaveHandler); } /** * Set session.save_handler * * @param string $phpSaveHandler * @return SessionConfig * @throws Exception\InvalidArgumentException */ public function setPhpSaveHandler($phpSaveHandler) { $this->saveHandler = $this->performSaveHandlerUpdate($phpSaveHandler); $this->options['save_handler'] = $this->saveHandler; return $this; } /** * Set session.save_path * * @param string $savePath * @return SessionConfig * @throws Exception\InvalidArgumentException On invalid path. */ public function setSavePath($savePath) { if ($this->getOption('save_handler') === 'files') { parent::setSavePath($savePath); } $this->savePath = $savePath; $this->setOption('save_path', $savePath); return $this; } /** * Set session.serialize_handler * * @param string $serializeHandler * @return SessionConfig * @throws Exception\InvalidArgumentException */ public function setSerializeHandler($serializeHandler) { $serializeHandler = (string) $serializeHandler; set_error_handler([$this, 'handleError']); ini_set('session.serialize_handler', $serializeHandler); restore_error_handler(); if ($this->phpErrorCode >= E_WARNING) { throw new Exception\InvalidArgumentException('Invalid serialize handler specified'); } $this->serializeHandler = (string) $serializeHandler; return $this; } // session.cache_limiter /** * Set cache limiter * * @param string $cacheLimiter * @return SessionConfig * @throws Exception\InvalidArgumentException */ public function setCacheLimiter($cacheLimiter) { $cacheLimiter = (string) $cacheLimiter; if (! in_array($cacheLimiter, $this->validCacheLimiters)) { throw new Exception\InvalidArgumentException('Invalid cache limiter provided'); } $this->setOption('cache_limiter', $cacheLimiter); ini_set('session.cache_limiter', $cacheLimiter); return $this; } /** * Set session.hash_function * * @deprecated removed in PHP 7.1 * * @param string|int $hashFunction * @return SessionConfig * @throws Exception\InvalidArgumentException */ public function setHashFunction($hashFunction) { if (PHP_VERSION_ID >= 70100) { trigger_error('session.hash_function is removed starting with PHP 7.1', E_USER_DEPRECATED); } $hashFunction = (string) $hashFunction; $validHashFunctions = $this->getHashFunctions(); if (! in_array($hashFunction, $validHashFunctions, true)) { throw new Exception\InvalidArgumentException('Invalid hash function provided'); } $this->setOption('hash_function', $hashFunction); ini_set('session.hash_function', $hashFunction); return $this; } /** * Set session.hash_bits_per_character * * @deprecated removed in PHP 7.1 * * @param int $hashBitsPerCharacter * @return SessionConfig * @throws Exception\InvalidArgumentException */ public function setHashBitsPerCharacter($hashBitsPerCharacter) { if (PHP_VERSION_ID >= 70100) { trigger_error('session.hash_bits_per_character is removed starting with PHP 7.1', E_USER_DEPRECATED); } if ( ! is_numeric($hashBitsPerCharacter) || ! in_array($hashBitsPerCharacter, $this->validHashBitsPerCharacters) ) { throw new Exception\InvalidArgumentException('Invalid hash bits per character provided'); } $hashBitsPerCharacter = (int) $hashBitsPerCharacter; $this->setOption('hash_bits_per_character', $hashBitsPerCharacter); ini_set('session.hash_bits_per_character', $hashBitsPerCharacter); return $this; } /** * Set session.sid_bits_per_character * * @param int $sidBitsPerCharacter * @return SessionConfig * @throws Exception\InvalidArgumentException */ public function setSidBitsPerCharacter($sidBitsPerCharacter) { if ( ! is_numeric($sidBitsPerCharacter) || ! in_array($sidBitsPerCharacter, $this->validSidBitsPerCharacters) ) { throw new Exception\InvalidArgumentException('Invalid sid bits per character provided'); } $sidBitsPerCharacter = (int) $sidBitsPerCharacter; $this->setOption('sid_bits_per_character', $sidBitsPerCharacter); ini_set('session.sid_bits_per_character', (string) $sidBitsPerCharacter); return $this; } /** * Retrieve list of valid hash functions * * @return array */ protected function getHashFunctions() { if (empty($this->validHashFunctions)) { /** * @link http://php.net/manual/en/session.configuration.php#ini.session.hash-function * "0" and "1" refer to MD5-128 and SHA1-160, respectively, and are * valid in addition to whatever is reported by hash_algos() */ $this->validHashFunctions = array_merge(['0', '1'], hash_algos()); } return $this->validHashFunctions; } /** * Handle PHP errors * * @param int $code * @param string $message * @return void */ protected function handleError($code, $message) { $this->phpErrorCode = $code; $this->phpErrorMessage = $message; } /** * Determine what save handlers are available. * * The only way to get at this information is via phpinfo(), and the output * of that function varies based on the SAPI. * * Strips the handler "user" from the list, as PHP 7.2 does not allow * setting that as a handler, because it essentially requires you to have * already set a custom handler via `session_set_save_handler()`. It * wasn't really valid in prior versions, either; the language simply did * not complain previously. * * @return array */ private function locateRegisteredSaveHandlers() { if (null !== $this->knownSaveHandlers) { return $this->knownSaveHandlers; } if (! preg_match('#Registered save handlers.*#m', $this->getPhpInfoForModules(), $matches)) { $this->knownSaveHandlers = []; return $this->knownSaveHandlers; } $content = array_shift($matches); $handlers = str_contains($content, '') ? $this->parseSaveHandlersFromHtml($content) : $this->parseSaveHandlersFromPlainText($content); if (false !== ($index = array_search('user', $handlers, true))) { unset($handlers[$index]); } $this->knownSaveHandlers = $handlers; return $this->knownSaveHandlers; } /** * Perform a session.save_handler update. * * Determines if the save handler represents a PHP built-in * save handler, and, if so, passes that value to session_module_name * in order to activate it. The save handler name is then returned. * * If it is not, it tests to see if it is a SessionHandlerInterface * implementation. If the string is a class implementing that interface, * it creates an instance of it. In such cases, it then calls * session_set_save_handler to activate it. The class name of the * handler is returned. * * In all other cases, an exception is raised. * * @param string|SessionHandlerInterface $phpSaveHandler * @return string * @throws Exception\InvalidArgumentException If an error occurs when * setting a PHP session save handler module. * @throws Exception\InvalidArgumentException If the $phpSaveHandler * is a string that does not represent a class implementing * SessionHandlerInterface. * @throws Exception\InvalidArgumentException If $phpSaveHandler is * a non-string value that does not implement SessionHandlerInterface. */ private function performSaveHandlerUpdate($phpSaveHandler) { if (is_string($phpSaveHandler)) { $knownHandlers = $this->locateRegisteredSaveHandlers(); if (in_array($phpSaveHandler, $knownHandlers, true)) { $phpSaveHandler = strtolower($phpSaveHandler); set_error_handler([$this, 'handleError']); $this->sessionModuleName($phpSaveHandler); restore_error_handler(); if ($this->phpErrorCode >= E_WARNING) { throw new Exception\InvalidArgumentException(sprintf( 'Error setting session save handler module "%s": %s', $phpSaveHandler, $this->phpErrorMessage )); } return $phpSaveHandler; } if ( ! class_exists($phpSaveHandler) || ! is_a($phpSaveHandler, SessionHandlerInterface::class, true) ) { throw new Exception\InvalidArgumentException(sprintf( 'Invalid save handler specified ("%s"); must be one of [%s]' . ' or a class implementing %s', $phpSaveHandler, implode(', ', $knownHandlers), SessionHandlerInterface::class )); } $phpSaveHandler = new $phpSaveHandler(); } if (! $phpSaveHandler instanceof SessionHandlerInterface) { throw new Exception\InvalidArgumentException(sprintf( 'Invalid save handler specified ("%s"); must implement %s', $phpSaveHandler::class, SessionHandlerInterface::class )); } session_set_save_handler($phpSaveHandler); return $phpSaveHandler::class; } /** * Grab module information from phpinfo. * * Requires capturing an output buffer, as phpinfo does not have an option * to return the value as a string. * * @return string */ private function getPhpInfoForModules() { $phpinfo = self::$phpinfo; ob_start(); $phpinfo(INFO_MODULES); return ob_get_clean(); } /** * Parse a list of PHP session save handlers from HTML. * * Format is "Registered save handlers{handlers}". * * @param string $content * @return array */ private function parseSaveHandlersFromHtml($content) { if (! preg_match('#(?P[^<]+)#', $content, $matches)) { return []; } $handlers = trim($matches['handlers']); return preg_split('#\s+#', $handlers); } /** * Parse a list of PHP session save handlers from plain text. * * Format is "Registered save handlers => ". * * @param string $content * @return array */ private function parseSaveHandlersFromPlainText($content) { [$prefix, $handlers] = explode('=>', $content); $handlers = trim($handlers); return preg_split('#\s+#', $handlers); } /** @return false|string */ private function sessionModuleName(?string $module = null) { $callback = self::$sessionModuleName; // session_module_name behaves differently when passed an explicit // `null` than it does when passed no arguments. if (null !== $module) { return $callback($module); } return $callback(); } }