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; //-------------------------------------------------------------------- /** * Takes an array of options to set the following possible class properties: * * - baseURI * - timeout * - any other request options to use as defaults. * * @param App $config * @param URI $uri * @param ResponseInterface $response * @param array $options */ public function __construct(App $config, URI $uri, ResponseInterface $response = null, array $options = []) { if ( ! function_exists('curl_version')) { throw new \RuntimeException('CURL must be enabled to use the CURLRequest class.'); } parent::__construct($config); $this->response = $response; $this->baseURI = $uri; $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 $method * @param string $url * @param array $options * * @return Response */ public function request($method, string $url, array $options = []): ResponseInterface { $this->parseOptions($options); $url = $this->prepareURL($url); $method = filter_var($method, FILTER_SANITIZE_STRING); $this->send($method, $url); return $this->response; } //-------------------------------------------------------------------- /** * Convenience method for sending a GET request. * * @param string $url * @param array $options * * @return Response */ public function get(string $url, array $options = []): ResponseInterface { return $this->request('get', $url, $options); } //-------------------------------------------------------------------- /** * Convenience method for sending a DELETE request. * * @param string $url * @param array $options * * @return Response */ public function delete(string $url, array $options = []): ResponseInterface { return $this->request('delete', $url, $options); } //-------------------------------------------------------------------- /** * Convenience method for sending a HEAD request. * * @param string $url * @param array $options * * @return Response */ public function head(string $url, array $options = []): ResponseInterface { return $this->request('head', $url, $options); } //-------------------------------------------------------------------- /** * Convenience method for sending an OPTIONS request. * * @param string $url * @param array $options * * @return Response */ public function options(string $url, array $options = []): ResponseInterface { return $this->request('options', $url, $options); } //-------------------------------------------------------------------- /** * Convenience method for sending a PATCH request. * * @param string $url * @param array $options * * @return Response */ public function patch(string $url, array $options = []): ResponseInterface { return $this->request('patch', $url, $options); } //-------------------------------------------------------------------- /** * Convenience method for sending a POST request. * * @param string $url * @param array $options * * @return Response */ public function post(string $url, array $options = []): ResponseInterface { return $this->request('post', $url, $options); } //-------------------------------------------------------------------- /** * Convenience method for sending a PUT request. * * @param string $url * @param array $options * * @return Response */ public function put(string $url, array $options = []): ResponseInterface { return $this->request('put', $url, $options); } //-------------------------------------------------------------------- /** * Sets the correct settings based on the options array * passed in. * * @param array $options */ 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']); } 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. * * @param string $url * * @return string */ 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); return (string)$uri; } //-------------------------------------------------------------------- /** * 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. * * @return string */ public function getMethod($upper = false): string { return ($upper) ? strtoupper($this->method) : strtolower($this->method); } //-------------------------------------------------------------------- /** * Fires the actual cURL request. * * @param string $method * @param string $url */ public function send(string $method, string $url) { // Reset our curl options so we're on a fresh slate. $curl_options = []; 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']); } $curl_options[CURLOPT_URL] = $url; $curl_options[CURLOPT_RETURNTRANSFER] = true; $curl_options[CURLOPT_HEADER] = true; $curl_options[CURLOPT_FRESH_CONNECT] = true; // Disable @file uploads in post data. $curl_options[CURLOPT_SAFE_UPLOAD] = true; $curl_options = $this->setCURLOptions($curl_options, $this->config); $curl_options = $this->applyMethod($method, $curl_options); $curl_options = $this->applyRequestHeaders($curl_options); // Do we need to delay this request? if ($this->delay > 0) { sleep($this->delay); } $output = $this->sendRequest($curl_options); // Split out our headers and body $break = strpos($output, "\r\n\r\n"); 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; } //-------------------------------------------------------------------- /** * Takes all headers current part of this request and adds them * to the cURL request. * * @param array $curl_options */ protected function applyRequestHeaders(array $curl_options = []): array { $headers = $this->getHeaders(); if (empty($head)) { return $curl_options; } $set = []; foreach ($headers as $name => $value) { $set[] = $name.': '.$this->getHeaderLine($name); } $curl_options[CURLOPT_HTTPHEADER] = $set; return $curl_options; } //-------------------------------------------------------------------- /** * Apply method * * @param type $method * @param array $curl_options * @return int */ protected function applyMethod($method, array $curl_options): array { $method = strtoupper($method); $this->method = $method; $curl_options[CURLOPT_CUSTOMREQUEST] = $method; $size = strlen($this->body); // Have content? if ($size === null || $size > 0) { $curl_options = $this->applyBody($curl_options); return $curl_options; } if ($method == 'PUT' || $method == 'POST') { // See http://tools.ietf.org/html/rfc7230#section-3.3.2 if (is_null($this->getHeader('content-length'))) { $this->setHeader('Content-Length', 0); } } else if ($method == 'HEAD') { $curl_options[CURLOPT_NOBODY] = 1; } return $curl_options; } //-------------------------------------------------------------------- /** * Apply body * * @param array $curl_options * @return type */ protected function applyBody(array $curl_options = []): array { if ( ! empty($this->body)) { $curl_options[CURLOPT_POSTFIELDS] = (string)$this->getBody(); } return $curl_options; } //-------------------------------------------------------------------- /** * Parses the header retrieved from the cURL response into * our Response object. * * @param array $headers */ 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); } else if (substr($header, 0, 4) == 'HTTP') { preg_match('#^HTTP\/([12]\.[01]) ([0-9]+) (.+)#', $header, $matches); if (isset($matches[1])) { $this->response->setProtocolVersion($matches[1]); } if (isset($matches[2])) { $this->response->setStatusCode($matches[2], isset($matches[3]) ? $matches[3] : null); } } } } //-------------------------------------------------------------------- /** * Set CURL options * * @param array $curl_options * @param array $config * @return type * @throws \InvalidArgumentException */ protected function setCURLOptions(array $curl_options = [], array $config = []) { // Auth Headers if ( ! empty($config['auth'])) { $curl_options[CURLOPT_USERPWD] = $config['auth'][0].':'.$config['auth'][1]; if ( ! empty($config['auth'][2]) && strtolower($config['auth'][2]) == 'digest') { $curl_options[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST; } else { $curl_options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; } } // Certificate if ( ! empty($config['cert'])) { $cert = $config['cert']; if (is_array($cert)) { $curl_options[CURLOPT_SSLCERTPASSWD] = $cert[1]; $cert = $cert[0]; } if ( ! file_exists($cert)) { throw new \InvalidArgumentException('SSL certificate not found at: '.$cert); } $curl_options[CURLOPT_SSLCERT] = $cert; } // SSL Verification if (isset($config['verify'])) { if (is_string($config['verify'])) { $file = realpath($config['ssl_key']); if ( ! $file) { throw new \InvalidArgumentException('Cannot set SSL Key. '.$config['ssl_key']. ' is not a valid file.'); } $curl_options[CURLOPT_CAINFO] = $file; $curl_options[CURLOPT_SSL_VERIFYPEER] = 1; } else if (is_bool($config['verify'])) { $curl_options[CURLOPT_SSL_VERIFYPEER] = $config['verify']; } } // Debug if (isset($config['debug'])) { $curl_options[CURLOPT_VERBOSE] = 1; $curl_options[CURLOPT_STDERR] = is_bool($config['debug']) ? fopen('php://output', 'w+') : $config['debug']; } // Decode Content if ( ! empty($config['decode_content'])) { $accept = $this->getHeaderLine('Accept-Encoding'); if ($accept) { $curl_options[CURLOPT_ENCODING] = $accept; } else { $curl_options[CURLOPT_ENCODING] = ''; $curl_options[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) { $curl_options[CURLOPT_FOLLOWLOCATION] = 0; } else { $curl_options[CURLOPT_FOLLOWLOCATION] = 1; $curl_options[CURLOPT_MAXREDIRS] = $settings['max']; if ($settings['strict'] === true) { $curl_options[CURLOPT_POSTREDIR] = 1|2|4; } $protocols = 0; foreach ($settings['protocols'] as $proto) { $protocols += constant('CURLPROTO_'.strtoupper($proto)); } $curl_options[CURLOPT_REDIR_PROTOCOLS] = $protocols; } } // Timeout $curl_options[CURLOPT_TIMEOUT_MS] = (float)$config['timeout'] * 1000; // Connection Timeout $curl_options[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'])) { $curl_options[CURLOPT_POSTFIELDS] = http_build_query($config['form_params']); if (empty($this->header('Content-Type'))) { $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 $curl_options[CURLOPT_POSTFIELDS] = $config['multipart']; } // HTTP Errors $curl_options[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()` $this->setBody(json_encode($config['json'])); $this->setHeader('Content-Type', 'application/json'); } // version if (! empty($config['version'])) { if ($config['version'] == 1.0) { $curl_options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; } else if ($config['version'] == 1.1) { $curl_options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; } } return $curl_options; } //-------------------------------------------------------------------- /** * Does the actual work of initializing cURL, setting the options, * and grabbing the output. * * @param array $curl_options * * @return string */ protected function sendRequest(array $curl_options = []): string { $ch = curl_init(); curl_setopt_array($ch, $curl_options); // Send the request and wait for a response. $output = curl_exec($ch); if ($output === false) { throw new \RuntimeException(curl_errno($ch).': '.curl_error($ch)); } curl_close($ch); return $output; } //-------------------------------------------------------------------- }