diff options
Diffstat (limited to 'vendor/miniflux/picofeed/lib/PicoFeed/Client/Curl.php')
-rw-r--r-- | vendor/miniflux/picofeed/lib/PicoFeed/Client/Curl.php | 402 |
1 files changed, 402 insertions, 0 deletions
diff --git a/vendor/miniflux/picofeed/lib/PicoFeed/Client/Curl.php b/vendor/miniflux/picofeed/lib/PicoFeed/Client/Curl.php new file mode 100644 index 00000000..f4a65782 --- /dev/null +++ b/vendor/miniflux/picofeed/lib/PicoFeed/Client/Curl.php @@ -0,0 +1,402 @@ +<?php + +namespace PicoFeed\Client; + +use PicoFeed\Logging\Logger; + +/** + * cURL HTTP client. + * + * @author Frederic Guillot + */ +class Curl extends Client +{ + protected $nbRedirects = 0; + + /** + * HTTP response body. + * + * @var string + */ + private $body = ''; + + /** + * Body size. + * + * @var int + */ + private $body_length = 0; + + /** + * HTTP response headers. + * + * @var array + */ + private $response_headers = array(); + + /** + * Counter on the number of header received. + * + * @var int + */ + private $response_headers_count = 0; + + /** + * cURL callback to read the HTTP body. + * + * If the function return -1, curl stop to read the HTTP response + * + * @param resource $ch cURL handler + * @param string $buffer Chunk of data + * + * @return int Length of the buffer + */ + public function readBody($ch, $buffer) + { + $length = strlen($buffer); + $this->body_length += $length; + + if ($this->body_length > $this->max_body_size) { + return -1; + } + + $this->body .= $buffer; + + return $length; + } + + /** + * cURL callback to read HTTP headers. + * + * @param resource $ch cURL handler + * @param string $buffer Header line + * + * @return int Length of the buffer + */ + public function readHeaders($ch, $buffer) + { + $length = strlen($buffer); + + if ($buffer === "\r\n" || $buffer === "\n") { + ++$this->response_headers_count; + } else { + if (!isset($this->response_headers[$this->response_headers_count])) { + $this->response_headers[$this->response_headers_count] = ''; + } + + $this->response_headers[$this->response_headers_count] .= $buffer; + } + + return $length; + } + + /** + * cURL callback to passthrough the HTTP body to the client. + * + * If the function return -1, curl stop to read the HTTP response + * + * @param resource $ch cURL handler + * @param string $buffer Chunk of data + * + * @return int Length of the buffer + */ + public function passthroughBody($ch, $buffer) + { + // do it only at the beginning of a transmission + if ($this->body_length === 0) { + list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1])); + + if ($this->isRedirection($status)) { + return $this->handleRedirection($headers['Location']); + } + + // Do not work with PHP-FPM + if (strpos(PHP_SAPI, 'cgi') !== false) { + header(':', true, $status); + } + + if (isset($headers['Content-Type'])) { + header('Content-Type:' .$headers['Content-Type']); + } + } + + $length = strlen($buffer); + $this->body_length += $length; + + echo $buffer; + + return $length; + } + + /** + * Prepare HTTP headers. + * + * @return string[] + */ + private function prepareHeaders() + { + $headers = array( + 'Connection: close', + ); + + if ($this->etag) { + $headers[] = 'If-None-Match: '.$this->etag; + $headers[] = 'A-IM: feed'; + } + + if ($this->last_modified) { + $headers[] = 'If-Modified-Since: '.$this->last_modified; + } + + $headers = array_merge($headers, $this->request_headers); + + return $headers; + } + + /** + * Prepare curl proxy context. + * + * @param resource $ch + * + * @return resource $ch + */ + private function prepareProxyContext($ch) + { + if ($this->proxy_hostname) { + Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port); + + curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxy_port); + curl_setopt($ch, CURLOPT_PROXYTYPE, 'HTTP'); + curl_setopt($ch, CURLOPT_PROXY, $this->proxy_hostname); + + if ($this->proxy_username) { + Logger::setMessage(get_called_class().' Proxy credentials: Yes'); + curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxy_username.':'.$this->proxy_password); + } else { + Logger::setMessage(get_called_class().' Proxy credentials: No'); + } + } + + return $ch; + } + + /** + * Prepare curl auth context. + * + * @param resource $ch + * + * @return resource $ch + */ + private function prepareAuthContext($ch) + { + if ($this->username && $this->password) { + curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password); + } + + return $ch; + } + + /** + * Set write/header functions. + * + * @param resource $ch + * + * @return resource $ch + */ + private function prepareDownloadMode($ch) + { + $this->body = ''; + $this->response_headers = array(); + $this->response_headers_count = 0; + $write_function = 'readBody'; + $header_function = 'readHeaders'; + + if ($this->isPassthroughEnabled()) { + $write_function = 'passthroughBody'; + } + + curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($this, $write_function)); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, $header_function)); + + return $ch; + } + + /** + * Prepare curl context. + * + * @return resource + */ + private function prepareContext() + { + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $this->url); + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); + curl_setopt($ch, CURLOPT_USERAGENT, $this->user_agent); + curl_setopt($ch, CURLOPT_HTTPHEADER, $this->prepareHeaders()); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + curl_setopt($ch, CURLOPT_ENCODING, ''); + curl_setopt($ch, CURLOPT_COOKIEJAR, 'php://memory'); + curl_setopt($ch, CURLOPT_COOKIEFILE, 'php://memory'); + + // Disable SSLv3 by enforcing TLSv1.x for curl >= 7.34.0 and < 7.39.0. + // Versions prior to 7.34 and at least when compiled against openssl + // interpret this parameter as "limit to TLSv1.0" which fails for sites + // which enforce TLS 1.1+. + // Starting with curl 7.39.0 SSLv3 is disabled by default. + $version = curl_version(); + if ($version['version_number'] >= 467456 && $version['version_number'] < 468736) { + curl_setopt($ch, CURLOPT_SSLVERSION, 1); + } + + $ch = $this->prepareDownloadMode($ch); + $ch = $this->prepareProxyContext($ch); + $ch = $this->prepareAuthContext($ch); + + return $ch; + } + + /** + * Execute curl context. + */ + private function executeContext() + { + $ch = $this->prepareContext(); + curl_exec($ch); + + Logger::setMessage(get_called_class().' cURL total time: '.curl_getinfo($ch, CURLINFO_TOTAL_TIME)); + Logger::setMessage(get_called_class().' cURL dns lookup time: '.curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME)); + Logger::setMessage(get_called_class().' cURL connect time: '.curl_getinfo($ch, CURLINFO_CONNECT_TIME)); + Logger::setMessage(get_called_class().' cURL speed download: '.curl_getinfo($ch, CURLINFO_SPEED_DOWNLOAD)); + Logger::setMessage(get_called_class().' cURL effective url: '.curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)); + + $curl_errno = curl_errno($ch); + + if ($curl_errno) { + Logger::setMessage(get_called_class().' cURL error: '.curl_error($ch)); + curl_close($ch); + + $this->handleError($curl_errno); + } + + // Update the url if there where redirects + $this->url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); + + curl_close($ch); + } + + /** + * Do the HTTP request. + * + * @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...] + */ + public function doRequest() + { + $this->executeContext(); + + list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1])); + + if ($this->isRedirection($status)) { + if (empty($headers['Location'])) { + $status = 200; + } else { + return $this->handleRedirection($headers['Location']); + } + } + + return array( + 'status' => $status, + 'body' => $this->body, + 'headers' => $headers, + ); + } + + /** + * Handle HTTP redirects + * + * @param string $location Redirected URL + * @return array + * @throws MaxRedirectException + */ + private function handleRedirection($location) + { + $result = array(); + $this->url = Url::resolve($location, $this->url); + $this->body = ''; + $this->body_length = 0; + $this->response_headers = array(); + $this->response_headers_count = 0; + + while (true) { + $this->nbRedirects++; + + if ($this->nbRedirects >= $this->max_redirects) { + throw new MaxRedirectException('Maximum number of redirections reached'); + } + + $result = $this->doRequest(); + + if ($this->isRedirection($result['status'])) { + $this->url = Url::resolve($result['headers']['Location'], $this->url); + $this->body = ''; + $this->body_length = 0; + $this->response_headers = array(); + $this->response_headers_count = 0; + } else { + break; + } + } + + return $result; + } + + /** + * Handle cURL errors (throw individual exceptions). + * + * We don't use constants because they are not necessary always available + * (depends of the version of libcurl linked to php) + * + * @see http://curl.haxx.se/libcurl/c/libcurl-errors.html + * + * @param int $errno cURL error code + * @throws InvalidCertificateException + * @throws InvalidUrlException + * @throws MaxRedirectException + * @throws MaxSizeException + * @throws TimeoutException + */ + private function handleError($errno) + { + switch ($errno) { + case 78: // CURLE_REMOTE_FILE_NOT_FOUND + throw new InvalidUrlException('Resource not found', $errno); + case 6: // CURLE_COULDNT_RESOLVE_HOST + throw new InvalidUrlException('Unable to resolve hostname', $errno); + case 7: // CURLE_COULDNT_CONNECT + throw new InvalidUrlException('Unable to connect to the remote host', $errno); + case 23: // CURLE_WRITE_ERROR + throw new MaxSizeException('Maximum response size exceeded', $errno); + case 28: // CURLE_OPERATION_TIMEDOUT + throw new TimeoutException('Operation timeout', $errno); + case 35: // CURLE_SSL_CONNECT_ERROR + case 51: // CURLE_PEER_FAILED_VERIFICATION + case 58: // CURLE_SSL_CERTPROBLEM + case 60: // CURLE_SSL_CACERT + case 59: // CURLE_SSL_CIPHER + case 64: // CURLE_USE_SSL_FAILED + case 66: // CURLE_SSL_ENGINE_INITFAILED + case 77: // CURLE_SSL_CACERT_BADFILE + case 83: // CURLE_SSL_ISSUER_ERROR + $msg = 'Invalid SSL certificate caused by CURL error number ' . $errno; + throw new InvalidCertificateException($msg, $errno); + case 47: // CURLE_TOO_MANY_REDIRECTS + throw new MaxRedirectException('Maximum number of redirections reached', $errno); + case 63: // CURLE_FILESIZE_EXCEEDED + throw new MaxSizeException('Maximum response size exceeded', $errno); + default: + throw new InvalidUrlException('Unable to fetch the URL', $errno); + } + } +} |