KD2 Framework  CacheCookie.php at 5.6

File src/lib/KD2/CacheCookie.php artifact 07876a6c4c part of check-in 5.6


<?php
/*
    This file is part of KD2FW -- <http://dev.kd2.org/>

    Copyright (c) 2001-2019 BohwaZ <http://bohwaz.net/>
    All rights reserved.

    KD2FW is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Foobar is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Foobar.  If not, see <https://www.gnu.org/licenses/>.
*/

namespace KD2;

/**
 * Cache Cookie
 * (C) 2011-2014 BohwaZ
 * Inspired by Frank Denis (C) 2011 Public domain
 * https://00f.net/2011/01/19/thoughts-on-php-sessions/
 */

class CacheCookie
{
    /**
     * Name of the cookie
     * @var string
     */
    protected $name = 'cache';

    /**
     * Secret key/random hash
     * @var string
     */
    protected $secret_key = null;

    /**
     * Digest method for hash_hmac
     * @var string
     */
    protected $digest_method = 'sha256';

    /**
     * Delay before expiration when we should renew the cookie
     * before it expires (in minutes)
     * @var integer
     */
    protected $auto_renew = 30;

    /**
     * Default cookie path
     * @var string
     */
    protected $path = '/';

    /**
     * Default cookie domain
     * @var string
     */
    protected $domain = null;

    /**
     * Default cookie duration, in minutes
     * Will also determine data validity
     * @var integer
     */
    protected $duration = 0;

    /**
     * True if the cookie should only be sent over a SSL/TLS connection
     * @var boolean
     */
    protected $secure = false;

    /**
     * Start timestamp used to store a shorter timestamp in the cookie
     * @var integer
     */
    protected $start_timestamp = 1391209200; //2014-02-01 00:00:00

    /**
     * Cookie content
     * @var array
     */
    protected $content = null;

    /**
     * Cookie HTTP only parameter
     * @var boolean
     */
    protected $httponly = false;

    /**
     * Create a new CacheCookie instance and setup default parameters
     * @param string $name     Cookie name
     * @param string $secret   Secret random hash (should stay the same for at least the cookie duration)
     * @param int    $duration Cookie duration, in minutes, set to 0 (zero) to make the cookie lasts for the
     * whole user agent session (cookie will be deleted when the browser is closed).
     * @param string $path     Cookie path
     * @param string $domain   Cookie domain, if left null the current HTTP_HOST or SERVER_NAME will be used
     * @param string $secure   Set to TRUE if the cookie should only be sent on a secure connection
     */
    public function __construct($name = null, $secret = null, $duration = null, $path = null, $domain = null, $secure = false, $httponly = false)
    {
        if (!is_null($name))
        {
            $this->name = $name;
        }

        if (!is_null($secret))
        {
            $this->secret = $secret;
        }
        else
        {
            // Default secret key
            $this->secret = \hash('sha256', (isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : ''));
        }

        if (!is_null($duration))
        {
            $this->duration = (int) $duration;
        }

        if (!is_null($path))
        {
            $this->path = $path;
        }

        if (!is_null($domain))
        {
            $this->domain = $domain;
        }

        $this->secure = (bool)$secure;
        $this->httponly = (bool)$httponly;
    }

    public function setAutoRenew($renew)
    {
        $this->auto_renew = (int) $renew;
    }

    /**
     * Gets the cookie content
     * @return array Data contained in the cookie
     */
    protected function _getCookie()
    {
        if (!is_null($this->content))
        {
            return $this->content;
        }

        $cookie = null;
        $this->content = new \stdClass;

        if (!empty($_COOKIE[$this->name]))
        {
            $cookie = $_COOKIE[$this->name];
        }

        if (!empty($cookie) && (substr_count($cookie, '|') >= 2))
        {
            list($digest, $expire, $data) = explode('|', $cookie, 3);

            // Check data expiration and integrity
            if (!empty($digest) && !empty($data) && !empty($expire) 
                && ($expire > round((time() - $this->start_timestamp) / 60))
                && hash_equals($digest, hash_hmac($this->digest_method, $expire . '|' . $data, $this->secret)))
            {
                if (substr($data, 0, 1) == '{')
                {
                    $this->content = (object) json_decode($data, true);
                }
                elseif (function_exists('msgpack_unpack'))
                {
                    $this->content = (object) msgpack_unpack($data);
                }

                // If the cookie will expire soon we try to renew it first
                if ($this->auto_renew && ($expire - round((time() - $this->start_timestamp)/60) <= $this->auto_renew))
                {
                    $this->save();
                }
            }
            else
            {
                // Invalid cookie: just remove it
                $this->save();
            }
        }

        return $this->content;
    }

    /**
     * Sends the cookie content to the user-agent
     * @return boolean TRUE for success, 
     * or RuntimeException if the HTTP headers have already been sent
     */
    public function save()
    {
        if (headers_sent())
        {
            throw new \RuntimeException('Cache cookie can not be saved as headers have '
                . 'already been sent to the user agent.');
        }

        $headers = headers_list(); // List all headers
        header_remove(); // remove all headers
        $regexp = '/^Set-Cookie\\s*:\\s*' . preg_quote($this->name) . '=/';

        foreach ($headers as $header)
        {
            // Re-add every header except the one for this cookie
            if (!preg_match($regexp, $header))
            {
                header($header, true);
            }
        }

        if (!empty($this->content) && count($this->content) > 0)
        {
            if (function_exists('msgpack_pack'))
            {
                $data = msgpack_pack($this->content);
            }
            else
            {
                $data = json_encode($this->content);
            }

            // Store expiration time in minutes
            $data = round((time() - $this->start_timestamp + $this->duration*60)/60) . '|' . $data;

            $cookie = hash_hmac($this->digest_method, $data, $this->secret) . '|' . $data;

            $duration = $this->duration ? time() + $this->duration * 60 : 0;

            if (strlen($cookie . $this->path . $duration . $this->domain . $this->name) > 4080)
            {
                throw new \OverflowException('Cache cookie can not be saved as its size exceeds 4KB.');
            }

            setcookie($this->name, $cookie, $duration, $this->path, $this->domain, $this->secure, true);
            $_COOKIE[$this->name] = $cookie;
        }
        else
        {
            setcookie($this->name, '', 1, $this->path, $this->domain, $this->secure, true);
            unset($_COOKIE[$this->name]);
        }

        return true;
    }

    /**
     * Set a key/value pair in the cache cookie
     * @param mixed  $key   Key (integer or string)
     * @param mixed  $value Value (integer, string, boolean, array, float...)
     */
    public function set($key, $value)
    {
        $this->_getCookie();

        if (is_null($value))
        {
            unset($this->content->$key);
        }
        else
        {
            $this->content->$key = $value;
        }

        return true;
    }

    /**
     * Get data from the cache cookie, if $key is NULL then all the keys will be returned
     * @param  mixed    $key Data key
     * @return mixed    NULL if the key is not found, or content of the requested key
     */
    public function get($key = null)
    {
        $content = $this->_getCookie();

        if (is_null($key))
        {
            return $content;
        }

        if (!array_key_exists($key, $content))
        {
            return null;
        }
        else
        {
            return $content->$key;
        }
    }

    /**
     * Delete the cookie and all its data
     * @return boolean TRUE
     */
    public function delete()
    {
        $content = $this->get();

        foreach ($content as $key=>$value)
        {
            $this->set($key, null);
        }

        return $this->save();
    }

    /**
     * Returns raw cookie data
     * @return string cookie content
     */
    public function getRawData()
    {
        if (isset($_COOKIE[$this->name]))
            return $_COOKIE[$this->name];

        return null;
    }
}