summaryrefslogtreecommitdiff
path: root/vendor/miniflux/picofeed/lib/PicoFeed/Client/Curl.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/miniflux/picofeed/lib/PicoFeed/Client/Curl.php')
-rw-r--r--vendor/miniflux/picofeed/lib/PicoFeed/Client/Curl.php402
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);
+ }
+ }
+}