<?php

/**
 * Part of the Joomla Framework Session Package
 *
 * @copyright  Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

namespace Joomla\Session\Storage;

use Joomla\Session\HandlerInterface;
use Joomla\Session\StorageInterface;

/**
 * Base class providing a session store
 *
 * @since  2.0.0
 */
class NativeStorage implements StorageInterface
{
    /**
     * Flag if the session is active
     *
     * @var    boolean
     * @since  2.0.0
     */
    private $active = false;

    /**
     * Internal flag identifying whether the session has been closed
     *
     * @var    boolean
     * @since  2.0.0
     */
    private $closed = false;

    /**
     * Session save handler
     *
     * @var    \SessionHandlerInterface
     * @since  2.0.0
     */
    private $handler;

    /**
     * Internal flag identifying whether the session has been started
     *
     * @var    boolean
     * @since  2.0.0
     */
    private $started = false;

    /**
     * Constructor
     *
     * @param   ?\SessionHandlerInterface  $handler  Session save handler
     * @param   array                      $options  Session options
     *
     * @since   1.0
     */
    public function __construct(?\SessionHandlerInterface $handler = null, array $options = [])
    {
        // Disable transparent sid support and default use cookies
        $options += [
            'use_cookies'   => 1,
            'use_trans_sid' => 0,
        ];

        if (!headers_sent()) {
            session_cache_limiter('none');
        }

        session_register_shutdown();

        $this->setOptions($options);
        $this->setHandler($handler);
    }

    /**
     * Retrieves all variables from the session store
     *
     * @return  array
     *
     * @since   2.0.0
     */
    public function all(): array
    {
        return $_SESSION;
    }

    /**
     * Clears all variables from the session store
     *
     * @return  void
     *
     * @since   2.0.0
     */
    public function clear(): void
    {
        $_SESSION = [];
    }

    /**
     * Writes session data and ends session
     *
     * @return  void
     *
     * @see     session_write_close()
     * @since   2.0.0
     */
    public function close(): void
    {
        session_write_close();

        $this->closed  = true;
        $this->started = false;
    }

    /**
     * Perform session data garbage collection
     *
     * @return  integer|boolean  Number of deleted sessions on success or boolean false on failure or if the function is unsupported
     *
     * @see     session_gc()
     * @since   2.0.0
     */
    public function gc()
    {
        if (!$this->isStarted()) {
            $this->start();
        }

        return session_gc();
    }

    /**
     * Aborts the current session
     *
     * @return  boolean
     *
     * @see     session_abort()
     * @since   2.0.0
     */
    public function abort(): bool
    {
        if (!$this->isStarted()) {
            return true;
        }

        return session_abort();
    }

    /**
     * Get data from the session store
     *
     * @param   string  $name     Name of a variable
     * @param   mixed   $default  Default value of a variable if not set
     *
     * @return  mixed  Value of a variable
     *
     * @since   2.0.0
     */
    public function get(string $name, $default)
    {
        if (!$this->isStarted()) {
            $this->start();
        }

        if (isset($_SESSION[$name])) {
            return $_SESSION[$name];
        }

        return $default;
    }

    /**
     * Gets the save handler instance
     *
     * @return  \SessionHandlerInterface|null
     *
     * @since   2.0.0
     */
    public function getHandler(): ?\SessionHandlerInterface
    {
        return $this->handler;
    }

    /**
     * Get the session ID
     *
     * @return  string  The session ID
     *
     * @since   2.0.0
     */
    public function getId(): string
    {
        return session_id();
    }

    /**
     * Get the session name
     *
     * @return  string  The session name
     *
     * @since   2.0.0
     */
    public function getName(): string
    {
        return session_name();
    }

    /**
     * Check whether data exists in the session store
     *
     * @param   string  $name  Name of variable
     *
     * @return  boolean
     *
     * @since   2.0.0
     */
    public function has(string $name): bool
    {
        if (!$this->isStarted()) {
            $this->start();
        }

        return isset($_SESSION[$name]);
    }

    /**
     * Check if the session is active
     *
     * @return  boolean
     *
     * @since   2.0.0
     */
    public function isActive(): bool
    {
        return $this->active = session_status() === \PHP_SESSION_ACTIVE;
    }

    /**
     * Check if the session is started
     *
     * @return  boolean
     *
     * @since   2.0.0
     */
    public function isStarted(): bool
    {
        return $this->started;
    }

    /**
     * Unset a variable from the session store
     *
     * @param   string  $name  Name of variable
     *
     * @return  mixed  The value from session or NULL if not set
     *
     * @since   2.0.0
     */
    public function remove(string $name)
    {
        if (!$this->isStarted()) {
            $this->start();
        }

        $old = $_SESSION[$name] ?? null;

        unset($_SESSION[$name]);

        return $old;
    }

    /**
     * Regenerates the session ID that represents this storage.
     *
     * This method must invoke session_regenerate_id($destroy) unless this interface is used for a storage object designed for unit
     * or functional testing where a real PHP session would interfere with testing.
     *
     * @param   boolean  $destroy  Destroy session when regenerating?
     *
     * @return  boolean  True on success
     *
     * @see     session_regenerate_id()
     * @since   2.0.0
     */
    public function regenerate(bool $destroy = false): bool
    {
        if (headers_sent() || !$this->isActive()) {
            return false;
        }

        return session_regenerate_id($destroy);
    }

    /**
     * Set data into the session store
     *
     * @param   string  $name   Name of a variable
     * @param   mixed   $value  Value of a variable
     *
     * @return  mixed  Old value of a variable
     *
     * @since   2.0.0
     */
    public function set(string $name, $value = null)
    {
        if (!$this->isStarted()) {
            $this->start();
        }

        $old = $_SESSION[$name] ?? null;

        $_SESSION[$name] = $value;

        return $old;
    }

    /**
     * Registers session save handler as a PHP session handler
     *
     * @param   ?\SessionHandlerInterface  $handler  The save handler to use
     *
     * @return  $this
     *
     * @since   2.0.0
     * @throws  \RuntimeException
     */
    public function setHandler(?\SessionHandlerInterface $handler = null): self
    {
        // If the handler is an instance of our HandlerInterface, check whether it is supported
        if ($handler instanceof HandlerInterface) {
            if (!$handler::isSupported()) {
                throw new \RuntimeException(
                    sprintf(
                        'The "%s" handler is not supported in this environment.',
                        \get_class($handler)
                    )
                );
            }
        }

        $this->handler = $handler;

        if (!headers_sent() && !$this->isActive()) {
            session_set_save_handler($this->handler, false);
        }

        return $this;
    }

    /**
     * Set the session ID
     *
     * @param   string  $id  The session ID
     *
     * @return  $this
     *
     * @since   2.0.0
     * @throws  \LogicException
     */
    public function setId(string $id)
    {
        if ($this->isActive()) {
            throw new \LogicException('Cannot change the ID of an active session');
        }

        session_id($id);

        return $this;
    }

    /**
     * Set the session name
     *
     * @param   string  $name  The session name
     *
     * @return  $this
     *
     * @since   2.0.0
     * @throws  \LogicException
     */
    public function setName(string $name)
    {
        if ($this->isActive()) {
            throw new \LogicException('Cannot change the name of an active session');
        }

        session_name($name);

        return $this;
    }

    /**
     * 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 array(key => value).
     *
     * @return  $this
     *
     * @note    Based on \Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage::setOptions()
     * @see     http://php.net/session.configuration
     * @since   2.0.0
     */
    public function setOptions(array $options): self
    {
        if (headers_sent() || $this->isActive()) {
            return $this;
        }

        $validOptions = array_flip(
            [
                'cache_limiter', 'cache_expire', 'cookie_domain', 'cookie_httponly', 'cookie_lifetime', 'cookie_path', 'cookie_secure', '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])) {
                ini_set('session.' . $key, $value);
            }
        }

        return $this;
    }

    /**
     * Start a session
     *
     * @return  void
     *
     * @since   2.0.0
     */
    public function start(): void
    {
        if ($this->isStarted()) {
            return;
        }

        if ($this->isActive()) {
            throw new \RuntimeException('Failed to start the session: already started by PHP.');
        }

        if (ini_get('session.use_cookies') && 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)
            );
        }

        if (!session_start()) {
            throw new \RuntimeException('Failed to start the session');
        }

        $this->isActive();
        $this->closed  = false;
        $this->started = true;
    }
}
