<?php

use Nextend\Framework\Misc\Base64;
use Nextend\Framework\Misc\HttpClient;

/**
 * tmhOAuth
 *
 * An OAuth library written in PHP.
 * The library supports file uploading using multipart/form as well as general
 * REST requests. OAuth authentication is sent using an Authorization Header.
 *
 * @author  themattharris
 * @version 0.8.4
 *
 * 06 Aug 2014
 */
class NTmhOAuth {

    const VERSION = '0.8.4';
    var $response = array();

    /**
     * Creates a new tmhOAuth object
     *
     * @param string $config , the configuration to use for this request
     *
     * @return void
     */
    public function __construct($config = array()) {
        $this->buffer = null;
        $this->reconfigure($config);
        $this->reset_request_settings();
        $this->set_user_agent();
    }

    public function reconfigure($config = array()) {
        $dir = dirname(__FILE__);
        // default configuration options
        $this->config = array_merge(array(
            // leave 'user_agent' blank for default, otherwise set this to
            // something that clearly identifies your app
            'user_agent' => '',
            'host'       => 'api.twitter.com',
            'method'     => 'GET',

            'consumer_key'           => '',
            'consumer_secret'        => '',
            'token'                  => '',
            'secret'                 => '',

            // OAuth2 bearer token. This should already be URL encoded
            'bearer'                 => '',

            // oauth signing variables that are not dynamic
            'oauth_version'          => '1.0',
            'oauth_signature_method' => 'HMAC-SHA1',

            // you probably don't want to change any of these curl values
            'curl_http_version'      => CURL_HTTP_VERSION_1_1,
            'curl_connecttimeout'    => 30,
            'curl_timeout'           => 10,

            // for security this should always be set to 2.
            'curl_ssl_verifyhost'    => 2,
            // for security this should always be set to true.
            'curl_ssl_verifypeer'    => true,
            // for security this should always be set to true.
            'use_ssl'                => true,

            // in some cases (very very odd ones) the SSL version must be set manually.
            // unless you know why your are changing this, you should leave it as false
            // to allow PHP to determine the value for this setting itself.
            'curl_sslversion'        => false,

            'curl_followlocation'        => false,
            // whether to follow redirects or not

            // support for proxy servers
            'curl_proxy'                 => false,
            // really you don't want to use this if you are using streaming
            'curl_proxyuserpwd'          => false,
            // format username:password for proxy, if required
            'curl_encoding'              => '',
            // leave blank for all supported formats, else use gzip, deflate, identity etc

            // streaming API configuration
            'is_streaming'               => false,
            'streaming_eol'              => "\r\n",
            'streaming_metrics_interval' => 10,

            // header or querystring. You should always use header!
            // this is just to help me debug other developers implementations
            'as_header'                  => true,
            'force_nonce'                => false,
            // used for checking signatures. leave as false for auto
            'force_timestamp'            => false,
            // used for checking signatures. leave as false for auto
        ), $config);
    }

    private function reset_request_settings($options = array()) {
        $this->request_settings = array(
            'params'    => array(),
            'headers'   => array(),
            'with_user' => true,
            'multipart' => false,
        );

        if (!empty($options)) $this->request_settings = array_merge($this->request_settings, $options);
    }

    /**
     * Sets the useragent for PHP to use
     * If '$this->config['user_agent']' already has a value it is used instead of one
     * being generated.
     *
     * @return void value is stored to the config array class variable
     */
    private function set_user_agent() {
        if (!empty($this->config['user_agent'])) return;

        $ssl                        = ($this->config['curl_ssl_verifyhost'] && $this->config['curl_ssl_verifypeer'] && $this->config['use_ssl']) ? '+' : '-';
        $ua                         = 'tmhOAuth ' . self::VERSION . $ssl . 'SSL - //github.com/themattharris/tmhOAuth';
        $this->config['user_agent'] = $ua;
    }

    /**
     * Generates a random OAuth nonce.
     * If 'force_nonce' is false a nonce will be generated, otherwise the value of '$this->config['force_nonce']' will
     * be used.
     *
     * @param string $length       how many characters the nonce should be before MD5 hashing. default 12
     * @param string $include_time whether to include time at the beginning of the nonce. default true
     *
     * @return $nonce as a string
     */
    private function nonce($length = 12, $include_time = true) {
        if ($this->config['force_nonce'] === false) {
            $prefix = $include_time ? microtime() : '';

            return md5(substr($prefix . uniqid(), 0, $length));
        } else {
            return $this->config['force_nonce'];
        }
    }

    /**
     * Generates a timestamp.
     * If 'force_timestamp' is false a timestamp will be generated, otherwise the value of
     * '$this->config['force_timestamp']' will be used.
     *
     * @return $time as a string
     */
    private function timestamp() {
        if ($this->config['force_timestamp'] === false) {
            $time = time();
        } else {
            $time = $this->config['force_timestamp'];
        }

        return (string)$time;
    }

    /**
     * Encodes the string or array passed in a way compatible with OAuth.
     * If an array is passed each array value will will be encoded.
     *
     * @param mixed $data the scalar or array to encode
     *
     * @return $data encoded in a way compatible with OAuth
     */
    private function safe_encode($data) {
        if (is_array($data)) {
            return array_map(array(
                $this,
                'safe_encode'
            ), $data);
        } else if (is_scalar($data)) {
            return str_ireplace(array(
                '+',
                '%7E'
            ), array(
                ' ',
                '~'
            ), rawurlencode($data));
        } else {
            return '';
        }
    }

    /**
     * Decodes the string or array from it's URL encoded form
     * If an array is passed each array value will will be decoded.
     *
     * @param mixed $data the scalar or array to decode
     *
     * @return string $data decoded from the URL encoded form
     */
    private function safe_decode($data) {
        if (is_array($data)) {
            return array_map(array(
                $this,
                'safe_decode'
            ), $data);
        } else if (is_scalar($data)) {
            return rawurldecode($data);
        } else {
            return '';
        }
    }

    /**
     * Prepares OAuth1 signing parameters.
     *
     * @return void all required OAuth parameters, safely encoded, are stored to the class variable
     *              '$this->request_settings['oauth1_params']'
     */
    private function prepare_oauth1_params() {
        $defaults = array(
            'oauth_nonce'            => $this->nonce(),
            'oauth_timestamp'        => $this->timestamp(),
            'oauth_version'          => $this->config['oauth_version'],
            'oauth_consumer_key'     => $this->config['consumer_key'],
            'oauth_signature_method' => $this->config['oauth_signature_method'],
        );

        // include the user token if it exists
        if ($oauth_token = $this->token()) $defaults['oauth_token'] = $oauth_token;

        $this->request_settings['oauth1_params'] = array();

        // safely encode
        foreach ($defaults as $k => $v) {
            $this->request_settings['oauth1_params'][$this->safe_encode($k)] = $this->safe_encode($v);
        }
    }

    private function token() {
        if ($this->request_settings['with_user']) {
            if (isset($this->config['token']) && !empty($this->config['token'])) return $this->config['token']; elseif (isset($this->config['user_token'])) return $this->config['user_token'];
        }

        return '';
    }

    private function secret() {
        if ($this->request_settings['with_user']) {
            if (isset($this->config['secret']) && !empty($this->config['secret'])) return $this->config['secret']; elseif (isset($this->config['user_secret'])) return $this->config['user_secret'];
        }

        return '';
    }

    /**
     * Extracts and decodes OAuth parameters from the passed string
     *
     * @param string $body the response body from an OAuth flow method
     *
     * @return array the response body safely decoded to an array of key => values
     */
    public function extract_params($body) {
        $kvs     = explode('&', $body);
        $decoded = array();
        foreach ($kvs as $kv) {
            $kv              = explode('=', $kv, 2);
            $kv[0]           = $this->safe_decode($kv[0]);
            $kv[1]           = $this->safe_decode($kv[1]);
            $decoded[$kv[0]] = $kv[1];
        }

        return $decoded;
    }

    /**
     * Prepares the HTTP method for use in the base string by converting it to
     * uppercase.
     *
     * @return void value is stored to the class variable '$this->request_settings['method']'
     */
    private function prepare_method() {
        $this->request_settings['method'] = strtoupper($this->request_settings['method']);
    }

    /**
     * Prepares the URL for use in the base string by ripping it apart and
     * reconstructing it.
     *
     * Ref: 3.4.1.2
     *
     * @return void value is stored to the class array variable '$this->request_settings['url']'
     */
    private function prepare_url() {
        $parts = parse_url($this->request_settings['url']);

        $port   = isset($parts['port']) ? $parts['port'] : false;
        $scheme = $parts['scheme'];
        $host   = $parts['host'];
        $path   = isset($parts['path']) ? $parts['path'] : false;

        $port or $port = ($scheme == 'https') ? '443' : '80';

        if (($scheme == 'https' && $port != '443') || ($scheme == 'http' && $port != '80')) {
            $host = "$host:$port";
        }

        // the scheme and host MUST be lowercase
        $this->request_settings['url'] = strtolower("$scheme://$host");
        // but not the path
        $this->request_settings['url'] .= $path;
    }

    /**
     * If the request uses multipart, and the parameter isn't a file path, prepend a space
     * otherwise return the original value. we chose a space here as twitter whitespace trims from
     * the beginning of the tweet. we don't use \0 here because it's the character for string
     * termination.
     *
     * @param the parameter value
     *
     * @return string the original or modified string, depending on the request and the input parameter
     */
    private function multipart_escape($value) {
        if (!$this->request_settings['multipart'] || strpos($value, '@') !== 0) return $value;

        // see if the parameter is a file.
        // we split on the semi-colon as it's the delimiter used on media uploads
        // for fields with semi-colons this will return the original string
        list($file) = explode(';', substr($value, 1), 2);
        if (file_exists($file)) return $value;

        return " $value";
    }


    /**
     * Prepares all parameters for the base string and request.
     * Multipart parameters are ignored as they are not defined in the specification,
     * all other types of parameter are encoded for compatibility with OAuth.
     *
     * @param array $params the parameters for the request
     *
     * @return void prepared values are stored in the class array variable '$this->request_settings'
     */
    private function prepare_params() {
        $doing_oauth1                              = false;
        $this->request_settings['prepared_params'] = array();
        $prepared                                  = &$this->request_settings['prepared_params'];
        $prepared_pairs                            = array();
        $prepared_pairs_with_oauth                 = array();

        if (isset($this->request_settings['oauth1_params'])) {
            $oauth1       = &$this->request_settings['oauth1_params'];
            $doing_oauth1 = true;
            $params       = array_merge($oauth1, $this->request_settings['params']);

            // Remove oauth_signature if present
            // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
            unset($params['oauth_signature']);

            // empty the oauth1 array. we reset these values later in this method
            $oauth1 = array();
        } else {
            $params = $this->request_settings['params'];
        }

        // Parameters are sorted by name, using lexicographical byte value ordering.
        // Ref: Spec: 9.1.1 (1)
        uksort($params, 'strcmp');

        // set this now so we're not doing it on every parameter
        $supports_curl_file = class_exists('CurlFile', false);

        // encode params unless we're doing multipart
        foreach ($params as $k => $v) {
            $k = $this->request_settings['multipart'] ? $k : $this->safe_encode($k);

            if (is_array($v)) $v = implode(',', $v);

            // we don't need to do the multipart escaping if we support curlfile
            if ($supports_curl_file && ($v instanceof CurlFile)) {
                // leave $v alone
            } elseif ($this->request_settings['multipart']) {
                $v = $this->multipart_escape($v);
            } else {
                $v = $this->safe_encode($v);
            }

            // split parameters for the basestring and authorization header, and recreate the oauth1 array
            if ($doing_oauth1) {
                // if we're doing multipart, only store the oauth_* params, ignore the users request params
                if ((strpos($k, 'oauth') === 0) || !$this->request_settings['multipart']) $prepared_pairs_with_oauth[] = "{$k}={$v}";

                if (strpos($k, 'oauth') === 0) {
                    $oauth1[$k] = $v;
                    continue;
                }
            }
            $prepared[$k] = $v;

            if (!$this->request_settings['multipart']) $prepared_pairs[] = "{$k}={$v}";
        }

        if ($doing_oauth1) {
            $this->request_settings['basestring_params'] = implode('&', $prepared_pairs_with_oauth);
        }

        // setup params for GET/POST/PUT method handling
        if (!empty($prepared)) {
            $content = implode('&', $prepared_pairs);

            switch ($this->request_settings['method']) {
                case 'PUT':
                    // fall through to POST as PUT should be treated the same
                case 'POST':
                    $this->request_settings['postfields'] = $this->request_settings['multipart'] ? $prepared : $content;
                    break;
                default:
                    $this->request_settings['querystring'] = $content;
                    break;
            }
        }
    }

    /**
     * Prepares the OAuth signing key
     *
     * @return void prepared signing key is stored in the class variable 'signing_key'
     */
    private function prepare_signing_key() {
        $left                                  = $this->safe_encode($this->config['consumer_secret']);
        $right                                 = $this->safe_encode($this->secret());
        $this->request_settings['signing_key'] = $left . '&' . $right;
    }

    /**
     * Prepare the base string.
     * Ref: Spec: 9.1.3 ("Concatenate Request Elements")
     *
     * @return void prepared base string is stored in the class variable 'base_string'
     */
    private function prepare_base_string() {
        $url = $this->request_settings['url'];

        # if the host header is set we need to rewrite the basestring to use
        # that, instead of the request host. otherwise the signature won't match
        # on the server side
        if (!empty($this->request_settings['headers']['Host'])) {
            $url = str_ireplace($this->config['host'], $this->request_settings['headers']['Host'], $url);
        }

        $base                                 = array(
            $this->request_settings['method'],
            $url,
            $this->request_settings['basestring_params']
        );
        $this->request_settings['basestring'] = implode('&', $this->safe_encode($base));
    }

    /**
     * Signs the OAuth 1 request
     *
     * @return void oauth_signature is added to the parameters in the class array variable '$this->request_settings'
     */
    private function prepare_oauth_signature() {
        $this->request_settings['oauth1_params']['oauth_signature'] = $this->safe_encode(Base64::encode(hash_hmac('sha1', $this->request_settings['basestring'], $this->request_settings['signing_key'], true)));
    }

    /**
     * Prepares the Authorization header
     *
     * @return void prepared authorization header is stored in the class variable headers['Authorization']
     */
    private function prepare_auth_header() {
        if (!$this->config['as_header']) return;

        // oauth1
        if (isset($this->request_settings['oauth1_params'])) {
            // sort again as oauth_signature was added post param preparation
            uksort($this->request_settings['oauth1_params'], 'strcmp');
            $encoded_quoted_pairs = array();
            foreach ($this->request_settings['oauth1_params'] as $k => $v) {
                $encoded_quoted_pairs[] = "{$k}=\"{$v}\"";
            }
            $header = 'OAuth ' . implode(', ', $encoded_quoted_pairs);
        } elseif (!empty($this->config['bearer'])) {
            $header = 'Bearer ' . $this->config['bearer'];
        }

        if (isset($header)) $this->request_settings['headers']['Authorization'] = $header;
    }

    /**
     * Create the bearer token for OAuth2 requests from the consumer_key and consumer_secret.
     *
     * @return string the bearer token
     */
    public function bearer_token_credentials() {
        $credentials = implode(':', array(
            $this->safe_encode($this->config['consumer_key']),
            $this->safe_encode($this->config['consumer_secret'])
        ));

        return Base64::encode($credentials);
    }

    /**
     * Make an HTTP request using this library. This method doesn't return anything.
     * Instead the response should be inspected directly.
     *
     * @param string $method    the HTTP method being used. e.g. POST, GET, HEAD etc
     * @param string $url       the request URL without query string parameters
     * @param array  $params    the request parameters as an array of key=value pairs. Default empty array
     * @param string $useauth   whether to use authentication when making the request. Default true
     * @param string $multipart whether this request contains multipart data. Default false
     * @param array  $headers   any custom headers to send with the request. Default empty array
     *
     * @return int the http response code for the request. 0 is returned if a connection could not be made
     */
    public function request($method, $url, $params = array(), $useauth = true, $multipart = false, $headers = array()) {
        $options = array(
            'method'    => $method,
            'url'       => $url,
            'params'    => $params,
            'with_user' => true,
            'multipart' => $multipart,
            'headers'   => $headers
        );
        $options = array_merge($this->default_options(), $options);

        if ($useauth) {
            return $this->user_request($options);
        } else {
            return $this->unauthenticated_request($options);
        }
    }

    public function apponly_request($options = array()) {
        $options = array_merge($this->default_options(), $options, array(
            'with_user' => false,
        ));
        $this->reset_request_settings($options);
        if ($options['without_bearer']) {
            return $this->oauth1_request();
        } else {
            $this->prepare_method();
            $this->prepare_url();
            $this->prepare_params();
            $this->prepare_auth_header();

            return $this->curlit();
        }
    }

    public function user_request($options = array()) {
        $options = array_merge($this->default_options(), $options, array(
            'with_user' => true,
        ));
        $this->reset_request_settings($options);

        return $this->oauth1_request();
    }

    public function unauthenticated_request($options = array()) {
        $options = array_merge($this->default_options(), $options, array(
            'with_user' => false,
        ));
        $this->reset_request_settings($options);
        $this->prepare_method();
        $this->prepare_url();
        $this->prepare_params();

        return $this->curlit();
    }

    /**
     * Signs the request and adds the OAuth signature. This runs all the request
     * parameter preparation methods.
     *
     * @param string  $method    the HTTP method being used. e.g. POST, GET, HEAD etc
     * @param string  $url       the request URL without query string parameters
     * @param array   $params    the request parameters as an array of key=value pairs
     * @param boolean $with_user whether to include the user credentials when making the request.
     *
     * @return void
     */
    private function oauth1_request() {
        $this->prepare_oauth1_params();
        $this->prepare_method();
        $this->prepare_url();
        $this->prepare_params();
        $this->prepare_base_string();
        $this->prepare_signing_key();
        $this->prepare_oauth_signature();
        $this->prepare_auth_header();

        return $this->curlit();
    }

    private function default_options() {
        return array(
            'method'         => 'GET',
            'params'         => array(),
            'with_user'      => true,
            'multipart'      => false,
            'headers'        => array(),
            'without_bearer' => false,
        );
    }

    /**
     * Make a long poll HTTP request using this library. This method is
     * different to the other request methods as it isn't supposed to disconnect
     *
     * Using this method expects a callback which will receive the streaming
     * responses.
     *
     * @param string $method   the HTTP method being used. e.g. POST, GET, HEAD etc
     * @param string $url      the request URL without query string parameters
     * @param array  $params   the request parameters as an array of key=value pairs
     * @param string $callback the callback function to stream the buffer to.
     *
     * @return void
     */
    public function streaming_request($method, $url, $params = array(), $callback = '') {
        if (!empty($callback)) {
            if (!is_callable($callback)) {
                return false;
            }
            $this->config['streaming_callback'] = $callback;
        }
        $this->metrics['start']          = time();
        $this->metrics['interval_start'] = $this->metrics['start'];
        $this->metrics['messages']       = 0;
        $this->metrics['last_messages']  = 0;
        $this->metrics['bytes']          = 0;
        $this->metrics['last_bytes']     = 0;
        $this->config['is_streaming']    = true;
        $this->request($method, $url, $params);
    }

    /**
     * Handles the updating of the current Streaming API metrics.
     *
     * @return array the metrics for the streaming api connection
     */
    private function update_metrics() {
        $now = time();
        if (($this->metrics['interval_start'] + $this->config['streaming_metrics_interval']) > $now) return null;

        $this->metrics['mps'] = round(($this->metrics['messages'] - $this->metrics['last_messages']) / $this->config['streaming_metrics_interval'], 2);
        $this->metrics['bps'] = round(($this->metrics['bytes'] - $this->metrics['last_bytes']) / $this->config['streaming_metrics_interval'], 2);

        $this->metrics['last_bytes']     = $this->metrics['bytes'];
        $this->metrics['last_messages']  = $this->metrics['messages'];
        $this->metrics['interval_start'] = $now;

        return $this->metrics;
    }

    /**
     * Utility function to create the request URL in the requested format.
     * If a fully-qualified URI is provided, it will be returned.
     * Any multi-slashes (except for the protocol) will be replaced with a single slash.
     *
     *
     * @param string $request   the API method without extension
     * @param string $extension the format of the response. Default json. Set to an empty string to exclude the format
     *
     * @return string the concatenation of the host, API version, API method and format, or $request if it begins with
     *                http
     */
    public function url($request, $extension = 'json') {
        // remove multi-slashes
        $request = preg_replace('$([^:])//+$', '$1/', $request);

        if (stripos($request, 'http') === 0 || stripos($request, '//') === 0) {
            return $request;
        }

        $extension = strlen($extension) > 0 ? ".$extension" : '';
        $proto     = $this->config['use_ssl'] ? 'https:/' : 'http:/';

        // trim trailing slash
        $request = ltrim($request, '/');

        $pos = strlen($request) - strlen($extension);
        if (substr($request, $pos) === $extension) $request = substr_replace($request, '', $pos);

        return implode('/', array(
            $proto,
            $this->config['host'],
            $request . $extension
        ));
    }

    /**
     * Public access to the private safe decode/encode methods
     *
     * @param string $text the text to transform
     * @param string $mode the transformation mode. either encode or decode
     *
     * @return string $text transformed by the given $mode
     */
    public function transformText($text, $mode = 'encode') {
        return $this->{"safe_$mode"}($text);
    }

    /**
     * Utility function to parse the returned curl headers and store them in the
     * class array variable.
     *
     * @param object $ch     curl handle
     * @param string $header the response headers
     *
     * @return string the length of the header
     */
    private function curlHeader($ch, $header) {
        $this->response['raw'] .= $header;

        list($key, $value) = array_pad(explode(':', $header, 2), 2, null);

        $key   = trim($key);
        $value = trim($value);

        if (!isset($this->response['headers'][$key])) {
            $this->response['headers'][$key] = $value;
        } else {
            if (!is_array($this->response['headers'][$key])) {
                $this->response['headers'][$key] = array($this->response['headers'][$key]);
            }
            $this->response['headers'][$key][] = $value;
        }

        return strlen($header);
    }

    /**
     * Utility function to parse the returned curl buffer and store them until
     * an EOL is found. The buffer for curl is an undefined size so we need
     * to collect the content until an EOL is found.
     *
     * This function calls the previously defined streaming callback method.
     *
     * @param object $ch   curl handle
     * @param string $data the current curl buffer
     *
     * @return int the length of the data string processed in this function
     */
    private function curlWrite($ch, $data) {
        $l = strlen($data);
        if (strpos($data, $this->config['streaming_eol']) === false) {
            $this->buffer .= $data;

            return $l;
        }

        $buffered = explode($this->config['streaming_eol'], $data);
        $content  = $this->buffer . $buffered[0];

        $this->metrics['messages']++;
        $this->metrics['bytes'] += strlen($content);

        if (!is_callable($this->config['streaming_callback'])) return 0;

        $metrics      = $this->update_metrics();
        $stop         = call_user_func($this->config['streaming_callback'], $content, strlen($content), $metrics);
        $this->buffer = $buffered[1];
        if ($stop) return 0;

        return $l;
    }

    /**
     * Makes a curl request. Takes no parameters as all should have been prepared
     * by the request method
     *
     * the response data is stored in the class variable 'response'
     *
     * @return int the http response code for the request. 0 is returned if a connection could not be made
     */
    private function curlit() {
        $this->response = array(
            'raw' => ''
        );

        // configure curl
        $c = curl_init();

        if ($this->request_settings['method'] == 'GET' && isset($this->request_settings['querystring'])) {
            $this->request_settings['url'] = $this->request_settings['url'] . '?' . $this->request_settings['querystring'];
        } elseif ($this->request_settings['method'] == 'POST' || $this->request_settings['method'] == 'PUT') {
            $postfields = array();
            if (isset($this->request_settings['postfields'])) $postfields = $this->request_settings['postfields'];

            curl_setopt($c, CURLOPT_POSTFIELDS, $postfields);
        }

        curl_setopt($c, CURLOPT_CUSTOMREQUEST, $this->request_settings['method']);

        curl_setopt_array($c, array(
            CURLOPT_HTTP_VERSION   => $this->config['curl_http_version'],
            CURLOPT_USERAGENT      => $this->config['user_agent'],
            CURLOPT_CONNECTTIMEOUT => $this->config['curl_connecttimeout'],
            CURLOPT_TIMEOUT        => $this->config['curl_timeout'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => $this->config['curl_ssl_verifypeer'],
            CURLOPT_SSL_VERIFYHOST => $this->config['curl_ssl_verifyhost'],

            CURLOPT_FOLLOWLOCATION => $this->config['curl_followlocation'],
            CURLOPT_PROXY          => $this->config['curl_proxy'],
            CURLOPT_ENCODING       => $this->config['curl_encoding'],
            CURLOPT_URL            => $this->request_settings['url'],
            // process the headers
            CURLOPT_HEADERFUNCTION => array(
                $this,
                'curlHeader'
            ),
            CURLOPT_HEADER         => false,
            CURLINFO_HEADER_OUT    => true,
        ));

        if ($this->config['curl_proxyuserpwd'] !== false) curl_setopt($c, CURLOPT_PROXYUSERPWD, $this->config['curl_proxyuserpwd']);

        if ($this->config['curl_sslversion'] !== false) curl_setopt($c, CURLOPT_SSLVERSION, $this->config['curl_sslversion']);

        if ($this->config['is_streaming']) {
            // process the body
            $this->response['content-length'] = 0;
            curl_setopt($c, CURLOPT_TIMEOUT, 0);
            curl_setopt($c, CURLOPT_WRITEFUNCTION, array(
                $this,
                'curlWrite'
            ));
        }

        if (!empty($this->request_settings['headers'])) {
            foreach ($this->request_settings['headers'] as $k => $v) {
                $headers[] = trim($k . ': ' . $v);
            }
            curl_setopt($c, CURLOPT_HTTPHEADER, $headers);
        }

        if (isset($this->config['block']) && (true === $this->config['block'])) return 0;

        // do it!
        $response = curl_exec($c);

        if (curl_errno($c) == 60) {
            curl_setopt($c, CURLOPT_CAINFO, HttpClient::getCacertPath());
            $response = curl_exec($c);
        }

        $code  = curl_getinfo($c, CURLINFO_HTTP_CODE);
        $info  = curl_getinfo($c);
        $error = curl_error($c);
        $errno = curl_errno($c);
        curl_close($c);

        // store the response
        $this->response['code']     = $code;
        $this->response['response'] = $response;
        $this->response['info']     = $info;
        $this->response['error']    = $error;
        $this->response['errno']    = $errno;

        if (!isset($this->response['raw'])) {
            $this->response['raw'] = '';
        }
        $this->response['raw'] .= $response;

        return $code;
    }
}
