Files
php-curl-class/src/Curl/Curl.php
T
Zach Borboa 2e10ae416e Fix #240: Make CURLINFO_HEADER_OUT optional.
CURLINFO_HEADER_OUT is true by default, but now optional. When
CURLINFO_HEADER_OUT is not true, requestHeaders will be null.
2015-09-04 09:09:07 -07:00

1064 lines
26 KiB
PHP

<?php
namespace Curl;
class Curl
{
const VERSION = '4.7.0';
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".
} elseif (is_string($value) && strpos($value, '@') === 0) {
$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)
{
$this->cookies[$key] = $value;
$this->setOpt(CURLOPT_COOKIE, str_replace(' ', '%20', urldecode(http_build_query($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)
{
$this->setOpt(CURLOPT_VERBOSE, $on);
}
/**
* 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'));
}
}
class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
{
private $container = array();
public function offsetSet($offset, $value)
{
if ($offset === null) {
$this->container[] = $value;
} else {
$index = array_search(strtolower($offset), array_keys(array_change_key_case($this->container, CASE_LOWER)));
if (!($index === false)) {
$keys = array_keys($this->container);
unset($this->container[$keys[$index]]);
}
$this->container[$offset] = $value;
}
}
public function offsetExists($offset)
{
return array_key_exists(strtolower($offset), array_change_key_case($this->container, CASE_LOWER));
}
public function offsetUnset($offset)
{
unset($this->container[$offset]);
}
public function offsetGet($offset)
{
$index = array_search(strtolower($offset), array_keys(array_change_key_case($this->container, CASE_LOWER)));
if ($index === false) {
return null;
}
$values = array_values($this->container);
return $values[$index];
}
public function count()
{
return count($this->container);
}
public function current()
{
return current($this->container);
}
public function next()
{
return next($this->container);
}
public function key()
{
return key($this->container);
}
public function valid()
{
return !($this->current() === false);
}
public function rewind()
{
reset($this->container);
}
}