AC_services_website_design/system/HTTP/CURLRequest.php

703 lines
20 KiB
PHP

<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\HTTP;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use Config\App;
use Config\CURLRequest as ConfigCURLRequest;
use InvalidArgumentException;
/**
* A lightweight HTTP client for sending synchronous HTTP requests via cURL.
*
* @see \CodeIgniter\HTTP\CURLRequestTest
*/
class CURLRequest extends OutgoingRequest
{
/**
* The response object associated with this request
*
* @var ResponseInterface|null
*/
protected $response;
/**
* The original response object associated with this request
*
* @var ResponseInterface|null
*/
protected $responseOrig;
/**
* The URI associated with this request
*
* @var URI
*/
protected $baseURI;
/**
* The setting values
*
* @var array
*/
protected $config;
/**
* The default setting values
*
* @var array
*/
protected $defaultConfig = [
'timeout' => 0.0,
'connect_timeout' => 150,
'debug' => false,
'verify' => true,
];
/**
* Default values for when 'allow_redirects'
* option is true.
*
* @var array
*/
protected $redirectDefaults = [
'max' => 5,
'strict' => true,
'protocols' => [
'http',
'https',
],
];
/**
* The number of milliseconds to delay before
* sending the request.
*
* @var float
*/
protected $delay = 0.0;
/**
* The default options from the constructor. Applied to all requests.
*/
private array $defaultOptions;
/**
* Whether share options between requests or not.
*
* If true, all the options won't be reset between requests.
* It may cause an error request with unnecessary headers.
*/
private bool $shareOptions;
/**
* Takes an array of options to set the following possible class properties:
*
* - baseURI
* - timeout
* - any other request options to use as defaults.
*/
public function __construct(App $config, URI $uri, ?ResponseInterface $response = null, array $options = [])
{
if (! function_exists('curl_version')) {
throw HTTPException::forMissingCurl(); // @codeCoverageIgnore
}
parent::__construct('GET', $uri);
$this->responseOrig = $response ?? new Response(config(App::class));
$this->baseURI = $uri->useRawQueryString();
$this->defaultOptions = $options;
/** @var ConfigCURLRequest|null $configCURLRequest */
$configCURLRequest = config(ConfigCURLRequest::class);
$this->shareOptions = $configCURLRequest->shareOptions ?? true;
$this->config = $this->defaultConfig;
$this->parseOptions($options);
}
/**
* Sends an HTTP request to the specified $url. If this is a relative
* URL, it will be merged with $this->baseURI to form a complete URL.
*
* @param string $method
*/
public function request($method, string $url, array $options = []): ResponseInterface
{
$this->response = clone $this->responseOrig;
$this->parseOptions($options);
$url = $this->prepareURL($url);
$method = esc(strip_tags($method));
$this->send($method, $url);
if ($this->shareOptions === false) {
$this->resetOptions();
}
return $this->response;
}
/**
* Reset all options to default.
*
* @return void
*/
protected function resetOptions()
{
// Reset headers
$this->headers = [];
$this->headerMap = [];
// Reset body
$this->body = null;
// Reset configs
$this->config = $this->defaultConfig;
// Set the default options for next request
$this->parseOptions($this->defaultOptions);
}
/**
* Convenience method for sending a GET request.
*/
public function get(string $url, array $options = []): ResponseInterface
{
return $this->request('get', $url, $options);
}
/**
* Convenience method for sending a DELETE request.
*/
public function delete(string $url, array $options = []): ResponseInterface
{
return $this->request('delete', $url, $options);
}
/**
* Convenience method for sending a HEAD request.
*/
public function head(string $url, array $options = []): ResponseInterface
{
return $this->request('head', $url, $options);
}
/**
* Convenience method for sending an OPTIONS request.
*/
public function options(string $url, array $options = []): ResponseInterface
{
return $this->request('options', $url, $options);
}
/**
* Convenience method for sending a PATCH request.
*/
public function patch(string $url, array $options = []): ResponseInterface
{
return $this->request('patch', $url, $options);
}
/**
* Convenience method for sending a POST request.
*/
public function post(string $url, array $options = []): ResponseInterface
{
return $this->request('post', $url, $options);
}
/**
* Convenience method for sending a PUT request.
*/
public function put(string $url, array $options = []): ResponseInterface
{
return $this->request('put', $url, $options);
}
/**
* Set the HTTP Authentication.
*
* @param string $type basic or digest
*
* @return $this
*/
public function setAuth(string $username, string $password, string $type = 'basic')
{
$this->config['auth'] = [
$username,
$password,
$type,
];
return $this;
}
/**
* Set form data to be sent.
*
* @param bool $multipart Set TRUE if you are sending CURLFiles
*
* @return $this
*/
public function setForm(array $params, bool $multipart = false)
{
if ($multipart) {
$this->config['multipart'] = $params;
} else {
$this->config['form_params'] = $params;
}
return $this;
}
/**
* Set JSON data to be sent.
*
* @param array|bool|float|int|object|string|null $data
*
* @return $this
*/
public function setJSON($data)
{
$this->config['json'] = $data;
return $this;
}
/**
* Sets the correct settings based on the options array
* passed in.
*
* @return void
*/
protected function parseOptions(array $options)
{
if (array_key_exists('baseURI', $options)) {
$this->baseURI = $this->baseURI->setURI($options['baseURI']);
unset($options['baseURI']);
}
if (array_key_exists('headers', $options) && is_array($options['headers'])) {
foreach ($options['headers'] as $name => $value) {
$this->setHeader($name, $value);
}
unset($options['headers']);
}
if (array_key_exists('delay', $options)) {
// Convert from the milliseconds passed in
// to the seconds that sleep requires.
$this->delay = (float) $options['delay'] / 1000;
unset($options['delay']);
}
if (array_key_exists('body', $options)) {
$this->setBody($options['body']);
unset($options['body']);
}
foreach ($options as $key => $value) {
$this->config[$key] = $value;
}
}
/**
* If the $url is a relative URL, will attempt to create
* a full URL by prepending $this->baseURI to it.
*/
protected function prepareURL(string $url): string
{
// If it's a full URI, then we have nothing to do here...
if (strpos($url, '://') !== false) {
return $url;
}
$uri = $this->baseURI->resolveRelativeURI($url);
// Create the string instead of casting to prevent baseURL muddling
return URI::createURIString(
$uri->getScheme(),
$uri->getAuthority(),
$uri->getPath(),
$uri->getQuery(),
$uri->getFragment()
);
}
/**
* Get the request method. Overrides the Request class' method
* since users expect a different answer here.
*
* @param bool|false $upper Whether to return in upper or lower case.
*/
public function getMethod(bool $upper = false): string
{
return ($upper) ? strtoupper($this->method) : strtolower($this->method);
}
/**
* Fires the actual cURL request.
*
* @return ResponseInterface
*/
public function send(string $method, string $url)
{
// Reset our curl options so we're on a fresh slate.
$curlOptions = [];
if (! empty($this->config['query']) && is_array($this->config['query'])) {
// This is likely too naive a solution.
// Should look into handling when $url already
// has query vars on it.
$url .= '?' . http_build_query($this->config['query']);
unset($this->config['query']);
}
$curlOptions[CURLOPT_URL] = $url;
$curlOptions[CURLOPT_RETURNTRANSFER] = true;
$curlOptions[CURLOPT_HEADER] = true;
$curlOptions[CURLOPT_FRESH_CONNECT] = true;
// Disable @file uploads in post data.
$curlOptions[CURLOPT_SAFE_UPLOAD] = true;
$curlOptions = $this->setCURLOptions($curlOptions, $this->config);
$curlOptions = $this->applyMethod($method, $curlOptions);
$curlOptions = $this->applyRequestHeaders($curlOptions);
// Do we need to delay this request?
if ($this->delay > 0) {
usleep((int) $this->delay * 1_000_000);
}
$output = $this->sendRequest($curlOptions);
// Set the string we want to break our response from
$breakString = "\r\n\r\n";
if (strpos($output, 'HTTP/1.1 100 Continue') === 0) {
$output = substr($output, strpos($output, $breakString) + 4);
}
if (strpos($output, 'HTTP/1.1 200 Connection established') === 0) {
$output = substr($output, strpos($output, $breakString) + 4);
}
// If request and response have Digest
if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && strpos($output, 'WWW-Authenticate: Digest') !== false) {
$output = substr($output, strpos($output, $breakString) + 4);
}
// Split out our headers and body
$break = strpos($output, $breakString);
if ($break !== false) {
// Our headers
$headers = explode("\n", substr($output, 0, $break));
$this->setResponseHeaders($headers);
// Our body
$body = substr($output, $break + 4);
$this->response->setBody($body);
} else {
$this->response->setBody($output);
}
return $this->response;
}
/**
* Adds $this->headers to the cURL request.
*/
protected function applyRequestHeaders(array $curlOptions = []): array
{
if (empty($this->headers)) {
return $curlOptions;
}
$set = [];
foreach (array_keys($this->headers) as $name) {
$set[] = $name . ': ' . $this->getHeaderLine($name);
}
$curlOptions[CURLOPT_HTTPHEADER] = $set;
return $curlOptions;
}
/**
* Apply method
*/
protected function applyMethod(string $method, array $curlOptions): array
{
$method = strtoupper($method);
$this->method = $method;
$curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
$size = strlen($this->body ?? '');
// Have content?
if ($size > 0) {
return $this->applyBody($curlOptions);
}
if ($method === 'PUT' || $method === 'POST') {
// See http://tools.ietf.org/html/rfc7230#section-3.3.2
if ($this->header('content-length') === null && ! isset($this->config['multipart'])) {
$this->setHeader('Content-Length', '0');
}
} elseif ($method === 'HEAD') {
$curlOptions[CURLOPT_NOBODY] = 1;
}
return $curlOptions;
}
/**
* Apply body
*/
protected function applyBody(array $curlOptions = []): array
{
if (! empty($this->body)) {
$curlOptions[CURLOPT_POSTFIELDS] = (string) $this->getBody();
}
return $curlOptions;
}
/**
* Parses the header retrieved from the cURL response into
* our Response object.
*
* @return void
*/
protected function setResponseHeaders(array $headers = [])
{
foreach ($headers as $header) {
if (($pos = strpos($header, ':')) !== false) {
$title = substr($header, 0, $pos);
$value = substr($header, $pos + 1);
$this->response->setHeader($title, $value);
} elseif (strpos($header, 'HTTP') === 0) {
preg_match('#^HTTP\/([12](?:\.[01])?) (\d+) (.+)#', $header, $matches);
if (isset($matches[1])) {
$this->response->setProtocolVersion($matches[1]);
}
if (isset($matches[2])) {
$this->response->setStatusCode((int) $matches[2], $matches[3] ?? null);
}
}
}
}
/**
* Set CURL options
*
* @return array
*
* @throws InvalidArgumentException
*/
protected function setCURLOptions(array $curlOptions = [], array $config = [])
{
// Auth Headers
if (! empty($config['auth'])) {
$curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1];
if (! empty($config['auth'][2]) && strtolower($config['auth'][2]) === 'digest') {
$curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST;
} else {
$curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
}
}
// Certificate
if (! empty($config['cert'])) {
$cert = $config['cert'];
if (is_array($cert)) {
$curlOptions[CURLOPT_SSLCERTPASSWD] = $cert[1];
$cert = $cert[0];
}
if (! is_file($cert)) {
throw HTTPException::forSSLCertNotFound($cert);
}
$curlOptions[CURLOPT_SSLCERT] = $cert;
}
// SSL Verification
if (isset($config['verify'])) {
if (is_string($config['verify'])) {
$file = realpath($config['verify']) ?: $config['verify'];
if (! is_file($file)) {
throw HTTPException::forInvalidSSLKey($config['verify']);
}
$curlOptions[CURLOPT_CAINFO] = $file;
$curlOptions[CURLOPT_SSL_VERIFYPEER] = true;
$curlOptions[CURLOPT_SSL_VERIFYHOST] = 2;
} elseif (is_bool($config['verify'])) {
$curlOptions[CURLOPT_SSL_VERIFYPEER] = $config['verify'];
$curlOptions[CURLOPT_SSL_VERIFYHOST] = $config['verify'] ? 2 : 0;
}
}
// Proxy
if (isset($config['proxy'])) {
$curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true;
$curlOptions[CURLOPT_PROXY] = $config['proxy'];
}
// Debug
if ($config['debug']) {
$curlOptions[CURLOPT_VERBOSE] = 1;
$curlOptions[CURLOPT_STDERR] = is_string($config['debug']) ? fopen($config['debug'], 'a+b') : fopen('php://stderr', 'wb');
}
// Decode Content
if (! empty($config['decode_content'])) {
$accept = $this->getHeaderLine('Accept-Encoding');
if ($accept !== '') {
$curlOptions[CURLOPT_ENCODING] = $accept;
} else {
$curlOptions[CURLOPT_ENCODING] = '';
$curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding';
}
}
// Allow Redirects
if (array_key_exists('allow_redirects', $config)) {
$settings = $this->redirectDefaults;
if (is_array($config['allow_redirects'])) {
$settings = array_merge($settings, $config['allow_redirects']);
}
if ($config['allow_redirects'] === false) {
$curlOptions[CURLOPT_FOLLOWLOCATION] = 0;
} else {
$curlOptions[CURLOPT_FOLLOWLOCATION] = 1;
$curlOptions[CURLOPT_MAXREDIRS] = $settings['max'];
if ($settings['strict'] === true) {
$curlOptions[CURLOPT_POSTREDIR] = 1 | 2 | 4;
}
$protocols = 0;
foreach ($settings['protocols'] as $proto) {
$protocols += constant('CURLPROTO_' . strtoupper($proto));
}
$curlOptions[CURLOPT_REDIR_PROTOCOLS] = $protocols;
}
}
// Timeout
$curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000;
// Connection Timeout
$curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) $config['connect_timeout'] * 1000;
// Post Data - application/x-www-form-urlencoded
if (! empty($config['form_params']) && is_array($config['form_params'])) {
$postFields = http_build_query($config['form_params']);
$curlOptions[CURLOPT_POSTFIELDS] = $postFields;
// Ensure content-length is set, since CURL doesn't seem to
// calculate it when HTTPHEADER is set.
$this->setHeader('Content-Length', (string) strlen($postFields));
$this->setHeader('Content-Type', 'application/x-www-form-urlencoded');
}
// Post Data - multipart/form-data
if (! empty($config['multipart']) && is_array($config['multipart'])) {
// setting the POSTFIELDS option automatically sets multipart
$curlOptions[CURLOPT_POSTFIELDS] = $config['multipart'];
}
// HTTP Errors
$curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true;
// JSON
if (isset($config['json'])) {
// Will be set as the body in `applyBody()`
$json = json_encode($config['json']);
$this->setBody($json);
$this->setHeader('Content-Type', 'application/json');
$this->setHeader('Content-Length', (string) strlen($json));
}
// version
if (! empty($config['version'])) {
if ($config['version'] === 1.0) {
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
} elseif ($config['version'] === 1.1) {
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
} elseif ($config['version'] === 2.0) {
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
}
}
// Cookie
if (isset($config['cookie'])) {
$curlOptions[CURLOPT_COOKIEJAR] = $config['cookie'];
$curlOptions[CURLOPT_COOKIEFILE] = $config['cookie'];
}
// User Agent
if (isset($config['user_agent'])) {
$curlOptions[CURLOPT_USERAGENT] = $config['user_agent'];
}
return $curlOptions;
}
/**
* Does the actual work of initializing cURL, setting the options,
* and grabbing the output.
*
* @codeCoverageIgnore
*/
protected function sendRequest(array $curlOptions = []): string
{
$ch = curl_init();
curl_setopt_array($ch, $curlOptions);
// Send the request and wait for a response.
$output = curl_exec($ch);
if ($output === false) {
throw HTTPException::forCurlError((string) curl_errno($ch), curl_error($ch));
}
curl_close($ch);
return $output;
}
}