diff --git a/CURL.php b/CURL.php new file mode 100644 index 0000000..74839d3 --- /dev/null +++ b/CURL.php @@ -0,0 +1,1060 @@ +', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', + // %x5D-7E + ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', + 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', + ), true); + } + + public static function RFC2616() { + return self::$RFC2616; + } + + public static function RFC6265() { + return self::$RFC6265; + } +} + +CurlCookieConst::Init(); + +class Curl +{ + const VERSION = '4.8.2'; + const DEFAULT_TIMEOUT = 30; + + public $curl; + public $id = null; + + public $error = false; + public $errorCode = 0; + public $errorMessage = null; + + public $curlError = false; + public $curlErrorCode = 0; + public $curlErrorMessage = null; + + public $httpError = false; + public $httpStatusCode = 0; + public $httpErrorMessage = null; + + public $baseUrl = null; + public $url = null; + public $requestHeaders = null; + public $responseHeaders = null; + public $rawResponseHeaders = ''; + public $response = null; + public $rawResponse = null; + + public $beforeSendFunction = null; + public $downloadCompleteFunction = null; + private $successFunction = null; + private $errorFunction = null; + private $completeFunction = null; + + private $cookies = array(); + private $responseCookies = array(); + private $headers = array(); + private $options = array(); + + private $jsonDecoder = null; + private $jsonPattern = '/^(?:application|text)\/(?:[a-z]+(?:[\.-][0-9a-z]+){0,}[\+\.]|x-)?json(?:-[a-z]+)?/i'; + private $xmlPattern = '~^(?:text/|application/(?:atom\+|rss\+)?)xml~i'; + + /** + * Construct + * + * @access public + * @param $base_url + * @throws \ErrorException + */ + public function __construct($base_url = null) + { + if (!extension_loaded('curl')) { + throw new \ErrorException('cURL library is not loaded'); + } + + $this->curl = curl_init(); + $this->id = 1; + $this->setDefaultUserAgent(); + $this->setDefaultJsonDecoder(); + $this->setDefaultTimeout(); + $this->setOpt(CURLINFO_HEADER_OUT, true); + $this->setOpt(CURLOPT_HEADERFUNCTION, array($this, 'headerCallback')); + $this->setOpt(CURLOPT_RETURNTRANSFER, true); + $this->headers = new CaseInsensitiveArray(); + $this->setURL($base_url); + } + + /** + * Before Send + * + * @access public + * @param $callback + */ + public function beforeSend($callback) + { + $this->beforeSendFunction = $callback; + } + + /** + * Build Post Data + * + * @access public + * @param $data + * + * @return array|string + */ + public function buildPostData($data) + { + if (is_array($data)) { + if (self::is_array_multidim($data)) { + if (isset($this->headers['Content-Type']) && + preg_match($this->jsonPattern, $this->headers['Content-Type'])) { + $json_str = json_encode($data); + if (!($json_str === false)) { + $data = $json_str; + } + } else { + $data = self::http_build_multi_query($data); + } + } else { + $binary_data = false; + foreach ($data as $key => $value) { + // Fix "Notice: Array to string conversion" when $value in curl_setopt($ch, CURLOPT_POSTFIELDS, + // $value) is an array that contains an empty array. + if (is_array($value) && empty($value)) { + $data[$key] = ''; + // Fix "curl_setopt(): The usage of the @filename API for file uploading is deprecated. Please use + // the CURLFile class instead". Ignore non-file values prefixed with the @ character. + } elseif (is_string($value) && strpos($value, '@') === 0 && is_file(substr($value, 1))) { + $binary_data = true; + if (class_exists('CURLFile')) { + $data[$key] = new \CURLFile(substr($value, 1)); + } + } elseif ($value instanceof \CURLFile) { + $binary_data = true; + } + } + + if (!$binary_data) { + if (isset($this->headers['Content-Type']) && + preg_match($this->jsonPattern, $this->headers['Content-Type'])) { + $json_str = json_encode($data); + if (!($json_str === false)) { + $data = $json_str; + } + } else { + $data = http_build_query($data, '', '&'); + } + } + } + } + + return $data; + } + + /** + * Call + * + * @access public + */ + public function call() + { + $args = func_get_args(); + $function = array_shift($args); + if (is_callable($function)) { + array_unshift($args, $this); + call_user_func_array($function, $args); + } + } + + /** + * Close + * + * @access public + */ + public function close() + { + if (is_resource($this->curl)) { + curl_close($this->curl); + } + $this->options = null; + $this->jsonDecoder = null; + } + + /** + * Complete + * + * @access public + * @param $callback + */ + public function complete($callback) + { + $this->completeFunction = $callback; + } + + /** + * Progress + * + * @access public + * @param $callback + */ + public function progress($callback) + { + $this->setOpt(CURLOPT_PROGRESSFUNCTION, $callback); + $this->setOpt(CURLOPT_NOPROGRESS, false); + } + + /** + * Delete + * + * @access public + * @param $url + * @param $query_parameters + * @param $data + * + * @return string + */ + public function delete($url, $query_parameters = array(), $data = array()) + { + if (is_array($url)) { + $data = $query_parameters; + $query_parameters = $url; + $url = $this->baseUrl; + } + + $this->setURL($url, $query_parameters); + $this->setOpt(CURLOPT_CUSTOMREQUEST, 'DELETE'); + $this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data)); + return $this->exec(); + } + + /** + * Download Complete + * + * @access public + * @param $fh + */ + public function downloadComplete($fh) + { + if (!$this->error && $this->downloadCompleteFunction) { + rewind($fh); + $this->call($this->downloadCompleteFunction, $fh); + $this->downloadCompleteFunction = null; + } + + if (is_resource($fh)) { + fclose($fh); + } + + // Fix "PHP Notice: Use of undefined constant STDOUT" when reading the + // PHP script from stdin. Using null causes "Warning: curl_setopt(): + // supplied argument is not a valid File-Handle resource". + if (!defined('STDOUT')) { + define('STDOUT', fopen('php://stdout', 'w')); + } + + // Reset CURLOPT_FILE with STDOUT to avoid: "curl_exec(): CURLOPT_FILE + // resource has gone away, resetting to default". + $this->setOpt(CURLOPT_FILE, STDOUT); + + // Reset CURLOPT_RETURNTRANSFER to tell cURL to return subsequent + // responses as the return value of curl_exec(). Without this, + // curl_exec() will revert to returning boolean values. + $this->setOpt(CURLOPT_RETURNTRANSFER, true); + } + + /** + * Download + * + * @access public + * @param $url + * @param $mixed_filename + * + * @return boolean + */ + public function download($url, $mixed_filename) + { + if (is_callable($mixed_filename)) { + $this->downloadCompleteFunction = $mixed_filename; + $fh = tmpfile(); + } else { + $filename = $mixed_filename; + $fh = fopen($filename, 'wb'); + } + + $this->setOpt(CURLOPT_FILE, $fh); + $this->get($url); + $this->downloadComplete($fh); + + return ! $this->error; + } + + /** + * Error + * + * @access public + * @param $callback + */ + public function error($callback) + { + $this->errorFunction = $callback; + } + + /** + * Exec + * + * @access public + * @param $ch + * + * @return string + */ + public function exec($ch = null) + { + $this->responseCookies = array(); + if (!($ch === null)) { + $this->rawResponse = curl_multi_getcontent($ch); + } else { + $this->call($this->beforeSendFunction); + $this->rawResponse = curl_exec($this->curl); + $this->curlErrorCode = curl_errno($this->curl); + } + $this->curlErrorMessage = curl_error($this->curl); + $this->curlError = !($this->curlErrorCode === 0); + $this->httpStatusCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); + $this->httpError = in_array(floor($this->httpStatusCode / 100), array(4, 5)); + $this->error = $this->curlError || $this->httpError; + $this->errorCode = $this->error ? ($this->curlError ? $this->curlErrorCode : $this->httpStatusCode) : 0; + + // NOTE: CURLINFO_HEADER_OUT set to true is required for requestHeaders + // to not be empty (e.g. $curl->setOpt(CURLINFO_HEADER_OUT, true);). + if ($this->getOpt(CURLINFO_HEADER_OUT) === true) { + $this->requestHeaders = $this->parseRequestHeaders(curl_getinfo($this->curl, CURLINFO_HEADER_OUT)); + } + $this->responseHeaders = $this->parseResponseHeaders($this->rawResponseHeaders); + list($this->response, $this->rawResponse) = $this->parseResponse($this->responseHeaders, $this->rawResponse); + + $this->httpErrorMessage = ''; + if ($this->error) { + if (isset($this->responseHeaders['Status-Line'])) { + $this->httpErrorMessage = $this->responseHeaders['Status-Line']; + } + } + $this->errorMessage = $this->curlError ? $this->curlErrorMessage : $this->httpErrorMessage; + + if (!$this->error) { + $this->call($this->successFunction); + } else { + $this->call($this->errorFunction); + } + + $this->call($this->completeFunction); + + return $this->response; + } + + /** + * Get + * + * @access public + * @param $url + * @param $data + * + * @return string + */ + public function get($url, $data = array()) + { + if (is_array($url)) { + $data = $url; + $url = $this->baseUrl; + } + $this->setURL($url, $data); + $this->setOpt(CURLOPT_CUSTOMREQUEST, 'GET'); + $this->setOpt(CURLOPT_HTTPGET, true); + return $this->exec(); + } + + /** + * Get Opt + * + * @access public + * @param $option + * + * @return mixed + */ + public function getOpt($option) + { + return $this->options[$option]; + } + + /** + * Head + * + * @access public + * @param $url + * @param $data + * + * @return string + */ + public function head($url, $data = array()) + { + if (is_array($url)) { + $data = $url; + $url = $this->baseUrl; + } + $this->setURL($url, $data); + $this->setOpt(CURLOPT_CUSTOMREQUEST, 'HEAD'); + $this->setOpt(CURLOPT_NOBODY, true); + return $this->exec(); + } + + /** + * Header Callback + * + * @access public + * @param $ch + * @param $header + * + * @return integer + */ + public function headerCallback($ch, $header) + { + if (preg_match('/^Set-Cookie:\s*([^=]+)=([^;]+)/mi', $header, $cookie) == 1) { + $this->responseCookies[$cookie[1]] = $cookie[2]; + } + $this->rawResponseHeaders .= $header; + return strlen($header); + } + + /** + * Options + * + * @access public + * @param $url + * @param $data + * + * @return string + */ + public function options($url, $data = array()) + { + if (is_array($url)) { + $data = $url; + $url = $this->baseUrl; + } + $this->setURL($url, $data); + $this->unsetHeader('Content-Length'); + $this->setOpt(CURLOPT_CUSTOMREQUEST, 'OPTIONS'); + return $this->exec(); + } + + /** + * Patch + * + * @access public + * @param $url + * @param $data + * + * @return string + */ + public function patch($url, $data = array()) + { + if (is_array($url)) { + $data = $url; + $url = $this->baseUrl; + } + + if (is_array($data) && empty($data)) { + $this->unsetHeader('Content-Length'); + } + + $this->setURL($url); + $this->setOpt(CURLOPT_CUSTOMREQUEST, 'PATCH'); + $this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data)); + return $this->exec(); + } + + /** + * Post + * + * @access public + * @param $url + * @param $data + * + * @return string + */ + public function post($url, $data = array()) + { + if (is_array($url)) { + $data = $url; + $url = $this->baseUrl; + } + + $this->setURL($url); + $this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST'); + $this->setOpt(CURLOPT_POST, true); + $this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data)); + return $this->exec(); + } + + /** + * Put + * + * @access public + * @param $url + * @param $data + * + * @return string + */ + public function put($url, $data = array()) + { + if (is_array($url)) { + $data = $url; + $url = $this->baseUrl; + } + $this->setURL($url); + $this->setOpt(CURLOPT_CUSTOMREQUEST, 'PUT'); + $put_data = $this->buildPostData($data); + if (empty($this->options[CURLOPT_INFILE]) && empty($this->options[CURLOPT_INFILESIZE])) { + $this->setHeader('Content-Length', strlen($put_data)); + } + $this->setOpt(CURLOPT_POSTFIELDS, $put_data); + return $this->exec(); + } + + /** + * Set Basic Authentication + * + * @access public + * @param $username + * @param $password + */ + public function setBasicAuthentication($username, $password = '') + { + $this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + $this->setOpt(CURLOPT_USERPWD, $username . ':' . $password); + } + + /** + * Set Digest Authentication + * + * @access public + * @param $username + * @param $password + */ + public function setDigestAuthentication($username, $password = '') + { + $this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + $this->setOpt(CURLOPT_USERPWD, $username . ':' . $password); + } + + /** + * Set Cookie + * + * @access public + * @param $key + * @param $value + */ + public function setCookie($key, $value) + { + $name_chars = array(); + foreach (str_split($key) as $name_char) { + if (!array_key_exists($name_char, CurlCookieConst::RFC2616())) { + $name_chars[] = rawurlencode($name_char); + } else { + $name_chars[] = $name_char; + } + } + + $value_chars = array(); + foreach (str_split($value) as $value_char) { + if (!array_key_exists($value_char, CurlCookieConst::RFC6265())) { + $value_chars[] = rawurlencode($value_char); + } else { + $value_chars[] = $value_char; + } + } + + $this->cookies[implode('', $name_chars)] = implode('', $value_chars); + $this->setOpt(CURLOPT_COOKIE, implode('; ', array_map(function($k, $v) { + return $k . '=' . $v; + }, array_keys($this->cookies), array_values($this->cookies)))); + } + + /** + * Get cookie. + * + * @access public + * @param $key + */ + public function getCookie($key) + { + return $this->getResponseCookie($key); + } + + /** + * Get response cookie. + * + * @access public + * @param $key + */ + public function getResponseCookie($key) + { + return isset($this->responseCookies[$key]) ? $this->responseCookies[$key] : null; + } + + /** + * Set Port + * + * @access public + * @param $port + */ + public function setPort($port) + { + $this->setOpt(CURLOPT_PORT, intval($port)); + } + + /** + * Set Connect Timeout + * + * @access public + * @param $seconds + */ + public function setConnectTimeout($seconds) + { + $this->setOpt(CURLOPT_CONNECTTIMEOUT, $seconds); + } + + /** + * Set Cookie File + * + * @access public + * @param $cookie_file + */ + public function setCookieFile($cookie_file) + { + $this->setOpt(CURLOPT_COOKIEFILE, $cookie_file); + } + + /** + * Set Cookie Jar + * + * @access public + * @param $cookie_jar + */ + public function setCookieJar($cookie_jar) + { + $this->setOpt(CURLOPT_COOKIEJAR, $cookie_jar); + } + + /** + * Set Default JSON Decoder + * + * @access public + */ + public function setDefaultJsonDecoder() + { + $this->jsonDecoder = function($response) { + $json_obj = json_decode($response, false); + if (!($json_obj === null)) { + $response = $json_obj; + } + return $response; + }; + } + + /** + * Set Default Timeout + * + * @access public + */ + public function setDefaultTimeout() + { + $this->setTimeout(self::DEFAULT_TIMEOUT); + } + + /** + * Set Default User Agent + * + * @access public + */ + public function setDefaultUserAgent() + { + $user_agent = 'PHP-Curl-Class/' . self::VERSION . ' (+https://github.com/php-curl-class/php-curl-class)'; + $user_agent .= ' PHP/' . PHP_VERSION; + $curl_version = curl_version(); + $user_agent .= ' curl/' . $curl_version['version']; + $this->setUserAgent($user_agent); + } + + /** + * Set Header + * + * @access public + * @param $key + * @param $value + * + * @return string + */ + public function setHeader($key, $value) + { + $this->headers[$key] = $value; + $headers = array(); + foreach ($this->headers as $key => $value) { + $headers[] = $key . ': ' . $value; + } + $this->setOpt(CURLOPT_HTTPHEADER, $headers); + } + + /** + * Set JSON Decoder + * + * @access public + * @param $function + */ + public function setJsonDecoder($function) + { + if (is_callable($function)) { + $this->jsonDecoder = $function; + } + } + + /** + * Set Opt + * + * @access public + * @param $option + * @param $value + * + * @return boolean + */ + public function setOpt($option, $value) + { + $required_options = array( + CURLOPT_RETURNTRANSFER => 'CURLOPT_RETURNTRANSFER', + ); + + if (in_array($option, array_keys($required_options), true) && !($value === true)) { + trigger_error($required_options[$option] . ' is a required option', E_USER_WARNING); + } + + $this->options[$option] = $value; + return curl_setopt($this->curl, $option, $value); + } + + /** + * Set Referer + * + * @access public + * @param $referer + */ + public function setReferer($referer) + { + $this->setReferrer($referer); + } + + /** + * Set Referrer + * + * @access public + * @param $referrer + */ + public function setReferrer($referrer) + { + $this->setOpt(CURLOPT_REFERER, $referrer); + } + + /** + * Set Timeout + * + * @access public + * @param $seconds + */ + public function setTimeout($seconds) + { + $this->setOpt(CURLOPT_TIMEOUT, $seconds); + } + + /** + * Set Url + * + * @access public + * @param $url + * @param $data + */ + public function setURL($url, $data = array()) + { + $this->baseUrl = $url; + $this->url = $this->buildURL($url, $data); + $this->setOpt(CURLOPT_URL, $this->url); + } + + /** + * Set User Agent + * + * @access public + * @param $user_agent + */ + public function setUserAgent($user_agent) + { + $this->setOpt(CURLOPT_USERAGENT, $user_agent); + } + + /** + * Success + * + * @access public + * @param $callback + */ + public function success($callback) + { + $this->successFunction = $callback; + } + + /** + * Unset Header + * + * @access public + * @param $key + */ + public function unsetHeader($key) + { + $this->setHeader($key, ''); + unset($this->headers[$key]); + } + + /** + * Verbose + * + * @access public + * @param $on + */ + public function verbose($on = true, $output=STDERR) + { + // Turn off CURLINFO_HEADER_OUT for verbose to work. This has the side + // effect of causing Curl::requestHeaders to be empty. + if ($on) { + $this->setOpt(CURLINFO_HEADER_OUT, false); + } + $this->setOpt(CURLOPT_VERBOSE, $on); + $this->setOpt(CURLOPT_STDERR, $output); + } + + /** + * Destruct + * + * @access public + */ + public function __destruct() + { + $this->close(); + } + + /** + * Build Url + * + * @access private + * @param $url + * @param $data + * + * @return string + */ + private function buildURL($url, $data = array()) + { + return $url . (empty($data) ? '' : '?' . http_build_query($data)); + } + + /** + * Parse Headers + * + * @access private + * @param $raw_headers + * + * @return array + */ + private function parseHeaders($raw_headers) + { + $raw_headers = preg_split('/\r\n/', $raw_headers, null, PREG_SPLIT_NO_EMPTY); + $http_headers = new CaseInsensitiveArray(); + + $raw_headers_count = count($raw_headers); + for ($i = 1; $i < $raw_headers_count; $i++) { + list($key, $value) = explode(':', $raw_headers[$i], 2); + $key = trim($key); + $value = trim($value); + // Use isset() as array_key_exists() and ArrayAccess are not compatible. + if (isset($http_headers[$key])) { + $http_headers[$key] .= ',' . $value; + } else { + $http_headers[$key] = $value; + } + } + + return array(isset($raw_headers['0']) ? $raw_headers['0'] : '', $http_headers); + } + + /** + * Parse Request Headers + * + * @access private + * @param $raw_headers + * + * @return array + */ + private function parseRequestHeaders($raw_headers) + { + $request_headers = new CaseInsensitiveArray(); + list($first_line, $headers) = $this->parseHeaders($raw_headers); + $request_headers['Request-Line'] = $first_line; + foreach ($headers as $key => $value) { + $request_headers[$key] = $value; + } + return $request_headers; + } + + /** + * Parse Response + * + * @access private + * @param $response_headers + * @param $raw_response + * + * @return array + */ + private function parseResponse($response_headers, $raw_response) + { + $response = $raw_response; + if (isset($response_headers['Content-Type'])) { + if (preg_match($this->jsonPattern, $response_headers['Content-Type'])) { + $json_decoder = $this->jsonDecoder; + if (is_callable($json_decoder)) { + $response = $json_decoder($response); + } + } elseif (preg_match($this->xmlPattern, $response_headers['Content-Type'])) { + $xml_obj = @simplexml_load_string($response); + if (!($xml_obj === false)) { + $response = $xml_obj; + } + } + } + + return array($response, $raw_response); + } + + /** + * Parse Response Headers + * + * @access private + * @param $raw_response_headers + * + * @return array + */ + private function parseResponseHeaders($raw_response_headers) + { + $response_header_array = explode("\r\n\r\n", $raw_response_headers); + $response_header = ''; + for ($i = count($response_header_array) - 1; $i >= 0; $i--) { + if (stripos($response_header_array[$i], 'HTTP/') === 0) { + $response_header = $response_header_array[$i]; + break; + } + } + + $response_headers = new CaseInsensitiveArray(); + list($first_line, $headers) = $this->parseHeaders($response_header); + $response_headers['Status-Line'] = $first_line; + foreach ($headers as $key => $value) { + $response_headers[$key] = $value; + } + return $response_headers; + } + + /** + * Http Build Multi Query + * + * @access public + * @param $data + * @param $key + * + * @return string + */ + public static function http_build_multi_query($data, $key = null) + { + $query = array(); + + if (empty($data)) { + return $key . '='; + } + + $is_array_assoc = self::is_array_assoc($data); + + foreach ($data as $k => $value) { + if (is_string($value) || is_numeric($value)) { + $brackets = $is_array_assoc ? '[' . $k . ']' : '[]'; + $query[] = urlencode($key === null ? $k : $key . $brackets) . '=' . rawurlencode($value); + } elseif (is_array($value)) { + $nested = $key === null ? $k : $key . '[' . $k . ']'; + $query[] = self::http_build_multi_query($value, $nested); + } + } + + return implode('&', $query); + } + + /** + * Is Array Assoc + * + * @access public + * @param $array + * + * @return boolean + */ + public static function is_array_assoc($array) + { + return (bool)count(array_filter(array_keys($array), 'is_string')); + } + + /** + * Is Array Multidim + * + * @access public + * @param $array + * + * @return boolean + */ + public static function is_array_multidim($array) + { + if (!is_array($array)) { + return false; + } + + return (bool)count(array_filter($array, 'is_array')); + } +} +?>