<?php

/**
 * @copyright Copyright (c) 2009-2019 ThemeCatcher (https://www.themecatcher.net)
 */
class Quform_Session
{
    /**
     * @var string
     */
    protected $name;

    /**
     * The name of the session table
     *
     * @var string
     */
    protected $table;

    /**
     * The existence state of the session
     *
     * @var bool
     */
    protected $exists;

    /**
     * @var string
     */
    protected $id;

    /**
     * The session data
     *
     * @var array
     */
    protected $data = array();

    /**
     * The lifetime of the session in seconds
     *
     * @var int
     */
    protected $lifetime;

    /**
     * Is the session started?
     *
     * @var bool
     */
    protected $started = false;

    /**
     * Has the session data been modified?
     *
     * @var bool
     */
    protected $dirty = false;

    /**
     * Get the name of the session table with the WP prefix added
     *
     * @return string
     */
    protected function getTableName()
    {
        global $wpdb;

        return $wpdb->prefix . 'quform_sessions';
    }

    /**
     * Get the serialized session data
     *
     * @param   string  $sessionId
     * @return  string
     */
    protected function read($sessionId)
    {
        global $wpdb;
        $data = '';

        $session = $wpdb->get_row($wpdb->prepare("SELECT * FROM " . $this->getTableName() ." WHERE id = %s", $sessionId), ARRAY_A);

        if ( ! is_null($session) && isset($session['payload'])) {
            $this->exists = true;
            $data = base64_decode($session['payload']);
        }

        return $data;
    }

    /**
     * Write the serialized session data
     *
     * @param  string  $sessionId
     * @param  string  $data
     */
    protected function write($sessionId, $data)
    {
        global $wpdb;

        if ($this->exists) {
            $wpdb->update($this->getTableName(), array(
                'payload' => base64_encode($data),
                'last_activity' => time()
            ), array('id' => $sessionId));
        } else {
            $wpdb->insert($this->getTableName(), array(
                'id' => $sessionId,
                'payload' => base64_encode($data),
                'last_activity' => time()
            ));
        }

        $this->exists = true;
    }

    /**
     * Destroy the session with the given ID
     *
     * @param string $sessionId
     */
    protected function destroy($sessionId)
    {
        global $wpdb;

        $wpdb->delete($this->getTableName(), array('id' => $sessionId));
    }

    /**
     * Garbage collection
     */
    public function gc()
    {
        global $wpdb;

        $wpdb->query("DELETE FROM " . $this->getTableName() . " WHERE last_activity <= " . (time() - $this->lifetime));
    }

    /**
     * Set the session ID
     *
     * @param $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * Get the session ID
     *
     * @return string
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Determine if this is a valid session ID.
     *
     * @param   string  $id
     * @return  bool
     */
    public function isValidId($id)
    {
        return is_string($id) && preg_match('/^[a-zA-Z0-9]{40}$/', $id);
    }

    /**
     * Get a new, random session ID
     *
     * @return string
     */
    protected function generateSessionId()
    {
        return Quform::randomString(40);
    }

    /**
     * Start the session
     *
     * @return bool
     */
    public function start()
    {
        $this->name = 'quform_session_' . COOKIEHASH;
        $this->lifetime = apply_filters('quform_session_lifetime', 86400); // 24 hours in seconds

        $id = Quform::get($_COOKIE, $this->name);

        if ( ! $this->isValidId($id)) {
            $id = $this->generateSessionId();
            $this->setSessionIdCookie($id);
        }

        $this->setId($id);

        $data = $this->read($this->getId());

        $this->data = Quform::isNonEmptyString($data) ? unserialize($data) : array();

        if ( ! $this->has('_token')) {
            $this->regenerateToken();
        }

        return $this->started = true;
    }

    /**
     * Set the cookie to store the session ID
     *
     * @param string $id
     */
    protected function setSessionIdCookie($id)
    {
        $expire = apply_filters('quform_session_cookie_expire', 0);
        $secure = apply_filters('quform_session_cookie_secure', is_ssl());
        $httpOnly = apply_filters('quform_session_cookie_http_only', true);

        Quform::setCookie($this->name, $id, $expire, $secure, $httpOnly);
    }

    /**
     * Save the session data, only if something has changed
     */
    public function save()
    {
        if ($this->dirty && $this->started) {
            $this->write($this->id, serialize($this->data));
            $this->dirty = false;
        }
    }

    /**
     * Regenerate the CSRF token value
     */
    public function regenerateToken()
    {
        $this->put('_token', Quform::randomString(40));
    }

    /**
     * Get the CSRF token value
     *
     * @return string
     */
    public function getToken()
    {
        return $this->get('_token');
    }

    /**
     * Does the session data contain the given key?
     *
     * @param   string  $key
     * @return  bool
     */
    public function has($key)
    {
        return ! is_null($this->get($key));
    }

    /**
     * Get the session data with the given key
     *
     * @param   string|null  $key      The key within the session data
     * @param   mixed|null   $default  The default to return if the key does not exist
     * @return  mixed
     */
    public function get($key = null, $default = null)
    {
        return Quform::get($this->data, $key, $default);
    }

    /**
     * Set the session data with the given key
     *
     * @param  string  $key    The key within the session data
     * @param  mixed   $value  The value to set
     */
    public function set($key, $value)
    {
        Quform::set($this->data, $key, $value);
        $this->dirty = true;
    }

    /**
     * Put a key / value pair or array of key / value pairs into the session
     *
     * @param  string|array  $key
     * @param  mixed|null  	 $value
     * @return void
     */
    public function put($key, $value = null)
    {
        if ( ! is_array($key)) $key = array($key => $value);

        foreach ($key as $arrayKey => $arrayValue) {
            $this->set($arrayKey, $arrayValue);
        }
    }

    /**
     * Remove the item(s) with the given key(s) from the session
     *
     * @param array|string $keys
     */
    public function forget($keys)
    {
        Quform::forget($this->data, $keys);
        $this->dirty = true;
    }

    /**
     * Schedule the garbage collection task
     */
    protected function scheduleGc()
    {
        if ( ! wp_next_scheduled('quform_session_gc')) {
            wp_schedule_event(time(), 'twicedaily', 'quform_session_gc');
        }
    }

    /**
     * Unschedule the garbage collection task
     */
    protected function unscheduleGc()
    {
        if ($timestamp = wp_next_scheduled('quform_session_gc')) {
            wp_unschedule_event($timestamp, 'quform_session_gc');
        }
    }

    /**
     * On plugin activation, schedule the garbage collection task
     */
    public function activate()
    {
        global $wpdb;

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';

        $sql = "CREATE TABLE " . $this->getTableName() . " (
            id VARCHAR(40) NOT NULL,
            payload longtext NOT NULL,
            last_activity INT UNSIGNED NOT NULL,
            UNIQUE KEY id (id)
        ) " . $wpdb->get_charset_collate() . ";";

        dbDelta($sql);

        $this->scheduleGc();
    }

    /**
     * On plugin deactivation, unschedule the garbage collection task and cleanup
     */
    public function deactivate()
    {
        $this->unscheduleGc();
        $this->gc();
    }

    /**
     * On plugin uninstall, unschedule the garbage collection task and remove the session table
     */
    public function uninstall()
    {
        global $wpdb;

        $this->unscheduleGc();

        $wpdb->query("DROP TABLE IF EXISTS " . $this->getTableName());
    }
}
