<?php
/**
 * @package    Proxim
 * @author     Dominic Tanui <dominic@codeworldweb.com.dev | https://codeworldweb.com.dev>
 * @copyright  2019 Proxim
 * @version    1.5.0
 * @since      File available since Release 1.0.0
 */

namespace Proxim;

use Defuse\Crypto\Key;

/**
 * Cookie
 *
 */
class Cookie {
    /** @var array Contain cookie content in a key => value format */
    protected $_content;

    /** @var array Crypted cookie name for setcookie() */
    protected $_name;

    /** @var array expiration date for setcookie() */
    protected $_expire;

    /** @var array Website domain for setcookie() */
    protected $_domain;

    /** @var array Path for setcookie() */
    protected $_path;

    /** @var array cipher tool instance */
    protected $cipherTool;

    protected $_modified = false;

    protected $_allow_writing;

    protected $_salt;

    protected $_secure = false;

    /**
     * Get data if the cookie exists and else initialize an new one.
     *
     * @param $name string Cookie name before encrypting
     * @param $path string
     */
    public function __construct($name, $path = '', $expire = null, $shared_urls = null, $secure = false)
    {
        $this->_content = array();
        $this->_expire = is_null($expire) ? time() + 1728000 : (int) $expire;
        $this->_path = '/';
        $this->_domain = $this->getDomain($shared_urls);
        $this->_name = 'Proxim-' . md5( $name . $this->_domain);
        // $this->_name = $name;
        $this->_allow_writing = true;
        $this->_salt = str_pad('', 8, md5('hp' . __FILE__));

        $asciiSafeString = \Defuse\Crypto\Encoding::saveBytesToChecksummedAsciiSafeString(Key::KEY_CURRENT_VERSION, str_pad($name, Key::KEY_BYTE_SIZE, __FILE__));
        $this->cipherTool = new PhpEncryption($asciiSafeString);

        $this->_secure = (bool) $secure;

        $this->update();
    }

    protected function getDomain($shared_urls = null)
    {
        $r = '!(?:(\w+)://)?(?:(\w+)\:(\w+)@)?([^/:]+)?(?:\:(\d*))?([^#?]+)?(?:\?([^#]+))?(?:#(.+$))?!i';

        if (!preg_match($r, Tools::getHttpHost(false, false), $out) || !isset($out[4])) {
            return false;
        }

        if (preg_match('/^(((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]{1}[0-9]|[1-9]).)' .
            '{1}((25[0-5]|2[0-4][0-9]|[1]{1}[0-9]{2}|[1-9]{1}[0-9]|[0-9]).)' .
            '{2}((25[0-5]|2[0-4][0-9]|[1]{1}[0-9]{2}|[1-9]{1}[0-9]|[0-9]){1}))$/', $out[4])) {
            return false;
        }
        if (!strstr(Tools::getHttpHost(false, false), '.')) {
            return false;
        }

        $domain = false;
        if ($shared_urls !== null) {
            foreach ($shared_urls as $shared_url) {
                if ($shared_url != $out[4]) {
                    continue;
                }
                if (preg_match('/^(?:.*\.)?([^.]*(?:.{2,4})?\..{2,3})$/Ui', $shared_url, $res)) {
                    $domain = '.' . $res[1];
                    break;
                }
            }
        }
        if (!$domain) {
            $domain = $out[4];
        }

        return $domain;
    }

    /**
     * Set expiration date.
     *
     * @param int $expire Expiration time from now
     */
    public function setExpire($expire)
    {
        $this->_expire = (int) ($expire);
    }

    /**
     * Magic method wich return cookie data from _content array.
     *
     * @param string $key key wanted
     *
     * @return string value corresponding to the key
     */
    public function __get($key)
    {
        return isset($this->_content[$key]) ? $this->_content[$key] : false;
    }

    /**
     * Magic method which check if key exists in the cookie.
     *
     * @param string $key key wanted
     *
     * @return bool key existence
     */
    public function __isset($key)
    {
        return isset($this->_content[$key]);
    }

    /**
     * Magic method which adds data into _content array.
     *
     * @param string $key Access key for the value
     * @param mixed $value Value corresponding to the key
     *
     * @throws Exception
     */
    public function __set($key, $value)
    {
        if (is_array($value)) {
            die(Tools::displayError());
        }
        if (preg_match('/¤|\|/', $key . $value)) {
            throw new Exception('Forbidden chars in cookie');
        }
        if (!$this->_modified && (!isset($this->_content[$key]) || (isset($this->_content[$key]) && $this->_content[$key] != $value))) {
            $this->_modified = true;
        }
        $this->_content[$key] = $value;
    }

    /**
     * Magic method wich delete data into _content array.
     *
     * @param string $key key wanted
     */
    public function __unset($key)
    {
        if (isset($this->_content[$key])) {
            $this->_modified = true;
        }
        unset($this->_content[$key]);
    }

    /**
     * Delete cookie
     */
    public function logout()
    {
        $this->_content = array();
        $this->encryptAndSetCookie();
        unset($_COOKIE[$this->_name]);
        $this->_modified = true;
    }

    /**
     * Soft logout, delete everything links to the user
     */
    public function userLogout()
    {
        unset($this->_content['logged']);
        unset($this->_content['email']);
        unset($this->_content['user_id']);
        $this->_modified = true;
    }

    /**
     * Get cookie content.
     */
    public function update($nullValues = false)
    {
        
        try {
            if (isset($_COOKIE[$this->_name])) {
                /* Decrypt cookie content */
                $content = $this->cipherTool->decrypt($_COOKIE[$this->_name]);
                //printf("\$content = %s<br />", $content);

                /* Get cookie checksum */
                $tmpTab = explode('¤', $content);
                array_pop($tmpTab);
                $content_for_checksum = implode('¤', $tmpTab) . '¤';
                $checksum = crc32($this->_salt . $content_for_checksum);
                //printf("\$checksum = %s<br />", $checksum);

                /* Unserialize cookie content */
                $tmpTab = explode('¤', $content);
                foreach ($tmpTab as $keyAndValue) {
                    $tmpTab2 = explode('|', $keyAndValue);
                    if (count($tmpTab2) == 2) {
                        $this->_content[$tmpTab2[0]] = $tmpTab2[1];
                    }
                }
                /* Check if cookie has not been modified */
                if (!isset($this->_content['checksum']) || $this->_content['checksum'] != $checksum) {
                    $this->logout();
                }

                if (!isset($this->_content['date_add'])) {
                    $this->_content['date_add'] = date('Y-m-d H:i:s');
                }
            } else {
                $this->_content['date_add'] = date('Y-m-d H:i:s');
            }
        } catch (\Exception $e) {
            // error decrypting
            $this->logout();
        }
    }

    /**
     * Encrypt and set the Cookie.
     *
     * @param string|null $cookie Cookie content
     *
     * @return bool Indicates whether the Cookie was successfully set
     *
     * @deprecated 1.7.0
     */
    protected function _setcookie($cookie = null)
    {
        return $this->encryptAndSetCookie($cookie);
    }

    /**
     * Encrypt and set the Cookie.
     *
     * @param string|null $cookie Cookie content
     *
     * @return bool Indicates whether the Cookie was successfully set
     *
     * @since 1.7.0
     */
    protected function encryptAndSetCookie($cookie = null)
    {
        // Check if the content fits in the Cookie
        $length = (ini_get('mbstring.func_overload') & 2) ? mb_strlen($cookie, ini_get('default_charset')) : strlen($cookie);
        if ($length >= 1048576) {
            return false;
        }
        if ($cookie) {
            $content = $this->cipherTool->encrypt($cookie);
            $time = $this->_expire;
        } else {
            $content = 0;
            $time = 1;
        }

        return setcookie($this->_name, $content, $time, $this->_path, $this->_domain, $this->_secure, true);
    }

    public function __destruct()
    {
        $this->write();
    }

    /**
     * Save cookie with setcookie().
     */
    public function write()
    {
        if (!$this->_modified || headers_sent() || !$this->_allow_writing) {
            return;
        }

        $cookie = '';

        /* Serialize cookie content */
        if (isset($this->_content['checksum'])) {
            unset($this->_content['checksum']);
        }
        foreach ($this->_content as $key => $value) {
            $cookie .= $key . '|' . $value . '¤';
        }

        /* Add checksum to cookie */
        $cookie .= 'checksum|' . crc32($this->_salt . $cookie);
        $this->_modified = false;
        /* Cookies are encrypted for evident security reasons */
        return $this->encryptAndSetCookie($cookie);
    }

    /**
     * Get a family of variables (e.g. "filter_").
     */
    public function getFamily($origin)
    {
        $result = array();
        if (count($this->_content) == 0) {
            return $result;
        }
        foreach ($this->_content as $key => $value) {
            if (strncmp($key, $origin, strlen($origin)) == 0) {
                $result[$key] = $value;
            }
        }

        return $result;
    }

    public function unsetFamily($origin)
    {
        $family = $this->getFamily($origin);
        foreach (array_keys($family) as $member) {
            unset($this->$member);
        }
    }

    public function getAll()
    {
        return $this->_content;
    }

    /**
     * @return string name of cookie
     */
    public function getName()
    {
        return $this->_name;
    }

    /**
     * Check if the cookie exists.
     *
     * @since 1.5.0
     *
     * @return bool
     */
    public function exists()
    {
        return isset($_COOKIE[$this->_name]);
    }

}