Login   Register  
PHP Classes
elePHPant
Icontem

File: Cookie_Jar.php

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  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: 11 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;
    }
}
?>