<?php
/**
 * Slim - a micro PHP 5 framework
 *
 * @author      Josh Lockhart <info@slimframework.com>
 * @copyright   2011 Josh Lockhart
 * @link        http://www.slimframework.com
 * @license     http://www.slimframework.com/license
 * @version     1.6.0
 * @package     Slim
 *
 * MIT LICENSE
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/**
 * Slim HTTP Utilities
 *
 * This class provides useful methods for handling HTTP requests.
 *
 * @package Slim
 * @author  Josh Lockhart
 * @since   1.0.0
 */
class Slim_Http_Util {
    /**
     * Strip slashes from string or array
     *
     * This method strips slashes from its input. By default, this method will only
     * strip slashes from its input if magic quotes are enabled. Otherwise, you may
     * override the magic quotes setting with either TRUE or FALSE as the send argument
     * to force this method to strip or not strip slashes from its input.
     *
     * @var     array|string    $rawData
     * @return  array|string
     */
    public static function stripSlashesIfMagicQuotes( $rawData, $overrideStripSlashes = null ) {
        $strip = is_null($overrideStripSlashes) ? get_magic_quotes_gpc() : $overrideStripSlashes;
        if ( $strip ) {
            return self::_stripSlashes($rawData);
        } else {
            return $rawData;
        }
    }

    /**
     * Strip slashes from string or array
     * @param   array|string    $rawData
     * @return  array|string
     */
    protected static function _stripSlashes( $rawData ) {
        return is_array($rawData) ? array_map(array('self', '_stripSlashes'), $rawData) : stripslashes($rawData);
    }

    /**
     * Encrypt data
     *
     * This method will encrypt data using a given key, vector, and cipher.
     * By default, this will encrypt data using the RIJNDAEL/AES 256 bit cipher. You
     * may override the default cipher and cipher mode by passing your own desired
     * cipher and cipher mode as the final key-value array argument.
     *
     * @param   string  $data       The unencrypted data
     * @param   string  $key        The encryption key
     * @param   string  $iv         The encryption initialization vector
     * @param   array   $settings   Optional key-value array with custom algorithm and mode
     * @return  string
     */
    public static function encrypt( $data, $key, $iv, $settings = array() ) {
        if ( $data === '' || !extension_loaded('mcrypt') ) {
            return $data;
        }

        //Merge settings with defaults
        $settings = array_merge(array(
            'algorithm' => MCRYPT_RIJNDAEL_256,
            'mode' => MCRYPT_MODE_CBC
        ), $settings);

        //Get module
        $module = mcrypt_module_open($settings['algorithm'], '', $settings['mode'], '');

        //Validate IV
        $ivSize = mcrypt_enc_get_iv_size($module);
        if ( strlen($iv) > $ivSize ) {
            $iv = substr($iv, 0, $ivSize);
        }

        //Validate key
        $keySize = mcrypt_enc_get_key_size($module);
        if ( strlen($key) > $keySize ) {
            $key = substr($key, 0, $keySize);
        }

        //Encrypt value
        mcrypt_generic_init($module, $key, $iv);
        $res = @mcrypt_generic($module, $data);
        mcrypt_generic_deinit($module);

        return $res;
    }

    /**
     * Decrypt data
     *
     * This method will decrypt data using a given key, vector, and cipher.
     * By default, this will decrypt data using the RIJNDAEL/AES 256 bit cipher. You
     * may override the default cipher and cipher mode by passing your own desired
     * cipher and cipher mode as the final key-value array argument.
     *
     * @param   string  $data       The encrypted data
     * @param   string  $key        The encryption key
     * @param   string  $iv         The encryption initialization vector
     * @param   array   $settings   Optional key-value array with custom algorithm and mode
     * @return  string
     */
    public static function decrypt( $data, $key, $iv, $settings = array() ) {
        if ( $data === '' || !extension_loaded('mcrypt') ) {
            return $data;
        }

        //Merge settings with defaults
        $settings = array_merge(array(
            'algorithm' => MCRYPT_RIJNDAEL_256,
            'mode' => MCRYPT_MODE_CBC
        ), $settings);

        //Get module
        $module = mcrypt_module_open($settings['algorithm'], '', $settings['mode'], '');

        //Validate IV
        $ivSize = mcrypt_enc_get_iv_size($module);
        if ( strlen($iv) > $ivSize ) {
            $iv = substr($iv, 0, $ivSize);
        }

        //Validate key
        $keySize = mcrypt_enc_get_key_size($module);
        if ( strlen($key) > $keySize ) {
            $key = substr($key, 0, $keySize);
        }

        //Decrypt value
        mcrypt_generic_init($module, $key, $iv);
        $decryptedData = @mdecrypt_generic($module, $data);
        $res = str_replace("\x0", '', $decryptedData);
        mcrypt_generic_deinit($module);

        return $res;
    }

    /**
     * Encode secure cookie value
     *
     * This method will create the secure value of an HTTP cookie. The
     * cookie value is encrypted and hashed so that its value is
     * secure and checked for integrity when read in subsequent requests.
     *
     * @param   string  $value      The unsecure HTTP cookie value
     * @param   int     $expires    The UNIX timestamp at which this cookie will expire
     * @param   string  $secret     The secret key used to hash the cookie value
     * @param   int     $algorithm  The algorithm to use for encryption
     * @param   int     $mode       The algorithm mode to use for encryption
     * @param   string
     */
    public static function encodeSecureCookie( $value, $expires, $secret, $algorithm, $mode ) {
        $key = hash_hmac('sha1', $expires, $secret);
        $iv = self::get_iv($expires, $secret);
        $secureString = base64_encode(self::encrypt($value, $key, $iv, array(
            'algorithm' => $algorithm,
            'mode' => $mode
        )));
        $verificationString = hash_hmac('sha1', $expires . $value, $key);
        return implode('|', array($expires, $secureString, $verificationString));
    }

    /**
     * Decode secure cookie value
     *
     * This method will decode the secure value of an HTTP cookie. The
     * cookie value is encrypted and hashed so that its value is
     * secure and checked for integrity when read in subsequent requests.
     *
     * @param   string  $value      The secure HTTP cookie value
     * @param   int     $expires    The UNIX timestamp at which this cookie will expire
     * @param   string  $secret     The secret key used to hash the cookie value
     * @param   int     $algorithm  The algorithm to use for encryption
     * @param   int     $mode       The algorithm mode to use for encryption
     * @param   string
     */
    public static function decodeSecureCookie( $value, $secret, $algorithm, $mode ) {
        if ( $value ) {
            $value = explode('|', $value);
            if ( count($value) === 3 && ( (int)$value[0] === 0 || (int)$value[0] > time() ) ) {
                $key = hash_hmac('sha1', $value[0], $secret);
                $iv = self::get_iv($value[0], $secret);
                $data = self::decrypt(base64_decode($value[1]), $key, $iv, array(
                    'algorithm' => $algorithm,
                    'mode' => $mode
                ));
                $verificationString = hash_hmac('sha1', $value[0] . $data, $key);
                if ( $verificationString === $value[2] ) {
                    return $data;
                }
            }
        }
        return false;
    }

    /**
     * Set HTTP cookie header
     *
     * This method will construct and set the HTTP `Set-Cookie` header. Slim
     * uses this method instead of PHP's native `setcookie` method. This allows
     * more control of the HTTP header irrespective of the native implementation's
     * dependency on PHP versions.
     *
     * This method accepts the Slim_Http_Headers object by reference as its
     * first argument; this method directly modifies this object instead of
     * returning a value.
     *
     * @param   array   $header
     * @param   string  $name
     * @param   string  $value
     * @return  void
     */
    public static function setCookieHeader( &$header, $name, $value ) {
        //Build cookie header
        if ( is_array($value) ) {
            $domain = '';
            $path = '';
            $expires = '';
            $secure = '';
            $httponly = '';
            if ( isset($value['domain']) && $value['domain'] ) {
                $domain = '; domain=' . $value['domain'];
            }
            if ( isset($value['path']) && $value['path'] ) {
                $path = '; path=' . $value['path'];
            }
            if ( isset($value['expires']) ) {
                if ( is_string($value['expires']) ) {
                    $timestamp = strtotime($value['expires']);
                } else {
                    $timestamp = (int)$value['expires'];
                }
                if ( $timestamp !== 0 ) {
                    $expires = '; expires=' . gmdate('D, d-M-Y H:i:s e', $timestamp);
                }
            }
            if ( isset($value['secure']) && $value['secure'] ) {
                $secure = '; secure';
            }
            if ( isset($value['httponly']) && $value['httponly'] ) {
                $httponly = '; HttpOnly';
            }
            $cookie = sprintf('%s=%s%s', urlencode($name), urlencode((string)$value['value']), $domain . $path . $expires . $secure . $httponly);
        } else {
            $cookie = sprintf('%s=%s', urlencode($name), urlencode((string)$value));
        }

        //Set cookie header
        if ( !isset($header['Set-Cookie']) || $header['Set-Cookie'] === '' ) {
            $header['Set-Cookie'] = $cookie;
        } else {
            $header['Set-Cookie'] = implode("\n", array($header['Set-Cookie'], $cookie));
        }
    }

    /**
     * Delete HTTP cookie header
     *
     * This method will construct and set the HTTP `Set-Cookie` header to invalidate
     * a client-side HTTP cookie. If a cookie with the same name (and, optionally, domain)
     * is already set in the HTTP response, it will also be removed. Slim uses this method
     * instead of PHP's native `setcookie` method. This allows more control of the HTTP header
     * irrespective of PHP's native implementation's dependency on PHP versions.
     *
     * This method accepts the Slim_Http_Headers object by reference as its
     * first argument; this method directly modifies this object instead of
     * returning a value.
     *
     * @param   array   $header
     * @param   string  $name
     * @param   string  $value
     * @return  void
     */
    public static function deleteCookieHeader( &$header, $name, $value = array() ) {
        //Remove affected cookies from current response header
        $cookiesOld = array();
        $cookiesNew = array();
        if ( isset($header['Set-Cookie']) ) {
            $cookiesOld = explode("\n", $header['Set-Cookie']);
        }
        foreach ( $cookiesOld as $c ) {
            if ( isset($value['domain']) && $value['domain'] ) {
                $regex = sprintf('@%s=.*domain=%s@', urlencode($name), preg_quote($value['domain']));
            } else {
                $regex = sprintf('@%s=@', urlencode($name));
            }
            if ( preg_match($regex, $c) === 0 ) {
                $cookiesNew[] = $c;
            }
        }
        if ( $cookiesNew ) {
            $header['Set-Cookie'] = implode("\n", $cookiesNew);
        } else {
            unset($header['Set-Cookie']);
        }

        //Set invalidating cookie to clear client-side cookie
        self::setCookieHeader($header, $name, array_merge(array('value' => '', 'path' => null, 'domain' => null, 'expires' => time() - 100), $value));
    }

    /**
     * Parse cookie header
     *
     * This method will parse the HTTP requst's `Cookie` header
     * and extract cookies into an associative array.
     *
     * @param   string
     * @return  array
     */
    public static function parseCookieHeader( $header ) {
        $cookies = array();
        $header = rtrim($header, "\r\n");
        $headerPieces = preg_split('@\s*;\s*@', $header);
        foreach ( $headerPieces as $c ) {
            $cParts = explode('=', $c);
            if ( count($cParts) === 2 ) {
                $key = urldecode($cParts[0]);
                $value = urldecode($cParts[1]);
                if ( isset($cookies[$key]) ) {
                    if ( is_array($cookies[$key]) ) {
                        $cookies[$key][] = $value;
                    } else {
                        $cookies[$key] = array($cookies[$key], $value);
                    }
                } else {
                    $cookies[$key] = $value;
                }
            }
        }
        return $cookies;
    }

    /**
     * Generate a random IV
     *
     * This method will generate a non-predictable IV for use with
     * the cookie encryption
     *
     * @param   int     $expires    The UNIX timestamp at which this cookie will expire
     * @param   string  $secret     The secret key used to hash the cookie value
     * @return  binary string with length 40
     */
    private static function get_iv($expires, $secret) {
        $data1 = hash_hmac('sha1', 'a'.$expires.'b', $secret);
        $data2 = hash_hmac('sha1', 'z'.$expires.'y', $secret);
        return pack("h*", $data1.$data2);
    }

}