PHP Classes

File: Cookie_Jar.php

Recommend this page to a friend!
  Classes of Keyvan Minoukadeh   Cookie Jar   Cookie_Jar.php   Download  
File: Cookie_Jar.php
Role: Class source
Content type: text/plain
Description: Main class
Class: Cookie Jar
Class for handling cookies (for HTTP clients)
Author: By
Last change: Modified add_cookie_header() to call the push_header() method of the HTTP Request class.
Modified extract_cookies() to call the get_header_array() method of the HTTP Response class.
Requires a very simple Debug class (Debug.php), use Debug::on() and Debug::off()
Date: 21 years ago
Size: 28,107 bytes
 

Contents

Class file image Download
<?php // $Id: Cookie_Jar.php,v 1.2 2003/01/22 12:25:30 k1m Exp $ // +----------------------------------------------------------------------+ // | Cookie Jar class 0.2 | // +----------------------------------------------------------------------+ // | Author: Keyvan Minoukadeh - keyvan@k1m.com - http://www.keyvan.net | // +----------------------------------------------------------------------+ // | PHP class for handling cookies (as defined by the Netscape spec: | // | <http://wp.netscape.com/newsref/std/cookie_spec.html>) | // +----------------------------------------------------------------------+ // | This program is free software; you can redistribute it and/or | // | modify it under the terms of the GNU General Public License | // | as published by the Free Software Foundation; either version 2 | // | of the License, or (at your option) any later version. | // | | // | This program 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 General Public License for more details. | // +----------------------------------------------------------------------+ if (!defined('HTTPNAV_ROOT')) define('HTTPNAV_ROOT', dirname(__FILE__).'/'); require_once(HTTPNAV_ROOT.'Debug.php'); /** * Cookie Jar class * * This class should be used to handle cookies (storing cookies from HTTP response messages, and * sending out cookies in HTTP request messages). * * This class is mainly based on Cookies.pm <http://search.cpan.org/author/GAAS/libwww-perl-5.65/ * lib/HTTP/Cookies.pm> from the libwww-perl collection <http://www.linpro.no/lwp/>. * Unlike Cookies.pm, this class only supports the Netscape cookie spec * <http://wp.netscape.com/newsref/std/cookie_spec.html>, not RFC 2965. * * I've been looking at both the Netscape cookie spec and RFC 2965, a lot of the functions will * be based on RFC 2965 simply because it covers details missed out by the netscape cookie spec. * * Please consider this class in 'alpha' state at the moment, I've still got a lot of testing * to do, and will need to compare cookie handling with some existing browsers. * Any feedback appreciated. * * Example: * $options = array( * 'file_persistent' => 'cookies.txt', * 'autosave' => true * ); * $jar =& new Cookie_Jar($options); * $jar->add_cookie_header($my_request); * $jar->destroy(); * * See test_Cookie_Jar.php file for usage examples * * CHANGES: * * 0.2 (20-Jan-2002) * - Modified add_cookie_header() to call the push_header() method of the HTTP Request class. * - Modified extract_cookies() to call the get_header_array() method of the HTTP Response class. * - Requires a very simple Debug class (Debug.php), use Debug::on() and Debug::off() * * 0.1 (09-Dec-2002) * - Initial release * * TODO: * - testing * * @author Keyvan Minoukadeh <keyvan@k1m.com> * @version 0.2 */ class Cookie_Jar { /** * Cookies - array containing all cookies. * * Cookies are stored like this: * [domain][path][name] = array * where array is: * 0 => value, 1 => secure, 2 => expires * * @var array * @access private */ var $cookies = array(); /** * Cookie options * @var array * @access private */ var $options = array(); /** * Constructor - will accept an associative array holding the cookie jar options * @param array $options */ function Cookie_Jar($options=null) { if (isset($options)) { $this->set_option($options); $this->load(); } } /** * Add cookie header - adds the relevant cookie header to the request message * * @param object $request request object * @return void */ function add_cookie_header(&$request) { $url =& $request->get_url(); $domain = $this->_get_host($request, $url); $request_secure = ($url->get_scheme() == 'https'); $request_path = urldecode($url->get_path()); // add "Cookie" header to request $param = array('domain'=>$domain, 'path'=>$request_path, 'secure'=>$request_secure); // push_header() will add a cookie header withour overwriting any existing cookie headers if ($cookies = $this->get_matching_cookies($param)) { $request->push_header('Cookie', $cookies); } } /** * Get matching cookies * * Only use this method if you cannot use add_cookie_header(), for example, if you want to use * this cookie jar class without using the request class. * * @param array $param associative array containing 'domain', 'path', 'secure' keys * @return string * @see add_cookie_header() */ function get_matching_cookies($param) { // RFC 2965 notes: // If multiple cookies satisfy the criteria above, they are ordered in // the Cookie header such that those with more specific Path attributes // precede those with less specific. Ordering with respect to other // attributes (e.g., Domain) is unspecified. $domain = $param['domain']; if (strpos($domain, '.') === false) $domain .= '.local'; $request_path = $param['path']; if ($request_path == '') $request_path = '/'; $request_secure = $param['secure']; $now = time(); $matched_cookies = array(); // domain - find matching domains Debug::debug('Finding matching domains for '.$domain, __FILE__, __LINE__); while (strpos($domain, '.') !== false) { if (isset($this->cookies[$domain])) { Debug::debug(' domain match found: '.$domain); $cookies =& $this->cookies[$domain]; } else { $domain = $this->_reduce_domain($domain); continue; } // paths - find matching paths starting from most specific Debug::debug(' - Finding matching paths for '.$request_path); $paths = array_keys($cookies); usort($paths, array(&$this, '_cmp_length')); foreach ($paths as $path) { // continue to next cookie if request path does not path-match cookie path if (!$this->_path_match($request_path, $path)) continue; // loop through cookie names Debug::debug(' path match found: '.$path); foreach ($cookies[$path] as $name => $values) { // if this cookie is secure but request isn't, continue to next cookie if ($values[1] && !$request_secure) continue; // if cookie is not a session cookie and has expired, continue to next cookie if (is_int($values[2]) && ($values[2] < $now)) continue; // cookie matches request Debug::debug(' cookie match: '.$name.'='.$values[0]); $matched_cookies[] = $name.'='.$values[0]; } } $domain = $this->_reduce_domain($domain); } // return cookies return implode('; ', $matched_cookies); } /** * Extract cookies - extracts cookies from the HTTP response message. * @param object $response * @return void */ function extract_cookies(&$response) { $set_cookies = $response->get_header_array('Set-Cookie'); if (!$set_cookies) return; $request =& $response->get_request(); $url = $request->get_url(); $request_host = $this->_get_host($request, $url); $request_path = urldecode($url->get_path()); $param = array('host'=>$request_host, 'path'=>$request_path); $this->parse_set_cookies($set_cookies, $param); } /** * Parse Set-Cookie values. * * Only use this method if you cannot use extract_cookies(), for example, if you want to use * this cookie jar class without using the response class. * * @param array $set_cookies array holding 1 or more "Set-Cookie" header values * @param array $param associative array containing 'host', 'path' keys * @return void * @see extract_cookies() */ function parse_set_cookies($set_cookies, $param) { if (count($set_cookies) == 0) return; $request_host = $param['host']; if (strpos($request_host, '.') === false) $request_host .= '.local'; $request_path = $param['path']; if ($request_path == '') $request_path = '/'; // // loop through set-cookie headers // foreach ($set_cookies as $set_cookie) { Debug::debug('Parsing: '.$set_cookie); // temporary cookie store (before adding to jar) $tmp_cookie = array(); $param = explode(';', $set_cookie); // loop through params for ($x=0; $x<count($param); $x++) { $key_val = explode('=', $param[$x], 2); if (count($key_val) != 2) { // if the first param isn't a name=value pair, continue to the next set-cookie // header if ($x == 0) continue 2; // check for secure flag if (strtolower(trim($key_val[0])) == 'secure') $tmp_cookie['secure'] == true; // continue to next param continue; } list($key, $val) = array_map('trim', $key_val); // first name=value pair is the cookie name and value // the name and value are stored under 'name' and 'value' to avoid conflicts // with later parameters. if ($x == 0) { $tmp_cookie = array('name'=>$key, 'value'=>$val); continue; } $key = strtolower($key); if (in_array($key, array('expires', 'path', 'domain', 'secure'))) { $tmp_cookie[$key] = $val; } } // // set cookie // // check domain if (isset($tmp_cookie['domain']) && ($tmp_cookie['domain'] != $request_host) && ($tmp_cookie['domain'] != ".$request_host")) { $domain = $tmp_cookie['domain']; if ((strpos($domain, '.') === false) && ($domain != 'local')) { Debug::debug(' - domain "'.$domain.'" has no dot and is not a local domain'); continue; } if (preg_match('/\.[0-9]+$/', $domain)) { Debug::debug(' - domain "'.$domain.'" appears to be an ip address'); continue; } if (strpos($domain, 0, 1) != '.') $domain = ".$domain"; if (!$this->_domain_match($request_host, $domain)) { Debug::debug(' - request host "'.$request_host.'" does not domain-match "'.$domain.'"'); continue; } } else { // if domain is not specified in the set-cookie header, domain will default to // the request host $domain = $request_host; } // check path if (isset($tmp_cookie['path']) && ($tmp_cookie['path'] != '')) { $path = urldecode($tmp_cookie['path']); if (!$this->_path_match($request_path, $path)) { Debug::debug(' - request path "'.$request_path.'" does not path-match "'.$path.'"'); continue; } } else { $path = $request_path; $path = substr($path, 0, strrpos($path, '/')); if ($path == '') $path = '/'; } // check if secure $secure = (isset($tmp_cookie['secure'])) ? true : false; // check expiry if (isset($tmp_cookie['expires'])) { if (($expires = strtotime($tmp_cookie['expires'])) < 0) { $expires = null; } } else { $expires = null; } // set cookie $this->set_cookie($domain, $path, $tmp_cookie['name'], $tmp_cookie['value'], $secure, $expires); } } /** * Set option - set cookie jar options. * * RECOGNISED OPTIONS: * - option name values(s) description * ------------------------------------------------------------------------------ * - file_persistent string persistent cookie file location * - file_session string session cookie file location * - autosave bool save cookies when destroy() is called * * @param mixed $option option name to set, or associative array to replace all existing options * @param string $value option value, null to delete option */ function set_option($option, $value=null) { if (is_array($option)) { $this->options = $option; return; } if (!isset($value)) { if (isset($this->options[$option])) unset($this->options[$option]); return; } $this->options[$option] = $value; return; } /** * Get option value * @param string $option option name * @return string false if option not found */ function get_option($option) { return (isset($this->options[$option])) ? $this->options[$option] : false; } /** * Set Cookie * @param string $domain * @param string $path * @param string $name cookie name * @param string $value cookie value * @param bool $secure * @param int $expires expiry time (null if session cookie, <= 0 will delete cookie) * @return void */ function set_cookie($domain, $path, $name, $value, $secure=false, $expires=null) { if ($domain == '') return; if ($path == '') return; if ($name == '') return; // check if cookie needs to go if (isset($expires) && ($expires <= 0)) { if (isset($this->cookies[$domain][$path][$name])) unset($this->cookies[$domain][$path][$name]); return; } if ($value == '') return; $this->cookies[$domain][$path][$name] = array($value, $secure, $expires); return; } /** * Clear cookies - [domain [,path [,name]]] - call method with no arguments to clear all cookies. * @param string $domain * @param string $path * @param string $name * @return void */ function clear($domain=null, $path=null, $name=null) { if (!isset($domain)) { $this->cookies = array(); } elseif (!isset($path)) { if (isset($this->cookies[$domain])) unset($this->cookies[$domain]); } elseif (!isset($name)) { if (isset($this->cookies[$domain][$path])) unset($this->cookies[$domain][$path]); } elseif (isset($name)) { if (isset($this->cookies[$domain][$path][$name])) unset($this->cookies[$domain][$path][$name]); } } /** * Clear session cookies - clears cookies which have no expiry time set */ function clear_session_cookies() { $callback = create_function('&$jar, $parts, $param', 'if (is_null($parts[\'expires\'])) '. '$jar->clear($parts[\'domain\'], $parts[\'path\'], $parts[\'name\']);'."\n". 'return true;'); $this->scan($callback); } /** * Scan - goes through all cookies passing the values through the callback function. * * The callback function can be a method from another object (eg. array(&$my_obj, 'my_method')). * The callback function should accept 3 arguments: * 1- A reference to the cookie jar object (&$jar) * 2- An array holding all cookie parts, array is associative with the following keys: * ('domain','path','name','value','expires','secure') * 3- An optional parameter which can be used for whatever your function wants :), * even though you might not have a use for this parameter, you need to define * your function to accept it. (Note: you can have this parameter be passed by reference) * The callback function should return a boolean, a value of 'true' will tell scan() you want * it to continue with the rest of the cookies, 'false' will tell scan() to not send any more * cookies to your callback function. * * Example: * // $jar is our cookie jar with some cookies loaded * $name_to_delete = 'bla'; * $jar->scan('delete_name', $name_to_delete); * * // our callback function defined here * function delete_name(&$jar, $cookie_parts, $name_to_delete) { * if ($cookie_parts['name'] == $name_to_delete) { * $jar->clear($cookie_parts['domain'], $cookie_parts['path'], $cookie_parts['name']); * } * // must return true to tell scan() to continue with cookies * return true; * } * * @param mixed $callback function name, or array holding an object and the method to call. * @param mixed $param passed as the 3rd argument to $callback */ function scan($callback, &$param) { if (is_array($callback)) $method =& $callback[1]; $cookies =& $this->cookies; $domains = array_keys($cookies); sort($domains); foreach ($domains as $domain) { $paths = array_keys($cookies[$domain]); usort($paths, array(&$this, '_cmp_length')); foreach ($paths as $path) { foreach($cookies[$domain][$path] as $name => $value) { $parts = array( 'domain' => $domain, 'path' => $path, 'name' => $name, 'value' => $value[0], 'secure' => $value[1], 'expires' => $value[2] ); if (is_string($callback)) { $res = $callback($this, $parts, $param); } else { $res = $callback[0]->$method($this, $parts, $param); } if (!$res) return; } } } } /** * Load - loads cookies from a netscape style cookies file. * @param string $file location of the file, or leave blank to use options * @return bool */ function load($file=null) { $success = true; if (isset($file)) return $this->_load($file); if ($file = $this->get_option('file_persistent')) { $success = $this->_load($file); } if ($file = $this->get_option('file_session')) { $succes = ($this->_load($file) && $success); } return $success; } /** * Save cookies using files specified in the options. * @return bool */ function save() { $success1 = true; $success2 = true; if ($this->get_option('file_persistent')) $success1 = $this->save_persistent_cookies(); if ($this->get_option('file_session')) $success2 = $this->save_session_cookies(); return ($success1 && $success2); } /** * Save session cookies * @param string $file file to save to, leave out to use the "file_session" option value * @return bool */ function save_session_cookies($file=null) { $file = (isset($file)) ? $file : $this->get_option('file_session'); return $this->_save('session:'.$file); } /** * Save persistent cookies * @param string $file file to save to, leave out to use the "file_persistent" option value * @return bool */ function save_persistent_cookies($file=null) { $file = (isset($file)) ? $file : $this->get_option('file_persistent'); return $this->_save('persistent:'.$file); } /** * Destroy - an opplication using a cookie jar must call this method when it has * finished with the cookies. */ function destroy() { if ($this->get_option('autosave')) $this->save(); $this->clear(); } /** * Save - saves cookies to disk * @param string $type_file either: session:/path/to/cookies or persistent:/path/to/cookies * @return bool * @access private */ function _save($type_file) { // extract file and type list($type, $file) = explode(':', $type_file, 2); Debug::debug('** Saving '.$type.' cookies to "'.$file.'" **'); // check if file is writable if (!$file || !is_writable($file)) { trigger_error('File "'.$file.'" is not writable', E_USER_WARNING); return false; } $data = '# HTTP Cookie File # http://www.netscape.com/newsref/std/cookie_spec.html # This is a generated file! Do not edit. '; // build up cookie list $option = array('type' => $type, 'string' => $data); $this->scan(array(&$this, '_as_string_callback'), $option); $data = $option['string']; $fp = fopen($file, 'w'); flock($fp, LOCK_EX); fwrite($fp, $data); flock($fp, LOCK_UN); fclose($fp); return true; } /** * Callback method to build up a netscape style cookies file. * @param object $jar referenc to cookie jar (passed by scan()) * @param array $parts cookie parts supplied by scan() * @param array $option holds type of cookies to return (session or persistent), and actual * string as it builds up. * @return bool * @access private */ function _as_string_callback(&$jar, $parts, &$option) { if (is_null($parts['expires']) && ($option['type'] == 'persistent')) return true; if (is_int($parts['expires']) && ($option['type'] == 'session')) return true; if (is_int($parts['expires']) && (time() > $parts['expires'])) return true; $p = array(); $p[] = str_replace("\t", ' ', $parts['domain']); $p[] = (substr($parts['domain'], 0, 1) == '.') ? 'TRUE' : 'FALSE'; $p[] = str_replace("\t", ' ', $parts['path']); $p[] = ($parts['secure']) ? 'TRUE' : 'FALSE'; $p[] = (is_null($parts['expires'])) ? 'session' : $parts['expires']; $p[] = str_replace("\t", ' ', $parts['name']); $p[] = str_replace("\t", ' ', $parts['value']); $option['string'] .= implode("\t", $p)."\n"; return true; } /** * Compare string length - used for sorting * @access private * @return int */ function _cmp_length($a, $b) { $la = strlen($a); $lb = strlen($b); if ($la == $lb) return 0; return ($la > $lb) ? -1 : 1; } /** * Reduce domain * @param string $domain * @return string * @access private */ function _reduce_domain($domain) { if ($domain == '') return ''; if (substr($domain, 0, 1) == '.') return substr($domain, 1); return substr($domain, strpos($domain, '.')); } /** * Path match - check if path1 path-matches path2 * * From RFC 2965: * For two strings that represent paths, P1 and P2, P1 path-matches P2 * if P2 is a prefix of P1 (including the case where P1 and P2 string- * compare equal). Thus, the string /tec/waldo path-matches /tec. * @param string $path1 * @param string $path2 * @return bool * @access private */ function _path_match($path1, $path2) { return (substr($path1, 0, strlen($path2)) == $path2); } /** * Domain match - check if domain1 domain-matches domain2 * * A few extracts from RFC 2965: * * A Set-Cookie2 from request-host y.x.foo.com for Domain=.foo.com * would be rejected, because H is y.x and contains a dot. * * * A Set-Cookie2 from request-host x.foo.com for Domain=.foo.com * would be accepted. * * * A Set-Cookie2 with Domain=.com or Domain=.com., will always be * rejected, because there is no embedded dot. * * * A Set-Cookie2 from request-host example for Domain=.local will * be accepted, because the effective host name for the request- * host is example.local, and example.local domain-matches .local. * * I'm ignoring the first point for now (must check to see how other browsers handle * this rule for Set-Cookie headers) * * @param string $domain1 * @param string $domain2 * @return bool * @access private */ function _domain_match($domain1, $domain2) { $domain1 = strtolower($domain1); $domain2 = strtolower($domain2); while (strpos($domain1, '.') !== false) { if ($domain1 == $domain2) return true; $domain1 = $this->_reduce_domain($domain1); continue; } return false; } /** * Get host - get host from the 'Host' header of a HTTP request, or the URL * @param object $request * @param object $url * @return string * @access private */ function _get_host(&$request, &$url) { if ($host = $request->get_header_string('Host', 1)) { if ($port_pos = strpos($host, ':')) return substr($host, 0, $port_pos); if ($port_pos === false) return $host; } return $url->get_host(); } /** * Load - loads cookies from a netscape style cookies file. * @param string $file location of the file * @return bool * @access private */ function _load($file) { Debug::debug('** Loading "'.$file.'" **'); // check if file is readable if (($file == '') || !is_readable($file)) { trigger_error('File "'.$file.'" is unreadable', E_USER_WARNING); return false; } $data = file($file); for ($x=0; $x<count($data); $x++) { $line = trim($data[$x]); // move on if line is a comment or empty if (($line == '') || ($line == '#')) continue; $parts = explode("\t", $line); if (count($parts) != 7) continue; list($domain, , $path, $secure, $expires, $name, $value) = $parts; $secure = ($secure == 'TRUE'); // because the netscape style cookie files are used for persistent cookies // you can't store session cookies (which might be useful for scripted // HTTP sessions). Using this cookie jar class you'll have an option // to save session cookies in a separate file with a minor change: // the expires field will simply hold the string "session". $expires = ($expires == 'session') ? null : (int)$expires; $this->set_cookie($domain, $path, $name, $value, $secure, $expires); } return true; } } ?>