This commit is contained in:
gaofeng
2026-05-12 18:27:28 +08:00
commit 6d9aee81aa
3664 changed files with 274415 additions and 0 deletions

View File

@@ -0,0 +1,350 @@
# 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.
<!-- CHANGELOG_PLACEHOLDER -->
## 12.0.0 - 2025-03-25
- Drop support for PHP 7.4 ([#937](https://github.com/php-curl-class/php-curl-class/pull/937))
## 11.1.0 - 2025-03-24
- Add methods like Curl::setGet() for each HTTP request method ([#936](https://github.com/php-curl-class/php-curl-class/pull/936))
## 11.0.5 - 2025-03-11
- Fix PHPStan static analysis errors ([#929](https://github.com/php-curl-class/php-curl-class/pull/929))
## 11.0.4 - 2025-02-20
- Increase psalm strictness ([#925](https://github.com/php-curl-class/php-curl-class/pull/925))
## 11.0.3 - 2025-02-19
- Use default for Psalm ensureOverrideAttribute ([#923](https://github.com/php-curl-class/php-curl-class/pull/923))
## 11.0.2 - 2025-02-18
- Fix CI (PHPUnit) ([#918](https://github.com/php-curl-class/php-curl-class/pull/918))
## 11.0.1 - 2025-01-13
- Increase Psalm strictness ([#909](https://github.com/php-curl-class/php-curl-class/pull/909))
- Increase PHPStan strictness ([#908](https://github.com/php-curl-class/php-curl-class/pull/908))
## 11.0.0 - 2024-08-22
- Drop support for PHP 7.3 ([#889](https://github.com/php-curl-class/php-curl-class/pull/889))
- Drop support for PHP 7.2 ([#888](https://github.com/php-curl-class/php-curl-class/pull/888))
- Drop support for PHP 7.1 ([#887](https://github.com/php-curl-class/php-curl-class/pull/887))
## 10.0.1 - 2024-08-21
- Use nullable type declaration ([#882](https://github.com/php-curl-class/php-curl-class/pull/882))
## 10.0.0 - 2024-08-20
- Drop support for PHP 7.0 ([#880](https://github.com/php-curl-class/php-curl-class/pull/880))
- Add public method getActiveCurls ([#871](https://github.com/php-curl-class/php-curl-class/pull/871))
## 9.19.2 - 2024-04-09
- Fix CI: Use nullable type declaration ([#859](https://github.com/php-curl-class/php-curl-class/pull/859))
## 9.19.1 - 2024-02-27
- Fix afterSend not being called ([#848](https://github.com/php-curl-class/php-curl-class/pull/848))
## 9.19.0 - 2024-01-18
- Allow displaying curl option value without specifying value ([#837](https://github.com/php-curl-class/php-curl-class/pull/837))
## 9.18.2 - 2023-09-11
- Fix use of mb_strpos() causing error when polyfill is used ([#813](https://github.com/php-curl-class/php-curl-class/pull/813))
## 9.18.1 - 2023-08-29
- Add additional check for decoding gzip-encoded responses ([#808](https://github.com/php-curl-class/php-curl-class/pull/808))
## 9.18.0 - 2023-08-28
- Implement Curl::setError() and MultiCurl::setError() ([#805](https://github.com/php-curl-class/php-curl-class/pull/805))
- Rename ::setError() to ::afterSend() ([#807](https://github.com/php-curl-class/php-curl-class/pull/807))
## 9.17.4 - 2023-07-10
- Add coding standards rule to use the null coalescing operator ?? where possible ([#801](https://github.com/php-curl-class/php-curl-class/pull/801))
- Replace isset with null coalescing operator ([#800](https://github.com/php-curl-class/php-curl-class/pull/800))
## 9.17.3 - 2023-07-04
- Update PHP_CodeSniffer ruleset: PSR2 → PSR12 ([#797](https://github.com/php-curl-class/php-curl-class/pull/797))
- Add additional coding standard checks ([#796](https://github.com/php-curl-class/php-curl-class/pull/796))
## 9.17.2 - 2023-06-27
- Use short array syntax ([#793](https://github.com/php-curl-class/php-curl-class/pull/793))
- Add PHP-CS-Fixer to check for unused imports ([#794](https://github.com/php-curl-class/php-curl-class/pull/794))
- Replace `uniqid` by `random_bytes` ([#792](https://github.com/php-curl-class/php-curl-class/pull/792))
## 9.17.1 - 2023-06-14
- Improve and add tests for Curl::fastDownload() ([#791](https://github.com/php-curl-class/php-curl-class/pull/791))
## 9.17.0 - 2023-06-13
- Make method to display curl option value public ([#790](https://github.com/php-curl-class/php-curl-class/pull/790))
## 9.16.1 - 2023-06-12
- Differentiate between internal options and user-set options ([#788](https://github.com/php-curl-class/php-curl-class/pull/788))
- Create method to display a curl option value ([#785](https://github.com/php-curl-class/php-curl-class/pull/785))
- Fix existing header overwritten after using MultiCurl::addCurl() ([#787](https://github.com/php-curl-class/php-curl-class/pull/787))
## 9.16.0 - 2023-05-25
- Graduate Curl::fastDownload() ([#783](https://github.com/php-curl-class/php-curl-class/pull/783))
## 9.15.1 - 2023-05-24
- Fix PHP CodeSniffer errors ([#782](https://github.com/php-curl-class/php-curl-class/pull/782))
## 9.15.0 - 2023-05-22
- Update Curl::diagnose() to detect bit flags with negative values ([#781](https://github.com/php-curl-class/php-curl-class/pull/781))
- Display bit flags in use when calling Curl::diagnose() ([#779](https://github.com/php-curl-class/php-curl-class/pull/779))
## 9.14.5 - 2023-05-16
- Handle missing content-type response header in Curl::diagnose() ([#778](https://github.com/php-curl-class/php-curl-class/pull/778))
## 9.14.4 - 2023-05-08
- Update article in Curl::diagnose() Allow header warning ([#776](https://github.com/php-curl-class/php-curl-class/pull/776))
## 9.14.3 - 2023-03-13
- Remove use of array_merge() inside loop ([#774](https://github.com/php-curl-class/php-curl-class/pull/774))
## 9.14.2 - 2023-03-09
- Clean up: Reduce nesting ([#771](https://github.com/php-curl-class/php-curl-class/pull/771))
## 9.14.1 - 2023-02-27
- Remove coding standard ruleset exclusion ([#768](https://github.com/php-curl-class/php-curl-class/pull/768))
## 9.14.0 - 2023-02-26
- Make https:// and http:// the allowed request protocols by default ([#767](https://github.com/php-curl-class/php-curl-class/pull/767))
## 9.13.1 - 2023-01-16
- Allow uploads with CURLStringFile type ([#762](https://github.com/php-curl-class/php-curl-class/pull/762))
## 9.13.0 - 2023-01-13
- Implement abstract class BaseCurl for Curl and MultiCurl ([#759](https://github.com/php-curl-class/php-curl-class/pull/759))
- Display error messages found in Curl::diagnose() ([#758](https://github.com/php-curl-class/php-curl-class/pull/758))
- Fix Curl::diagnose() request type output for POST requests ([#757](https://github.com/php-curl-class/php-curl-class/pull/757))
## 9.12.6 - 2023-01-11
- Replace use of #[\AllowDynamicProperties] ([#756](https://github.com/php-curl-class/php-curl-class/pull/756))
- silence PHP 8.2 deprecation notices ([#754](https://github.com/php-curl-class/php-curl-class/pull/754))
## 9.12.5 - 2022-12-20
- Fix static analysis error ([#752](https://github.com/php-curl-class/php-curl-class/pull/752))
## 9.12.4 - 2022-12-17
- Exclude additional files from git archive ([#751](https://github.com/php-curl-class/php-curl-class/pull/751))
## 9.12.3 - 2022-12-13
- Ensure string response before gzip decode ([#749](https://github.com/php-curl-class/php-curl-class/pull/749))
## 9.12.2 - 2022-12-11
- Disable warning when gzip-decoding response errors ([#748](https://github.com/php-curl-class/php-curl-class/pull/748))
## 9.12.1 - 2022-12-08
- Include option constant that uses the CURLINFO_ prefix ([#745](https://github.com/php-curl-class/php-curl-class/pull/745))
## 9.12.0 - 2022-12-07
- Add automatic gzip decoding of response ([#744](https://github.com/php-curl-class/php-curl-class/pull/744))
## 9.11.1 - 2022-12-06
- change: remove unused namespace import ([#743](https://github.com/php-curl-class/php-curl-class/pull/743))
## 9.11.0 - 2022-12-05
- Add Curl::diagnose() HTTP method check matches methods allowed ([#741](https://github.com/php-curl-class/php-curl-class/pull/741))
- Add temporary fix missing template params ([#742](https://github.com/php-curl-class/php-curl-class/pull/742))
## 9.10.0 - 2022-11-07
- Display request options in Curl::diagnose() output ([#739](https://github.com/php-curl-class/php-curl-class/pull/739))
## 9.9.0 - 2022-11-06
- Fix MultiCurl::setCookieString() ([#738](https://github.com/php-curl-class/php-curl-class/pull/738))
- Pass MultiCurl options to new Curl instances earlier ([#737](https://github.com/php-curl-class/php-curl-class/pull/737))
- Add deferred constant curlErrorCodeConstants ([#736](https://github.com/php-curl-class/php-curl-class/pull/736))
## 9.8.0 - 2022-10-01
- Include curl error code constant in curl error message ([#733](https://github.com/php-curl-class/php-curl-class/pull/733))
## 9.7.0 - 2022-09-29
- Implement ArrayUtil::arrayRandomIndex() ([#732](https://github.com/php-curl-class/php-curl-class/pull/732))
## 9.6.3 - 2022-09-24
- Remove filter flag constants deprecated as of PHP 7.3 ([#730](https://github.com/php-curl-class/php-curl-class/pull/730))
## 9.6.2 - 2022-09-24
- Call MultiCurl::beforeSend() before each request is made ([#723](https://github.com/php-curl-class/php-curl-class/pull/723))
- Encode keys for post data with numeric keys ([#726](https://github.com/php-curl-class/php-curl-class/pull/726))
- Fix building post data with object ([#728](https://github.com/php-curl-class/php-curl-class/pull/728))
## 9.6.1 - 2022-06-30
### Fixed
- Attempt to stop active requests when `MultiCurl::stop()` is called
[#714](https://github.com/php-curl-class/php-curl-class/issues/714)
[#718](https://github.com/php-curl-class/php-curl-class/issues/718)
- Retain keys for arrays with null values when building post data
[#712](https://github.com/php-curl-class/php-curl-class/issues/712)
## 9.6.0 - 2022-03-17
### Added
- Method `MultiCurl::stop()` for stopping subsequent requests
[#708](https://github.com/php-curl-class/php-curl-class/issues/708)
## 9.5.1 - 2021-12-14
### Fixed
- Silence PHP 8.1 deprecations [#691](https://github.com/php-curl-class/php-curl-class/issues/691)
- Remove data parameter from additional request types
[#689](https://github.com/php-curl-class/php-curl-class/issues/689)
## 9.5.0 - 2021-11-21
### Added
- Method `Curl::setStop()` for stopping requests early without downloading the full response body
[#681](https://github.com/php-curl-class/php-curl-class/issues/681)
### Fixed
- Fixed constructing request url when using `MultiCurl::addPost()`
[#686](https://github.com/php-curl-class/php-curl-class/issues/686)
## 9.4.0 - 2021-09-04
### Changed
- Method `Url::parseUrl()` is now public
### Fixed
- Fix parsing schemeless urls [#679](https://github.com/php-curl-class/php-curl-class/issues/679)
## 9.3.1 - 2021-08-05
### Changed
- Enabled strict types (`declare(strict_types=1);`)
### Fixed
- Fixed `Curl::downloadFileName` not being set correctly
## 9.3.0 - 2021-07-23
### Added
- Method `Curl::diagnose()` for troubleshooting requests
## 9.2.0 - 2021-06-23
### Added
- Additional Curl::set\* and MultiCurl::set\* helper methods
```
Curl::setAutoReferer()
Curl::setAutoReferrer()
Curl::setFollowLocation()
Curl::setForbidReuse()
Curl::setMaximumRedirects()
MultiCurl::setAutoReferer()
MultiCurl::setAutoReferrer()
MultiCurl::setFollowLocation()
MultiCurl::setForbidReuse()
MultiCurl::setMaximumRedirects()
```
### Fixed
- Closing curl handles [#670](https://github.com/php-curl-class/php-curl-class/issues/670)
- Use of "$this" in non-object context [#671](https://github.com/php-curl-class/php-curl-class/pull/671)
## 9.1.0 - 2021-03-24
### Added
- Support for using relative urls with MultiCurl::add\*() methods [#628](https://github.com/php-curl-class/php-curl-class/issues/628)
## 9.0.0 - 2021-03-19
### Changed
- Use short array syntax
### Removed
- Support for PHP 5.3, 5.4, 5.5, and 5.6 [#380](https://github.com/php-curl-class/php-curl-class/issues/380)
## Manual Review
A manual review of changes is possible using 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/"
View only the source log and code changes between a release and the current checked-out commit:
$ git log 8.0.0...head "src/"
$ git diff 8.0.0...head "src/"

View File

@@ -0,0 +1,24 @@
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/>

View File

@@ -0,0 +1,408 @@
# PHP Curl Class: HTTP requests made easy
[![](https://img.shields.io/github/release/php-curl-class/php-curl-class.svg?style=for-the-badge&sort=semver)](https://github.com/php-curl-class/php-curl-class/releases/)
[![](https://img.shields.io/github/license/php-curl-class/php-curl-class.svg?style=for-the-badge)](https://github.com/php-curl-class/php-curl-class/blob/master/LICENSE)
[![](https://img.shields.io/github/actions/workflow/status/php-curl-class/php-curl-class/ci.yml?style=for-the-badge&label=build&branch=master)](https://github.com/php-curl-class/php-curl-class/actions/workflows/ci.yml)
[![](https://img.shields.io/github/actions/workflow/status/php-curl-class/php-curl-class/release.yml?style=for-the-badge&label=release&branch=master)](https://github.com/php-curl-class/php-curl-class/releases/)
[![](https://img.shields.io/github/actions/workflow/status/php-curl-class/php-curl-class/dependabot/dependabot-updates?style=for-the-badge&label=Dependabot&branch=master)](https://github.com/php-curl-class/php-curl-class/actions/workflows/dependabot/dependabot-updates)
[![](https://img.shields.io/packagist/dt/php-curl-class/php-curl-class.svg?style=for-the-badge)](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](#%EF%B8%8F-installation)
- [📋 Requirements](#-requirements)
- [🚀 Quick Start and Examples](#-quick-start-and-examples)
- [📖 Available Methods](#-available-methods)
- [🔒 Security](#-security)
- [🛠️ Troubleshooting](#%EF%B8%8F-troubleshooting)
- [🧪 Testing](#-testing)
- [🤝 Contributing](#-contributing)
---
### ⚙️ Installation
To install PHP Curl Class, run the following command:
composer require php-curl-class/php-curl-class
To install the latest commit version:
composer require php-curl-class/php-curl-class @dev
Installation instructions to use the `composer` command can be found on https://github.com/composer/composer.
### 📋 Requirements
PHP Curl Class works with PHP 8.4, 8.3, 8.2, 8.1, and 8.0.
### 🚀 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->errorMessage . "\n";
$curl->diagnose();
} 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', [
'q' => 'keyword',
]);
```
```php
$curl = new Curl();
$curl->post('https://www.example.com/login/', [
'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->errorMessage . "\n";
} else {
echo 'Response:' . "\n";
var_dump($curl->response);
}
var_dump($curl->requestHeaders);
var_dump($curl->responseHeaders);
```
```php
$curl = new Curl();
$curl->setFollowLocation();
$curl->get('https://shortn.example.com/bHbVsP');
```
```php
$curl = new Curl();
$curl->put('https://api.example.com/user/', [
'first_name' => 'Zach',
'last_name' => 'Borboa',
]);
```
```php
$curl = new Curl();
$curl->patch('https://api.example.com/profile/', [
'image' => '@path/to/file.jpg',
]);
```
```php
$curl = new Curl();
$curl->patch('https://api.example.com/profile/', [
'image' => new CURLFile('path/to/file.jpg'),
]);
```
```php
$curl = new Curl();
$curl->delete('https://api.example.com/user/', [
'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', [
'q' => 'hello world',
]);
$multi_curl->addGet('https://duckduckgo.com/', [
'q' => 'hello world',
]);
$multi_curl->addGet('https://www.bing.com/search', [
'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, $options = [])
Curl::__destruct()
Curl::__get($name)
Curl::__isset($name)
Curl::afterSend($callback)
Curl::attemptRetry()
Curl::beforeSend($callback)
Curl::buildPostData($data)
Curl::call()
Curl::close()
Curl::complete($callback)
Curl::delete($url, $query_parameters = [], $data = [])
Curl::diagnose($return = false)
Curl::disableTimeout()
Curl::displayCurlOptionValue($option, $value = null)
Curl::download($url, $mixed_filename)
Curl::error($callback)
Curl::exec($ch = null)
Curl::execDone()
Curl::fastDownload($url, $filename, $connections = 4)
Curl::get($url, $data = [])
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::getOptions()
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::getUserSetOptions()
Curl::getXmlDecoder()
Curl::head($url, $data = [])
Curl::isChildOfMultiCurl()
Curl::isCurlError()
Curl::isError()
Curl::isHttpError()
Curl::options($url, $data = [])
Curl::patch($url, $data = [])
Curl::post($url, $data = '', $follow_303_with_post = false)
Curl::progress($callback)
Curl::put($url, $data = [])
Curl::removeHeader($key)
Curl::reset()
Curl::search($url, $data = [])
Curl::setAutoReferer($auto_referer = true)
Curl::setAutoReferrer($auto_referrer = true)
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::setDefaultHeaderOut()
Curl::setDefaultJsonDecoder()
Curl::setDefaultTimeout()
Curl::setDefaultUserAgent()
Curl::setDefaultXmlDecoder()
Curl::setDelete($url, $query_parameters = [], $data = [])
Curl::setDigestAuthentication($username, $password = '')
Curl::setFile($file)
Curl::setFollowLocation($follow_location = true)
Curl::setForbidReuse($forbid_reuse = true)
Curl::setGet($url, $data = [])
Curl::setHead($url, $data = [])
Curl::setHeader($key, $value)
Curl::setHeaders($headers)
Curl::setInterface($interface)
Curl::setJsonDecoder($mixed)
Curl::setMaxFilesize($bytes)
Curl::setMaximumRedirects($maximum_redirects)
Curl::setOpt($option, $value)
Curl::setOptions($url, $data = [])
Curl::setOpts($options)
Curl::setPatch($url, $data = [])
Curl::setPort($port)
Curl::setPost($url, $data = '', $follow_303_with_post = false)
Curl::setProtocols($protocols)
Curl::setProxy($proxy, $port = null, $username = null, $password = null)
Curl::setProxyAuth($auth)
Curl::setProxyTunnel($tunnel = true)
Curl::setProxyType($type)
Curl::setPut($url, $data = [])
Curl::setRange($range)
Curl::setRedirectProtocols($redirect_protocols)
Curl::setReferer($referer)
Curl::setReferrer($referrer)
Curl::setRetry($mixed)
Curl::setSearch($url, $data = [])
Curl::setStop($callback = null)
Curl::setTimeout($seconds)
Curl::setUrl($url, $mixed_data = '')
Curl::setUserAgent($user_agent)
Curl::setXmlDecoder($mixed)
Curl::stop()
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 = [], $data = [])
MultiCurl::addDownload($url, $mixed_filename)
MultiCurl::addGet($url, $data = [])
MultiCurl::addHead($url, $data = [])
MultiCurl::addOptions($url, $data = [])
MultiCurl::addPatch($url, $data = [])
MultiCurl::addPost($url, $data = '', $follow_303_with_post = false)
MultiCurl::addPut($url, $data = [])
MultiCurl::addSearch($url, $data = [])
MultiCurl::afterSend($callback)
MultiCurl::beforeSend($callback)
MultiCurl::close()
MultiCurl::complete($callback)
MultiCurl::disableTimeout()
MultiCurl::error($callback)
MultiCurl::getActiveCurls()
MultiCurl::getOpt($option)
MultiCurl::removeHeader($key)
MultiCurl::setAutoReferer($auto_referer = true)
MultiCurl::setAutoReferrer($auto_referrer = true)
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::setFile($file)
MultiCurl::setFollowLocation($follow_location = true)
MultiCurl::setForbidReuse($forbid_reuse = true)
MultiCurl::setHeader($key, $value)
MultiCurl::setHeaders($headers)
MultiCurl::setInterface($interface)
MultiCurl::setJsonDecoder($mixed)
MultiCurl::setMaximumRedirects($maximum_redirects)
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::setRange($range)
MultiCurl::setRateLimit($rate_limit)
MultiCurl::setReferer($referer)
MultiCurl::setReferrer($referrer)
MultiCurl::setRequestTimeAccuracy()
MultiCurl::setRetry($mixed)
MultiCurl::setTimeout($seconds)
MultiCurl::setUrl($url, $mixed_data = '')
MultiCurl::setUserAgent($user_agent)
MultiCurl::setXmlDecoder($mixed)
MultiCurl::start()
MultiCurl::stop()
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 help troubleshooting.
### 🧪 Testing
See [TESTING](https://github.com/php-curl-class/php-curl-class/blob/master/TESTING.md) for testing information.
### 🤝 Contributing
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.

View File

@@ -0,0 +1,127 @@
# 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 = ['http', 'https']) {
$valid_url = filter_var($url, FILTER_VALIDATE_URL) !== 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.');
}
$curl = new Curl();
$curl->setProtocols(CURLPROTO_HTTPS);
$curl->setRedirectProtocols(CURLPROTO_HTTPS);
$curl->get($url);
```
### 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.
* Use `Curl::setProtocols()` and `Curl::setRedirectProtocols()` to restrict allowed protocols.
```php
// Allow only HTTPS protocols.
$curl->setProtocols(CURLPROTO_HTTPS);
$curl->setRedirectProtocols(CURLPROTO_HTTPS);
```
```php
// Allow HTTPS and HTTP protocols.
$curl->setProtocols(CURLPROTO_HTTPS | CURLPROTO_HTTP);
$curl->setRedirectProtocols(CURLPROTO_HTTPS | CURLPROTO_HTTP);
```
### 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/', [
'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.

View File

@@ -0,0 +1,53 @@
{
"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"
},
{
"name": "Contributors",
"homepage": "https://github.com/php-curl-class/php-curl-class/graphs/contributors"
}
],
"require": {
"php": ">=8.0",
"ext-curl": "*"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "*",
"ext-gd": "*",
"friendsofphp/php-cs-fixer": "*",
"phpcompatibility/php-compatibility": "dev-develop",
"phpcsstandards/phpcsutils": "@alpha",
"phpstan/phpstan": "*",
"phpunit/phpunit": "*",
"squizlabs/php_codesniffer": "*"
},
"suggest": {
"ext-mbstring": "*"
},
"autoload": {
"psr-4": {
"Curl\\": "src/Curl/"
}
},
"autoload-dev": {
"files": [
"./tests/Helper.php",
"./tests/User.php"
]
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

View File

@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace Curl;
class ArrayUtil
{
/**
* Is Array Assoc
*
* @param $array
* @return bool
*/
public static function isArrayAssoc($array)
{
return (
$array instanceof CaseInsensitiveArray ||
(bool)count(array_filter(array_keys($array), 'is_string'))
);
}
/**
* Is Array Assoc
*
* @deprecated Use ArrayUtil::isArrayAssoc().
* @param $array
* @return bool
*/
public static function is_array_assoc($array)
{
return static::isArrayAssoc($array);
}
/**
* Is Array Multidim
*
* @param $array
* @return bool
*/
public static function isArrayMultidim($array)
{
if (!is_array($array)) {
return false;
}
return (bool)count(array_filter($array, 'is_array'));
}
/**
* Is Array Multidim
*
* @deprecated Use ArrayUtil::isArrayMultidim().
* @param $array
* @return bool
*/
public static function is_array_multidim($array)
{
return static::isArrayMultidim($array);
}
/**
* Array Flatten Multidim
*
* @param $array
* @param $prefix
* @return array
*/
public static function arrayFlattenMultidim($array, $prefix = false)
{
$return = [];
if (is_array($array) || is_object($array)) {
if (empty($array)) {
$return[$prefix] = '';
} else {
$arrays_to_merge = [];
foreach ($array as $key => $value) {
if (is_scalar($value)) {
if ($prefix) {
$arrays_to_merge[] = [
$prefix . '[' . $key . ']' => $value,
];
} else {
$arrays_to_merge[] = [
$key => $value,
];
}
} elseif ($value instanceof \CURLFile) {
$arrays_to_merge[] = [
$key => $value,
];
} elseif ($value instanceof \CURLStringFile) {
$arrays_to_merge[] = [
$key => $value,
];
} else {
$arrays_to_merge[] = self::arrayFlattenMultidim(
$value,
$prefix ? $prefix . '[' . $key . ']' : $key
);
}
}
$return = array_merge($return, ...$arrays_to_merge);
}
} elseif ($array === null) {
$return[$prefix] = $array;
}
return $return;
}
/**
* Array Flatten Multidim
*
* @deprecated Use ArrayUtil::arrayFlattenMultidim().
* @param $array
* @param $prefix
* @return array
*/
public static function array_flatten_multidim($array, $prefix = false)
{
return static::arrayFlattenMultidim($array, $prefix);
}
/**
* Array Random
*
* @param $array
* @return mixed
*/
public static function arrayRandom($array)
{
return $array[static::arrayRandomIndex($array)];
}
/**
* Array Random Index
*
* @param $array
* @return int
*/
public static function arrayRandomIndex($array)
{
return mt_rand(0, count($array) - 1);
}
/**
* Array Random
*
* @deprecated Use ArrayUtil::arrayRandom().
* @param $array
* @return mixed
*/
public static function array_random($array)
{
return static::arrayRandom($array);
}
}

View File

@@ -0,0 +1,422 @@
<?php
declare(strict_types=1);
namespace Curl;
abstract class BaseCurl
{
public $beforeSendCallback = null;
public $afterSendCallback = null;
public $successCallback = null;
public $errorCallback = null;
public $completeCallback = null;
protected $options = [];
protected $userSetOptions = [];
/**
* Before Send
*
* @param $callback callable|null
*/
public function beforeSend($callback)
{
$this->beforeSendCallback = $callback;
}
abstract public function close();
/**
* Complete
*
* @param $callback callable|null
*/
public function complete($callback)
{
$this->completeCallback = $callback;
}
/**
* Disable Timeout
*/
public function disableTimeout()
{
$this->setTimeout(null);
}
/**
* Error
*
* @param $callback callable|null
*/
public function error($callback)
{
$this->errorCallback = $callback;
}
/**
* Get Opt
*
* @param $option
* @return mixed
*/
public function getOpt($option)
{
return $this->options[$option] ?? null;
}
/**
* Remove Header
*
* Remove an internal header from the request.
* Using `curl -H "Host:" ...' is equivalent to $curl->removeHeader('Host');.
*
* @param $key
*/
public function removeHeader($key)
{
$this->setHeader($key, '');
}
/**
* Set auto referer
*
* @param mixed $auto_referer
*/
public function setAutoReferer($auto_referer = true)
{
$this->setAutoReferrer($auto_referer);
}
/**
* Set auto referrer
*
* @param mixed $auto_referrer
*/
public function setAutoReferrer($auto_referrer = true)
{
$this->setOpt(CURLOPT_AUTOREFERER, $auto_referrer);
}
/**
* Set Basic Authentication
*
* @param $username
* @param $password
*/
public function setBasicAuthentication($username, $password = '')
{
$this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
$this->setOpt(CURLOPT_USERPWD, $username . ':' . $password);
}
/**
* Set Connect Timeout
*
* @param $seconds
*/
public function setConnectTimeout($seconds)
{
$this->setOpt(CURLOPT_CONNECTTIMEOUT, $seconds);
}
abstract public function setCookie($key, $value);
abstract public function setCookieFile($cookie_file);
abstract public function setCookieJar($cookie_jar);
abstract public function setCookieString($string);
abstract public function setCookies($cookies);
/**
* Set Digest Authentication
*
* @param $username
* @param $password
*/
public function setDigestAuthentication($username, $password = '')
{
$this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
$this->setOpt(CURLOPT_USERPWD, $username . ':' . $password);
}
/**
* After Send
*
* This function is called after the request has been sent.
*
* It can be used to override whether or not the request errored. The
* instance is passed as the first argument to the function and the instance
* has attributes like $instance->httpStatusCode and $instance->response to
* help decide if the request errored. Set $instance->error to true or false
* within the function.
*
* When $instance->error is true indicating a request error, the error
* callback set by Curl::error() is called. When $instance->error is false,
* the success callback set by Curl::success() is called.
*
* @param $callback callable|null
*/
public function afterSend($callback)
{
$this->afterSendCallback = $callback;
}
/**
* Set File
*
* @param $file
*/
public function setFile($file)
{
$this->setOpt(CURLOPT_FILE, $file);
}
protected function setFileInternal($file)
{
$this->setOptInternal(CURLOPT_FILE, $file);
}
/**
* Set follow location
*
* @param mixed $follow_location
* @see Curl::setMaximumRedirects()
*/
public function setFollowLocation($follow_location = true)
{
$this->setOpt(CURLOPT_FOLLOWLOCATION, $follow_location);
}
/**
* Set forbid reuse
*
* @param mixed $forbid_reuse
*/
public function setForbidReuse($forbid_reuse = true)
{
$this->setOpt(CURLOPT_FORBID_REUSE, $forbid_reuse);
}
abstract public function setHeader($key, $value);
abstract public function setHeaders($headers);
/**
* Set Interface
*
* The name of the outgoing network interface to use.
* This can be an interface name, an IP address or a host name.
*
* @param $interface
*/
public function setInterface($interface)
{
$this->setOpt(CURLOPT_INTERFACE, $interface);
}
abstract public function setJsonDecoder($mixed);
/**
* Set maximum redirects
*
* @param mixed $maximum_redirects
* @see Curl::setFollowLocation()
*/
public function setMaximumRedirects($maximum_redirects)
{
$this->setOpt(CURLOPT_MAXREDIRS, $maximum_redirects);
}
abstract public function setOpt($option, $value);
protected function setOptInternal($option, $value)
{
}
abstract public function setOpts($options);
/**
* Set Port
*
* @param $port
*/
public function setPort($port)
{
$this->setOpt(CURLOPT_PORT, (int) $port);
}
/**
* Set Proxy
*
* Set an HTTP proxy to tunnel requests through.
*
* @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.
*
* @param $auth
*/
public function setProxyAuth($auth)
{
$this->setOpt(CURLOPT_PROXYAUTH, $auth);
}
/**
* Set Proxy Tunnel
*
* Set the proxy to tunnel through HTTP proxy.
*
* @param $tunnel boolean
*/
public function setProxyTunnel($tunnel = true)
{
$this->setOpt(CURLOPT_HTTPPROXYTUNNEL, $tunnel);
}
/**
* Set Proxy Type
*
* Set the proxy protocol type.
*
* @param $type
*/
public function setProxyType($type)
{
$this->setOpt(CURLOPT_PROXYTYPE, $type);
}
/**
* Set Range
*
* @param $range
*/
public function setRange($range)
{
$this->setOpt(CURLOPT_RANGE, $range);
}
protected function setRangeInternal($range)
{
$this->setOptInternal(CURLOPT_RANGE, $range);
}
/**
* Set Referer
*
* @param $referer
*/
public function setReferer($referer)
{
$this->setReferrer($referer);
}
/**
* Set Referrer
*
* @param $referrer
*/
public function setReferrer($referrer)
{
$this->setOpt(CURLOPT_REFERER, $referrer);
}
abstract public function setRetry($mixed);
/**
* Set Timeout
*
* @param $seconds
*/
public function setTimeout($seconds)
{
$this->setOpt(CURLOPT_TIMEOUT, $seconds);
}
protected function setTimeoutInternal($seconds)
{
$this->setOptInternal(CURLOPT_TIMEOUT, $seconds);
}
abstract public function setUrl($url, $mixed_data = '');
/**
* Set User Agent
*
* @param $user_agent
*/
public function setUserAgent($user_agent)
{
$this->setOpt(CURLOPT_USERAGENT, $user_agent);
}
protected function setUserAgentInternal($user_agent)
{
$this->setOptInternal(CURLOPT_USERAGENT, $user_agent);
}
abstract public function setXmlDecoder($mixed);
abstract public function stop();
/**
* Success
*
* @param $callback callable|null
*/
public function success($callback)
{
$this->successCallback = $callback;
}
abstract public function unsetHeader($key);
/**
* Unset Proxy
*
* Disable use of the proxy.
*/
public function unsetProxy()
{
$this->setOpt(CURLOPT_PROXY, null);
}
/**
* Verbose
*
* @param bool $on
* @param resource|string $output
*/
public function verbose($on = true, $output = 'STDERR')
{
if ($output === 'STDERR') {
if (defined('STDERR')) {
$output = STDERR;
} else {
$output = fopen('php://stderr', 'wb');
}
}
// 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);
}
}

View File

@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Curl;
/**
* @psalm-suppress MissingTemplateParam
*/
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 = [];
/**
* @var string[] Case-sensitive keys.
*
* @see offsetSet()
* @see offsetUnset()
* @see key()
*/
private $keys = [];
/**
* 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
*/
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.
*
* @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
* @see https://secure.php.net/manual/en/arrayaccess.offsetset.php
*/
#[\Override]
#[\ReturnTypeWillChange]
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.
*
* @param string $offset Offset to check
* @return bool If the offset exists.
* @see https://secure.php.net/manual/en/arrayaccess.offsetexists.php
*/
#[\Override]
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return 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.
*
* @param string $offset The offset to unset.
* @return void
* @see https://secure.php.net/manual/en/arrayaccess.offsetunset.php
*/
#[\Override]
#[\ReturnTypeWillChange]
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.
*
* @param string $offset Offset to lookup.
* @return mixed The data stored at the offset.
* @see https://secure.php.net/manual/en/arrayaccess.offsetget.php
*/
#[\Override]
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
$offsetlower = strtolower($offset);
return $this->data[$offsetlower] ?? null;
}
/**
* Count
*
* @return int The number of elements stored in the array.
* @see https://secure.php.net/manual/en/countable.count.php
*/
#[\Override]
#[\ReturnTypeWillChange]
public function count()
{
return count($this->data);
}
/**
* Current
*
* @return mixed Data at the current position.
* @see https://secure.php.net/manual/en/iterator.current.php
*/
#[\Override]
#[\ReturnTypeWillChange]
public function current()
{
return current($this->data);
}
/**
* Next
*
* @return void
* @see https://secure.php.net/manual/en/iterator.next.php
*/
#[\Override]
#[\ReturnTypeWillChange]
public function next()
{
next($this->data);
}
/**
* Key
*
* @return mixed Case-sensitive key at current position.
* @see https://secure.php.net/manual/en/iterator.key.php
*/
#[\Override]
#[\ReturnTypeWillChange]
public function key()
{
$key = key($this->data);
return $this->keys[$key] ?? $key;
}
/**
* Valid
*
* @return bool If the current position is valid.
* @see https://secure.php.net/manual/en/iterator.valid.php
*/
#[\Override]
#[\ReturnTypeWillChange]
public function valid()
{
return (key($this->data) !== null);
}
/**
* Rewind
*
* @return void
* @see https://secure.php.net/manual/en/iterator.rewind.php
*/
#[\Override]
#[\ReturnTypeWillChange]
public function rewind()
{
reset($this->data);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Curl;
class Decoder
{
/**
* Decode JSON
*
* @param $json
* @param $assoc
* @param $depth
* @param $options
*/
public static function decodeJson()
{
$args = func_get_args();
$response = call_user_func_array('json_decode', $args);
if ($response === null && isset($args['0'])) {
$response = $args['0'];
}
return $response;
}
/**
* Decode XML
*
* @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 && array_key_exists('0', $args)) {
$response = $args['0'];
}
return $response;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Curl;
class Encoder
{
/**
* Encode JSON
*
* Wrap json_encode() to throw error when the value being encoded fails.
*
* @param $value
* @param $options
* @param $depth
* @return string
* @throws \ErrorException
*/
public static function encodeJson()
{
$args = func_get_args();
$value = call_user_func_array('json_encode', $args);
if (json_last_error() !== JSON_ERROR_NONE) {
$error_message = 'json_encode error: ' . json_last_error_msg();
throw new \ErrorException($error_message);
}
return $value;
}
}

View File

@@ -0,0 +1,976 @@
<?php
declare(strict_types=1);
namespace Curl;
class MultiCurl extends BaseCurl
{
public $baseUrl = null;
public $multiCurl = null;
public $startTime = null;
public $stopTime = null;
private $queuedCurls = [];
private $activeCurls = [];
private $isStarted = false;
private $currentStartTime = null;
private $currentRequestCount = 0;
private $concurrency = 25;
private $nextCurlId = 0;
private $preferRequestTimeAccuracy = false;
private $rateLimit = null;
private $rateLimitEnabled = false;
private $rateLimitReached = false;
private $maxRequests = null;
private $interval = null;
private $intervalSeconds = null;
private $unit = null;
private $retry = null;
private $cookies = [];
private $headers = [];
private $instanceSpecificOptions = [];
private $proxies = null;
private $jsonDecoder = null;
private $xmlDecoder = null;
/**
* Construct
*
* @param $base_url
*/
public function __construct($base_url = null)
{
$this->multiCurl = curl_multi_init();
$this->headers = new CaseInsensitiveArray();
if ($base_url !== null) {
$this->setUrl($base_url);
}
}
/**
* Add Delete
*
* @param $url
* @param $query_parameters
* @param $data
* @return object
*/
public function addDelete($url, $query_parameters = [], $data = [])
{
if (is_array($url)) {
$data = $query_parameters;
$query_parameters = $url;
$url = $this->baseUrl;
}
$curl = new Curl($this->baseUrl, $this->options);
$this->queueHandle($curl);
$this->setUrl($url, $query_parameters);
$curl->setUrl($url, $query_parameters);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'DELETE');
$curl->setOpt(CURLOPT_POSTFIELDS, $curl->buildPostData($data));
return $curl;
}
/**
* Add Download
*
* @param $url
* @param $mixed_filename
* @return object
*/
public function addDownload($url, $mixed_filename)
{
$curl = new Curl($this->baseUrl, $this->options);
$this->queueHandle($curl);
$this->setUrl($url);
$curl->setUrl($url);
// Use tmpfile() or php://temp to avoid "Too many open files" error.
if (is_callable($mixed_filename)) {
$curl->downloadCompleteCallback = $mixed_filename;
$curl->downloadFileName = null;
$curl->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';
$curl->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 . '-';
$curl->setRange($range);
$curl->fileHandle = fopen($download_filename, 'ab');
// Move the downloaded temporary file to the destination save path.
$curl->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);
};
} else {
$curl->fileHandle = fopen('php://temp', 'wb');
$curl->downloadCompleteCallback = function ($instance, $fh) use ($filename) {
$contents = stream_get_contents($fh);
if ($contents !== false) {
file_put_contents($filename, $contents);
}
};
}
}
$curl->setFile($curl->fileHandle);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'GET');
$curl->setOpt(CURLOPT_HTTPGET, true);
return $curl;
}
/**
* Add Get
*
* @param $url
* @param $data
* @return object
*/
public function addGet($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl($this->baseUrl, $this->options);
$this->queueHandle($curl);
$this->setUrl($url, $data);
$curl->setUrl($url, $data);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'GET');
$curl->setOpt(CURLOPT_HTTPGET, true);
return $curl;
}
/**
* Add Head
*
* @param $url
* @param $data
* @return object
*/
public function addHead($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl($this->baseUrl, $this->options);
$this->queueHandle($curl);
$this->setUrl($url, $data);
$curl->setUrl($url, $data);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'HEAD');
$curl->setOpt(CURLOPT_NOBODY, true);
return $curl;
}
/**
* Add Options
*
* @param $url
* @param $data
* @return object
*/
public function addOptions($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl($this->baseUrl, $this->options);
$this->queueHandle($curl);
$this->setUrl($url, $data);
$curl->setUrl($url, $data);
$curl->removeHeader('Content-Length');
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'OPTIONS');
return $curl;
}
/**
* Add Patch
*
* @param $url
* @param $data
* @return object
*/
public function addPatch($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl($this->baseUrl, $this->options);
if (is_array($data) && empty($data)) {
$curl->removeHeader('Content-Length');
}
$this->queueHandle($curl);
$this->setUrl($url);
$curl->setUrl($url);
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'PATCH');
$curl->setOpt(CURLOPT_POSTFIELDS, $curl->buildPostData($data));
return $curl;
}
/**
* Add 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). 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->baseUrl, $this->options);
$this->queueHandle($curl);
$this->setUrl($url);
if (is_array($data) && empty($data)) {
$curl->removeHeader('Content-Length');
}
$curl->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) {
$curl->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
}
$curl->setOpt(CURLOPT_POST, true);
$curl->setOpt(CURLOPT_POSTFIELDS, $curl->buildPostData($data));
return $curl;
}
/**
* Add Put
*
* @param $url
* @param $data
* @return object
*/
public function addPut($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl($this->baseUrl, $this->options);
$this->queueHandle($curl);
$this->setUrl($url);
$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
*
* @param $url
* @param $data
* @return object
*/
public function addSearch($url, $data = [])
{
if (is_array($url)) {
$data = $url;
$url = $this->baseUrl;
}
$curl = new Curl($this->baseUrl, $this->options);
$this->queueHandle($curl);
$this->setUrl($url);
$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.
*
* @param $curl
* @return object
*/
public function addCurl(Curl $curl)
{
$this->queueHandle($curl);
return $curl;
}
/**
* Close
*/
#[\Override]
public function close()
{
foreach ($this->queuedCurls as $curl) {
$curl->close();
}
if (is_resource($this->multiCurl) || $this->multiCurl instanceof \CurlMultiHandle) {
curl_multi_close($this->multiCurl);
}
$this->multiCurl = null;
}
/**
* Set Concurrency
*
* @param $concurrency
*/
public function setConcurrency($concurrency)
{
$this->concurrency = $concurrency;
}
/**
* Set Cookie
*
* @param $key
* @param $value
*/
#[\Override]
public function setCookie($key, $value)
{
$this->cookies[$key] = $value;
}
/**
* Set Cookies
*
* @param $cookies
*/
#[\Override]
public function setCookies($cookies)
{
foreach ($cookies as $key => $value) {
$this->cookies[$key] = $value;
}
}
/**
* Set Cookie String
*
* @param $string
*/
#[\Override]
public function setCookieString($string)
{
$this->setOpt(CURLOPT_COOKIE, $string);
}
/**
* Set Cookie File
*
* @param $cookie_file
*/
#[\Override]
public function setCookieFile($cookie_file)
{
$this->setOpt(CURLOPT_COOKIEFILE, $cookie_file);
}
/**
* Set Cookie Jar
*
* @param $cookie_jar
*/
#[\Override]
public function setCookieJar($cookie_jar)
{
$this->setOpt(CURLOPT_COOKIEJAR, $cookie_jar);
}
/**
* Set Header
*
* Add extra header to include in the request.
*
* @param $key
* @param $value
*/
#[\Override]
public function setHeader($key, $value)
{
$this->headers[$key] = $value;
$this->updateHeaders();
}
/**
* 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;
}
}
$this->updateHeaders();
}
/**
* Set JSON Decoder
*
* @param $mixed boolean|callable
*/
#[\Override]
public function setJsonDecoder($mixed)
{
if ($mixed === false) {
$this->jsonDecoder = false;
} elseif (is_callable($mixed)) {
$this->jsonDecoder = $mixed;
}
}
/**
* Set XML Decoder
*
* @param $mixed boolean|callable
*/
#[\Override]
public function setXmlDecoder($mixed)
{
if ($mixed === false) {
$this->xmlDecoder = false;
} elseif (is_callable($mixed)) {
$this->xmlDecoder = $mixed;
}
}
/**
* Set Proxies
*
* Set proxies to tunnel requests through. When set, a random proxy will be
* used for the request.
*
* @param $proxies array - A list of HTTP proxies to tunnel requests
* through. May include port number.
*/
public function setProxies($proxies)
{
$this->proxies = $proxies;
}
/**
* Set Opt
*
* @param $option
* @param $value
*/
#[\Override]
public function setOpt($option, $value)
{
$this->options[$option] = $value;
// Make changing the url an instance-specific option. Set the value of
// existing instances when they have not already been set to avoid
// unexpectedly changing the request url after is has been specified.
if ($option === CURLOPT_URL) {
foreach ($this->queuedCurls as $curl_id => $curl) {
if (
!isset($this->instanceSpecificOptions[$curl_id][$option]) ||
$this->instanceSpecificOptions[$curl_id][$option] === null
) {
$this->instanceSpecificOptions[$curl_id][$option] = $value;
}
}
}
}
/**
* Set Opts
*
* @param $options
*/
#[\Override]
public function setOpts($options)
{
foreach ($options as $option => $value) {
$this->setOpt($option, $value);
}
}
/**
* Set Rate Limit
*
* @param $rate_limit string (e.g. "60/1m").
* @throws \UnexpectedValueException
*/
public function setRateLimit($rate_limit)
{
$rate_limit_pattern =
'/' . // delimiter
'^' . // assert start
'(\d+)' . // digit(s)
'\/' . // slash
'(\d+)?' . // digit(s), optional
'(s|m|h)' . // unit, s for seconds, m for minutes, h for hours
'$' . // assert end
'/' . // delimiter
'i' . // case-insensitive matches
'';
if (!preg_match($rate_limit_pattern, $rate_limit, $matches)) {
throw new \UnexpectedValueException(
'rate limit must be formatted as $max_requests/$interval(s|m|h) ' .
'(e.g. "60/1m" for a maximum of 60 requests per 1 minute)'
);
}
$max_requests = (int)$matches['1'];
if ($matches['2'] === '') {
$interval = 1;
} else {
$interval = (int)$matches['2'];
}
$unit = strtolower($matches['3']);
// Convert interval to seconds based on unit.
$interval_seconds = '';
if ($unit === 's') {
$interval_seconds = $interval * 1;
} elseif ($unit === 'm') {
$interval_seconds = $interval * 60;
} elseif ($unit === 'h') {
$interval_seconds = $interval * 3600;
}
$this->rateLimit = (string)$max_requests . '/' . (string)$interval . $unit;
$this->rateLimitEnabled = true;
$this->maxRequests = $max_requests;
$this->interval = $interval;
$this->intervalSeconds = $interval_seconds;
$this->unit = $unit;
}
/**
* 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)
{
$this->retry = $mixed;
}
/**
* Set Url
*
* @param $url
* @param $mixed_data
*/
#[\Override]
public function setUrl($url, $mixed_data = '')
{
$built_url = Url::buildUrl($url, $mixed_data);
if ($this->baseUrl === null) {
$this->baseUrl = (string)new Url($built_url);
} else {
$this->baseUrl = (string)new Url($this->baseUrl, $built_url);
}
$this->setOpt(CURLOPT_URL, $this->baseUrl);
}
/**
* Start
*
* @throws \ErrorException
*/
public function start()
{
if ($this->isStarted) {
return;
}
$this->isStarted = true;
$this->startTime = microtime(true);
$this->currentStartTime = microtime(true);
$this->currentRequestCount = 0;
do {
while (
count($this->queuedCurls) &&
count($this->activeCurls) < $this->concurrency &&
(!$this->rateLimitEnabled || $this->hasRequestQuota())
) {
$this->initHandle();
}
if ($this->rateLimitEnabled && !count($this->activeCurls) && !$this->hasRequestQuota()) {
$this->waitUntilRequestQuotaAvailable();
}
if ($this->preferRequestTimeAccuracy) {
// 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
//
// Also, use a shorter curl_multi_select() timeout instead the default of one second. This allows
// pending requests to have more accurate start times. Without a shorter timeout, it can be nearly a
// full second before available request quota is rechecked and pending requests can be initialized.
if (curl_multi_select($this->multiCurl, 0.2) === -1) {
usleep(100000);
}
curl_multi_exec($this->multiCurl, $active);
} else {
// Use multiple loops to get data off of the multi handler. Without this, the following error may appear
// intermittently on certain versions of PHP:
// curl_multi_exec(): supplied resource is not a valid cURL handle resource
// Clear out the curl buffer.
do {
$status = curl_multi_exec($this->multiCurl, $active);
} while ($status === CURLM_CALL_MULTI_PERFORM);
// Wait for more information and then get that information.
while ($active && $status === CURLM_OK) {
// Check if the network socket has some data.
if (curl_multi_select($this->multiCurl) !== -1) {
// Process the data for as long as the system tells us to keep getting it.
do {
$status = curl_multi_exec($this->multiCurl, $active);
} while ($status === CURLM_CALL_MULTI_PERFORM);
}
}
}
while (
(is_resource($this->multiCurl) || $this->multiCurl instanceof \CurlMultiHandle) &&
(($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)
);
}
$curl->call($curl->beforeSendCallback);
} else {
$curl->execDone();
// Remove completed instance from active curls.
unset($this->activeCurls[$key]);
// Remove handle of the completed instance.
curl_multi_remove_handle($this->multiCurl, $curl->curl);
// Clean up completed instance.
$curl->close();
}
break;
}
}
}
}
} while ($active || count($this->activeCurls) || count($this->queuedCurls));
$this->isStarted = false;
$this->stopTime = microtime(true);
}
/**
* Stop
*/
#[\Override]
public function stop()
{
// Remove any queued curl requests.
while (count($this->queuedCurls)) {
$curl = array_pop($this->queuedCurls);
$curl->close();
}
// Attempt to stop active curl requests.
while (count($this->activeCurls)) {
// Remove instance from active curls.
$curl = array_pop($this->activeCurls);
// Remove active curl handle.
curl_multi_remove_handle($this->multiCurl, $curl->curl);
$curl->stop();
}
}
/**
* Unset Header
*
* Remove extra header previously set using Curl::setHeader().
*
* @param $key
*/
#[\Override]
public function unsetHeader($key)
{
unset($this->headers[$key]);
}
/**
* Set request time accuracy
*/
public function setRequestTimeAccuracy()
{
$this->preferRequestTimeAccuracy = true;
}
/**
* Destruct
*/
public function __destruct()
{
$this->close();
}
/**
* Update Headers
*/
private function updateHeaders()
{
foreach ($this->queuedCurls as $curl) {
$curl->setHeaders($this->headers);
}
}
/**
* Queue Handle
*
* @param $curl
*/
private function queueHandle($curl)
{
// Use sequential ids to allow for ordered post processing.
$curl->id = $this->nextCurlId++;
$curl->childOfMultiCurl = true;
$this->queuedCurls[$curl->id] = $curl;
// Avoid overwriting any existing header.
if ($curl->getOpt(CURLOPT_HTTPHEADER) === null) {
$curl->setHeaders($this->headers);
}
}
/**
* Init Handle
*
* @param $curl
* @throws \ErrorException
*/
private function initHandle()
{
$curl = array_shift($this->queuedCurls);
if ($curl === null) {
return;
}
// Add instance to list of active curls.
$this->currentRequestCount += 1;
$this->activeCurls[$curl->id] = $curl;
// Set callbacks if not already individually set.
if ($curl->beforeSendCallback === null) {
$curl->beforeSend($this->beforeSendCallback);
}
if ($curl->afterSendCallback === null) {
$curl->afterSend($this->afterSendCallback);
}
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);
}
// Set instance-specific options on the Curl instance when present.
if (isset($this->instanceSpecificOptions[$curl->id])) {
$curl->setOpts($this->instanceSpecificOptions[$curl->id]);
}
$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::arrayRandom($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));
}
$curl->call($curl->beforeSendCallback);
}
/**
* Has Request Quota
*
* Checks if there is any available quota to make additional requests while
* rate limiting is enabled.
*/
private function hasRequestQuota()
{
// Calculate if there's request quota since ratelimiting is enabled.
if ($this->rateLimitEnabled) {
// Determine if the limit of requests per interval has been reached.
if ($this->currentRequestCount >= $this->maxRequests) {
$micro_time = microtime(true);
$elapsed_seconds = $micro_time - $this->currentStartTime;
if ($elapsed_seconds <= $this->intervalSeconds) {
$this->rateLimitReached = true;
return false;
} elseif ($this->rateLimitReached) {
$this->rateLimitReached = false;
$this->currentStartTime = $micro_time;
$this->currentRequestCount = 0;
}
}
return true;
} else {
return true;
}
}
/**
* Wait Until Request Quota Available
*
* Waits until there is available request quota available based on the rate limit.
*/
private function waitUntilRequestQuotaAvailable()
{
$sleep_until = (float)($this->currentStartTime + $this->intervalSeconds);
$sleep_seconds = $sleep_until - microtime(true);
// Avoid using time_sleep_until() as it appears to be less precise and not sleep long enough.
// Avoid using usleep(): "Values larger than 1000000 (i.e. sleeping for
// more than a second) may not be supported by the operating system.
// Use sleep() instead."
$sleep_seconds_int = (int)$sleep_seconds;
if ($sleep_seconds_int >= 1) {
sleep($sleep_seconds_int);
}
// Ensure that enough time has passed as usleep() may not have waited long enough.
$this->currentStartTime = microtime(true);
if ($this->currentStartTime < $sleep_until) {
do {
usleep(1000000 / 4);
$this->currentStartTime = microtime(true);
} while ($this->currentStartTime < $sleep_until);
}
$this->currentRequestCount = 0;
}
public function getActiveCurls()
{
return $this->activeCurls;
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
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.
*
* @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);
}
}
}

View File

@@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
namespace Curl;
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(): string
{
return $this->absolutizeUrl();
}
/**
* Remove dot segments.
*
* Interpret and remove the special "." and ".." path segments from a referenced path.
*
* @param mixed $input
*/
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;
}
/**
* Build Url
*
* @param $url
* @param $mixed_data
* @return string
*/
public static 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;
}
/**
* Absolutize url.
*
* Combine the base and relative url into an absolute url.
*/
private function absolutizeUrl()
{
$b = self::parseUrl($this->baseUrl);
if (!isset($b['path'])) {
$b['path'] = '/';
}
if ($this->relativeUrl === null) {
return $this->unparseUrl($b);
}
$r = self::parseUrl($this->relativeUrl);
$r['authorized'] = isset($r['scheme']) || isset($r['host']) || isset($r['port'])
|| isset($r['user']) || isset($r['pass']);
$target = [];
if (isset($r['scheme'])) {
$target['scheme'] = $r['scheme'];
$target['host'] = $r['host'] ?? null;
$target['port'] = $r['port'] ?? null;
$target['user'] = $r['user'] ?? null;
$target['pass'] = $r['pass'] ?? null;
$target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null;
$target['query'] = $r['query'] ?? null;
} else {
$target['scheme'] = $b['scheme'] ?? null;
if ($r['authorized']) {
$target['host'] = $r['host'] ?? null;
$target['port'] = $r['port'] ?? null;
$target['user'] = $r['user'] ?? null;
$target['pass'] = $r['pass'] ?? null;
$target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null;
$target['query'] = $r['query'] ?? null;
} else {
$target['host'] = $b['host'] ?? null;
$target['port'] = $b['port'] ?? null;
$target['user'] = $b['user'] ?? null;
$target['pass'] = $b['pass'] ?? null;
if (!isset($r['path']) || $r['path'] === '') {
$target['path'] = $b['path'];
$target['query'] = $r['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'] = $r['query'] ?? null;
}
}
}
if ($this->relativeUrl === '') {
$target['fragment'] = $b['fragment'] ?? null;
} else {
$target['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.
*
* @param mixed $url
*/
public static function parseUrl($url)
{
$parts = parse_url((string) $url);
if (isset($parts['path'])) {
$parts['path'] = self::percentEncodeChars($parts['path']);
}
return $parts;
}
/**
* Percent-encode characters.
*
* Percent-encode characters to represent a data octet in a component when
* that octet's corresponding character is outside the allowed set.
*
* @param mixed $chars
*/
private static function percentEncodeChars($chars)
{
// 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}))/';
$percent_encoded_chars = preg_replace_callback(
$pattern,
function ($matches) {
return rawurlencode($matches[0]);
},
$chars
);
return $percent_encoded_chars;
}
/**
* Unparse url.
*
* Combine url components into a url.
*
* @param mixed $parsed_url
*/
private function unparseUrl($parsed_url)
{
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$user = $parsed_url['user'] ?? '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? $pass . '@' : '';
$host = $parsed_url['host'] ?? '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$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;
}
}