<?php /** * A default backend for the setting and getting of cookies * * This backend allows one to better test Cookie setting and separate cookie * handling from the core * * @todo Create a config array for defaults (eg: httpOnly, secure, path, domain, expiry) * @todo A getter for cookies that haven't been sent to the browser yet * @todo Tests / a way to set the state without hacking with $_COOKIE * @todo Store the meta information around cookie setting (path, domain, secure, etc) * * @package framework * @subpackage misc */ class CookieJar implements Cookie_Backend { /** * Hold the cookies that were existing at time of instantiation (ie: The ones * sent to PHP by the browser) * * @var array Existing cookies sent by the browser */ protected $existing = array(); /** * Hold the current cookies (ie: a mix of those that were sent to us and we * have set without the ones we've cleared) * * @var array The state of cookies once we've sent the response */ protected $current = array(); /** * Hold any NEW cookies that were set by the application and will be sent * in the next response * * @var array New cookies set by the application */ protected $new = array(); /** * When creating the backend we want to store the existing cookies in our * "existing" array. This allows us to distinguish between cookies we received * or we set ourselves (and didn't get from the browser) * * @param array $cookies The existing cookies to load into the cookie jar. * Omit this to default to $_COOKIE */ public function __construct($cookies = array()) { $this->current = $this->existing = func_num_args() ? ($cookies ?: array()) // Convert empty values to blank arrays : $_COOKIE; } /** * Set a cookie * * @param string $name The name of the cookie * @param string $value The value for the cookie to hold * @param int $expiry The number of days until expiry; 0 indicates a cookie valid for the current session * @param string $path The path to save the cookie on (falls back to site base) * @param string $domain The domain to make the cookie available on * @param boolean $secure Can the cookie only be sent over SSL? * @param boolean $httpOnly Prevent the cookie being accessible by JS */ public function set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = true) { //are we setting or clearing a cookie? false values are reserved for clearing cookies (see PHP manual) $clear = false; if ($value === false || $value === '' || $expiry < 0) { $clear = true; $value = false; } //expiry === 0 is a special case where we set a cookie for the current user session if ($expiry !== 0) { //don't do the maths if we are clearing $expiry = $clear ? -1 : SS_Datetime::now()->Format('U') + (86400 * $expiry); } //set the path up $path = $path ? $path : Director::baseURL(); //send the cookie $this->outputCookie($name, $value, $expiry, $path, $domain, $secure, $httpOnly); //keep our variables in check if ($clear) { unset ($this->new[$name], $this->current[$name]); } else { $this->new[$name] = $this->current[$name] = $value; } } /** * Get the cookie value by name * * Cookie names are normalised to work around PHP's behaviour of replacing incoming variable name . with _ * * @param string $name The name of the cookie to get * @param boolean $includeUnsent Include cookies we've yet to send when fetching values * * @return string|null The cookie value or null if unset */ public function get($name, $includeUnsent = true) { $cookies = $includeUnsent ? $this->current : $this->existing; if (isset($cookies[$name])) { return $cookies[$name]; } //Normalise cookie names by replacing '.' with '_' $safeName = str_replace('.', '_', $name); if (isset($cookies[$safeName])) { return $cookies[$safeName]; } } /** * Get all the cookies * * @param boolean $includeUnsent Include cookies we've yet to send * @return array All the cookies */ public function getAll($includeUnsent = true) { return $includeUnsent ? $this->current : $this->existing; } /** * Force the expiry of a cookie by name * * @param string $name The name of the cookie to expire * @param string $path The path to save the cookie on (falls back to site base) * @param string $domain The domain to make the cookie available on * @param boolean $secure Can the cookie only be sent over SSL? * @param boolean $httpOnly Prevent the cookie being accessible by JS */ public function forceExpiry($name, $path = null, $domain = null, $secure = false, $httpOnly = true) { $this->set($name, false, -1, $path, $domain, $secure, $httpOnly); } /** * The function that actually sets the cookie using PHP * * @see http://uk3.php.net/manual/en/function.setcookie.php * * @param string $name The name of the cookie * @param string|array|false $value The value for the cookie to hold * @param int $expiry The number of days until expiry * @param string $path The path to save the cookie on (falls back to site base) * @param string $domain The domain to make the cookie available on * @param boolean $secure Can the cookie only be sent over SSL? * @param boolean $httpOnly Prevent the cookie being accessible by JS * @return boolean If the cookie was set or not; doesn't mean it's accepted by the browser */ protected function outputCookie( $name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = true ) { // if headers aren't sent, we can set the cookie if(!headers_sent($file, $line)) { return setcookie($name, $value, $expiry, $path, $domain, $secure, $httpOnly); } else if(Config::inst()->get('Cookie', 'report_errors')) { throw new LogicException( "Cookie '$name' can't be set. The site started outputting content at line $line in $file" ); } } }