<?php
/**
 * core.framework
 *
 * @category  Core
 * @package   Core_Request
 * @copyright Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */

/**
 * Request. Encapsulates access to superglobals.
 *
 * Does not provide any kind of sanitation.
 *
 * @category  Core
 * @package   Core_Request
 * @copyright Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */
class Core_Request
{
    const CONTENT_TYPE      = 'type';
    const CONTENT_CHARSET   = 'charset';
    const CONTENT_FORMAT    = 'format';
    const CONTENT_PARAMS    = 'params';

    const FORMAT_JSON       = 'json';
    const FORMAT_XML        = 'xml';
    const FORMAT_URLENCODED = 'urlencoded';

    const METHOD_POST       = 'POST';
    const METHOD_GET        = 'GET';
    const METHOD_PUT        = 'PUT';
    const METHOD_PATCH      = 'PATCH';
    const METHOD_DELETE     = 'DELETE';
    const METHOD_OPTIONS    = 'OPTIONS';
    const METHOD_HEAD       = 'HEAD';
    const METHOD_TRACE      = 'TRACE';

    const PROTOCOL_HTTP     = 'http';
    const PROTOCOL_HTTPS    = 'https';

    /**
     * @var string
     */
    protected $_error;

    /**
     * @var array php://input
     */
    protected $_rawInput;

    /**
     * @var array php://input, parsed
     */
    protected $_input       = array();

    /**
     * @var array User-set request params.
     */
    protected $_params      = array();

    /**
     * @var array Content-Type header parsed
     */
    protected $_contentType = array();

    /**
     * @var array Accept header parsed
     */
    protected $_accept      = array();
    
    /**
     * @var boolean
     */
    protected $_proxied     = false;

    /**
     * @var boolean Magic quotes on/off, detected from environment,
     */
    protected $_magicQuotesEnabled;

    /**
     * Request constructor
     */
    public function __construct()
    {
        $method = $this->getMethod();
        if (in_array($method, array(self::METHOD_POST, self::METHOD_PUT, self::METHOD_PATCH))) {
            $rawInput = $this->getRawInput();
            if (null !== ($sentChecksum = $this->getServer('CONTENT_MD5'))) {
                // MD5 sent, calculate it
                $controlChecksum = base64_encode(md5($rawInput));
                if ($sentChecksum !== $controlChecksum) {
                    $message      = 'Invalid Content-MD5, expected %s, got %s';
                    $this->_error = sprintf($message, $controlChecksum, $sentChecksum);
                }
            }

            switch($this->getContentFormat()) {
                case self::FORMAT_XML:
                    $errors  = libxml_use_internal_errors(true);
                    $input   = simplexml_load_string($rawInput);
                    libxml_use_internal_errors($errors);
                    if (false !== $input) {
                        $this->_input = $this->_xmlToArray($input);
                    } else {
                        $this->_error = 'Invalid XML input';
                    }
                    break;
                case self::FORMAT_JSON:
                    $input   = json_decode($rawInput, true);
                    if (null !== $input) {
                        $this->_input = $input;
                    } else {
                        $this->_error = 'Invalid JSON input';
                    }
                    break;
                case self::FORMAT_URLENCODED:
                    parse_str($rawInput, $this->_input);
                    break;
            }

            if (!empty($this->_input) && self::METHOD_POST === $method && 'application/x-www-form-urlencoded' !== $this->getContentType()) {
                $_POST = $this->_input;
            }
        }
    }

    /**
     * Is the current request valid?
     *
     * A request is considered not-valid if you specify a payload type JSON or XML, but the payload is malformed.
     *
     * @return boolean
     */
    public function isValid()
    {
        return !$this->_error;
    }

    /**
     * @return string
     */
    public function getError()
    {
        return $this->_error;
    }

    /**
     * Is the current request a POST?
     *
     * @return boolean
     */
    public function isPost()
    {
        return $this->isMethod(self::METHOD_POST);
    }

    /**
     * Is the current request a GET?
     *
     * @return boolean
     */
    public function isGet()
    {
        return $this->isMethod(self::METHOD_GET);
    }

    /**
     * Is the current request a PUT?
     *
     * @return boolean
     */
    public function isPut()
    {
        return $this->isMethod(self::METHOD_PUT);
    }

    /**
     * Is the current request a PATCH?
     *
     * @return boolean
     */
    public function isPatch()
    {
        return $this->isMethod(self::METHOD_PATCH);
    }

    /**
     * Is the current request a DELETE?
     *
     * @return boolean
     */
    public function isDelete()
    {
        return $this->isMethod(self::METHOD_DELETE);
    }

    /**
     * Is the current request an OPTIONS?
     *
     * @return boolean
     */
    public function isOptions()
    {
        return $this->isMethod(self::METHOD_OPTIONS);
    }

    /**
     * Is the current request a HEAD?
     *
     * @return boolean
     */
    public function isHead()
    {
        return $this->isMethod(self::METHOD_HEAD);
    }

    /**
     * Is the current request a TRACE?
     *
     * @return boolean
     */
    public function isTrace()
    {
        return $this->isMethod(self::METHOD_TRACE);
    }

    /**
     * Test if the current request method is the named method, case insensitive.
     *
     * @param string $method Method name to test (like "POST" or "get")
     *
     * @return boolean
     */
    public function isMethod($method)
    {
        return (strtoupper($method) === $this->getMethod());
    }

    /**
     * Is the current request made via XHR?
     *
     * @return boolean
     */
    public function isXmlHttpRequest()
    {
        return ($this->getHeader('X-Requested-With') == 'XMLHttpRequest');
    }

    /**
     * Is the current request made by a known bot?
     *
     * @return boolean
     */
    public function isBot()
    {
        return $this->isHeaderRegexMatch('User-Agent', '/(generator|bot|spider|crawl|miner|index|yahoo|\(\+https?\:\/\/)/') || $this->getValue('isBot');
    }

    /**
     * Is the current request made via HTTPS?
     *
     * @return boolean
     */
    public function isSecure()
    {
        return ($this->getProtocol() === self::PROTOCOL_HTTPS);
    }

    /**
     * @return string
     */
    public function getRawInput()
    {
        if (null === $this->_rawInput) {
            $rawInput = $this->getEnv('CF_TEST_REQUEST_INPUT');
            if (null === $rawInput) {
                $rawInput     = file_get_contents("php://input");
            } else {
                // used for unit testing only
                $this->_input = $rawInput;
            }
            $this->_rawInput  = $rawInput;
        }
        return $this->_rawInput;
    }

    /**
     * @return string
     */
    public function getContentType()
    {
        $this->_parseMimeType($this->getServer('CONTENT_TYPE'), $this->_contentType);
        return $this->_contentType[self::CONTENT_TYPE];
    }

    /**
     * @return string
     */
    public function getContentCharset()
    {
        return $this->getContentParam(self::CONTENT_CHARSET, 'UTF-8');
    }

    /**
     * @return string
     */
    public function getContentFormat()
    {
        $this->_parseMimeType($this->getServer('CONTENT_TYPE'), $this->_contentType);
        return $this->_contentType[self::CONTENT_FORMAT];
    }

    /**
     * @return string
     */
    public function getContentParams()
    {
        $this->_parseMimeType($this->getServer('CONTENT_TYPE'), $this->_contentType);
        return $this->_contentType[self::CONTENT_PARAMS];
    }

    /**
     * @param $name
     * @param mixed $default
     *
     * @return string
     */
    public function getContentParam($name, $default = null)
    {
        $params = $this->getContentParams();
        return array_key_exists($name, $params) ? $params[$name] : $default;
    }

    /**
     * @return string
     */
    public function getAcceptType()
    {
        $this->_parseMimeType($this->getHeader('Accept'), $this->_accept);
        return $this->_accept[self::CONTENT_TYPE];
    }

    /**
     * @return string
     */
    public function getAcceptCharset()
    {
        $encoding = $this->getHeader('Accept-Encoding', 'UTF-8');
        return $encoding;
    }

    /**
     * @return string
     */
    public function getAcceptFormat()
    {
        $this->_parseMimeType($this->getHeader('Accept'), $this->_accept);
        return $this->_accept[self::CONTENT_FORMAT];
    }

    /**
     * @return string
     */
    public function getAcceptParams()
    {
        $this->_parseMimeType($this->getServer('ACCEPT'), $this->_accept);
        return $this->_accept[self::CONTENT_PARAMS];
    }

    /**
     * @param $name
     * @param mixed $default
     *
     * @return string
     */
    public function getAcceptParam($name, $default = null)
    {
        $params = $this->getAcceptParams();
        return array_key_exists($name, $params) ? $params[$name] : $default;
    }
    
    /**
     * Heuristic to find which language to use for the response based on Accept-Language header and languages available to the app.
     * 
     * @param array  $available Languages available to the app. If not provided, will use the highest language only (en, not en_US)
     * @param string $default   Default to pick if no other match found. If not provided, will use best available.
     * 
     * @return string
     */
    public function getAcceptLanguage(array $available = null, $default = null)
    {
        $header = $this->getHeader('Accept-Language');
        return $this->_parseAccept($header, $available, $default);
    }

    /**
     * Fetch user-set request params.
     *
     * @return array
     */
    public function getParams()
    {
        return $this->_params;
    }

    /**
     * Fetch user-set request param by name.
     *
     * @param string $key Param name.
     *
     * @return mixed
     */
    public function getParam($key)
    {
        if ($this->hasParam($key)) {
            return $this->_params[$key];
        }
    }

    /**
     * Fetch a value from request, looking up in various sources.
     *
     * Will search (in this exact order):
     * - user params
     * - GET
     * - POST
     * - php://input
     * - COOKIE
     * - SERVER
     * - ENV
     *
     * and, optionally, strip slashes if magic_quotes_gpc = 1
     *
     * @param string $key     Variable name to look for.
     * @param mixed  $default Default value to report back if none is found elsewhere.
     *
     * @return mixed
     */
    public function get($key, $default = null)
    {
        switch (true) {
            case isset($this->_params[$key]):
                return $this->getParam($key);
            case isset($_GET[$key]):
                return $this->getQuery($key);
            case isset($_POST[$key]):
                return $this->getPost($key);
            case isset($this->_input[$key]):
                return $this->getInput($key);
            case isset($_COOKIE[$key]):
                return $this->getCookie($key);
            case isset($_SERVER[$key]):
                return $this->getServer($key);
            case isset($_ENV[$key]):
                return $this->getEnv($key);
        }

        return $default;
    }

    /**
     * Proxy to Core_Request::get
     *
     * @param string $key
     *
     * @return mixed
     */
    public function __get($key)
    {
        return $this->get($key);
    }

    /**
     * Proxy to Core_Request::get
     *
     * @param string $key     Variable name to look for.
     * @param mixed  $default Default value to report back if none is found elsewhere.
     *
     * @return mixed
     */
    public function getValue($key, $default = null)
    {
        return $this->get($key, $default);
    }

    /**
     * Set user param to this request.
     *
     * @param string $key   Param name
     * @param mixed  $value Param value
     *
     * @return Core_Request
     */
    public function setParam($key, $value)
    {
        $this->_params[$key] = $value;
        return $this;
    }

    /**
     * Proxy to Core_Request::setParam
     *
     * @param string $key   Param name
     * @param mixed  $value Param value
     *
     * @return Core_Request
     */
    public function set($key, $value)
    {
        return $this->setParam($key, $value);
    }

    /**
     * Proxy to Core_Request::set
     *
     * @param string $key   Param name
     * @param mixed  $value Param value
     *
     * @return Core_Request
     */
    public function __set($key, $value)
    {
        return $this->set($key, $value);
    }

    /**
     * Proxy to Core_Request::set
     *
     * @param string $key   Param name
     * @param mixed  $value Param value
     *
     * @return Core_Request
     */
    public function setValue($key, $value)
    {
        return $this->set($key, $value);
    }

    /**
     * Check if a named user param exists.
     *
     * @param string $key Param name.
     *
     * @return boolean
     */
    public function hasParam($key)
    {
        return isset($this->_params[$key]);
    }

    /**
     * Check for existance of named variable.
     *
     * Will search (in this exact order):
     * - user params
     * - GET
     * - POST
     * - php://input
     * - COOKIE
     * - SERVER
     * - ENV
     *
     * @param string $key Variable name to look for.
     *
     * @return boolean
     */
    public function has($key)
    {
        switch (true) {
            case isset($this->_params[$key]):
                return true;
            case isset($_GET[$key]):
                return true;
            case isset($_POST[$key]):
                return true;
            case isset($this->_input[$key]):
                return true;
            case isset($_COOKIE[$key]):
                return true;
            case isset($_SERVER[$key]):
                return true;
            case isset($_ENV[$key]):
                return true;
            default:
                return false;
        }
    }

    /**
     * Proxy to Core_Request::has
     *
     * @param string $key
     *
     * @return boolean
     */
    public function __isset($key)
    {
        return $this->has($key);
    }

    /**
     * Get all the request values, without user params.
     *
     * If this is a POST to /?a=23, it will *not* include "a".
     *
     * @param array $names
     *
     * @return array
     */
    public function getValues(array $names = null)
    {
        // extract and secure REQUEST_METHOD params
        $method = $this->getMethod();
        switch ($method) {
            case self::METHOD_GET:
                return $this->getQuery($names);
                break;
            case self::METHOD_POST:
                return $this->getPost($names);
                break;
            case self::METHOD_PUT:
            case self::METHOD_PATCH:
                return $this->getInput($names);
                break;
        }
        return array();
    }

    /**
     * Proxy to Core_Request::getValues
     *
     * @return array
     */
    public function toArray()
    {
        return $this->getValues();
    }

    /**
     * Fetch a variable from $_GET.
     *
     * Also, stripslashes if required.
     *
     * @param mixed $key     Variable name(s) to fetch. If null, all the variables are returned
     * @param mixed $default Default variable to return if no variable is found.
     *
     * @return array|mixed
     */
    public function getQuery($key = null, $default = null)
    {
        if (null === $key) {
            return $this->_stripSlashesIfNeeded($_GET);
        } else if (is_array($key)) {
            $values = array();
            foreach ($key as $name) {
                $values[$name] = $this->getQuery($name, $default);
            }
            return $values;
        }
        return (isset($_GET[$key])) ? $this->_stripSlashesIfNeeded($_GET[$key]) : $default;
    }

    /**
     * Fetch a variable from $_POST.
     *
     * Also, stripslashes if required.
     *
     * @param mixed $key     Variable name(s) to fetch. If null, all the variables are returned
     * @param mixed $default Default variable to return if no variable is found.
     *
     * @return array|mixed
     */
    public function getPost($key = null, $default = null)
    {
        if (null === $key) {
            return $this->_stripSlashesIfNeeded($_POST);
        } else if (is_array($key)) {
            $values = array();
            foreach ($key as $name) {
                $values[$name] = $this->getPost($name, $default);
            }
            return $values;
        }
        return (isset($_POST[$key])) ? $this->_stripSlashesIfNeeded($_POST[$key]) : $default;
    }

    /**
     * Fetch a variable from php://input.
     *
     * Also, stripslashes if required.
     *
     * @param mixed $key     Variable name(s) to fetch. If null, all the variables are returned
     * @param mixed $default Default variable to return if no variable is found.
     *
     * @return array|mixed
     */
    public function getInput($key = null, $default = null)
    {
        if (null === $key) {
            return $this->_stripSlashesIfNeeded($this->_input);
        } else if (is_array($key)) {
            $values = array();
            foreach ($key as $name) {
                $values[$name] = $this->getInput($name, $default);
            }
            return $values;
        }

        return (isset($this->_input[$key])) ? $this->_stripSlashesIfNeeded($this->_input[$key]) : $default;
    }

    /**
     * Fetch a variable from $_COOKIE.
     *
     * Also, stripslashes if required.
     *
     * @param mixed $key     Variable name(s) to fetch. If null, all the variables are returned
     * @param mixed $default Default variable to return if no variable is found.
     *
     * @return array|mixed
     */
    public function getCookie($key = null, $default = null)
    {
        if (null === $key) {
            return $this->_stripSlashesIfNeeded($_COOKIE);
        } else if (is_array($key)) {
            $values = array();
            foreach ($key as $name) {
                $values[$name] = $this->getCookie($name, $default);
            }
            return $values;
        }

        return (isset($_COOKIE[$key])) ? $this->_stripSlashesIfNeeded($_COOKIE[$key]) : $default;
    }

    /**
     * Fetch a variable from $_SERVER.
     *
     * @param mixed $key     Variable name(s) to fetch. If null, all the variables are returned
     * @param mixed $default Default variable to return if no variable is found.
     *
     * @return array|mixed
     */
    public function getServer($key = null, $default = null)
    {
        if (null === $key) {
            return $_SERVER;
        } else if (is_array($key)) {
            $values = array();
            foreach ($key as $name) {
                $values[$name] = $this->getServer($name, $default);
            }
            return $values;
        }

        if (!isset($_SERVER[$key])) {
            return $default;
        }

        if ($key === 'REQUEST_URI') {
            return urldecode($_SERVER['REQUEST_URI']);
        } else {
            return $_SERVER[$key];
        }
    }

    /**
     * Fetch a variable from $_ENV.
     *
     * @param mixed $key     Variable name(s) to fetch. If null, all the variables are returned
     * @param mixed $default Default variable to return if no variable is found.
     *
     * @return array|mixed
     */
    public function getEnv($key = null, $default = null)
    {
        if (null === $key) {
            return $_ENV;
        } else if (is_array($key)) {
            $values = array();
            foreach ($key as $name) {
                $values[$name] = $this->getEnv($name, $default);
            }
            return $values;
        }

        return (isset($_ENV[$key])) ? $_ENV[$key] : $default;
    }

    /**
     * Fetch a HTTP prefixed variable from $_SERVER (a header).
     *
     * @param string $header  Variable name to fetch.
     * @param mixed  $default Default variable to return if no variable is found.
     *
     * @return mixed
     * @throws InvalidArgumentException If no header name is passed.
     */
    public function getHeader($header, $default = null)
    {
        if (empty($header)) {
            throw new InvalidArgumentException('A HTTP header name is required');
        }

        // Try to get it from the $_SERVER array first
        $name = 'HTTP_' . strtoupper(str_replace('-', '_', $header));
        if (!empty($_SERVER[$name])) {
            return $_SERVER[$name];
        }

        return $default;
    }
    
    /**
     * @param string $header Header name
     * @param mixed  $value  Value to compare to the header value
     * 
     * @return boolean
     */
    public function isHeader($header, $value)
    {
        return (0 === strcasecmp($value, $this->getHeader($header)));
    }
    
    /**
     * @param string $header Header name
     * @param string $regex  Regex to match against
     * 
     * @return boolean
     */
    public function isHeaderRegexMatch($header, $regex)
    {
        // add case-insensitve flag to regex if not already there
        $delimeter = substr($regex, 0, 1);
        $flags     = substr($regex, strrpos($regex, $delimeter) + 1);
        if (false === strpos($flags, 'i')) {
            $regex .= 'i';
        }
        return (1 === preg_match($regex, $this->getHeader($header)));
    }

    /**
     * Fetch client IP.
     *
     * First trying to fetch HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR (proxied).
     * Fallback to REMOTE_ADDR.
     *
     * @return string
     */
    public function getClientIp()
    {
        if (null !== ($clientIp = $this->getHeader('Client-IP'))) {
            // HTTP_CLIENT_IP
        } else if (null !== ($clientIp = $this->getHeader('X-Forwarded-For'))) {
            // HTTP_X_FORWARDED_FOR
        } else {
            $clientIp = $this->getServer('REMOTE_ADDR');
        }

        return $clientIp;
    }

    /**
     * Get request referrer.
     *
     * @return string
     */
    public function getReferrer()
    {
        return $this->getHeader('Referer');
    }

    /**
     * Get request method, uppercased.
     *
     * @return string
     */
    public function getMethod()
    {
        // this is set by NPAPI-constrained HTTP requests
        // like the ones made by Flash
        // it allows for overriding HTTP method verb with a additional HTTP header
        // but only if set by POST
        $method = strtoupper($this->getServer('REQUEST_METHOD'));
        $header = $this->getHeader('X-HTTP-Method-Override');
        if ('POST' === $method && null !== $header) {
            $method = strtoupper($header);
        }
        return $method;
    }

    /**
     * Get request protocol, lowercased.
     * 
     * Supports proxied setups with terminated SSL where the proxy adds a X-Forwarded-Proto: https header.
     * This feature needs to be turned on by setting setProxied(true).
     *
     * @return string
     */
    public function getProtocol()
    {
        if ($this->isProxied()) {
            $protocol = strtolower($this->getHeader('X-Forwarded-Proto', self::PROTOCOL_HTTP));
            $allowed  = array(self::PROTOCOL_HTTP, self::PROTOCOL_HTTPS);
            if (!in_array($protocol, $allowed)) {
                $message = sprintf('Invalid "X-Forwarded-Proto: %s" set, must be one of "%s"', $protocol, implode('", "', $allowed));
                throw new RuntimeException($message);
            }
        } else if ('on' === $this->getServer('HTTPS')) {
            $protocol = self::PROTOCOL_HTTPS;
        } else {
            $protocol = self::PROTOCOL_HTTP;
        }
        return $protocol;
    }

    /**
     * Fetch local hostname without port.
     *
     * Try HTTP_HOST first (equivalent to ServerName defined in vhost),
     * default to SERVER_NAME (global server name)
     *
     * @return string
     */
    public function getHostname()
    {
        $hostname = $this->getServer('HTTP_HOST', $this->getServer('SERVER_NAME', 'localhost'));
        if (false !== ($idx = strpos($hostname, ':'))) {
            $hostname = substr($hostname, 0, $idx);
        }
        return $hostname;
    }

    /**
     * Fetch local port.
     *
     * @return int
     */
    public function getPort()
    {
        return (int) $this->getServer('SERVER_PORT', 80);
    }

    /**
     * Get hostname:port
     *
     * If:
     * - HTTP and port is 80, returns hostname
     * - HTTPS and port is 443, returns hostname
     * - returns hostname:port
     *
     * @return string
     */
    public function getHost()
    {
        $protocol = $this->getProtocol();
        $name     = $this->getHostname();
        $port     = $this->getPort();

        if ((self::PROTOCOL_HTTP == $protocol && $port == 80) || (self::PROTOCOL_HTTPS == $protocol && $port == 443) || true === $this->isProxied()) {
            return $name;
        } else {
            return $name.':'.$port;
        }
    }

    /**
     * Get query as a string, without the leading ?
     *
     * &-separated.
     *
     * @return string
     */
    public function getQueryString()
    {
        return $this->getServer('QUERY_STRING');
    }

    /**
     * Get path name.
     *
     * Example, for URI http://www.example.com/abc/bcd/?foo=23 path name is /abc/bcd/
     *
     * @return string
     */
    public function getPathName()
    {
        $requestUri = $this->getServer('REQUEST_URI');

        // Remove the query string from REQUEST_URI
        if (false !== ($pos = strpos($requestUri, '?'))) {
            $requestUri = substr($requestUri, 0, $pos);
        }
        return $requestUri;
    }

    /**
     * Get path name.
     *
     * Example, for URI http://www.example.com/abc/bcd/?foo=23 path name is /abc/bcd/?foo=23
     *
     * @return string
     */
    public function getPath()
    {
        $pathName = $this->getPathName();
        $query    = $this->getQueryString();

        return $pathName . ($query ? '?'. $query : null);
    }

    /**
     * Get URI base.
     *
     * Example, for URI http://www.example.com/abc/bcd/?foo=23 URI base is http://www.example.com
     *
     * @return string
     */
    public function getUriBase()
    {
        return $this->getProtocol() .'://'. $this->getHost();
    }

    /**
     * Get URI path name.
     *
     * Example, for URI http://www.example.com/abc/bcd/?foo=23 URI base is http://www.example.com/abc/bcd/
     *
     * @return string
     */
    public function getUriPathName()
    {
        return $this->getUriBase() . $this->getPathName();
    }

    /**
     * Get URI path name.
     *
     * Example, http://www.example.com/abc/bcd/?foo=23
     *
     * @return string
     */
    public function getUri()
    {
        return $this->getUriBase() . $this->getPath();
    }
    
    /**
     * @param bool $proxied
     * 
     * @return \Core_Request
     */
    public function setProxied($proxied)
    {
        $this->_proxied = (bool) $proxied;
        return $this;
    }
    
    /**
     * @return bool
     */
    public function isProxied()
    {
        return $this->_proxied;
    }

    /**
     * If not set, will query environment (ini_get('magic_quotes_gpc'))
     *
     * @return boolean
     */
    public function hasMagicQuotesEnabled()
    {
        if (null === $this->_magicQuotesEnabled) {
            $this->_magicQuotesEnabled = (bool) ini_get('magic_quotes_gpc');
        }
        return $this->_magicQuotesEnabled;
    }

    /**
     * @param boolean $magicQuotesEnabled
     *
     * @return Core_Request
     */
    public function setMagicQuotesEnabled($magicQuotesEnabled = true)
    {
        $this->_magicQuotesEnabled = (bool) $magicQuotesEnabled;
        return $this;
    }

    /**
     * Strip slashes from value if magic quotes are enabled.
     *
     * @param array|mixed $value Value (or array of values) to cleanup
     *
     * @return array|mixed Value (or array of values) cleaned up
     */
    protected function _stripSlashesIfNeeded($value)
    {
        if ($value && $this->hasMagicQuotesEnabled()) {
            if (is_array($value)) {
                $value = array_map(array($this, '_stripSlashesIfNeeded'), $value);
            } else {
                $value = stripslashes($value);
            }
        }
        return $value;
    }
    
    /**
     * @param string $header    Header to parse
     * @param array  $available Array of availble items which to match against
     * @param string $default   The default if nothing better found
     * 
     * @return string
     */
    protected function _parseAccept($header, $available, $default)
    {
        $parsed = false;
        $tokens = array();
        foreach(explode(',', $header) as $token) {
            $qualifier = 1;
            if (false !== strpos($token, ';')) {
                list($token, $qualifier) = explode(';q=', $token);
            }
            $tokens[$token] = floatval($qualifier);
        }
        arsort($tokens);
        $items = array_keys($tokens);
        if (null === $available) {
            // no available languages sent
            $parsed = current($items);
        } else {
            foreach ($items as $item) {
                if (in_array($item, $available)) {
                    $parsed = $item;
                    break;
                }
            }
            if (false === $parsed) {
                if (null === $default) {
                    $parsed = current($available);
                } else {
                    $parsed = $default;
                }
            }
        }
        return $parsed;
    }

    /**
     * @param string $header
     * @param array  $target
     */
    protected function _parseMimeType($header, array &$target)
    {
        if (empty($target)) {
            $format   = self::FORMAT_URLENCODED;
            $params   = array();
            $matches  = array();
            $type     = $header;
            if (preg_match_all('/;\s*(?<name>[^=]+)=(?<value>[^;]+)/', $header, $matches, PREG_SET_ORDER)) {
                foreach($matches as $match) {
                    $params[$match['name']] = $match['value'];
                }
                $type = substr($type, 0, strpos($type, ';'));
            }
            if (strpos($type, '+')) {
                list($type, $format) = explode('+', $type);
            } else if (strpos($type, '/')) {
                list($ignored, $format) = explode('/', $type);
                if ('x-www-form-urlencoded' === $format) {
                    $format = self::FORMAT_URLENCODED;
                }
            }
            if (empty($type)) {
                $type = 'text/plain';
            }

            $target = array(
                self::CONTENT_TYPE     => strtolower($type),
                self::CONTENT_FORMAT   => strtolower($format),
                self::CONTENT_PARAMS   => $params,
            );
        }
    }

    /**
     * @param SimpleXMLElement $xml
     *
     * @return array
     */
    protected function _xmlToArray(SimpleXMLElement $xml)
    {
        $array = (array) $xml;
        foreach ($array as $key => $value) {
            if ($value instanceof SimpleXMLElement) {
                $array[$key] = empty($value) ? null : $this->_xmlToArray($value);
            }
        }
        return $array;
    }
}
