Files
php-curl-class/src/Curl/Curl.php
T
Zach Borboa 45a7b1b671 Add methods like Curl::setGet() for each HTTP request method (#936)
* Adding methods like Curl::setGet() for each HTTP request method

Adding these methods to separate the request initialization from the
execution.

Methods added:
Curl::setDelete()
Curl::setGet()
Curl::setHead()
Curl::setOptions()
Curl::setPatch()
Curl::setPost()
Curl::setPut()
Curl::setSearch()

* Fix loading local test servers

Error message:
    +++ declare -A pids
    run_phpunit.sh: line 77: declare: -A: invalid option
    declare: usage: declare [-afFirtx] [-p] [name[=value] ...]

Error message:
    +++ server_count=7
    +++ pids=()
    run_phpunit.sh: line 85: "7" - 1: syntax error: operand expected (error token is ""7" - 1")

* Clean up
2025-03-23 05:00:39 -04:00

2276 lines
71 KiB
PHP

<?php
declare(strict_types=1);
namespace Curl;
class Curl extends BaseCurl
{
public const VERSION = '11.0.5';
public const DEFAULT_TIMEOUT = 30;
public $curl = null;
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 $url = null;
public $requestHeaders = [];
public $responseHeaders = null;
public $rawResponseHeaders = '';
public $responseCookies = [];
public $response = null;
public $rawResponse = null;
public $downloadCompleteCallback = null;
public $fileHandle = null;
public $downloadFileName = null;
public $attempts = 0;
public $retries = 0;
public $childOfMultiCurl = false;
public $remainingRetries = 0;
public $retryDecider = null;
public $jsonDecoder = null;
public $xmlDecoder = null;
private $headerCallbackData;
private $cookies = [];
private $headers = [];
private $jsonDecoderArgs = [];
private $jsonPattern = '/^(?:application|text)\/(?:[a-z]+(?:[\.-][0-9a-z]+){0,}[\+\.]|x-)?json(?:-[a-z]+)?/i';
private $xmlDecoderArgs = [];
private $xmlPattern = '~^(?:text/|application/(?:atom\+|rss\+|soap\+)?)xml~i';
private $defaultDecoder = null;
public static $RFC2616 = [
// RFC 2616: "any CHAR except CTLs or separators".
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)>
// separators = "(" | ")" | "<" | ">" | "@"
// | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "="
// | "{" | "}" | SP | HT
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
// <"> = <US-ASCII double-quote mark (34)>
'!', '#', '$', '%', '&', "'", '*', '+', '-', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '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', '^', '_', '`', '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', '|', '~',
];
public static $RFC6265 = [
// RFC 6265: "US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash".
// %x21
'!',
// %x23-2B
'#', '$', '%', '&', "'", '(', ')', '*', '+',
// %x2D-3A
'-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':',
// %x3C-5B
'<', '=', '>', '?', '@', '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', '{', '|', '}', '~',
];
public $curlErrorCodeConstant;
public $curlErrorCodeConstants;
private static $deferredProperties = [
'curlErrorCodeConstant',
'curlErrorCodeConstants',
'curlOptionCodeConstants',
'effectiveUrl',
'rfc2616',
'rfc6265',
'totalTime',
];
private array $deferredValues = [];
/**
* Construct
*
* @param $base_url
* @param mixed $options
* @throws \ErrorException
*/
public function __construct($base_url = null, $options = [])
{
if (!extension_loaded('curl')) {
throw new \ErrorException('cURL library is not loaded');
}
unset($this->deferredValues['curlErrorCodeConstant']);
unset($this->deferredValues['curlErrorCodeConstants']);
unset($this->deferredValues['curlOptionCodeConstants']);
unset($this->deferredValues['effectiveUrl']);
unset($this->deferredValues['rfc2616']);
unset($this->deferredValues['rfc6265']);
unset($this->deferredValues['totalTime']);
$this->curl = curl_init();
$this->initialize($base_url, $options);
}
/**
* Build Post Data
*
* @param $data
* @return array|string
* @throws \ErrorException
*/
public function buildPostData($data)
{
$binary_data = false;
// Return JSON-encoded string when the request's content-type is JSON and the data is serializable.
if (
isset($this->headers['Content-Type']) &&
preg_match($this->jsonPattern, $this->headers['Content-Type']) &&
(
is_array($data) ||
(
is_object($data) &&
interface_exists('JsonSerializable', false) &&
$data instanceof \JsonSerializable
)
)
) {
$data = \Curl\Encoder::encodeJson($data);
} elseif (is_array($data)) {
// Manually build a single-dimensional array from a multi-dimensional array as using curl_setopt($ch,
// CURLOPT_POSTFIELDS, $data) doesn't correctly handle multi-dimensional arrays when files are
// referenced.
if (ArrayUtil::isArrayMultidim($data)) {
$data = ArrayUtil::arrayFlattenMultidim($data);
}
// Modify array values to ensure any referenced files are properly handled depending on the support of
// the @filename API or CURLFile usage. This also fixes the warning "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.
foreach ($data as $key => $value) {
if (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;
} elseif ($value instanceof \CURLStringFile) {
$binary_data = true;
}
}
}
if (
!$binary_data &&
(is_array($data) || is_object($data)) &&
(
!isset($this->headers['Content-Type']) ||
!preg_match('/^multipart\/form-data/', $this->headers['Content-Type'])
)
) {
// Avoid using http_build_query() as keys with null values are
// unexpectedly excluded from the resulting string.
//
// $ php -a
// php > echo http_build_query(['a' => '1', 'b' => null, 'c' => '3']);
// a=1&c=3
// php > echo http_build_query(['a' => '1', 'b' => '', 'c' => '3']);
// a=1&b=&c=3
//
// $data = http_build_query($data, '', '&');
$data = implode('&', array_map(function ($k, $v) {
// Encode keys and values using urlencode() to match the default
// behavior http_build_query() where $encoding_type is
// PHP_QUERY_RFC1738.
//
// Use strval() as urlencode() expects a string parameter:
// TypeError: urlencode() expects parameter 1 to be string, integer given
// TypeError: urlencode() expects parameter 1 to be string, null given
//
// php_raw_url_encode()
// php_url_encode()
// https://github.com/php/php-src/blob/master/ext/standard/http.c
return urlencode(strval($k)) . '=' . urlencode(strval($v));
}, array_keys((array)$data), array_values((array)$data)));
}
return $data;
}
/**
* Call
*/
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
*/
#[\Override]
public function close()
{
if (is_resource($this->curl) || $this->curl instanceof \CurlHandle) {
curl_close($this->curl);
}
$this->curl = null;
$this->options = null;
$this->userSetOptions = null;
$this->jsonDecoder = null;
$this->jsonDecoderArgs = null;
$this->xmlDecoder = null;
$this->xmlDecoderArgs = null;
$this->headerCallbackData = null;
$this->defaultDecoder = null;
}
/**
* Progress
*
* @param $callback callable|null
*/
public function progress($callback)
{
$this->setOpt(CURLOPT_PROGRESSFUNCTION, $callback);
$this->setOpt(CURLOPT_NOPROGRESS, false);
}
private function progressInternal($callback)
{
$this->setOptInternal(CURLOPT_PROGRESSFUNCTION, $callback);
$this->setOptInternal(CURLOPT_NOPROGRESS, false);
}
/**
* Delete
*
* @param $url
* @param $query_parameters
* @param $data
* @return mixed Returns the value provided by exec.
*/
public function delete($url, $query_parameters = [], $data = [])
{
$this->setDelete($url, $query_parameters, $data);
return $this->exec();
}
public function setDelete($url, $query_parameters = [], $data = [])
{
if (is_array($url)) {
$data = $query_parameters;
$query_parameters = $url;
$url = (string)$this->url;
}
$this->setUrl($url, $query_parameters);
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'DELETE');
// Avoid including a content-length header in DELETE requests unless there is a message body. The following
// would include "Content-Length: 0" in the request header:
// curl_setopt($ch, CURLOPT_POSTFIELDS, []);
// RFC 2616 4.3 Message Body:
// The presence of a message-body in a request is signaled by the
// inclusion of a Content-Length or Transfer-Encoding header field in
// the request's message-headers.
if (!empty($data)) {
$this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data));
}
}
/**
* Download
*
* @param $url
* @param $mixed_filename
* @return bool
*/
public function download($url, $mixed_filename)
{
// Use tmpfile() or php://temp to avoid "Too many open files" error.
if (is_callable($mixed_filename)) {
$this->downloadCompleteCallback = $mixed_filename;
$this->downloadFileName = null;
$this->fileHandle = tmpfile();
} else {
$filename = $mixed_filename;
// Use a temporary file when downloading. Not using a temporary file can cause an error when an existing
// file has already fully completed downloading and a new download is started with the same destination save
// path. The download request will include header "Range: bytes=$filesize-" which is syntactically valid,
// but unsatisfiable.
$download_filename = $filename . '.pccdownload';
$this->downloadFileName = $download_filename;
// Attempt to resume download only when a temporary download file exists and is not empty.
if (is_file($download_filename) && $filesize = filesize($download_filename)) {
$first_byte_position = $filesize;
$range = (string)$first_byte_position . '-';
$this->setRange($range);
$this->fileHandle = fopen($download_filename, 'ab');
} else {
$this->fileHandle = fopen($download_filename, 'wb');
}
// Move the downloaded temporary file to the destination save path.
$this->downloadCompleteCallback = function ($instance, $fh) use ($download_filename, $filename) {
// Close the open file handle before renaming the file.
if (is_resource($fh)) {
fclose($fh);
}
rename($download_filename, $filename);
};
}
$this->setFile($this->fileHandle);
$this->get($url);
return ! $this->error;
}
/**
* Fast download
*
* @param $url
* @param $filename
* @param $connections
* @return bool
*/
public function fastDownload($url, $filename, $connections = 4)
{
// Retrieve content length from the "Content-Length" header from the url
// to download. Use an HTTP GET request without a body instead of a HEAD
// request because not all hosts support HEAD requests.
$curl = new Curl();
$curl->setOptInternal(CURLOPT_NOBODY, true);
// Pass user-specified options to the instance checking for content-length.
$curl->setOpts($this->userSetOptions);
$curl->get($url);
// Exit early when an error occurred.
if ($curl->error) {
return false;
}
$content_length = $curl->responseHeaders['Content-Length'] ?? null;
// Use a regular download when content length could not be determined.
if (!$content_length) {
return $this->download($url, $filename);
}
// Divide chunk_size across the number of connections.
$chunk_size = (int)ceil($content_length / $connections);
// Keep track of file name parts.
$part_file_names = [];
$multi_curl = new MultiCurl();
$multi_curl->setConcurrency($connections);
for ($part_number = 1; $part_number <= $connections; $part_number++) {
$range_start = ($part_number - 1) * $chunk_size;
$range_end = $range_start + $chunk_size - 1;
if ($part_number === $connections) {
$range_end = '';
}
$range = (string)$range_start . '-' . (string)$range_end;
$part_file_name = $filename . '.part' . (string)$part_number;
// Save the file name of this part.
$part_file_names[] = $part_file_name;
// Remove any existing file part.
if (is_file($part_file_name)) {
unlink($part_file_name);
}
// Create file part.
$file_handle = tmpfile();
// Setup the instance downloading a part.
$curl = new Curl();
$curl->setUrl($url);
// Pass user-specified options to the instance downloading a part.
$curl->setOpts($this->userSetOptions);
$curl->setOptInternal(CURLOPT_CUSTOMREQUEST, 'GET');
$curl->setOptInternal(CURLOPT_HTTPGET, true);
$curl->setRangeInternal($range);
$curl->setFileInternal($file_handle);
$curl->fileHandle = $file_handle;
$curl->downloadCompleteCallback = function ($instance, $tmpfile) use ($part_file_name) {
$fh = fopen($part_file_name, 'wb');
if ($fh !== false) {
stream_copy_to_stream($tmpfile, $fh);
fclose($fh);
}
};
$multi_curl->addCurl($curl);
}
// Start the simultaneous downloads for each of the ranges in parallel.
$multi_curl->start();
// Remove existing download file name at destination.
if (is_file($filename)) {
unlink($filename);
}
// Combine downloaded chunks into a single file.
$main_file_handle = fopen($filename, 'w');
if ($main_file_handle === false) {
return false;
}
foreach ($part_file_names as $part_file_name) {
if (!is_file($part_file_name)) {
return false;
}
$file_handle = fopen($part_file_name, 'r');
if ($file_handle === false) {
return false;
}
stream_copy_to_stream($file_handle, $main_file_handle);
fclose($file_handle);
unlink($part_file_name);
}
fclose($main_file_handle);
return true;
}
/**
* Exec
*
* @param $ch
* @return mixed Returns the value provided by parseResponse.
*/
public function exec($ch = null)
{
$this->attempts += 1;
if ($this->jsonDecoder === null) {
$this->setDefaultJsonDecoder();
}
if ($this->xmlDecoder === null) {
$this->setDefaultXmlDecoder();
}
if ($ch === null) {
$this->responseCookies = [];
$this->call($this->beforeSendCallback);
$this->rawResponse = curl_exec($this->curl);
$this->curlErrorCode = curl_errno($this->curl);
$this->curlErrorMessage = curl_error($this->curl);
} else {
$this->rawResponse = curl_multi_getcontent($ch);
$this->curlErrorMessage = curl_error($ch);
}
$this->curlError = $this->curlErrorCode !== 0;
// Ensure Curl::rawResponse is a string as curl_exec() can return false.
// Without this, calling strlen($curl->rawResponse) will error when the
// strict types setting is enabled.
if (!is_string($this->rawResponse)) {
$this->rawResponse = '';
}
// Transfer the header callback data and release the temporary store to avoid memory leak.
$this->rawResponseHeaders = $this->headerCallbackData->rawResponseHeaders;
$this->responseCookies = $this->headerCallbackData->responseCookies;
$this->headerCallbackData->rawResponseHeaders = '';
$this->headerCallbackData->responseCookies = [];
$this->headerCallbackData->stopRequestDecider = null;
$this->headerCallbackData->stopRequest = false;
// Include additional error code information in error message when possible.
if ($this->curlError) {
$curl_error_message = curl_strerror($this->curlErrorCode);
if (isset($this->curlErrorCodeConstant)) {
$curl_error_message .= ' (' . $this->curlErrorCodeConstant . ')';
}
if (!empty($this->curlErrorMessage)) {
$curl_error_message .= ': ' . $this->curlErrorMessage;
}
$this->curlErrorMessage = $curl_error_message;
}
// 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($this->getInfo(CURLINFO_HEADER_OUT));
}
$this->responseHeaders = $this->parseResponseHeaders($this->rawResponseHeaders);
$this->response = $this->parseResponse($this->responseHeaders, $this->rawResponse);
$this->httpStatusCode = $this->getInfo(CURLINFO_HTTP_CODE);
$this->httpError = in_array((int) floor($this->httpStatusCode / 100), [4, 5], true);
$this->error = $this->curlError || $this->httpError;
$this->call($this->afterSendCallback);
if (!in_array($this->error, [true, false], true)) {
trigger_error('$instance->error MUST be set to true or false', E_USER_WARNING);
}
$this->errorCode = $this->error ? ($this->curlError ? $this->curlErrorCode : $this->httpStatusCode) : 0;
$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;
// Reset select deferred properties so that they may be recalculated.
unset($this->deferredValues['curlErrorCodeConstant']);
unset($this->deferredValues['effectiveUrl']);
unset($this->deferredValues['totalTime']);
// Reset content-length header possibly set from a PUT or SEARCH request.
$this->unsetHeader('Content-Length');
// Reset nobody setting possibly set from a HEAD request.
$this->setOptInternal(CURLOPT_NOBODY, false);
// Allow multicurl to attempt retry as needed.
if ($this->isChildOfMultiCurl()) {
return;
}
if ($this->attemptRetry()) {
return $this->exec($ch);
}
$this->execDone();
return $this->response;
}
public function execDone()
{
if ($this->error) {
$this->call($this->errorCallback);
} else {
$this->call($this->successCallback);
}
$this->call($this->completeCallback);
// Close open file handles and reset the curl instance.
if ($this->fileHandle !== null) {
$this->downloadComplete($this->fileHandle);
}
}
/**
* Get
*
* @param $url
* @param $data
* @return mixed Returns the value provided by exec.
*/
public function get($url, $data = [])
{
$this->setGet($url, $data);
return $this->exec();
}
public function setGet($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = (string)$this->url;
}
$this->setUrl($url, $data);
$this->setOptInternal(CURLOPT_CUSTOMREQUEST, 'GET');
$this->setOptInternal(CURLOPT_HTTPGET, true);
}
/**
* Get Info
*
* @param $opt
* @return mixed
*/
public function getInfo($opt = null)
{
$args = [];
$args[] = $this->curl;
if (func_num_args()) {
$args[] = $opt;
}
return call_user_func_array('curl_getinfo', $args);
}
/**
* Head
*
* @param $url
* @param $data
* @return mixed Returns the value provided by exec.
*/
public function head($url, $data = [])
{
$this->setHead($url, $data);
return $this->exec();
}
public function setHead($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = (string)$this->url;
}
$this->setUrl($url, $data);
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'HEAD');
$this->setOpt(CURLOPT_NOBODY, true);
}
/**
* Options
*
* @param $url
* @param $data
* @return mixed Returns the value provided by exec.
*/
public function options($url, $data = [])
{
$this->setOptions($url, $data);
return $this->exec();
}
public function setOptions($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = (string)$this->url;
}
$this->setUrl($url, $data);
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'OPTIONS');
}
/**
* Patch
*
* @param $url
* @param $data
* @return mixed Returns the value provided by exec.
*/
public function patch($url, $data = [])
{
$this->setPatch($url, $data);
return $this->exec();
}
public function setPatch($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = (string)$this->url;
}
if (is_array($data) && empty($data)) {
$this->removeHeader('Content-Length');
}
$this->setUrl($url);
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'PATCH');
$this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data));
}
/**
* Post
*
* @param $url
* @param $data
* @param $follow_303_with_post
* If true, will cause 303 redirections to be followed using a POST request
* (default: false).
* Notes:
* - Redirections are only followed if the CURLOPT_FOLLOWLOCATION option is set
* to true.
* - According to the HTTP specs (see [1]), a 303 redirection should be followed
* using the GET method. 301 and 302 must not.
* - In order to force a 303 redirection to be performed using the same method,
* the underlying cURL object must be set in a special state (the
* CURLOPT_CUSTOMREQUEST option must be set to the method to use after the
* redirection). Due to a limitation of the cURL extension of PHP < 5.5.11 ([2],
* [3]), it is not possible to reset this option. Using these PHP engines, it is
* therefore impossible to restore this behavior on an existing php-curl-class
* Curl object.
* @return mixed Returns the value provided by exec.
*
* [1] https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.2
* [2] https://github.com/php/php-src/pull/531
* [3] http://php.net/ChangeLog-5.php#5.5.11
*/
public function post($url, $data = '', $follow_303_with_post = false)
{
$this->setPost($url, $data, $follow_303_with_post);
return $this->exec();
}
public function setPost($url, $data = '', $follow_303_with_post = false)
{
if (is_array($url)) {
$follow_303_with_post = (bool)$data;
$data = $url;
$url = (string)$this->url;
}
$this->setUrl($url);
// Set the request method to "POST" when following a 303 redirect with
// an additional POST request is desired. This is equivalent to setting
// the -X, --request command line option where curl won't change the
// request method according to the HTTP 30x response code.
if ($follow_303_with_post) {
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
} elseif (isset($this->options[CURLOPT_CUSTOMREQUEST])) {
// Unset the CURLOPT_CUSTOMREQUEST option so that curl does not use
// a POST request after a post/redirect/get redirection. Without
// this, curl will use the method string specified for all requests.
$this->setOpt(CURLOPT_CUSTOMREQUEST, null);
}
$this->setOpt(CURLOPT_POST, true);
$this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data));
}
/**
* Put
*
* @param $url
* @param $data
* @return mixed Returns the value provided by exec.
*/
public function put($url, $data = [])
{
$this->setPut($url, $data);
return $this->exec();
}
public function setPut($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = (string)$this->url;
}
$this->setUrl($url);
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'PUT');
$put_data = $this->buildPostData($data);
if (empty($this->options[CURLOPT_INFILE]) && empty($this->options[CURLOPT_INFILESIZE])) {
if (is_string($put_data)) {
$this->setHeader('Content-Length', strlen($put_data));
}
}
if (!empty($put_data)) {
$this->setOpt(CURLOPT_POSTFIELDS, $put_data);
}
}
/**
* Search
*
* @param $url
* @param $data
* @return mixed Returns the value provided by exec.
*/
public function search($url, $data = [])
{
$this->setSearch($url, $data);
return $this->exec();
}
public function setSearch($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = (string)$this->url;
}
$this->setUrl($url);
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'SEARCH');
$put_data = $this->buildPostData($data);
if (empty($this->options[CURLOPT_INFILE]) && empty($this->options[CURLOPT_INFILESIZE])) {
if (is_string($put_data)) {
$this->setHeader('Content-Length', strlen($put_data));
}
}
if (!empty($put_data)) {
$this->setOpt(CURLOPT_POSTFIELDS, $put_data);
}
}
/**
* Set Cookie
*
* @param $key
* @param $value
*/
#[\Override]
public function setCookie($key, $value)
{
$this->setEncodedCookie($key, $value);
$this->buildCookies();
}
/**
* Set Cookies
*
* @param $cookies
*/
#[\Override]
public function setCookies($cookies)
{
foreach ($cookies as $key => $value) {
$this->setEncodedCookie($key, $value);
}
$this->buildCookies();
}
/**
* Get Cookie
*
* @param $key
* @return mixed
*/
public function getCookie($key)
{
return $this->getResponseCookie($key);
}
/**
* Get Response Cookie
*
* @param $key
* @return mixed
*/
public function getResponseCookie($key)
{
return $this->responseCookies[$key] ?? null;
}
/**
* Set Max Filesize
*
* @param $bytes
*/
public function setMaxFilesize($bytes)
{
$callback = function ($resource, $download_size, $downloaded, $upload_size, $uploaded) use ($bytes) {
// Abort the transfer when $downloaded bytes exceeds maximum $bytes by returning a non-zero value.
return $downloaded > $bytes ? 1 : 0;
};
$this->progress($callback);
}
/**
* Set Cookie String
*
* @param $string
* @return bool
*/
#[\Override]
public function setCookieString($string)
{
return $this->setOpt(CURLOPT_COOKIE, $string);
}
/**
* Set Cookie File
*
* @param $cookie_file
* @return bool
*/
#[\Override]
public function setCookieFile($cookie_file)
{
return $this->setOpt(CURLOPT_COOKIEFILE, $cookie_file);
}
/**
* Set Cookie Jar
*
* @param $cookie_jar
* @return bool
*/
#[\Override]
public function setCookieJar($cookie_jar)
{
return $this->setOpt(CURLOPT_COOKIEJAR, $cookie_jar);
}
/**
* Set Default JSON Decoder
*
* @param $assoc
* @param $depth
* @param $options
*/
public function setDefaultJsonDecoder()
{
$this->jsonDecoder = '\Curl\Decoder::decodeJson';
$this->jsonDecoderArgs = func_get_args();
}
/**
* Set Default XML Decoder
*
* @param $class_name
* @param $options
* @param $ns
* @param $is_prefix
*/
public function setDefaultXmlDecoder()
{
$this->xmlDecoder = '\Curl\Decoder::decodeXml';
$this->xmlDecoderArgs = func_get_args();
}
/**
* Set Default Decoder
*
* @param $mixed boolean|callable|string
*/
public function setDefaultDecoder($mixed = 'json')
{
if ($mixed === false) {
$this->defaultDecoder = false;
} elseif ($mixed === 'json') {
$this->defaultDecoder = '\Curl\Decoder::decodeJson';
} elseif ($mixed === 'xml') {
$this->defaultDecoder = '\Curl\Decoder::decodeXml';
} elseif (is_callable($mixed)) {
$this->defaultDecoder = $mixed;
}
}
/**
* Set Default Header Out
*/
public function setDefaultHeaderOut()
{
$this->setOpt(CURLINFO_HEADER_OUT, true);
}
private function setDefaultHeaderOutInternal()
{
$this->setOptInternal(CURLINFO_HEADER_OUT, true);
}
/**
* Set Default Timeout
*/
public function setDefaultTimeout()
{
$this->setTimeout(self::DEFAULT_TIMEOUT);
}
private function setDefaultTimeoutInternal()
{
$this->setTimeoutInternal(self::DEFAULT_TIMEOUT);
}
/**
* Set Default User Agent
*/
public function setDefaultUserAgent()
{
$this->setUserAgent($this->getDefaultUserAgent());
}
private function setDefaultUserAgentInternal()
{
$this->setUserAgentInternal($this->getDefaultUserAgent());
}
private function getDefaultUserAgent()
{
$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'];
return $user_agent;
}
/**
* Set Header
*
* Add extra header to include in the request.
*
* @param $key
* @param $value
*/
#[\Override]
public function setHeader($key, $value)
{
$this->headers[$key] = $value;
$headers = [];
foreach ($this->headers as $key => $value) {
$headers[] = $key . ': ' . $value;
}
$this->setOpt(CURLOPT_HTTPHEADER, $headers);
}
/**
* Set Headers
*
* Add extra headers to include in the request.
*
* @param $headers
*/
#[\Override]
public function setHeaders($headers)
{
if (ArrayUtil::isArrayAssoc($headers)) {
foreach ($headers as $key => $value) {
$key = trim($key);
$value = trim($value);
$this->headers[$key] = $value;
}
} else {
foreach ($headers as $header) {
list($key, $value) = array_pad(explode(':', $header, 2), 2, '');
$key = trim($key);
$value = trim($value);
$this->headers[$key] = $value;
}
}
$headers = [];
foreach ($this->headers as $key => $value) {
$headers[] = $key . ': ' . $value;
}
$this->setOpt(CURLOPT_HTTPHEADER, $headers);
}
/**
* Set JSON Decoder
*
* @param $mixed boolean|callable
*/
#[\Override]
public function setJsonDecoder($mixed)
{
if ($mixed === false || is_callable($mixed)) {
$this->jsonDecoder = $mixed;
$this->jsonDecoderArgs = [];
}
}
/**
* Set XML Decoder
*
* @param $mixed boolean|callable
*/
#[\Override]
public function setXmlDecoder($mixed)
{
if ($mixed === false || is_callable($mixed)) {
$this->xmlDecoder = $mixed;
$this->xmlDecoderArgs = [];
}
}
/**
* Set Opt
*
* @param $option
* @param $value
* @return bool
*/
#[\Override]
public function setOpt($option, $value)
{
$required_options = [
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);
}
$success = curl_setopt($this->curl, $option, $value);
if ($success) {
$this->options[$option] = $value;
$this->userSetOptions[$option] = $value;
}
return $success;
}
/**
* Set Opt Internal
*
* @param $option
* @param $value
* @return bool
*/
#[\Override]
protected function setOptInternal($option, $value)
{
$success = curl_setopt($this->curl, $option, $value);
if ($success) {
$this->options[$option] = $value;
}
return $success;
}
/**
* Set Opts
*
* @param $options
* @return bool
* Returns true if all options were successfully set. If an
* option could not be successfully set, false is immediately
* returned, ignoring any future options in the options array.
* Similar to curl_setopt_array().
*/
#[\Override]
public function setOpts($options)
{
if (!count($options)) {
return true;
}
foreach ($options as $option => $value) {
if (!$this->setOpt($option, $value)) {
return false;
}
}
return true;
}
/**
* Set Protocols
*
* Limit what protocols libcurl will accept for a request.
*
* @param $protocols
* @see Curl::setRedirectProtocols()
*/
public function setProtocols($protocols)
{
$this->setOpt(CURLOPT_PROTOCOLS, $protocols);
}
private function setProtocolsInternal($protocols)
{
$this->setOptInternal(CURLOPT_PROTOCOLS, $protocols);
}
/**
* Set Retry
*
* Number of retries to attempt or decider callable.
*
* When using a number of retries to attempt, the maximum number of attempts
* for the request is $maximum_number_of_retries + 1.
*
* When using a callable decider, the request will be retried until the
* function returns a value which evaluates to false.
*
* @param $mixed
*/
#[\Override]
public function setRetry($mixed)
{
if (is_callable($mixed)) {
$this->retryDecider = $mixed;
} elseif (is_int($mixed)) {
$maximum_number_of_retries = $mixed;
$this->remainingRetries = $maximum_number_of_retries;
}
}
/**
* Set Redirect Protocols
*
* Limit what protocols libcurl will accept when following a redirect.
*
* @param $redirect_protocols
* @see Curl::setProtocols()
*/
public function setRedirectProtocols($redirect_protocols)
{
$this->setOpt(CURLOPT_REDIR_PROTOCOLS, $redirect_protocols);
}
private function setRedirectProtocolsInternal($redirect_protocols)
{
$this->setOptInternal(CURLOPT_REDIR_PROTOCOLS, $redirect_protocols);
}
/**
* Set Url
*
* @param $url
* @param $mixed_data
*/
#[\Override]
public function setUrl($url, $mixed_data = '')
{
$built_url = Url::buildUrl($url, $mixed_data);
if ($this->url === null) {
$this->url = (string)new Url($built_url);
} else {
$this->url = (string)new Url($this->url, $built_url);
}
$this->setOpt(CURLOPT_URL, $this->url);
}
/**
* Attempt Retry
*/
public function attemptRetry()
{
$attempt_retry = false;
if ($this->error) {
if ($this->retryDecider === null) {
$attempt_retry = $this->remainingRetries >= 1;
} else {
$attempt_retry = call_user_func($this->retryDecider, $this);
}
if ($attempt_retry) {
$this->retries += 1;
if ($this->remainingRetries) {
$this->remainingRetries -= 1;
}
}
}
return $attempt_retry;
}
/**
* Unset Header
*
* Remove extra header previously set using Curl::setHeader().
*
* @param $key
*/
#[\Override]
public function unsetHeader($key)
{
unset($this->headers[$key]);
$headers = [];
foreach ($this->headers as $key => $value) {
$headers[] = $key . ': ' . $value;
}
$this->setOpt(CURLOPT_HTTPHEADER, $headers);
}
/**
* Diagnose
*
* @param bool $return
*/
public function diagnose($return = false)
{
if ($return) {
ob_start();
}
echo "\n";
echo '--- Begin PHP Curl Class diagnostic output ---' . "\n";
echo 'PHP Curl Class version: ' . self::VERSION . "\n";
echo 'PHP version: ' . PHP_VERSION . "\n";
$curl_version = curl_version();
echo 'Curl version: ' . $curl_version['version'] . "\n";
if ($this->attempts === 0) {
echo 'No HTTP requests have been made.' . "\n";
} else {
$request_types = [
'DELETE' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'DELETE',
'GET' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'GET' || $this->getOpt(CURLOPT_HTTPGET),
'HEAD' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'HEAD',
'OPTIONS' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'OPTIONS',
'PATCH' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'PATCH',
'POST' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'POST' || $this->getOpt(CURLOPT_POST),
'PUT' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'PUT',
'SEARCH' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'SEARCH',
];
$request_method = '';
foreach ($request_types as $http_method_name => $http_method_used) {
if ($http_method_used) {
$request_method = $http_method_name;
break;
}
}
$request_url = $this->getOpt(CURLOPT_URL);
$request_options_count = count($this->options);
$request_headers_count = count($this->requestHeaders);
$request_body_empty = empty($this->getOpt(CURLOPT_POSTFIELDS));
$response_header_length = $this->responseHeaders['Content-Length'] ?? '(not specified in response header)';
$response_calculated_length = is_string($this->rawResponse) ?
strlen($this->rawResponse) : '(' . var_export($this->rawResponse, true) . ')';
$response_headers_count = count($this->responseHeaders);
echo
'Request contained ' . (string)$request_options_count . ' ' . (
$request_options_count === 1 ? 'option:' : 'options:'
) . "\n";
if ($request_options_count) {
$i = 1;
foreach ($this->options as $option => $value) {
echo ' ' . (string)$i . ' ';
$this->displayCurlOptionValue($option, $value);
$i += 1;
}
}
echo
'Sent an HTTP ' . $request_method . ' request to "' . $request_url . '".' . "\n" .
'Request contained ' . (string)$request_headers_count . ' ' . (
$request_headers_count === 1 ? 'header:' : 'headers:'
) . "\n";
if ($request_headers_count) {
$i = 1;
foreach ($this->requestHeaders as $key => $value) {
echo ' ' . (string)$i . ' ' . $key . ': ' . $value . "\n";
$i += 1;
}
}
echo 'Request contained ' . ($request_body_empty ? 'no body' : 'a body') . '.' . "\n";
if (
$request_headers_count === 0 && (
$this->getOpt(CURLOPT_VERBOSE) ||
!$this->getOpt(CURLINFO_HEADER_OUT)
)
) {
echo
'Warning: Request headers (Curl::requestHeaders) are expected to be empty ' .
'(CURLOPT_VERBOSE was enabled or CURLINFO_HEADER_OUT was disabled).' . "\n";
}
if (isset($this->responseHeaders['allow'])) {
$allowed_request_types = array_map(function ($v) {
return trim($v);
}, explode(',', strtoupper($this->responseHeaders['allow'])));
foreach ($request_types as $http_method_name => $http_method_used) {
if ($http_method_used && !in_array($http_method_name, $allowed_request_types, true)) {
echo
'Warning: An HTTP ' . $http_method_name . ' request was made, but only the following ' .
'request types are allowed: ' . implode(', ', $allowed_request_types) . "\n";
}
}
}
echo
'Response contains ' . (string)$response_headers_count . ' ' . (
$response_headers_count === 1 ? 'header:' : 'headers:'
) . "\n";
if ($this->responseHeaders !== null) {
$i = 1;
foreach ($this->responseHeaders as $key => $value) {
echo ' ' . (string)$i . ' ' . $key . ': ' . $value . "\n";
$i += 1;
}
}
if (!isset($this->responseHeaders['Content-Type'])) {
echo 'Response did not set a content type.' . "\n";
} elseif (preg_match($this->jsonPattern, $this->responseHeaders['Content-Type'])) {
echo 'Response appears to be JSON.' . "\n";
} elseif (preg_match($this->xmlPattern, $this->responseHeaders['Content-Type'])) {
echo 'Response appears to be XML.' . "\n";
}
if ($this->curlError) {
echo
'A curl error (' . $this->curlErrorCode . ') occurred ' .
'with message "' . $this->curlErrorMessage . '".' . "\n";
}
if (!empty($this->httpStatusCode)) {
echo 'Received an HTTP status code of ' . $this->httpStatusCode . '.' . "\n";
}
if ($this->httpError) {
echo
'Received an HTTP ' . $this->httpStatusCode . ' error response ' .
'with message "' . $this->httpErrorMessage . '".' . "\n";
}
if ($this->rawResponse === null) {
echo 'Received no response body (response=null).' . "\n";
} elseif ($this->rawResponse === '') {
echo 'Received an empty response body (response="").' . "\n";
} else {
echo 'Received a non-empty response body.' . "\n";
if (isset($this->responseHeaders['Content-Length'])) {
echo 'Response content length (from content-length header): ' . $response_header_length . "\n";
} else {
echo 'Response content length (calculated): ' . (string)$response_calculated_length . "\n";
}
if (
isset($this->responseHeaders['Content-Type']) &&
preg_match($this->jsonPattern, $this->responseHeaders['Content-Type'])
) {
$parsed_response = json_decode($this->rawResponse, true);
if ($parsed_response !== null) {
$messages = [];
array_walk_recursive($parsed_response, function ($value, $key) use (&$messages) {
if (in_array($key, ['code', 'error', 'message'], true)) {
$message = $key . ': ' . $value;
$messages[] = $message;
}
});
$messages = array_unique($messages);
$messages_count = count($messages);
if ($messages_count) {
echo
'Found ' . (string)$messages_count . ' ' .
($messages_count === 1 ? 'message' : 'messages') .
' in response:' . "\n";
$i = 1;
foreach ($messages as $message) {
echo ' ' . (string)$i . ' ' . $message . "\n";
$i += 1;
}
}
}
}
}
}
echo '--- End PHP Curl Class diagnostic output ---' . "\n";
echo "\n";
if ($return) {
$output = ob_get_contents();
ob_end_clean();
return $output;
}
}
/**
* Reset
*/
public function reset()
{
if (is_resource($this->curl) || $this->curl instanceof \CurlHandle) {
curl_reset($this->curl);
} else {
$this->curl = curl_init();
}
$this->setDefaultUserAgentInternal();
$this->setDefaultTimeoutInternal();
$this->setDefaultHeaderOutInternal();
$this->initialize();
}
public function getCurl()
{
return $this->curl;
}
public function getId()
{
return $this->id;
}
public function isError()
{
return $this->error;
}
public function getErrorCode()
{
return $this->errorCode;
}
public function getErrorMessage()
{
return $this->errorMessage;
}
public function isCurlError()
{
return $this->curlError;
}
public function getCurlErrorCode()
{
return $this->curlErrorCode;
}
public function getCurlErrorMessage()
{
return $this->curlErrorMessage;
}
public function isHttpError()
{
return $this->httpError;
}
public function getHttpStatusCode()
{
return $this->httpStatusCode;
}
public function getHttpErrorMessage()
{
return $this->httpErrorMessage;
}
public function getUrl()
{
return $this->url;
}
public function getOptions()
{
return $this->options;
}
public function getUserSetOptions()
{
return $this->userSetOptions;
}
public function getRequestHeaders()
{
return $this->requestHeaders;
}
public function getResponseHeaders()
{
return $this->responseHeaders;
}
public function getRawResponseHeaders()
{
return $this->rawResponseHeaders;
}
public function getResponseCookies()
{
return $this->responseCookies;
}
public function getResponse()
{
return $this->response;
}
public function getRawResponse()
{
return $this->rawResponse;
}
public function getBeforeSendCallback()
{
return $this->beforeSendCallback;
}
public function getDownloadCompleteCallback()
{
return $this->downloadCompleteCallback;
}
public function getDownloadFileName()
{
return $this->downloadFileName;
}
public function getSuccessCallback()
{
return $this->successCallback;
}
public function getErrorCallback()
{
return $this->errorCallback;
}
public function getCompleteCallback()
{
return $this->completeCallback;
}
public function getFileHandle()
{
return $this->fileHandle;
}
public function getAttempts()
{
return $this->attempts;
}
public function getRetries()
{
return $this->retries;
}
public function isChildOfMultiCurl()
{
return $this->childOfMultiCurl;
}
public function getRemainingRetries()
{
return $this->remainingRetries;
}
public function getRetryDecider()
{
return $this->retryDecider;
}
public function getJsonDecoder()
{
return $this->jsonDecoder;
}
public function getXmlDecoder()
{
return $this->xmlDecoder;
}
/**
* Destruct
*/
public function __destruct()
{
$this->close();
}
public function __get($name)
{
if (in_array($name, self::$deferredProperties, true)) {
if (isset($this->deferredValues[$name])) {
return $this->deferredValues[$name];
} elseif (is_callable([$this, $getter = 'get' . ucfirst($name)])) {
$this->deferredValues[$name] = $this->$getter();
return $this->deferredValues[$name];
}
}
return null;
}
public function __isset($name)
{
if (in_array($name, self::$deferredProperties, true)) {
if (isset($this->deferredValues[$name])) {
return true;
} elseif (is_callable([$this, $getter = 'get' . ucfirst($name)])) {
$this->deferredValues[$name] = $this->$getter();
return true;
} else {
return false;
}
}
return isset($this->$name);
}
/**
* Get Curl Error Code Constants
*/
private function getCurlErrorCodeConstants()
{
$constants = get_defined_constants(true);
$filtered_array = array_filter(
$constants['curl'],
function ($key) {
return strpos($key, 'CURLE_') !== false;
},
ARRAY_FILTER_USE_KEY
);
$curl_const_by_code = array_flip($filtered_array);
return $curl_const_by_code;
}
/**
* Get Curl Error Code Constant
*/
private function getCurlErrorCodeConstant()
{
$curl_const_by_code = $this->curlErrorCodeConstants ?? [];
if (isset($curl_const_by_code[$this->curlErrorCode])) {
return $curl_const_by_code[$this->curlErrorCode];
}
return '';
}
/**
* Get Curl Option Code Constants
*/
private function getCurlOptionCodeConstants()
{
$constants = get_defined_constants(true);
$filtered_array = array_filter(
$constants['curl'],
function ($key) {
return strpos($key, 'CURLOPT_') !== false;
},
ARRAY_FILTER_USE_KEY
);
$curl_const_by_code = array_flip($filtered_array);
if (!isset($curl_const_by_code[CURLINFO_HEADER_OUT])) {
$curl_const_by_code[CURLINFO_HEADER_OUT] = 'CURLINFO_HEADER_OUT';
}
return $curl_const_by_code;
}
/**
* Display Curl Option Value.
*
* @param $option
* @param $value
*/
public function displayCurlOptionValue($option, $value = null)
{
if ($value === null) {
$value = $this->getOpt($option);
}
if (isset($this->curlOptionCodeConstants[$option])) {
echo $this->curlOptionCodeConstants[$option] . ':';
} else {
echo $option . ':';
}
if (is_string($value)) {
echo ' "' . $value . '"' . "\n";
} elseif (is_int($value)) {
echo ' ' . (string)$value;
$bit_flag_lookups = [
'CURLOPT_HTTPAUTH' => 'CURLAUTH_',
'CURLOPT_PROTOCOLS' => 'CURLPROTO_',
'CURLOPT_PROXYAUTH' => 'CURLAUTH_',
'CURLOPT_PROXY_SSL_OPTIONS' => 'CURLSSLOPT_',
'CURLOPT_REDIR_PROTOCOLS' => 'CURLPROTO_',
'CURLOPT_SSH_AUTH_TYPES' => 'CURLSSH_AUTH_',
'CURLOPT_SSL_OPTIONS' => 'CURLSSLOPT_',
];
if (isset($this->curlOptionCodeConstants[$option])) {
$option_name = $this->curlOptionCodeConstants[$option];
if (in_array($option_name, array_keys($bit_flag_lookups), true)) {
$curl_const_prefix = $bit_flag_lookups[$option_name];
$constants = get_defined_constants(true);
$curl_constants = array_filter(
$constants['curl'],
function ($key) use ($curl_const_prefix) {
return strpos($key, $curl_const_prefix) !== false;
},
ARRAY_FILTER_USE_KEY
);
$bit_flags = [];
foreach ($curl_constants as $const_name => $const_value) {
// Attempt to detect bit flags in use that use constants with negative values (e.g.
// CURLAUTH_ANY, CURLAUTH_ANYSAFE, CURLPROTO_ALL, CURLSSH_AUTH_ANY,
// CURLSSH_AUTH_DEFAULT, etc.)
if ($value < 0 && $value === $const_value) {
$bit_flags[] = $const_name;
break;
} elseif ($value >= 0 && $const_value >= 0 && ($value & $const_value)) {
$bit_flags[] = $const_name;
}
}
if (count($bit_flags)) {
asort($bit_flags);
echo ' (' . implode(' | ', $bit_flags) . ')';
}
}
}
echo "\n";
} elseif (is_bool($value)) {
echo ' ' . ($value ? 'true' : 'false') . "\n";
} elseif (is_array($value)) {
echo ' ';
var_dump($value);
} elseif (is_callable($value)) {
echo ' (callable)' . "\n";
} else {
echo ' ' . gettype($value) . ':' . "\n";
var_dump($value);
}
}
/**
* Get Effective Url
*/
private function getEffectiveUrl()
{
return $this->getInfo(CURLINFO_EFFECTIVE_URL);
}
/**
* Get RFC 2616
*/
private function getRfc2616()
{
return array_fill_keys(self::$RFC2616, true);
}
/**
* Get RFC 6265
*/
private function getRfc6265()
{
return array_fill_keys(self::$RFC6265, true);
}
/**
* Get Total Time
*/
private function getTotalTime()
{
return $this->getInfo(CURLINFO_TOTAL_TIME);
}
/**
* Build Cookies
*/
private function buildCookies()
{
// Avoid changing CURLOPT_COOKIE if there are no cookies set.
if (count($this->cookies)) {
// Avoid using http_build_query() as unnecessary encoding is performed.
// http_build_query($this->cookies, '', '; ');
$cookies = [];
foreach ($this->cookies as $key => $value) {
$cookies[] = $key . '=' . $value;
}
$this->setOpt(CURLOPT_COOKIE, implode('; ', $cookies));
}
}
/**
* Download Complete
*
* @param $fh
*/
private function downloadComplete($fh)
{
if ($this->error && is_file((string) $this->downloadFileName)) {
@unlink($this->downloadFileName);
} elseif (!$this->error && $this->downloadCompleteCallback) {
rewind($fh);
$this->call($this->downloadCompleteCallback, $fh);
$this->downloadCompleteCallback = 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')) {
$output = STDOUT;
} else {
$output = fopen('php://stdout', 'w');
}
// Reset CURLOPT_FILE with STDOUT to avoid: "curl_exec(): CURLOPT_FILE
// resource has gone away, resetting to default".
$this->setFile($output);
// 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);
}
/**
* Parse Headers
*
* @param $raw_headers
* @return array
*/
private function parseHeaders($raw_headers)
{
$http_headers = new CaseInsensitiveArray();
$raw_headers = preg_split('/\r\n/', (string) $raw_headers, -1, PREG_SPLIT_NO_EMPTY);
if ($raw_headers === false) {
return ['', $http_headers];
}
$raw_headers_count = count($raw_headers);
for ($i = 1; $i < $raw_headers_count; $i++) {
if (strpos($raw_headers[$i], ':') !== false) {
list($key, $value) = array_pad(explode(':', $raw_headers[$i], 2), 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 [$raw_headers['0'] ?? '', $http_headers];
}
/**
* Parse Request Headers
*
* @param $raw_headers
* @return \Curl\CaseInsensitiveArray
*/
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
*
* @param $response_headers
* @param $raw_response
* @return mixed
* If the response content-type is json: Returns the json decoder's return value: A stdClass object
* when the default json decoder is used.
*
* If the response content-type is xml: Returns the xml decoder's return value: A SimpleXMLElement
* object when the default xml decoder is used.
*
* If the response content-type is something else: Returns the original raw response unless a default
* decoder has been set.
*
* If the response content-type cannot be determined: Returns the original raw response.
*
* If the response content-encoding is gzip: Returns the response gzip-decoded.
*/
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'])) {
if ($this->jsonDecoder) {
$args = $this->jsonDecoderArgs;
array_unshift($args, $response);
$response = call_user_func_array($this->jsonDecoder, $args);
}
} elseif (preg_match($this->xmlPattern, $response_headers['Content-Type'])) {
if ($this->xmlDecoder) {
$args = $this->xmlDecoderArgs;
array_unshift($args, $response);
$response = call_user_func_array($this->xmlDecoder, $args);
}
} else {
if ($this->defaultDecoder) {
$response = call_user_func($this->defaultDecoder, $response);
}
}
}
if (
(
// Ensure that the server says the response is compressed with
// gzip and the response has not already been decoded. Use
// is_string() to ensure that $response is a string being passed
// to mb_strpos() and gzdecode(). Use extension_loaded() to
// ensure that mb_strpos() uses the mbstring extension and not a
// polyfill.
isset($response_headers['Content-Encoding']) &&
$response_headers['Content-Encoding'] === 'gzip' &&
is_string($response) &&
(
(
extension_loaded('mbstring') &&
mb_strpos($response, "\x1f" . "\x8b" . "\x08", 0, 'US-ASCII') === 0
) ||
!extension_loaded('mbstring')
)
) || (
// Or ensure that the response looks like it is compressed with
// gzip. Use is_string() to ensure that $response is a string
// being passed to mb_strpos() and gzdecode(). Use
// extension_loaded() to ensure that mb_strpos() uses the
// mbstring extension and not a polyfill.
is_string($response) &&
extension_loaded('mbstring') &&
mb_strpos($response, "\x1f" . "\x8b" . "\x08", 0, 'US-ASCII') === 0
)
) {
// Use @ to suppress message "Warning gzdecode(): data error".
$decoded_response = @gzdecode($response);
if ($decoded_response !== false) {
$response = $decoded_response;
}
}
return $response;
}
/**
* Parse Response Headers
*
* @param $raw_response_headers
* @return \Curl\CaseInsensitiveArray
*/
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 (isset($response_header_array[$i]) && 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;
}
/**
* Set Encoded Cookie
*
* @param $key
* @param $value
*/
private function setEncodedCookie($key, $value)
{
$name_chars = [];
foreach (str_split($key) as $name_char) {
if (isset($this->rfc2616[$name_char])) {
$name_chars[] = $name_char;
} else {
$name_chars[] = rawurlencode($name_char);
}
}
$value_chars = [];
foreach (str_split($value) as $value_char) {
if (isset($this->rfc6265[$value_char])) {
$value_chars[] = $value_char;
} else {
$value_chars[] = rawurlencode($value_char);
}
}
$this->cookies[implode('', $name_chars)] = implode('', $value_chars);
}
/**
* Initialize
*
* @param $base_url
* @param mixed $options
*/
private function initialize($base_url = null, $options = [])
{
$this->setProtocolsInternal(CURLPROTO_HTTPS | CURLPROTO_HTTP);
$this->setRedirectProtocolsInternal(CURLPROTO_HTTPS | CURLPROTO_HTTP);
if (isset($options)) {
$this->setOpts($options);
}
$this->id = bin2hex(random_bytes(16));
// Only set default user agent if not already set.
if (!array_key_exists(CURLOPT_USERAGENT, $this->options)) {
$this->setDefaultUserAgentInternal();
}
// Only set default timeout if not already set.
if (!array_key_exists(CURLOPT_TIMEOUT, $this->options)) {
$this->setDefaultTimeoutInternal();
}
if (!array_key_exists(CURLINFO_HEADER_OUT, $this->options)) {
$this->setDefaultHeaderOutInternal();
}
// Create a placeholder to temporarily store the header callback data.
$header_callback_data = new \stdClass();
$header_callback_data->rawResponseHeaders = '';
$header_callback_data->responseCookies = [];
$header_callback_data->stopRequestDecider = null;
$header_callback_data->stopRequest = false;
$this->headerCallbackData = $header_callback_data;
$this->setStopInternal();
$this->setOptInternal(CURLOPT_HEADERFUNCTION, createHeaderCallback($header_callback_data));
$this->setOptInternal(CURLOPT_RETURNTRANSFER, true);
$this->headers = new CaseInsensitiveArray();
if ($base_url !== null) {
$this->setUrl($base_url);
}
}
/**
* Set Stop
*
* Specify a callable decider to stop the request early without waiting for
* the full response to be received.
*
* The callable is passed two parameters. The first is the cURL resource,
* the second is a string with header data. Both parameters match the
* parameters in the CURLOPT_HEADERFUNCTION callback.
*
* The callable must return a truthy value for the request to be stopped
* early.
*
* The callable may be set to null to avoid calling the stop request decider
* callback and instead just check the value of stopRequest for attempting
* to stop the request as used by Curl::stop().
*
* @param $callback callable|null
*/
public function setStop($callback = null)
{
$this->headerCallbackData->stopRequestDecider = $callback;
$this->headerCallbackData->stopRequest = false;
$header_callback_data = $this->headerCallbackData;
$this->progress(createStopRequestFunction($header_callback_data));
}
private function setStopInternal($callback = null)
{
$this->headerCallbackData->stopRequestDecider = $callback;
$this->headerCallbackData->stopRequest = false;
$header_callback_data = $this->headerCallbackData;
$this->progressInternal(createStopRequestFunction($header_callback_data));
}
/**
* Stop
*
* Attempt to stop request.
*
* Used by MultiCurl::stop() when making multiple parallel requests.
*/
#[\Override]
public function stop()
{
$this->headerCallbackData->stopRequest = true;
}
}
/**
* Create Header Callback
*
* Gather headers and parse cookies as response headers are received. Keep this function separate from the class so that
* unset($curl) automatically calls __destruct() as expected. Otherwise, manually calling $curl->close() will be
* necessary to prevent a memory leak.
*
* @param $header_callback_data
* @return callable
*/
function createHeaderCallback($header_callback_data)
{
return function ($ch, $header) use ($header_callback_data) {
if (preg_match('/^Set-Cookie:\s*([^=]+)=([^;]+)/mi', $header, $cookie) === 1) {
$header_callback_data->responseCookies[$cookie[1]] = trim($cookie[2], " \n\r\t\0\x0B");
}
if ($header_callback_data->stopRequestDecider !== null) {
$stop_request_decider = $header_callback_data->stopRequestDecider;
if ($stop_request_decider($ch, $header)) {
$header_callback_data->stopRequest = true;
}
}
$header_callback_data->rawResponseHeaders .= $header;
return strlen($header);
};
}
/**
* Create Stop Request Function
*
* Create a function for Curl::progress() that stops a request early when the
* stopRequest flag is on. Keep this function separate from the class to prevent
* a memory leak.
*
* @param $header_callback_data
* @return callable
*/
function createStopRequestFunction($header_callback_data)
{
return function (
$resource,
$download_size,
$downloaded,
$upload_size,
$uploaded
) use (
$header_callback_data
) {
// Abort the transfer when the stop request flag has been set by returning a non-zero value.
return $header_callback_data->stopRequest ? 1 : 0;
};
}