| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507 | <?php/* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */namespace Symfony\Component\HttpFoundation\Session\Storage;use Symfony\Component\HttpFoundation\Session\SessionBagInterface;use Symfony\Component\HttpFoundation\Session\SessionUtils;use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;// Help opcache.preload discover always-needed symbolsclass_exists(MetadataBag::class);class_exists(StrictSessionHandler::class);class_exists(SessionHandlerProxy::class);/** * This provides a base class for session attribute storage. * * @author Drak <drak@zikula.org> */class NativeSessionStorage implements SessionStorageInterface{    /**     * @var SessionBagInterface[]     */    protected $bags = [];    /**     * @var bool     */    protected $started = false;    /**     * @var bool     */    protected $closed = false;    /**     * @var AbstractProxy|\SessionHandlerInterface     */    protected $saveHandler;    /**     * @var MetadataBag     */    protected $metadataBag;    /**     * @var string|null     */    private $emulateSameSite;    /**     * Depending on how you want the storage driver to behave you probably     * want to override this constructor entirely.     *     * List of options for $options array with their defaults.     *     * @see https://php.net/session.configuration for options     * but we omit 'session.' from the beginning of the keys for convenience.     *     * ("auto_start", is not supported as it tells PHP to start a session before     * PHP starts to execute user-land code. Setting during runtime has no effect).     *     * cache_limiter, "" (use "0" to prevent headers from being sent entirely).     * cache_expire, "0"     * cookie_domain, ""     * cookie_httponly, ""     * cookie_lifetime, "0"     * cookie_path, "/"     * cookie_secure, ""     * cookie_samesite, null     * gc_divisor, "100"     * gc_maxlifetime, "1440"     * gc_probability, "1"     * lazy_write, "1"     * name, "PHPSESSID"     * referer_check, ""     * serialize_handler, "php"     * use_strict_mode, "1"     * use_cookies, "1"     * use_only_cookies, "1"     * use_trans_sid, "0"     * sid_length, "32"     * sid_bits_per_character, "5"     * trans_sid_hosts, $_SERVER['HTTP_HOST']     * trans_sid_tags, "a=href,area=href,frame=src,form="     *     * @param AbstractProxy|\SessionHandlerInterface|null $handler     */    public function __construct(array $options = [], $handler = null, ?MetadataBag $metaBag = null)    {        if (!\extension_loaded('session')) {            throw new \LogicException('PHP extension "session" is required.');        }        $options += [            'cache_limiter' => '',            'cache_expire' => 0,            'use_cookies' => 1,            'lazy_write' => 1,            'use_strict_mode' => 1,        ];        session_register_shutdown();        $this->setMetadataBag($metaBag);        $this->setOptions($options);        $this->setSaveHandler($handler);    }    /**     * Gets the save handler instance.     *     * @return AbstractProxy|\SessionHandlerInterface     */    public function getSaveHandler()    {        return $this->saveHandler;    }    /**     * {@inheritdoc}     */    public function start()    {        if ($this->started) {            return true;        }        if (\PHP_SESSION_ACTIVE === session_status()) {            throw new \RuntimeException('Failed to start the session: already started by PHP.');        }        if (filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN) && headers_sent($file, $line)) {            throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line));        }        $sessionId = $_COOKIE[session_name()] ?? null;        /*         * Explanation of the session ID regular expression: `/^[a-zA-Z0-9,-]{22,250}$/`.         *         * ---------- Part 1         *         * The part `[a-zA-Z0-9,-]` is related to the PHP ini directive `session.sid_bits_per_character` defined as 6.         * See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-bits-per-character.         * Allowed values are integers such as:         * - 4 for range `a-f0-9`         * - 5 for range `a-v0-9`         * - 6 for range `a-zA-Z0-9,-`         *         * ---------- Part 2         *         * The part `{22,250}` is related to the PHP ini directive `session.sid_length`.         * See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length.         * Allowed values are integers between 22 and 256, but we use 250 for the max.         *         * Where does the 250 come from?         * - The length of Windows and Linux filenames is limited to 255 bytes. Then the max must not exceed 255.         * - The session filename prefix is `sess_`, a 5 bytes string. Then the max must not exceed 255 - 5 = 250.         *         * ---------- Conclusion         *         * The parts 1 and 2 prevent the warning below:         * `PHP Warning: SessionHandler::read(): Session ID is too long or contains illegal characters. Only the A-Z, a-z, 0-9, "-", and "," characters are allowed.`         *         * The part 2 prevents the warning below:         * `PHP Warning: SessionHandler::read(): open(filepath, O_RDWR) failed: No such file or directory (2).`         */        if ($sessionId && $this->saveHandler instanceof AbstractProxy && 'files' === $this->saveHandler->getSaveHandlerName() && !preg_match('/^[a-zA-Z0-9,-]{22,250}$/', $sessionId)) {            // the session ID in the header is invalid, create a new one            session_id(session_create_id());        }        // ok to try and start the session        if (!session_start()) {            throw new \RuntimeException('Failed to start the session.');        }        if (null !== $this->emulateSameSite) {            $originalCookie = SessionUtils::popSessionCookie(session_name(), session_id());            if (null !== $originalCookie) {                header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false);            }        }        $this->loadSession();        return true;    }    /**     * {@inheritdoc}     */    public function getId()    {        return $this->saveHandler->getId();    }    /**     * {@inheritdoc}     */    public function setId(string $id)    {        $this->saveHandler->setId($id);    }    /**     * {@inheritdoc}     */    public function getName()    {        return $this->saveHandler->getName();    }    /**     * {@inheritdoc}     */    public function setName(string $name)    {        $this->saveHandler->setName($name);    }    /**     * {@inheritdoc}     */    public function regenerate(bool $destroy = false, ?int $lifetime = null)    {        // Cannot regenerate the session ID for non-active sessions.        if (\PHP_SESSION_ACTIVE !== session_status()) {            return false;        }        if (headers_sent()) {            return false;        }        if (null !== $lifetime && $lifetime != \ini_get('session.cookie_lifetime')) {            $this->save();            ini_set('session.cookie_lifetime', $lifetime);            $this->start();        }        if ($destroy) {            $this->metadataBag->stampNew();        }        $isRegenerated = session_regenerate_id($destroy);        if (null !== $this->emulateSameSite) {            $originalCookie = SessionUtils::popSessionCookie(session_name(), session_id());            if (null !== $originalCookie) {                header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false);            }        }        return $isRegenerated;    }    /**     * {@inheritdoc}     */    public function save()    {        // Store a copy so we can restore the bags in case the session was not left empty        $session = $_SESSION;        foreach ($this->bags as $bag) {            if (empty($_SESSION[$key = $bag->getStorageKey()])) {                unset($_SESSION[$key]);            }        }        if ($_SESSION && [$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) {            unset($_SESSION[$key]);        }        // Register error handler to add information about the current save handler        $previousHandler = set_error_handler(function ($type, $msg, $file, $line) use (&$previousHandler) {            if (\E_WARNING === $type && str_starts_with($msg, 'session_write_close():')) {                $handler = $this->saveHandler instanceof SessionHandlerProxy ? $this->saveHandler->getHandler() : $this->saveHandler;                $msg = sprintf('session_write_close(): Failed to write session data with "%s" handler', \get_class($handler));            }            return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false;        });        try {            session_write_close();        } finally {            restore_error_handler();            // Restore only if not empty            if ($_SESSION) {                $_SESSION = $session;            }        }        $this->closed = true;        $this->started = false;    }    /**     * {@inheritdoc}     */    public function clear()    {        // clear out the bags        foreach ($this->bags as $bag) {            $bag->clear();        }        // clear out the session        $_SESSION = [];        // reconnect the bags to the session        $this->loadSession();    }    /**     * {@inheritdoc}     */    public function registerBag(SessionBagInterface $bag)    {        if ($this->started) {            throw new \LogicException('Cannot register a bag when the session is already started.');        }        $this->bags[$bag->getName()] = $bag;    }    /**     * {@inheritdoc}     */    public function getBag(string $name)    {        if (!isset($this->bags[$name])) {            throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name));        }        if (!$this->started && $this->saveHandler->isActive()) {            $this->loadSession();        } elseif (!$this->started) {            $this->start();        }        return $this->bags[$name];    }    public function setMetadataBag(?MetadataBag $metaBag = null)    {        if (null === $metaBag) {            $metaBag = new MetadataBag();        }        $this->metadataBag = $metaBag;    }    /**     * Gets the MetadataBag.     *     * @return MetadataBag     */    public function getMetadataBag()    {        return $this->metadataBag;    }    /**     * {@inheritdoc}     */    public function isStarted()    {        return $this->started;    }    /**     * Sets session.* ini variables.     *     * For convenience we omit 'session.' from the beginning of the keys.     * Explicitly ignores other ini keys.     *     * @param array $options Session ini directives [key => value]     *     * @see https://php.net/session.configuration     */    public function setOptions(array $options)    {        if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {            return;        }        $validOptions = array_flip([            'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly',            'cookie_lifetime', 'cookie_path', 'cookie_secure', 'cookie_samesite',            'gc_divisor', 'gc_maxlifetime', 'gc_probability',            'lazy_write', 'name', 'referer_check',            'serialize_handler', 'use_strict_mode', 'use_cookies',            'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled',            'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name',            'upload_progress.freq', 'upload_progress.min_freq', 'url_rewriter.tags',            'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags',        ]);        foreach ($options as $key => $value) {            if (isset($validOptions[$key])) {                if (str_starts_with($key, 'upload_progress.')) {                    trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. The settings prefixed with "session.upload_progress." can not be changed at runtime.', $key);                    continue;                }                if ('url_rewriter.tags' === $key) {                    trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. Use "trans_sid_tags" instead.', $key);                }                if ('cookie_samesite' === $key && \PHP_VERSION_ID < 70300) {                    // PHP < 7.3 does not support same_site cookies. We will emulate it in                    // the start() method instead.                    $this->emulateSameSite = $value;                    continue;                }                if ('cookie_secure' === $key && 'auto' === $value) {                    continue;                }                ini_set('url_rewriter.tags' !== $key ? 'session.'.$key : $key, $value);            }        }    }    /**     * Registers session save handler as a PHP session handler.     *     * To use internal PHP session save handlers, override this method using ini_set with     * session.save_handler and session.save_path e.g.     *     *     ini_set('session.save_handler', 'files');     *     ini_set('session.save_path', '/tmp');     *     * or pass in a \SessionHandler instance which configures session.save_handler in the     * constructor, for a template see NativeFileSessionHandler.     *     * @see https://php.net/session-set-save-handler     * @see https://php.net/sessionhandlerinterface     * @see https://php.net/sessionhandler     *     * @param AbstractProxy|\SessionHandlerInterface|null $saveHandler     *     * @throws \InvalidArgumentException     */    public function setSaveHandler($saveHandler = null)    {        if (!$saveHandler instanceof AbstractProxy            && !$saveHandler instanceof \SessionHandlerInterface            && null !== $saveHandler        ) {            throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.');        }        // Wrap $saveHandler in proxy and prevent double wrapping of proxy        if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) {            $saveHandler = new SessionHandlerProxy($saveHandler);        } elseif (!$saveHandler instanceof AbstractProxy) {            $saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler()));        }        $this->saveHandler = $saveHandler;        if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {            return;        }        if ($this->saveHandler instanceof SessionHandlerProxy) {            session_set_save_handler($this->saveHandler, false);        }    }    /**     * Load the session with attributes.     *     * After starting the session, PHP retrieves the session from whatever handlers     * are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()).     * PHP takes the return value from the read() handler, unserializes it     * and populates $_SESSION with the result automatically.     */    protected function loadSession(?array &$session = null)    {        if (null === $session) {            $session = &$_SESSION;        }        $bags = array_merge($this->bags, [$this->metadataBag]);        foreach ($bags as $bag) {            $key = $bag->getStorageKey();            $session[$key] = isset($session[$key]) && \is_array($session[$key]) ? $session[$key] : [];            $bag->initialize($session[$key]);        }        $this->started = true;        $this->closed = false;    }}
 |