Commit f2b9b4f6 authored by zw's avatar zw

11

parent 9fa9f03f
# Change Log
PHP Curl Class uses semantic versioning with version numbers written as `MAJOR.MINOR.PATCH`. You may safely update
`MINOR` and `PATCH` version changes. It is recommended to review `MAJOR` changes prior to upgrade as there may be
backwards-incompatible changes that will affect existing usage.
### Changes
(TODO: Add changes for next `MAJOR` version release.)
### Manual Review
Manually view changes on the [comparison page](https://github.com/php-curl-class/php-curl-class/compare/). For example,
visit [7.4.0...8.0.0](https://github.com/php-curl-class/php-curl-class/compare/7.4.0...8.0.0) to compare the changes for
the `MAJOR` upgrade from 7.4.0 to 8.0.0. Comparing against `HEAD` is also possible using the `tag...HEAD` syntax
([8.3.0...HEAD](https://github.com/php-curl-class/php-curl-class/compare/8.3.0...HEAD)).
View the log between releases:
$ git fetch --tags
$ git log 7.4.0...8.0.0
View the code changes between releases:
$ git fetch --tags
$ git diff 7.4.0...8.0.0
View only the source log and code changes between releases:
$ git log 7.4.0...8.0.0 "src/"
$ git diff 7.4.0...8.0.0 "src/"
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
# PHP Curl Class: HTTP requests made easy
[![](https://img.shields.io/github/release/php-curl-class/php-curl-class.svg)](https://github.com/php-curl-class/php-curl-class/releases/)
[![](https://img.shields.io/github/license/php-curl-class/php-curl-class.svg)](https://github.com/php-curl-class/php-curl-class/blob/master/LICENSE)
[![](https://img.shields.io/travis/php-curl-class/php-curl-class.svg)](https://travis-ci.org/php-curl-class/php-curl-class/)
[![](https://img.shields.io/packagist/dt/php-curl-class/php-curl-class.svg)](https://github.com/php-curl-class/php-curl-class/releases/)
PHP Curl Class makes it easy to send HTTP requests and integrate with web APIs.
![PHP Curl Class screencast](www/img/screencast.gif)
---
- [Installation](#installation)
- [Requirements](#requirements)
- [Quick Start and Examples](#quick-start-and-examples)
- [Available Methods](#available-methods)
- [Security](#security)
- [Troubleshooting](#troubleshooting)
- [Run Tests](#run-tests)
- [Contribute](#contribute)
---
### Installation
To install PHP Curl Class, simply:
$ composer require php-curl-class/php-curl-class
For latest commit version:
$ composer require php-curl-class/php-curl-class @dev
### Requirements
PHP Curl Class works with PHP 5.3, 5.4, 5.5, 5.6, 7.0, 7.1, 7.2, 7.3, and HHVM.
### Quick Start and Examples
More examples are available under [/examples](https://github.com/php-curl-class/php-curl-class/tree/master/examples).
```php
require __DIR__ . '/vendor/autoload.php';
use \Curl\Curl;
$curl = new Curl();
$curl->get('https://www.example.com/');
if ($curl->error) {
echo 'Error: ' . $curl->errorCode . ': ' . $curl->errorMessage . "\n";
} else {
echo 'Response:' . "\n";
var_dump($curl->response);
}
```
```php
// https://www.example.com/search?q=keyword
$curl = new Curl();
$curl->get('https://www.example.com/search', array(
'q' => 'keyword',
));
```
```php
$curl = new Curl();
$curl->post('https://www.example.com/login/', array(
'username' => 'myusername',
'password' => 'mypassword',
));
```
```php
$curl = new Curl();
$curl->setBasicAuthentication('username', 'password');
$curl->setUserAgent('MyUserAgent/0.0.1 (+https://www.example.com/bot.html)');
$curl->setReferrer('https://www.example.com/url?url=https%3A%2F%2Fwww.example.com%2F');
$curl->setHeader('X-Requested-With', 'XMLHttpRequest');
$curl->setCookie('key', 'value');
$curl->get('https://www.example.com/');
if ($curl->error) {
echo 'Error: ' . $curl->errorCode . ': ' . $curl->errorMessage . "\n";
} else {
echo 'Response:' . "\n";
var_dump($curl->response);
}
var_dump($curl->requestHeaders);
var_dump($curl->responseHeaders);
```
```php
$curl = new Curl();
$curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
$curl->get('https://shortn.example.com/bHbVsP');
```
```php
$curl = new Curl();
$curl->put('https://api.example.com/user/', array(
'first_name' => 'Zach',
'last_name' => 'Borboa',
));
```
```php
$curl = new Curl();
$curl->patch('https://api.example.com/profile/', array(
'image' => '@path/to/file.jpg',
));
```
```php
$curl = new Curl();
$curl->patch('https://api.example.com/profile/', array(
'image' => new CURLFile('path/to/file.jpg'),
));
```
```php
$curl = new Curl();
$curl->delete('https://api.example.com/user/', array(
'id' => '1234',
));
```
```php
// Enable all supported encoding types and download a file.
$curl = new Curl();
$curl->setOpt(CURLOPT_ENCODING , '');
$curl->download('https://www.example.com/file.bin', '/tmp/myfile.bin');
```
```php
// Case-insensitive access to headers.
$curl = new Curl();
$curl->download('https://www.example.com/image.png', '/tmp/myimage.png');
echo $curl->responseHeaders['Content-Type'] . "\n"; // image/png
echo $curl->responseHeaders['CoNTeNT-TyPE'] . "\n"; // image/png
```
```php
// Manual clean up.
$curl->close();
```
```php
// Example access to curl object.
curl_set_opt($curl->curl, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1');
curl_close($curl->curl);
```
```php
require __DIR__ . '/vendor/autoload.php';
use \Curl\MultiCurl;
// Requests in parallel with callback functions.
$multi_curl = new MultiCurl();
$multi_curl->success(function($instance) {
echo 'call to "' . $instance->url . '" was successful.' . "\n";
echo 'response:' . "\n";
var_dump($instance->response);
});
$multi_curl->error(function($instance) {
echo 'call to "' . $instance->url . '" was unsuccessful.' . "\n";
echo 'error code: ' . $instance->errorCode . "\n";
echo 'error message: ' . $instance->errorMessage . "\n";
});
$multi_curl->complete(function($instance) {
echo 'call completed' . "\n";
});
$multi_curl->addGet('https://www.google.com/search', array(
'q' => 'hello world',
));
$multi_curl->addGet('https://duckduckgo.com/', array(
'q' => 'hello world',
));
$multi_curl->addGet('https://www.bing.com/search', array(
'q' => 'hello world',
));
$multi_curl->start(); // Blocks until all items in the queue have been processed.
```
More examples are available under [/examples](https://github.com/php-curl-class/php-curl-class/tree/master/examples).
### Available Methods
```php
Curl::__construct($base_url = null)
Curl::__destruct()
Curl::__get($name)
Curl::attemptRetry()
Curl::beforeSend($callback)
Curl::buildPostData($data)
Curl::call()
Curl::close()
Curl::complete($callback)
Curl::delete($url, $query_parameters = array(), $data = array())
Curl::download($url, $mixed_filename)
Curl::error($callback)
Curl::exec($ch = null)
Curl::execDone()
Curl::get($url, $data = array())
Curl::getAttempts()
Curl::getBeforeSendCallback()
Curl::getCompleteCallback()
Curl::getCookie($key)
Curl::getCurl()
Curl::getCurlErrorCode()
Curl::getCurlErrorMessage()
Curl::getDownloadCompleteCallback()
Curl::getDownloadFileName()
Curl::getErrorCallback()
Curl::getErrorCode()
Curl::getErrorMessage()
Curl::getFileHandle()
Curl::getHttpErrorMessage()
Curl::getHttpStatusCode()
Curl::getId()
Curl::getInfo($opt = null)
Curl::getJsonDecoder()
Curl::getOpt($option)
Curl::getRawResponse()
Curl::getRawResponseHeaders()
Curl::getRemainingRetries()
Curl::getRequestHeaders()
Curl::getResponse()
Curl::getResponseCookie($key)
Curl::getResponseCookies()
Curl::getResponseHeaders()
Curl::getRetries()
Curl::getRetryDecider()
Curl::getSuccessCallback()
Curl::getUrl()
Curl::getXmlDecoder()
Curl::head($url, $data = array())
Curl::isChildOfMultiCurl()
Curl::isCurlError()
Curl::isError()
Curl::isHttpError()
Curl::options($url, $data = array())
Curl::patch($url, $data = array())
Curl::post($url, $data = '', $follow_303_with_post = false)
Curl::progress($callback)
Curl::put($url, $data = array())
Curl::removeHeader($key)
Curl::reset()
Curl::search($url, $data = array())
Curl::setBasicAuthentication($username, $password = '')
Curl::setConnectTimeout($seconds)
Curl::setCookie($key, $value)
Curl::setCookieFile($cookie_file)
Curl::setCookieJar($cookie_jar)
Curl::setCookieString($string)
Curl::setCookies($cookies)
Curl::setDefaultDecoder($mixed = 'json')
Curl::setDefaultJsonDecoder()
Curl::setDefaultTimeout()
Curl::setDefaultUserAgent()
Curl::setDefaultXmlDecoder()
Curl::setDigestAuthentication($username, $password = '')
Curl::setHeader($key, $value)
Curl::setHeaders($headers)
Curl::setJsonDecoder($mixed)
Curl::setMaxFilesize($bytes)
Curl::setOpt($option, $value)
Curl::setOpts($options)
Curl::setPort($port)
Curl::setProxy($proxy, $port = null, $username = null, $password = null)
Curl::setProxyAuth($auth)
Curl::setProxyTunnel($tunnel = true)
Curl::setProxyType($type)
Curl::setReferer($referer)
Curl::setReferrer($referrer)
Curl::setRetry($mixed)
Curl::setTimeout($seconds)
Curl::setUrl($url, $mixed_data = '')
Curl::setUserAgent($user_agent)
Curl::setXmlDecoder($mixed)
Curl::success($callback)
Curl::unsetHeader($key)
Curl::unsetProxy()
Curl::verbose($on = true, $output = STDERR)
MultiCurl::__construct($base_url = null)
MultiCurl::__destruct()
MultiCurl::addCurl(Curl $curl)
MultiCurl::addDelete($url, $query_parameters = array(), $data = array())
MultiCurl::addDownload($url, $mixed_filename)
MultiCurl::addGet($url, $data = array())
MultiCurl::addHead($url, $data = array())
MultiCurl::addOptions($url, $data = array())
MultiCurl::addPatch($url, $data = array())
MultiCurl::addPost($url, $data = '', $follow_303_with_post = false)
MultiCurl::addPut($url, $data = array())
MultiCurl::addSearch($url, $data = array())
MultiCurl::beforeSend($callback)
MultiCurl::close()
MultiCurl::complete($callback)
MultiCurl::error($callback)
MultiCurl::getOpt($option)
MultiCurl::removeHeader($key)
MultiCurl::setBasicAuthentication($username, $password = '')
MultiCurl::setConcurrency($concurrency)
MultiCurl::setConnectTimeout($seconds)
MultiCurl::setCookie($key, $value)
MultiCurl::setCookieFile($cookie_file)
MultiCurl::setCookieJar($cookie_jar)
MultiCurl::setCookieString($string)
MultiCurl::setCookies($cookies)
MultiCurl::setDigestAuthentication($username, $password = '')
MultiCurl::setHeader($key, $value)
MultiCurl::setHeaders($headers)
MultiCurl::setJsonDecoder($mixed)
MultiCurl::setOpt($option, $value)
MultiCurl::setOpts($options)
MultiCurl::setPort($port)
MultiCurl::setProxies($proxies)
MultiCurl::setProxy($proxy, $port = null, $username = null, $password = null)
MultiCurl::setProxyAuth($auth)
MultiCurl::setProxyTunnel($tunnel = true)
MultiCurl::setProxyType($type)
MultiCurl::setReferer($referer)
MultiCurl::setReferrer($referrer)
MultiCurl::setRetry($mixed)
MultiCurl::setTimeout($seconds)
MultiCurl::setUrl($url)
MultiCurl::setUserAgent($user_agent)
MultiCurl::setXmlDecoder($mixed)
MultiCurl::start()
MultiCurl::success($callback)
MultiCurl::unsetHeader($key)
MultiCurl::unsetProxy()
MultiCurl::verbose($on = true, $output = STDERR)
```
### Security
See [SECURITY](https://github.com/php-curl-class/php-curl-class/blob/master/SECURITY.md) for security considerations.
### Troubleshooting
See [TROUBLESHOOTING](https://github.com/php-curl-class/php-curl-class/blob/master/TROUBLESHOOTING.md) for troubleshooting.
### Run Tests
To run tests:
$ git clone https://github.com/php-curl-class/php-curl-class.git
$ cd php-curl-class/
$ composer update
$ ./tests/run.sh
To test all PHP versions in containers:
$ git clone https://github.com/php-curl-class/php-curl-class.git
$ cd php-curl-class/
$ ./tests/test_all.sh
### Contribute
1. Check for open issues or open a new issue to start a discussion around a bug or feature.
1. Fork the repository on GitHub to start making your changes.
1. Write one or more tests for the new feature or that expose the bug.
1. Make code changes to implement the feature or fix the bug.
1. Send a pull request to get your changes merged and published.
# Security Considerations
### Url may point to system files
* Don't blindly accept urls from users as they may point to system files. Curl supports many protocols including `FILE`.
The following would show the contents of `file:///etc/passwd`.
```bash
# Attacker.
$ curl https://www.example.com/display_webpage.php?url=file%3A%2F%2F%2Fetc%2Fpasswd
```
```php
// display_webpage.php
$url = $_GET['url']; // DANGER!
$curl = new Curl();
$curl->get($url);
echo $curl->response;
```
Safer:
```php
function is_allowed_url($url, $allowed_url_schemes = array('http', 'https')) {
$valid_url = filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED) !== false;
if ($valid_url) {
$scheme = parse_url($url, PHP_URL_SCHEME);
return in_array($scheme, $allowed_url_schemes, true);
}
$valid_ip = filter_var($url, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
return $valid_ip;
}
$url = $_GET['url'];
if (!is_allowed_url($url)) {
die('Unsafe url detected.');
}
```
### Url may point to internal urls
* Url may point to internal urls including those behind a firewall (e.g. http://192.168.0.1/ or ftp://192.168.0.1/). Use
a whitelist to allow certain urls rather than a blacklist.
### Request data may refer to system files
* Request data prefixed with the `@` character may have special interpretation and read from system files.
```bash
# Attacker.
$ curl https://www.example.com/upload_photo.php --data "photo=@/etc/passwd"
```
```php
// upload_photo.php
$curl = new Curl();
$curl->post('http://www.anotherwebsite.com/', array(
'photo' => $_POST['photo'], // DANGER!
));
```
### Unsafe response with redirection enabled
* Requests with redirection enabled may return responses from unexpected sources.
Downloading https://www.example.com/image.png may redirect and download https://www.evil.com/virus.exe
```php
$curl = new Curl();
$curl->setOpt(CURLOPT_FOLLOWLOCATION, true); // DANGER!
$curl->download('https://www.example.com/image.png', 'my_image.png');
```
```php
$curl = new Curl();
$curl->setOpt(CURLOPT_FOLLOWLOCATION, true); // DANGER!
$curl->get('https://www.example.com/image.png');
```
### Keep SSL protections enabled
* Do not disable SSL protections.
```php
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // DANGER!
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // DANGER!
```
### Prevent XML External Entity injection
* Set the following when using the default PHP XML parser to prevent XML external entity injection.
```php
libxml_disable_entity_loader(true);
```
### Prevent PHP execution of library files
PHP files in this library are not intended to be accessible by users browsing websites. Prevent direct access to library files by moving the library folder at least one level higher than the web root directory. Alternatively, configure the server to disable php file execution for all library files.
#### For WordPress plugin developers
WordPress plugin developers that wish to incorporate the PHP Curl Class library into their plugin, should take special care to include only the "core" library files.
Do one of the following:
Option 1. Download an official release from the [releases page](https://github.com/php-curl-class/php-curl-class/releases) and incorporate the files contained in the compressed file into the plugin. The releases include only the necessary php files for the library to function.
Option 2. Manually copy only the [src/](https://github.com/php-curl-class/php-curl-class/tree/master/src) directory into your plugin. Be sure not to copy any other php files as they may be executable by users visiting the php files directly.
{
"name": "php-curl-class/php-curl-class",
"description": "PHP Curl Class makes it easy to send HTTP requests and integrate with web APIs.",
"homepage": "https://github.com/php-curl-class/php-curl-class",
"license": "Unlicense",
"keywords": [
"php", "curl", "class", "api", "api-client", "client", "framework", "http", "http-client", "http-proxy", "json",
"php-curl", "php-curl-library", "proxy", "requests", "restful", "web-scraper", "web-scraping", "web-service",
"xml"
],
"authors": [
{
"name": "Zach Borboa"
}
],
"require": {
"php": ">=5.3",
"ext-curl": "*"
},
"require-dev": {
"ext-gd": "*",
"phpunit/phpunit": "*",
"squizlabs/php_codesniffer": "*"
},
"suggest": {
"ext-mbstring": "*"
},
"autoload": {
"psr-4": {
"Curl\\": "src/Curl/"
}
}
}
<?php
namespace Curl;
class ArrayUtil
{
/**
* 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'));
}
/**
* Array Flatten Multidim
*
* @access public
* @param $array
* @param $prefix
*
* @return array
*/
public static function array_flatten_multidim($array, $prefix = false)
{
$return = array();
if (is_array($array) || is_object($array)) {
if (empty($array)) {
$return[$prefix] = '';
} else {
foreach ($array as $key => $value) {
if (is_scalar($value)) {
if ($prefix) {
$return[$prefix . '[' . $key . ']'] = $value;
} else {
$return[$key] = $value;
}
} else {
if ($value instanceof \CURLFile) {
$return[$key] = $value;
} else {
$return = array_merge(
$return,
self::array_flatten_multidim(
$value,
$prefix ? $prefix . '[' . $key . ']' : $key
)
);
}
}
}
}
} elseif ($array === null) {
$return[$prefix] = $array;
}
return $return;
}
/**
* Array Random
*
* @access public
* @param $array
*
* @return mixed
*/
public static function array_random($array)
{
return $array[mt_rand(0, count($array) - 1)];
}
}
<?php
namespace Curl;
class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
{
/**
* @var mixed[] Data storage with lowercase keys.
* @see offsetSet()
* @see offsetExists()
* @see offsetUnset()
* @see offsetGet()
* @see count()
* @see current()
* @see next()
* @see key()
*/
private $data = array();
/**
* @var string[] Case-sensitive keys.
* @see offsetSet()
* @see offsetUnset()
* @see key()
*/
private $keys = array();
/**
* Construct
*
* Allow creating an empty array or converting an existing array to a
* case-insensitive array. Caution: Data may be lost when converting
* case-sensitive arrays to case-insensitive arrays.
*
* @param mixed[] $initial (optional) Existing array to convert.
*
* @return CaseInsensitiveArray
*
* @access public
*/
public function __construct(array $initial = null)
{
if ($initial !== null) {
foreach ($initial as $key => $value) {
$this->offsetSet($key, $value);
}
}
}
/**
* Offset Set
*
* Set data at a specified offset. Converts the offset to lowercase, and
* stores the case-sensitive offset and the data at the lowercase indexes in
* $this->keys and @this->data.
*
* @see https://secure.php.net/manual/en/arrayaccess.offsetset.php
*
* @param string $offset The offset to store the data at (case-insensitive).
* @param mixed $value The data to store at the specified offset.
*
* @return void
*
* @access public
*/
public function offsetSet($offset, $value)
{
if ($offset === null) {
$this->data[] = $value;
} else {
$offsetlower = strtolower($offset);
$this->data[$offsetlower] = $value;
$this->keys[$offsetlower] = $offset;
}
}
/**
* Offset Exists
*
* Checks if the offset exists in data storage. The index is looked up with
* the lowercase version of the provided offset.
*
* @see https://secure.php.net/manual/en/arrayaccess.offsetexists.php
*
* @param string $offset Offset to check
*
* @return bool If the offset exists.
*
* @access public
*/
public function offsetExists($offset)
{
return (bool) array_key_exists(strtolower($offset), $this->data);
}
/**
* Offset Unset
*
* Unsets the specified offset. Converts the provided offset to lowercase,
* and unsets the case-sensitive key, as well as the stored data.
*
* @see https://secure.php.net/manual/en/arrayaccess.offsetunset.php
*
* @param string $offset The offset to unset.
*
* @return void
*
* @access public
*/
public function offsetUnset($offset)
{
$offsetlower = strtolower($offset);
unset($this->data[$offsetlower]);
unset($this->keys[$offsetlower]);
}
/**
* Offset Get
*
* Return the stored data at the provided offset. The offset is converted to
* lowercase and the lookup is done on the data store directly.
*
* @see https://secure.php.net/manual/en/arrayaccess.offsetget.php
*
* @param string $offset Offset to lookup.
*
* @return mixed The data stored at the offset.
*
* @access public
*/
public function offsetGet($offset)
{
$offsetlower = strtolower($offset);
return isset($this->data[$offsetlower]) ? $this->data[$offsetlower] : null;
}
/**
* Count
*
* @see https://secure.php.net/manual/en/countable.count.php
*
* @param void
*
* @return int The number of elements stored in the array.
*
* @access public
*/
public function count()
{
return (int) count($this->data);
}
/**
* Current
*
* @see https://secure.php.net/manual/en/iterator.current.php
*
* @param void
*
* @return mixed Data at the current position.
*
* @access public
*/
public function current()
{
return current($this->data);
}
/**
* Next
*
* @see https://secure.php.net/manual/en/iterator.next.php
*
* @param void
*
* @return void
*
* @access public
*/
public function next()
{
next($this->data);
}
/**
* Key
*
* @see https://secure.php.net/manual/en/iterator.key.php
*
* @param void
*
* @return mixed Case-sensitive key at current position.
*
* @access public
*/
public function key()
{
$key = key($this->data);
return isset($this->keys[$key]) ? $this->keys[$key] : $key;
}
/**
* Valid
*
* @see https://secure.php.net/manual/en/iterator.valid.php
*
* @return bool If the current position is valid.
*
* @access public
*/
public function valid()
{
return (bool) !(key($this->data) === null);
}
/**
* Rewind
*
* @see https://secure.php.net/manual/en/iterator.rewind.php
*
* @param void
*
* @return void
*
* @access public
*/
public function rewind()
{
reset($this->data);
}
}
<?php
namespace Curl;
use Curl\ArrayUtil;
use Curl\Decoder;
class Curl
{
const VERSION = '8.6.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 $url = null;
public $requestHeaders = null;
public $responseHeaders = null;
public $rawResponseHeaders = '';
public $responseCookies = array();
public $response = null;
public $rawResponse = null;
public $beforeSendCallback = null;
public $downloadCompleteCallback = null;
public $successCallback = null;
public $errorCallback = null;
public $completeCallback = null;
public $fileHandle = null;
private $downloadFileName = null;
public $attempts = 0;
public $retries = 0;
public $childOfMultiCurl = false;
public $remainingRetries = 0;
public $retryDecider = null;
public $jsonDecoder = null;
public $xmlDecoder = null;
private $cookies = array();
private $headers = array();
private $options = array();
private $jsonDecoderArgs = array();
private $jsonPattern = '/^(?:application|text)\/(?:[a-z]+(?:[\.-][0-9a-z]+){0,}[\+\.]|x-)?json(?:-[a-z]+)?/i';
private $xmlDecoderArgs = array();
private $xmlPattern = '~^(?:text/|application/(?:atom\+|rss\+|soap\+)?)xml~i';
private $defaultDecoder = null;
public static $RFC2616 = array(
// 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 = array(
// 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', '{', '|', '}', '~',
);
private static $deferredProperties = array(
'effectiveUrl',
'rfc2616',
'rfc6265',
'totalTime',
);
/**
* 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->initialize($base_url);
}
/**
* Before Send
*
* @access public
* @param $callback
*/
public function beforeSend($callback)
{
$this->beforeSendCallback = $callback;
}
/**
* Build Post Data
*
* @access public
* @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::is_array_multidim($data)) {
$data = ArrayUtil::array_flatten_multidim($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;
}
}
}
if (!$binary_data &&
(is_array($data) || is_object($data)) &&
(
!isset($this->headers['Content-Type']) ||
!preg_match('/^multipart\/form-data/', $this->headers['Content-Type'])
)) {
$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;
$this->jsonDecoderArgs = null;
$this->xmlDecoder = null;
$this->xmlDecoderArgs = null;
$this->defaultDecoder = null;
}
/**
* Complete
*
* @access public
* @param $callback
*/
public function complete($callback)
{
$this->completeCallback = $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 mixed Returns the value provided by exec.
*/
public function delete($url, $query_parameters = array(), $data = array())
{
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, array());
// 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));
}
return $this->exec();
}
/**
* Download
*
* @access public
* @param $url
* @param $mixed_filename
*
* @return boolean
*/
public function download($url, $mixed_filename)
{
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';
$mode = 'wb';
// Attempt to resume download only when a temporary download file exists and is not empty.
if (is_file($download_filename) && $filesize = filesize($download_filename)) {
$mode = 'ab';
$first_byte_position = $filesize;
$range = $first_byte_position . '-';
$this->setOpt(CURLOPT_RANGE, $range);
}
$this->downloadFileName = $download_filename;
$this->fileHandle = fopen($download_filename, $mode);
// 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->setOpt(CURLOPT_FILE, $this->fileHandle);
$this->get($url);
return ! $this->error;
}
/**
* Error
*
* @access public
* @param $callback
*/
public function error($callback)
{
$this->errorCallback = $callback;
}
/**
* Exec
*
* @access public
* @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 = array();
$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);
// 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 = array();
// Include additional error code information in error message when possible.
if ($this->curlError && function_exists('curl_strerror')) {
$this->curlErrorMessage =
curl_strerror($this->curlErrorCode) . (
empty($this->curlErrorMessage) ? '' : ': ' . $this->curlErrorMessage
);
}
$this->httpStatusCode = $this->getInfo(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($this->getInfo(CURLINFO_HEADER_OUT));
}
$this->responseHeaders = $this->parseResponseHeaders($this->rawResponseHeaders);
$this->response = $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;
// Reset select deferred properties so that they may be recalculated.
unset($this->effectiveUrl);
unset($this->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->setOpt(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
*
* @access public
* @param $url
* @param $data
*
* @return mixed Returns the value provided by exec.
*/
public function get($url, $data = array())
{
if (is_array($url)) {
$data = $url;
$url = (string)$this->url;
}
$this->setUrl($url, $data);
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'GET');
$this->setOpt(CURLOPT_HTTPGET, true);
return $this->exec();
}
/**
* Get Info
*
* @access public
* @param $opt
*
* @return mixed
*/
public function getInfo($opt = null)
{
$args = array();
$args[] = $this->curl;
if (func_num_args()) {
$args[] = $opt;
}
return call_user_func_array('curl_getinfo', $args);
}
/**
* Get Opt
*
* @access public
* @param $option
*
* @return mixed
*/
public function getOpt($option)
{
return isset($this->options[$option]) ? $this->options[$option] : null;
}
/**
* Head
*
* @access public
* @param $url
* @param $data
*
* @return mixed Returns the value provided by exec.
*/
public function head($url, $data = array())
{
if (is_array($url)) {
$data = $url;
$url = (string)$this->url;
}
$this->setUrl($url, $data);
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'HEAD');
$this->setOpt(CURLOPT_NOBODY, true);
return $this->exec();
}
/**
* Options
*
* @access public
* @param $url
* @param $data
*
* @return mixed Returns the value provided by exec.
*/
public function options($url, $data = array())
{
if (is_array($url)) {
$data = $url;
$url = (string)$this->url;
}
$this->setUrl($url, $data);
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'OPTIONS');
return $this->exec();
}
/**
* Patch
*
* @access public
* @param $url
* @param $data
*
* @return mixed Returns the value provided by exec.
*/
public function patch($url, $data = array())
{
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));
return $this->exec();
}
/**
* Post
*
* @access public
* @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_CURSTOMREQUEST
* 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]) and of HHVM, 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)
{
if (is_array($url)) {
$follow_303_with_post = (bool)$data;
$data = $url;
$url = (string)$this->url;
}
$this->setUrl($url);
if ($follow_303_with_post) {
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
} else {
if (isset($this->options[CURLOPT_CUSTOMREQUEST])) {
if ((version_compare(PHP_VERSION, '5.5.11') < 0) || defined('HHVM_VERSION')) {
trigger_error(
'Due to technical limitations of PHP <= 5.5.11 and HHVM, it is not possible to '
. 'perform a post-redirect-get request using a php-curl-class Curl object that '
. 'has already been used to perform other types of requests. Either use a new '
. 'php-curl-class Curl object or upgrade your PHP engine.',
E_USER_ERROR
);
} else {
$this->setOpt(CURLOPT_CUSTOMREQUEST, null);
}
}
}
$this->setOpt(CURLOPT_POST, true);
$this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data));
return $this->exec();
}
/**
* Put
*
* @access public
* @param $url
* @param $data
*
* @return mixed Returns the value provided by exec.
*/
public function put($url, $data = array())
{
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);
}
return $this->exec();
}
/**
* Search
*
* @access public
* @param $url
* @param $data
*
* @return mixed Returns the value provided by exec.
*/
public function search($url, $data = array())
{
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);
}
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->setEncodedCookie($key, $value);
$this->buildCookies();
}
/**
* Set Cookies
*
* @access public
* @param $cookies
*/
public function setCookies($cookies)
{
foreach ($cookies as $key => $value) {
$this->setEncodedCookie($key, $value);
}
$this->buildCookies();
}
/**
* Get Cookie
*
* @access public
* @param $key
*
* @return mixed
*/
public function getCookie($key)
{
return $this->getResponseCookie($key);
}
/**
* Get Response Cookie
*
* @access public
* @param $key
*
* @return mixed
*/
public function getResponseCookie($key)
{
return isset($this->responseCookies[$key]) ? $this->responseCookies[$key] : null;
}
/**
* Set Max Filesize
*
* @access public
* @param $bytes
*/
public function setMaxFilesize($bytes)
{
// Make compatible with PHP version both before and after 5.5.0. PHP 5.5.0 added the cURL resource as the first
// argument to the CURLOPT_PROGRESSFUNCTION callback.
$gte_v550 = version_compare(PHP_VERSION, '5.5.0') >= 0;
if ($gte_v550) {
$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;
};
} else {
$callback = function ($download_size, $downloaded, $upload_size, $uploaded) use ($bytes) {
return $downloaded > $bytes ? 1 : 0;
};
}
$this->progress($callback);
}
/**
* 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 String
*
* @access public
* @param $string
*
* @return bool
*/
public function setCookieString($string)
{
return $this->setOpt(CURLOPT_COOKIE, $string);
}
/**
* Set Cookie File
*
* @access public
* @param $cookie_file
*
* @return boolean
*/
public function setCookieFile($cookie_file)
{
return $this->setOpt(CURLOPT_COOKIEFILE, $cookie_file);
}
/**
* Set Cookie Jar
*
* @access public
* @param $cookie_jar
*
* @return boolean
*/
public function setCookieJar($cookie_jar)
{
return $this->setOpt(CURLOPT_COOKIEJAR, $cookie_jar);
}
/**
* Set Default JSON Decoder
*
* @access public
* @param $assoc
* @param $depth
* @param $options
*/
public function setDefaultJsonDecoder()
{
$this->jsonDecoder = '\Curl\Decoder::decodeJson';
$this->jsonDecoderArgs = func_get_args();
}
/**
* Set Default XML Decoder
*
* @access public
* @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
*
* @access public
* @param $mixed boolean|callable|string
*/
public function setDefaultDecoder($mixed = 'json')
{
if ($mixed === false) {
$this->defaultDecoder = false;
} elseif (is_callable($mixed)) {
$this->defaultDecoder = $mixed;
} else {
if ($mixed === 'json') {
$this->defaultDecoder = '\Curl\Decoder::decodeJson';
} elseif ($mixed === 'xml') {
$this->defaultDecoder = '\Curl\Decoder::decodeXml';
}
}
}
/**
* 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
*
* Add extra header to include in the request.
*
* @access public
* @param $key
* @param $value
*/
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 Headers
*
* Add extra headers to include in the request.
*
* @access public
* @param $headers
*/
public function setHeaders($headers)
{
foreach ($headers as $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 $mixed boolean|callable
*/
public function setJsonDecoder($mixed)
{
if ($mixed === false || is_callable($mixed)) {
$this->jsonDecoder = $mixed;
$this->jsonDecoderArgs = array();
}
}
/**
* Set XML Decoder
*
* @access public
* @param $mixed boolean|callable
*/
public function setXmlDecoder($mixed)
{
if ($mixed === false || is_callable($mixed)) {
$this->xmlDecoder = $mixed;
$this->xmlDecoderArgs = array();
}
}
/**
* 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);
}
$success = curl_setopt($this->curl, $option, $value);
if ($success) {
$this->options[$option] = $value;
}
return $success;
}
/**
* Set Opts
*
* @access public
* @param $options
*
* @return boolean
* 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().
*/
public function setOpts($options)
{
foreach ($options as $option => $value) {
if (!$this->setOpt($option, $value)) {
return false;
}
}
return true;
}
/**
* Set Proxy
*
* Set an HTTP proxy to tunnel requests through.
*
* @access public
* @param $proxy - The HTTP proxy to tunnel requests through. May include port number.
* @param $port - The port number of the proxy to connect to. This port number can also be set in $proxy.
* @param $username - The username to use for the connection to the proxy.
* @param $password - The password to use for the connection to the proxy.
*/
public function setProxy($proxy, $port = null, $username = null, $password = null)
{
$this->setOpt(CURLOPT_PROXY, $proxy);
if ($port !== null) {
$this->setOpt(CURLOPT_PROXYPORT, $port);
}
if ($username !== null && $password !== null) {
$this->setOpt(CURLOPT_PROXYUSERPWD, $username . ':' . $password);
}
}
/**
* Set Proxy Auth
*
* Set the HTTP authentication method(s) to use for the proxy connection.
*
* @access public
* @param $auth
*/
public function setProxyAuth($auth)
{
$this->setOpt(CURLOPT_PROXYAUTH, $auth);
}
/**
* Set Proxy Type
*
* Set the proxy protocol type.
*
* @access public
* @param $type
*/
public function setProxyType($type)
{
$this->setOpt(CURLOPT_PROXYTYPE, $type);
}
/**
* Set Proxy Tunnel
*
* Set the proxy to tunnel through HTTP proxy.
*
* @access public
* @param $tunnel boolean
*/
public function setProxyTunnel($tunnel = true)
{
$this->setOpt(CURLOPT_HTTPPROXYTUNNEL, $tunnel);
}
/**
* Unset Proxy
*
* Disable use of the proxy.
*
* @access public
*/
public function unsetProxy()
{
$this->setOpt(CURLOPT_PROXY, null);
}
/**
* 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 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.
*
* @access public
* @param $mixed
*/
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 Timeout
*
* @access public
* @param $seconds
*/
public function setTimeout($seconds)
{
$this->setOpt(CURLOPT_TIMEOUT, $seconds);
}
/**
* Set Url
*
* @access public
* @param $url
* @param $mixed_data
*/
public function setUrl($url, $mixed_data = '')
{
$built_url = $this->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);
}
/**
* Set User Agent
*
* @access public
* @param $user_agent
*/
public function setUserAgent($user_agent)
{
$this->setOpt(CURLOPT_USERAGENT, $user_agent);
}
/**
* Attempt Retry
*
* @access public
*/
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;
}
/**
* Success
*
* @access public
* @param $callback
*/
public function success($callback)
{
$this->successCallback = $callback;
}
/**
* Unset Header
*
* Remove extra header previously set using Curl::setHeader().
*
* @access public
* @param $key
*/
public function unsetHeader($key)
{
unset($this->headers[$key]);
$headers = array();
foreach ($this->headers as $key => $value) {
$headers[] = $key . ': ' . $value;
}
$this->setOpt(CURLOPT_HTTPHEADER, $headers);
}
/**
* Remove Header
*
* Remove an internal header from the request.
* Using `curl -H "Host:" ...' is equivalent to $curl->removeHeader('Host');.
*
* @access public
* @param $key
*/
public function removeHeader($key)
{
$this->setHeader($key, '');
}
/**
* Verbose
*
* @access public
* @param bool $on
* @param resource $output
*/
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);
}
/**
* Reset
*
* @access public
*/
public function reset()
{
if (function_exists('curl_reset') && is_resource($this->curl)) {
curl_reset($this->curl);
} else {
$this->curl = curl_init();
}
$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 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
*
* @access public
*/
public function __destruct()
{
$this->close();
}
public function __get($name)
{
$return = null;
if (in_array($name, self::$deferredProperties) && is_callable(array($this, $getter = '__get_' . $name))) {
$return = $this->$name = $this->$getter();
}
return $return;
}
/**
* Get Effective Url
*
* @access private
*/
private function __get_effectiveUrl()
{
return $this->getInfo(CURLINFO_EFFECTIVE_URL);
}
/**
* Get RFC 2616
*
* @access private
*/
private function __get_rfc2616()
{
return array_fill_keys(self::$RFC2616, true);
}
/**
* Get RFC 6265
*
* @access private
*/
private function __get_rfc6265()
{
return array_fill_keys(self::$RFC6265, true);
}
/**
* Get Total Time
*
* @access private
*/
private function __get_totalTime()
{
return $this->getInfo(CURLINFO_TOTAL_TIME);
}
/**
* Build Cookies
*
* @access private
*/
private function buildCookies()
{
// Avoid using http_build_query() as unnecessary encoding is performed.
// http_build_query($this->cookies, '', '; ');
$this->setOpt(CURLOPT_COOKIE, implode('; ', array_map(function ($k, $v) {
return $k . '=' . $v;
}, array_keys($this->cookies), array_values($this->cookies))));
}
/**
* Build Url
*
* @access private
* @param $url
* @param $mixed_data
*
* @return string
*/
private function buildUrl($url, $mixed_data = '')
{
$query_string = '';
if (!empty($mixed_data)) {
$query_mark = strpos($url, '?') > 0 ? '&' : '?';
if (is_string($mixed_data)) {
$query_string .= $query_mark . $mixed_data;
} elseif (is_array($mixed_data)) {
$query_string .= $query_mark . http_build_query($mixed_data, '', '&');
}
}
return $url . $query_string;
}
/**
* Download Complete
*
* @access private
* @param $fh
*/
private function downloadComplete($fh)
{
if ($this->error && is_file($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')) {
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);
}
/**
* 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++) {
if (strpos($raw_headers[$i], ':') !== false) {
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 \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
*
* @access private
* @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.
*/
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);
}
}
}
return $response;
}
/**
* Parse Response Headers
*
* @access private
* @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 (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
*
* @access private
* @param $key
* @param $value
*/
private function setEncodedCookie($key, $value)
{
$name_chars = array();
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 = array();
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
*
* @access private
* @param $base_url
*/
private function initialize($base_url = null)
{
$this->id = uniqid('', true);
$this->setDefaultUserAgent();
$this->setDefaultTimeout();
$this->setOpt(CURLINFO_HEADER_OUT, true);
// Create a placeholder to temporarily store the header callback data.
$header_callback_data = new \stdClass();
$header_callback_data->rawResponseHeaders = '';
$header_callback_data->responseCookies = array();
$this->headerCallbackData = $header_callback_data;
$this->setOpt(CURLOPT_HEADERFUNCTION, createHeaderCallback($header_callback_data));
$this->setOpt(CURLOPT_RETURNTRANSFER, true);
$this->headers = new CaseInsensitiveArray();
$this->setUrl($base_url);
}
}
/**
* 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");
}
$header_callback_data->rawResponseHeaders .= $header;
return strlen($header);
};
}
<?php
namespace Curl;
class Decoder
{
/**
* Decode JSON
*
* @access public
* @param $json
* @param $assoc
* @param $depth
* @param $options
*/
public static function decodeJson()
{
$args = func_get_args();
// Call json_decode() without the $options parameter in PHP
// versions less than 5.4.0 as the $options parameter was added in
// PHP version 5.4.0.
if (version_compare(PHP_VERSION, '5.4.0', '<')) {
$args = array_slice($args, 0, 3);
}
$response = call_user_func_array('json_decode', $args);
if ($response === null) {
$response = $args['0'];
}
return $response;
}
/**
* Decode XML
*
* @access public
* @param $data
* @param $class_name
* @param $options
* @param $ns
* @param $is_prefix
*/
public static function decodeXml()
{
$args = func_get_args();
$response = @call_user_func_array('simplexml_load_string', $args);
if ($response === false) {
$response = $args['0'];
}
return $response;
}
}
<?php
namespace Curl;
class Encoder
{
/**
* Encode JSON
*
* Wrap json_encode() to throw error when the value being encoded fails.
*
* @access public
* @param $value
* @param $options
* @param $depth
*
* @return string
* @throws \ErrorException
*/
public static function encodeJson()
{
$args = func_get_args();
// Call json_encode() without the $depth parameter in PHP
// versions less than 5.5.0 as the $depth parameter was added in
// PHP version 5.5.0.
if (version_compare(PHP_VERSION, '5.5.0', '<')) {
$args = array_slice($args, 0, 2);
}
$value = call_user_func_array('json_encode', $args);
if (!(json_last_error() === JSON_ERROR_NONE)) {
if (function_exists('json_last_error_msg')) {
$error_message = 'json_encode error: ' . json_last_error_msg();
} else {
$error_message = 'json_encode error';
}
throw new \ErrorException($error_message);
}
return $value;
}
}
<?php
namespace Curl;
use Curl\ArrayUtil;
class MultiCurl
{
public $baseUrl = null;
public $multiCurl;
private $curls = array();
private $activeCurls = array();
private $isStarted = false;
private $concurrency = 25;
private $nextCurlId = 0;
private $beforeSendCallback = null;
private $successCallback = null;
private $errorCallback = null;
private $completeCallback = null;
private $retry = null;
private $cookies = array();
private $headers = array();
private $options = array();
private $proxies = null;
private $jsonDecoder = null;
private $xmlDecoder = null;
/**
* Construct
*
* @access public
* @param $base_url
*/
public function __construct($base_url = null)
{
$this->multiCurl = curl_multi_init();
$this->headers = new CaseInsensitiveArray();
$this->setUrl($base_url);
}
/**
* Add Delete
*
* @access public
* @param $url
* @param $query_parameters
* @param $data
*
* @return object
*/
public function addDelete($url, $query_parameters = array(), $data = array())
{
if (is_array($url)) {
$data = $query_parameters;
$query_parameters = $url;
$url = $this->baseUrl;
}
$curl = new Curl();
$this->queueHandle($curl);
$curl->setUrl($url, $query_parameters);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'DELETE');
$curl->setOpt(CURLOPT_POSTFIELDS, $curl->buildPostData($data));
return $curl;
}
/**
* Add Download
*
* @access public
* @param $url
* @param $mixed_filename
*
* @return object
*/
public function addDownload($url, $mixed_filename)
{
$curl = new Curl();
$this->queueHandle($curl);
$curl->setUrl($url);
// Use tmpfile() or php://temp to avoid "Too many open files" error.
if (is_callable($mixed_filename)) {
$callback = $mixed_filename;
$curl->downloadCompleteCallback = $callback;
$curl->fileHandle = tmpfile();
} else {
$filename = $mixed_filename;
$curl->downloadCompleteCallback = function ($instance, $fh) use ($filename) {
file_put_contents($filename, stream_get_contents($fh));
};
$curl->fileHandle = fopen('php://temp', 'wb');
}
$curl->setOpt(CURLOPT_FILE, $curl->fileHandle);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'GET');
$curl->setOpt(CURLOPT_HTTPGET, true);
return $curl;
}
/**
* Add Get
*
* @access public
* @param $url
* @param $data
*
* @return object
*/
public function addGet($url, $data = array())
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl();
$this->queueHandle($curl);
$curl->setUrl($url, $data);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'GET');
$curl->setOpt(CURLOPT_HTTPGET, true);
return $curl;
}
/**
* Add Head
*
* @access public
* @param $url
* @param $data
*
* @return object
*/
public function addHead($url, $data = array())
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl();
$this->queueHandle($curl);
$curl->setUrl($url, $data);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'HEAD');
$curl->setOpt(CURLOPT_NOBODY, true);
return $curl;
}
/**
* Add Options
*
* @access public
* @param $url
* @param $data
*
* @return object
*/
public function addOptions($url, $data = array())
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl();
$this->queueHandle($curl);
$curl->setUrl($url, $data);
$curl->removeHeader('Content-Length');
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'OPTIONS');
return $curl;
}
/**
* Add Patch
*
* @access public
* @param $url
* @param $data
*
* @return object
*/
public function addPatch($url, $data = array())
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl();
if (is_array($data) && empty($data)) {
$curl->removeHeader('Content-Length');
}
$this->queueHandle($curl);
$curl->setUrl($url);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'PATCH');
$curl->setOpt(CURLOPT_POSTFIELDS, $curl->buildPostData($data));
return $curl;
}
/**
* Add Post
*
* @access public
* @param $url
* @param $data
* @param $follow_303_with_post
* If true, will cause 303 redirections to be followed using GET requests (default: false).
* Note: Redirections are only followed if the CURLOPT_FOLLOWLOCATION option is set to true.
*
* @return object
*/
public function addPost($url, $data = '', $follow_303_with_post = false)
{
if (is_array($url)) {
$follow_303_with_post = (bool)$data;
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl();
$this->queueHandle($curl);
if (is_array($data) && empty($data)) {
$curl->removeHeader('Content-Length');
}
$curl->setUrl($url);
/*
* For post-redirect-get requests, the CURLOPT_CUSTOMREQUEST option must not
* be set, otherwise cURL will perform POST requests for redirections.
*/
if (!$follow_303_with_post) {
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
}
$curl->setOpt(CURLOPT_POST, true);
$curl->setOpt(CURLOPT_POSTFIELDS, $curl->buildPostData($data));
return $curl;
}
/**
* Add Put
*
* @access public
* @param $url
* @param $data
*
* @return object
*/
public function addPut($url, $data = array())
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl();
$this->queueHandle($curl);
$curl->setUrl($url);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'PUT');
$put_data = $curl->buildPostData($data);
if (is_string($put_data)) {
$curl->setHeader('Content-Length', strlen($put_data));
}
$curl->setOpt(CURLOPT_POSTFIELDS, $put_data);
return $curl;
}
/**
* Add Search
*
* @access public
* @param $url
* @param $data
*
* @return object
*/
public function addSearch($url, $data = array())
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl();
$this->queueHandle($curl);
$curl->setUrl($url);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'SEARCH');
$put_data = $curl->buildPostData($data);
if (is_string($put_data)) {
$curl->setHeader('Content-Length', strlen($put_data));
}
$curl->setOpt(CURLOPT_POSTFIELDS, $put_data);
return $curl;
}
/**
* Add Curl
*
* Add a Curl instance to the handle queue.
*
* @access public
* @param $curl
*
* @return object
*/
public function addCurl(Curl $curl)
{
$this->queueHandle($curl);
return $curl;
}
/**
* Before Send
*
* @access public
* @param $callback
*/
public function beforeSend($callback)
{
$this->beforeSendCallback = $callback;
}
/**
* Close
*
* @access public
*/
public function close()
{
foreach ($this->curls as $curl) {
$curl->close();
}
if (is_resource($this->multiCurl)) {
curl_multi_close($this->multiCurl);
}
}
/**
* Complete
*
* @access public
* @param $callback
*/
public function complete($callback)
{
$this->completeCallback = $callback;
}
/**
* Error
*
* @access public
* @param $callback
*/
public function error($callback)
{
$this->errorCallback = $callback;
}
/**
* Get Opt
*
* @access public
* @param $option
*
* @return mixed
*/
public function getOpt($option)
{
return isset($this->options[$option]) ? $this->options[$option] : null;
}
/**
* 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 Concurrency
*
* @access public
* @param $concurrency
*/
public function setConcurrency($concurrency)
{
$this->concurrency = $concurrency;
}
/**
* 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;
}
/**
* Set Cookies
*
* @access public
* @param $cookies
*/
public function setCookies($cookies)
{
foreach ($cookies as $key => $value) {
$this->cookies[$key] = $value;
}
}
/**
* 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 String
*
* @access public
* @param $string
*/
public function setCookieString($string)
{
$this->setOpt(CURLOPT_COOKIE, $string);
}
/**
* 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 Header
*
* Add extra header to include in the request.
*
* @access public
* @param $key
* @param $value
*/
public function setHeader($key, $value)
{
$this->headers[$key] = $value;
$this->updateHeaders();
}
/**
* Set Headers
*
* Add extra headers to include in the request.
*
* @access public
* @param $headers
*/
public function setHeaders($headers)
{
foreach ($headers as $key => $value) {
$this->headers[$key] = $value;
}
$this->updateHeaders();
}
/**
* Set JSON Decoder
*
* @access public
* @param $mixed boolean|callable
*/
public function setJsonDecoder($mixed)
{
if ($mixed === false) {
$this->jsonDecoder = false;
} elseif (is_callable($mixed)) {
$this->jsonDecoder = $mixed;
}
}
/**
* Set XML Decoder
*
* @access public
* @param $mixed boolean|callable
*/
public function setXmlDecoder($mixed)
{
if ($mixed === false) {
$this->xmlDecoder = false;
} elseif (is_callable($mixed)) {
$this->xmlDecoder = $mixed;
}
}
/**
* Set Proxy
*
* Set an HTTP proxy to tunnel requests through.
*
* @access public
* @param $proxy - The HTTP proxy to tunnel requests through. May include port number.
* @param $port - The port number of the proxy to connect to. This port number can also be set in $proxy.
* @param $username - The username to use for the connection to the proxy.
* @param $password - The password to use for the connection to the proxy.
*/
public function setProxy($proxy, $port = null, $username = null, $password = null)
{
$this->setOpt(CURLOPT_PROXY, $proxy);
if ($port !== null) {
$this->setOpt(CURLOPT_PROXYPORT, $port);
}
if ($username !== null && $password !== null) {
$this->setOpt(CURLOPT_PROXYUSERPWD, $username . ':' . $password);
}
}
/**
* Set Proxies
*
* Set proxies to tunnel requests through. When set, a random proxy will be
* used for the request.
*
* @access public
* @param $proxies array - A list of HTTP proxies to tunnel requests
* through. May include port number.
*/
public function setProxies($proxies)
{
$this->proxies = $proxies;
}
/**
* Set Proxy Auth
*
* Set the HTTP authentication method(s) to use for the proxy connection.
*
* @access public
* @param $auth
*/
public function setProxyAuth($auth)
{
$this->setOpt(CURLOPT_PROXYAUTH, $auth);
}
/**
* Set Proxy Type
*
* Set the proxy protocol type.
*
* @access public
* @param $type
*/
public function setProxyType($type)
{
$this->setOpt(CURLOPT_PROXYTYPE, $type);
}
/**
* Set Proxy Tunnel
*
* Set the proxy to tunnel through HTTP proxy.
*
* @access public
* @param $tunnel boolean
*/
public function setProxyTunnel($tunnel = true)
{
$this->setOpt(CURLOPT_HTTPPROXYTUNNEL, $tunnel);
}
/**
* Unset Proxy
*
* Disable use of the proxy.
*
* @access public
*/
public function unsetProxy()
{
$this->setOpt(CURLOPT_PROXY, null);
}
/**
* Set Opt
*
* @access public
* @param $option
* @param $value
*/
public function setOpt($option, $value)
{
$this->options[$option] = $value;
}
/**
* Set Opts
*
* @access public
* @param $options
*/
public function setOpts($options)
{
foreach ($options as $option => $value) {
$this->setOpt($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 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.
*
* @access public
* @param $mixed
*/
public function setRetry($mixed)
{
$this->retry = $mixed;
}
/**
* Set Timeout
*
* @access public
* @param $seconds
*/
public function setTimeout($seconds)
{
$this->setOpt(CURLOPT_TIMEOUT, $seconds);
}
/**
* Set Url
*
* @access public
* @param $url
*/
public function setUrl($url)
{
$this->baseUrl = $url;
}
/**
* Set User Agent
*
* @access public
* @param $user_agent
*/
public function setUserAgent($user_agent)
{
$this->setOpt(CURLOPT_USERAGENT, $user_agent);
}
/**
* Start
*
* @access public
*/
public function start()
{
if ($this->isStarted) {
return;
}
$this->isStarted = true;
$concurrency = $this->concurrency;
if ($concurrency > count($this->curls)) {
$concurrency = count($this->curls);
}
for ($i = 0; $i < $concurrency; $i++) {
$this->initHandle(array_shift($this->curls));
}
do {
// Wait for activity on any curl_multi connection when curl_multi_select (libcurl) fails to correctly block.
// https://bugs.php.net/bug.php?id=63411
if (curl_multi_select($this->multiCurl) === -1) {
usleep(100000);
}
curl_multi_exec($this->multiCurl, $active);
while (!($info_array = curl_multi_info_read($this->multiCurl)) === false) {
if ($info_array['msg'] === CURLMSG_DONE) {
foreach ($this->activeCurls as $key => $curl) {
if ($curl->curl === $info_array['handle']) {
// Set the error code for multi handles using the "result" key in the array returned by
// curl_multi_info_read(). Using curl_errno() on a multi handle will incorrectly return 0
// for errors.
$curl->curlErrorCode = $info_array['result'];
$curl->exec($curl->curl);
if ($curl->attemptRetry()) {
// Remove completed handle before adding again in order to retry request.
curl_multi_remove_handle($this->multiCurl, $curl->curl);
$curlm_error_code = curl_multi_add_handle($this->multiCurl, $curl->curl);
if (!($curlm_error_code === CURLM_OK)) {
throw new \ErrorException(
'cURL multi add handle error: ' . curl_multi_strerror($curlm_error_code)
);
}
} else {
$curl->execDone();
// Remove completed instance from active curls.
unset($this->activeCurls[$key]);
// Start new requests before removing the handle of the completed one.
while (count($this->curls) >= 1 && count($this->activeCurls) < $this->concurrency) {
$this->initHandle(array_shift($this->curls));
}
curl_multi_remove_handle($this->multiCurl, $curl->curl);
// Clean up completed instance.
$curl->close();
}
break;
}
}
}
}
if (!$active) {
$active = count($this->activeCurls);
}
} while ($active > 0);
$this->isStarted = false;
}
/**
* Success
*
* @access public
* @param $callback
*/
public function success($callback)
{
$this->successCallback = $callback;
}
/**
* Unset Header
*
* Remove extra header previously set using Curl::setHeader().
*
* @access public
* @param $key
*/
public function unsetHeader($key)
{
unset($this->headers[$key]);
}
/**
* Remove Header
*
* Remove an internal header from the request.
* Using `curl -H "Host:" ...' is equivalent to $curl->removeHeader('Host');.
*
* @access public
* @param $key
*/
public function removeHeader($key)
{
$this->setHeader($key, '');
}
/**
* Verbose
*
* @access public
* @param bool $on
* @param resource $output
*/
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();
}
/**
* Update Headers
*
* @access private
*/
private function updateHeaders()
{
foreach ($this->curls as $curl) {
$curl->setHeaders($this->headers);
}
}
/**
* Queue Handle
*
* @access private
* @param $curl
*/
private function queueHandle($curl)
{
// Use sequential ids to allow for ordered post processing.
$curl->id = $this->nextCurlId++;
$curl->childOfMultiCurl = true;
$this->curls[$curl->id] = $curl;
$curl->setHeaders($this->headers);
}
/**
* Init Handle
*
* @access private
* @param $curl
* @throws \ErrorException
*/
private function initHandle($curl)
{
// Set callbacks if not already individually set.
if ($curl->beforeSendCallback === null) {
$curl->beforeSend($this->beforeSendCallback);
}
if ($curl->successCallback === null) {
$curl->success($this->successCallback);
}
if ($curl->errorCallback === null) {
$curl->error($this->errorCallback);
}
if ($curl->completeCallback === null) {
$curl->complete($this->completeCallback);
}
// Set decoders if not already individually set.
if ($curl->jsonDecoder === null) {
$curl->setJsonDecoder($this->jsonDecoder);
}
if ($curl->xmlDecoder === null) {
$curl->setXmlDecoder($this->xmlDecoder);
}
$curl->setOpts($this->options);
$curl->setRetry($this->retry);
$curl->setCookies($this->cookies);
// Use a random proxy for the curl instance when proxies have been set
// and the curl instance doesn't already have a proxy set.
if (is_array($this->proxies) && $curl->getOpt(CURLOPT_PROXY) === null) {
$random_proxy = ArrayUtil::array_random($this->proxies);
$curl->setProxy($random_proxy);
}
$curlm_error_code = curl_multi_add_handle($this->multiCurl, $curl->curl);
if (!($curlm_error_code === CURLM_OK)) {
throw new \ErrorException('cURL multi add handle error: ' . curl_multi_strerror($curlm_error_code));
}
$this->activeCurls[$curl->id] = $curl;
$curl->call($curl->beforeSendCallback);
}
}
<?php
namespace Curl;
class StringUtil
{
public static function characterReversePosition($haystack, $needle, $part = false)
{
if (function_exists('\mb_strrchr')) {
return \mb_strrchr($haystack, $needle, $part);
} else {
return \strrchr($haystack, $needle);
}
}
public static function length($string)
{
if (function_exists('\mb_strlen')) {
return \mb_strlen($string);
} else {
return \strlen($string);
}
}
public static function position($haystack, $needle, $offset = 0)
{
if (function_exists('\mb_strpos')) {
return \mb_strpos($haystack, $needle, $offset);
} else {
return \strpos($haystack, $needle, $offset);
}
}
public static function reversePosition($haystack, $needle, $offset = 0)
{
if (function_exists('\mb_strrpos')) {
return \mb_strrpos($haystack, $needle, $offset);
} else {
return \strrpos($haystack, $needle, $offset);
}
}
/**
* Return true when $haystack starts with $needle.
*
* @access public
* @param $haystack
* @param $needle
*
* @return bool
*/
public static function startsWith($haystack, $needle)
{
return self::substring($haystack, 0, self::length($needle)) === $needle;
}
public static function substring($string, $start, $length)
{
if (function_exists('\mb_substr')) {
return \mb_substr($string, $start, $length);
} else {
return \substr($string, $start, $length);
}
}
}
<?php
namespace Curl;
use Curl\StringUtil;
class Url
{
private $baseUrl = null;
private $relativeUrl = null;
public function __construct($base_url, $relative_url = null)
{
$this->baseUrl = $base_url;
$this->relativeUrl = $relative_url;
}
public function __toString()
{
return $this->absolutizeUrl();
}
/**
* Remove dot segments.
*
* Interpret and remove the special "." and ".." path segments from a referenced path.
*/
public static function removeDotSegments($input)
{
// 1. The input buffer is initialized with the now-appended path
// components and the output buffer is initialized to the empty
// string.
$output = '';
// 2. While the input buffer is not empty, loop as follows:
while (!empty($input)) {
// A. If the input buffer begins with a prefix of "../" or "./",
// then remove that prefix from the input buffer; otherwise,
if (StringUtil::startsWith($input, '../')) {
$input = substr($input, 3);
} elseif (StringUtil::startsWith($input, './')) {
$input = substr($input, 2);
// B. if the input buffer begins with a prefix of "/./" or "/.",
// where "." is a complete path segment, then replace that
// prefix with "/" in the input buffer; otherwise,
} elseif (StringUtil::startsWith($input, '/./')) {
$input = substr($input, 2);
} elseif ($input === '/.') {
$input = '/';
// C. if the input buffer begins with a prefix of "/../" or "/..",
// where ".." is a complete path segment, then replace that
// prefix with "/" in the input buffer and remove the last
// segment and its preceding "/" (if any) from the output
// buffer; otherwise,
} elseif (StringUtil::startsWith($input, '/../')) {
$input = substr($input, 3);
$output = substr_replace($output, '', StringUtil::reversePosition($output, '/'));
} elseif ($input === '/..') {
$input = '/';
$output = substr_replace($output, '', StringUtil::reversePosition($output, '/'));
// D. if the input buffer consists only of "." or "..", then remove
// that from the input buffer; otherwise,
} elseif ($input === '.' || $input === '..') {
$input = '';
// E. move the first path segment in the input buffer to the end of
// the output buffer, including the initial "/" character (if
// any) and any subsequent characters up to, but not including,
// the next "/" character or the end of the input buffer.
} elseif (!(($pos = StringUtil::position($input, '/', 1)) === false)) {
$output .= substr($input, 0, $pos);
$input = substr_replace($input, '', 0, $pos);
} else {
$output .= $input;
$input = '';
}
}
// 3. Finally, the output buffer is returned as the result of
// remove_dot_segments.
return $output . $input;
}
/**
* Absolutize url.
*
* Combine the base and relative url into an absolute url.
*/
private function absolutizeUrl()
{
$b = $this->parseUrl($this->baseUrl);
if (!isset($b['path'])) {
$b['path'] = '/';
}
if ($this->relativeUrl === null) {
return $this->unparseUrl($b);
}
$r = $this->parseUrl($this->relativeUrl);
$r['authorized'] = isset($r['scheme']) || isset($r['host']) || isset($r['port'])
|| isset($r['user']) || isset($r['pass']);
$target = array();
if (isset($r['scheme'])) {
$target['scheme'] = $r['scheme'];
$target['host'] = isset($r['host']) ? $r['host'] : null;
$target['port'] = isset($r['port']) ? $r['port'] : null;
$target['user'] = isset($r['user']) ? $r['user'] : null;
$target['pass'] = isset($r['pass']) ? $r['pass'] : null;
$target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null;
$target['query'] = isset($r['query']) ? $r['query'] : null;
} else {
$target['scheme'] = isset($b['scheme']) ? $b['scheme'] : null;
if ($r['authorized']) {
$target['host'] = isset($r['host']) ? $r['host'] : null;
$target['port'] = isset($r['port']) ? $r['port'] : null;
$target['user'] = isset($r['user']) ? $r['user'] : null;
$target['pass'] = isset($r['pass']) ? $r['pass'] : null;
$target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null;
$target['query'] = isset($r['query']) ? $r['query'] : null;
} else {
$target['host'] = isset($b['host']) ? $b['host'] : null;
$target['port'] = isset($b['port']) ? $b['port'] : null;
$target['user'] = isset($b['user']) ? $b['user'] : null;
$target['pass'] = isset($b['pass']) ? $b['pass'] : null;
if (!isset($r['path']) || $r['path'] === '') {
$target['path'] = $b['path'];
$target['query'] = isset($r['query']) ? $r['query'] : (isset($b['query']) ? $b['query'] : null);
} else {
if (StringUtil::startsWith($r['path'], '/')) {
$target['path'] = self::removeDotSegments($r['path']);
} else {
$base = StringUtil::characterReversePosition($b['path'], '/', true);
if ($base === false) {
$base = '';
}
$target['path'] = self::removeDotSegments($base . '/' . $r['path']);
}
$target['query'] = isset($r['query']) ? $r['query'] : null;
}
}
}
if ($this->relativeUrl === '') {
$target['fragment'] = isset($b['fragment']) ? $b['fragment'] : null;
} else {
$target['fragment'] = isset($r['fragment']) ? $r['fragment'] : null;
}
$absolutized_url = $this->unparseUrl($target);
return $absolutized_url;
}
/**
* Parse url.
*
* Parse url into components of a URI as specified by RFC 3986.
*/
private function parseUrl($url)
{
// ALPHA = A-Z / a-z
$alpha = 'A-Za-z';
// DIGIT = 0-9
$digit = '0-9';
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
$unreserved = $alpha . $digit . preg_quote('-._~');
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
// / "*" / "+" / "," / ";" / "=" / "#"
$sub_delims = preg_quote('!$&\'()*+,;=#');
// HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
$hexdig = $digit . 'A-F';
// "The uppercase hexadecimal digits 'A' through 'F' are equivalent to
// the lowercase digits 'a' through 'f', respectively."
$hexdig .= 'a-f';
$pattern = '/(?:[^' . $unreserved . $sub_delims . preg_quote(':@%/?', '/') . ']++|%(?![' . $hexdig . ']{2}))/';
$url = preg_replace_callback(
$pattern,
function ($matches) {
return rawurlencode($matches[0]);
},
$url
);
return parse_url($url);
}
/**
* Unparse url.
*
* Combine url components into a url.
*/
private function unparseUrl($parsed_url)
{
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? $pass . '@' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
$unparsed_url = $scheme . $user . $pass . $host . $port . $path . $query . $fragment;
return $unparsed_url;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment