PHP Classes
Icontem

File: Cookie_Jar.php


  Search   All class groups All class groups   Latest entries Latest entries   Top 10 charts Top 10 charts   Newsletter Newsletter   Blog Blog   Forums Forums   Help FAQ Help FAQ  
  Login   Register  
Recommend this page to a friend! ReTweet ReTweet Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Keyvan Minoukadeh  >  Cookie Jar  >  Cookie_Jar.php  
File: Cookie_Jar.php
Role: Class source
Content type: text/plain
Description: Main class
Class: Cookie Jar
Class for handling cookies (for HTTP clients)
 

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;
    }
}
?>

 
  Advertise on this site Advertise on this site   Site map Site map   Statistics Statistics   Site tips Site tips   Privacy policy Privacy policy   Contact Contact  

For more information send a message to :
info at phpclasses dot org.
Copyright (c) Icontem 1999-2009 PHP Classes - PHP Class Scripts
  PHP Book Reviews - Reviews of books and other products