miniflux-legacy/vendor/miniflux/picofeed/lib/PicoFeed/Client/Curl.php

403 lines
12 KiB
PHP
Raw Normal View History

2013-07-17 01:54:44 +02:00
<?php
namespace PicoFeed\Client;
2013-07-17 01:54:44 +02:00
use PicoFeed\Logging\Logger;
2014-05-20 20:20:27 +02:00
/**
* cURL HTTP client.
2014-05-20 20:20:27 +02:00
*
* @author Frederic Guillot
*/
class Curl extends Client
2013-07-17 01:54:44 +02:00
{
2016-12-26 23:32:18 +01:00
protected $nbRedirects = 0;
2015-02-01 20:48:05 +01:00
/**
* HTTP response body.
2015-02-01 20:48:05 +01:00
*
* @var string
*/
private $body = '';
/**
* Body size.
2015-02-01 20:48:05 +01:00
*
* @var int
2015-02-01 20:48:05 +01:00
*/
private $body_length = 0;
/**
* HTTP response headers.
2015-02-01 20:48:05 +01:00
*
* @var array
*/
2015-04-11 02:34:48 +02:00
private $response_headers = array();
2015-02-01 20:48:05 +01:00
/**
* Counter on the number of header received.
2015-02-01 20:48:05 +01:00
*
* @var int
2015-02-01 20:48:05 +01:00
*/
2015-04-11 02:34:48 +02:00
private $response_headers_count = 0;
2015-02-01 20:48:05 +01:00
2014-05-20 20:20:27 +02:00
/**
* cURL callback to read the HTTP body.
2014-05-20 20:20:27 +02:00
*
* 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
2014-05-20 20:20:27 +02:00
*/
2013-07-17 01:54:44 +02:00
public function readBody($ch, $buffer)
{
$length = strlen($buffer);
$this->body_length += $length;
2014-05-20 20:20:27 +02:00
if ($this->body_length > $this->max_body_size) {
return -1;
}
2013-07-17 01:54:44 +02:00
$this->body .= $buffer;
return $length;
}
2014-05-20 20:20:27 +02:00
/**
* cURL callback to read HTTP headers.
*
* @param resource $ch cURL handler
* @param string $buffer Header line
2014-05-20 20:20:27 +02:00
*
* @return int Length of the buffer
2014-05-20 20:20:27 +02:00
*/
2013-07-17 01:54:44 +02:00
public function readHeaders($ch, $buffer)
{
$length = strlen($buffer);
2015-04-28 18:08:42 +02:00
if ($buffer === "\r\n" || $buffer === "\n") {
++$this->response_headers_count;
} else {
if (!isset($this->response_headers[$this->response_headers_count])) {
2015-04-11 02:34:48 +02:00
$this->response_headers[$this->response_headers_count] = '';
2013-07-17 01:54:44 +02:00
}
2015-04-11 02:34:48 +02:00
$this->response_headers[$this->response_headers_count] .= $buffer;
2013-07-17 01:54:44 +02:00
}
return $length;
}
2015-02-06 03:16:34 +01:00
/**
* cURL callback to passthrough the HTTP body to the client.
2015-02-06 03:16:34 +01:00
*
* 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
2015-02-06 03:16:34 +01:00
*/
public function passthroughBody($ch, $buffer)
{
2015-12-16 01:26:15 +01:00
// 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']);
}
2017-01-10 00:20:34 +01:00
// Do not work with PHP-FPM
2017-01-15 22:05:50 +01:00
if (strpos(PHP_SAPI, 'cgi') !== false) {
2017-01-10 00:20:34 +01:00
header(':', true, $status);
}
2015-12-16 01:26:15 +01:00
if (isset($headers['Content-Type'])) {
header('Content-Type:' .$headers['Content-Type']);
}
}
$length = strlen($buffer);
$this->body_length += $length;
2015-02-06 03:16:34 +01:00
echo $buffer;
2015-12-16 01:26:15 +01:00
return $length;
2015-02-06 03:16:34 +01:00
}
2014-05-20 20:20:27 +02:00
/**
* Prepare HTTP headers.
2014-05-20 20:20:27 +02:00
*
* @return string[]
2014-05-20 20:20:27 +02:00
*/
2014-10-19 20:42:31 +02:00
private function prepareHeaders()
2013-07-17 01:54:44 +02:00
{
2014-10-19 20:42:31 +02:00
$headers = array(
'Connection: close',
);
2013-07-17 01:54:44 +02:00
2014-10-19 20:42:31 +02:00
if ($this->etag) {
$headers[] = 'If-None-Match: '.$this->etag;
2016-12-26 23:32:18 +01:00
$headers[] = 'A-IM: feed';
2014-10-19 20:42:31 +02:00
}
2013-07-17 01:54:44 +02:00
2014-10-19 20:42:31 +02:00
if ($this->last_modified) {
$headers[] = 'If-Modified-Since: '.$this->last_modified;
}
2013-07-17 01:54:44 +02:00
2015-04-11 02:34:48 +02:00
$headers = array_merge($headers, $this->request_headers);
2014-10-19 20:42:31 +02:00
return $headers;
}
2013-09-24 01:22:13 +02:00
2014-10-19 20:42:31 +02:00
/**
* Prepare curl proxy context.
*
* @param resource $ch
2014-10-19 20:42:31 +02:00
*
* @return resource $ch
2014-10-19 20:42:31 +02:00
*/
private function prepareProxyContext($ch)
{
2014-05-20 20:20:27 +02:00
if ($this->proxy_hostname) {
Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port);
2014-05-20 20:20:27 +02:00
curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxy_port);
2013-09-24 01:22:13 +02:00
curl_setopt($ch, CURLOPT_PROXYTYPE, 'HTTP');
2014-05-20 20:20:27 +02:00
curl_setopt($ch, CURLOPT_PROXY, $this->proxy_hostname);
2013-09-24 01:22:13 +02:00
2014-05-20 20:20:27 +02:00
if ($this->proxy_username) {
Logger::setMessage(get_called_class().' Proxy credentials: Yes');
2014-05-20 20:20:27 +02:00
curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxy_username.':'.$this->proxy_password);
} else {
Logger::setMessage(get_called_class().' Proxy credentials: No');
2013-09-24 01:22:13 +02:00
}
}
2014-10-19 20:42:31 +02:00
return $ch;
}
2014-12-24 19:50:20 +01:00
/**
* Prepare curl auth context.
*
* @param resource $ch
2014-12-24 19:50:20 +01:00
*
* @return resource $ch
*/
private function prepareAuthContext($ch)
{
if ($this->username && $this->password) {
curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password);
}
return $ch;
}
2015-02-06 03:16:34 +01:00
/**
* Set write/header functions.
*
* @param resource $ch
2015-02-06 03:16:34 +01:00
*
* @return resource $ch
*/
private function prepareDownloadMode($ch)
{
2016-12-26 23:32:18 +01:00
$this->body = '';
$this->response_headers = array();
$this->response_headers_count = 0;
2015-02-06 03:16:34 +01:00
$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;
}
2014-10-19 20:42:31 +02:00
/**
* Prepare curl context.
2014-10-19 20:42:31 +02:00
*
* @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);
2015-04-11 15:40:09 +02:00
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
2014-10-19 20:42:31 +02:00
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
2015-03-01 19:56:11 +01:00
curl_setopt($ch, CURLOPT_USERAGENT, $this->user_agent);
2014-10-19 20:42:31 +02:00
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->prepareHeaders());
2015-08-02 20:08:21 +02:00
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
2014-10-19 20:42:31 +02:00
curl_setopt($ch, CURLOPT_ENCODING, '');
curl_setopt($ch, CURLOPT_COOKIEJAR, 'php://memory');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'php://memory');
2015-03-01 19:56:11 +01:00
// 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);
}
2014-10-19 20:42:31 +02:00
2015-02-06 03:16:34 +01:00
$ch = $this->prepareDownloadMode($ch);
2014-10-19 20:42:31 +02:00
$ch = $this->prepareProxyContext($ch);
2014-12-24 19:50:20 +01:00
$ch = $this->prepareAuthContext($ch);
2014-10-19 20:42:31 +02:00
return $ch;
}
/**
* Execute curl context.
2014-10-19 20:42:31 +02:00
*/
private function executeContext()
{
$ch = $this->prepareContext();
2013-07-17 01:54:44 +02:00
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));
2013-07-17 01:54:44 +02:00
$curl_errno = curl_errno($ch);
if ($curl_errno) {
Logger::setMessage(get_called_class().' cURL error: '.curl_error($ch));
2013-07-17 01:54:44 +02:00
curl_close($ch);
$this->handleError($curl_errno);
2013-07-17 01:54:44 +02:00
}
// Update the url if there where redirects
$this->url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
2013-07-17 01:54:44 +02:00
curl_close($ch);
2014-10-19 20:42:31 +02:00
}
2014-10-19 20:42:31 +02:00
/**
* Do the HTTP request.
2014-10-19 20:42:31 +02:00
*
* @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...]
2014-10-19 20:42:31 +02:00
*/
2015-12-16 01:26:15 +01:00
public function doRequest()
2014-10-19 20:42:31 +02:00
{
$this->executeContext();
2015-04-28 18:08:42 +02:00
list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1]));
2015-12-16 01:26:15 +01:00
if ($this->isRedirection($status)) {
2017-04-07 04:08:27 +02:00
if (empty($headers['Location'])) {
$status = 200;
} else {
return $this->handleRedirection($headers['Location']);
}
}
2013-07-17 01:54:44 +02:00
return array(
'status' => $status,
'body' => $this->body,
'headers' => $headers,
2013-07-17 01:54:44 +02:00
);
}
2014-10-19 20:42:31 +02:00
2015-02-01 20:48:05 +01:00
/**
2015-12-16 01:26:15 +01:00
* Handle HTTP redirects
*
* @param string $location Redirected URL
2015-02-01 20:48:05 +01:00
* @return array
2016-12-26 23:32:18 +01:00
* @throws MaxRedirectException
2015-02-01 20:48:05 +01:00
*/
private function handleRedirection($location)
{
$result = array();
$this->url = Url::resolve($location, $this->url);
$this->body = '';
$this->body_length = 0;
2015-04-11 02:34:48 +02:00
$this->response_headers = array();
$this->response_headers_count = 0;
2015-02-01 20:48:05 +01:00
while (true) {
2016-12-26 23:32:18 +01:00
$this->nbRedirects++;
2015-02-01 20:48:05 +01:00
2016-12-26 23:32:18 +01:00
if ($this->nbRedirects >= $this->max_redirects) {
2015-02-01 20:48:05 +01:00
throw new MaxRedirectException('Maximum number of redirections reached');
}
2015-12-16 01:26:15 +01:00
$result = $this->doRequest();
2015-02-01 20:48:05 +01:00
if ($this->isRedirection($result['status'])) {
2015-02-01 20:48:05 +01:00
$this->url = Url::resolve($result['headers']['Location'], $this->url);
$this->body = '';
$this->body_length = 0;
2015-04-11 02:34:48 +02:00
$this->response_headers = array();
$this->response_headers_count = 0;
} else {
2015-02-01 20:48:05 +01:00
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
2017-01-10 00:20:34 +01:00
* @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
2017-01-10 00:20:34 +01:00
$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);
}
2014-10-19 20:42:31 +02:00
}
2014-05-20 20:20:27 +02:00
}