<?php
/*
 * Description:
 *    RFC 2965 describes the HTTP State Management mechanism.
 *    From section "3.1 Syntax: General":
 *      av-pairs    =     av-pair *(";" av-pair)
 *      av-pair     =     attr ["=" value]              ; optional value
 *      attr        =     token
 *      value       =     token | quoted-string
 *
 *    PHP 4.3.11 does not handle the case of "quoted-string" values.
 *    See RFC 2616 section "2.2 Basic Rules" for a definition of "quoted-string".
 *        quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
 *        qdtext         = <any TEXT except <">>
 *
 *      The backslash character ("\") MAY be used as a single-character
 *      quoting mechanism only within quoted-string and comment constructs.
 *
 *        quoted-pair    = "\" CHAR
 *
 *    PHP 4.3.11 urlencodes all cookie name = value pairs. Therefore, it can handle
 *    values that contain the separators "," and ";". But the RFC 2965 describes that
 *    a HTTP Cookie header sent from the user agent to the server may have av-pairs,
 *    where the value may be a token or a quoted string.
 *
 *    If one sets a cookie not with PHP's setCookie() method, but directly with header(),
 *    then it is sent correctly to the user agent and the user agent returns it also
 *    correctly. But when PHP reads the HTTP Cookie header back into $_COOKIE, it does
 *    not handle quoted-strings.
 *
 *    Result:
 *      Wrong cookie values in $_COOKIE. 
 *
 *    The bug is in PHP's source in function
 *       SAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data)
 *    It parses the HTTP Cookie header and directly uses "," and ";" as separators.
 *    A slightly more complicated handling of the HTTP Cookie header is required.
 *    In addition to the current code, one has to handle:
 *       - quoted-strings: separators "," and ";" may be in quoted-strings
 *       - double-quote marks escaped by "\" don't end a quoted-string 
 *
 *    Cookies with values that are not urlencoded may come from:
 *      - non-PHP applications on the same host
 *      - RFC 2965 compliant PHP cookies that were set with header() instead of setcookie().
 *
 *    Example:
 *      In PHP script:
 *        header('Set-Cookie: TestCookie = "value with , perfectly ; allowed 8-Bit characters ' .
 *                'and escaped \" double-quote marks!"');
 *      The cookie is successfully sent to the user agent. The user agent sends it back with a
 *      perfectly intact value.
 *      PHP receives the HTTP Cookie header from the webserver (I inserted the line break):
 *        Cookie: TestCookie="value with , perfectly ; allowed 8-Bit characters and escaped \"
 *                double-quote marks!"\r\n
 *      Then PHP parses the HTTP Cookie header ignoring the case of quoted-strings and fills the
 *      superglobal $_COOKIE with:
 *         ["TestCookie"]=>
 *            string(24) ""value with , perfectly "
 *         ["allowed_8-BIT_characters_and_escaped_\"_double-quote_marks!""]=>
 *            string(0) ""
 *      If PHP handled the HTTP Cookie header correctly, one would get:
 *         ["TestCookie"]=>
 *            string(86) "value with , perfectly ; allowed 8-BIT characters and escaped \" double-quote marks!"
 *
 *      I even think, if PHP handled "," and ";" as separators, the current PHP should have
 *      created three cookies out of the above name = value pair.
 *
 * References:
 *    RFC 2965: http://rfc.net/rfc2965.html
 *    RFC 2616: http://rfc.net/rfc2616.html
 */


/**
 * Fix the superglobal $_COOKIE to conform with RFC 2965
 *
 * This function reevaluates the HTTP Cookie header and populates $_COOKIE with the correct
 * cookies. We fix only non-array and non '[', ']' containing cookies for simplicity.
 */
function fixCookieVars() {
    if (empty(
$_SERVER['HTTP_COOKIE'])) {
    return;
    }
    
/*
     * Array to keep track of fixed cookies to not make the same mistake as php, i.e.
     * don't assign values to cookies that were already fixed / set before. 
     */
    
$fixedCookies = array();
    
/* Check if the Cookie header contains quoted-strings */
    
if (strstr($_SERVER['HTTP_COOKIE'], '"') === false) {
    
/*
     * Use fast method, no quoted-strings in the header.
     * Get rid of less specific cookies if multiple cookies with the same NAME
     * are present. Do this by going from left/first cookie to right/last cookie.
     */
    
$tok strtok($_SERVER['HTTP_COOKIE'], ',;');
    while (
$tok) {
        
_registerCookieAttr($tok$fixedCookies);
        
$tok strtok(',;');
    }
    } else {
    
/*
     * We can't just tokenize the Cookie header string because there are
     * quoted-strings and delimiters in quoted-string should be handled as values
     * and not as delimiters.
     * Thus, we have to parse it character by character.
     */
    
$quotedStringOpen false;
    
$string $_SERVER['HTTP_COOKIE'];
    
$len strlen($string);
    
$i 0;
    
$lastPos 0;
    while (
$i $len) {
        switch (
$string{$i}) {
        
/* the two attr-pair separators */
        
case ',':
        case 
';':
        if (
$quotedStringOpen) {
            
/* Ignore separators within quoted-strings */
        
} else {
            
/* else, this is an attr[=value] pair */
            
_registerCookieAttr(substr($string$lastPos,
                                 
$i $lastPos),
                              
$fixedCookies);
            
$lastPos $i+1/* next attr starts at next char */
            
        
}
        break;
        case 
'"':
        
$quotedStringOpen = !$quotedStringOpen;
        break;
        case 
'\\':
        
/* escape the next character = jump over it */
        
$i++;
        break;
        }
        
$i++;
    }
    
/* register last attr in header, but only if the syntax is correct */
    
if (!$quotedStringOpen) {
        
_registerCookieAttr(substr($string$lastPos),
                          
$fixedCookies);
    }
    }
}



/**
 * Register a Cookie Var safely
 * @param string the cookie var attr, NAME [=VALUE]
 * @param array (string already registered cookie name)
 */
function _registerCookieAttr($attr, &$fixedCookies) {
    
/*
     * Split NAME [=VALUE], value is optional for all attributes
     * but the cookie name
     */
    
if (($pos strpos($attr'=')) !== false) {
    
$val substr($attr$pos+1);
    
$key substr($attr0$pos);
    } else {
    
/* No cookie name=value attr, we can ignore it */
    
return;
    }
    
/* Urldecode header data (php-style of name = attr handling) */
    
$key trim(urldecode($key));
    
/* Don't accept zero length key */
    
if (($len strlen($key)) == 0) {
    return;
    }

    
/* Ommitting array cookies and binary safe names for simplicity */
    
    /*
     * Don't register non-NAME attributes like domain, path, ... which are all
     * starting with a dollar sign according to RFC 2965.
     */
    
if (strpos($key'$') === 0) {
    return;
    }
    
/* Urldecode value */
    
$val trim(urldecode($val));
    
/* Addslashes if magic_quotes_gpc is on */
    
if (get_magic_quotes_gpc()) {
    
$key addslashes($key);
    
$val addslashes($val);
    }
    if (!isset(
$fixedCookies[$key])) {
    
$_COOKIE[$key] = $val;
    
$fixedCookies[$key] = true;
    }
}

highlight_file(__FILE__);
?>