123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712 |
- <?php
- namespace PicoFeed\Client;
- use DateTime;
- use Exception;
- use LogicException;
- use PicoFeed\Logging\Logger;
- use PicoFeed\Config\Config;
- /**
- * Client class.
- *
- * @author Frederic Guillot
- */
- abstract class Client
- {
- /**
- * Flag that say if the resource have been modified.
- *
- * @var bool
- */
- private $is_modified = true;
- /**
- * HTTP Content-Type.
- *
- * @var string
- */
- private $content_type = '';
- /**
- * HTTP encoding.
- *
- * @var string
- */
- private $encoding = '';
- /**
- * HTTP request headers.
- *
- * @var array
- */
- protected $request_headers = array();
- /**
- * HTTP Etag header.
- *
- * @var string
- */
- protected $etag = '';
- /**
- * HTTP Last-Modified header.
- *
- * @var string
- */
- protected $last_modified = '';
- /**
- * Expiration DateTime
- *
- * @var DateTime
- */
- protected $expiration = null;
- /**
- * Proxy hostname.
- *
- * @var string
- */
- protected $proxy_hostname = '';
- /**
- * Proxy port.
- *
- * @var int
- */
- protected $proxy_port = 3128;
- /**
- * Proxy username.
- *
- * @var string
- */
- protected $proxy_username = '';
- /**
- * Proxy password.
- *
- * @var string
- */
- protected $proxy_password = '';
- /**
- * Basic auth username.
- *
- * @var string
- */
- protected $username = '';
- /**
- * Basic auth password.
- *
- * @var string
- */
- protected $password = '';
- /**
- * CURL options.
- *
- * @var array
- */
- protected $additional_curl_options = array();
- /**
- * Client connection timeout.
- *
- * @var int
- */
- protected $timeout = 10;
- /**
- * User-agent.
- *
- * @var string
- */
- protected $user_agent = 'PicoFeed (https://github.com/miniflux/picoFeed)';
- /**
- * Real URL used (can be changed after a HTTP redirect).
- *
- * @var string
- */
- protected $url = '';
- /**
- * Page/Feed content.
- *
- * @var string
- */
- protected $content = '';
- /**
- * Number maximum of HTTP redirections to avoid infinite loops.
- *
- * @var int
- */
- protected $max_redirects = 5;
- /**
- * Maximum size of the HTTP body response.
- *
- * @var int
- */
- protected $max_body_size = 2097152; // 2MB
- /**
- * HTTP response status code.
- *
- * @var int
- */
- protected $status_code = 0;
- /**
- * Enables direct passthrough to requesting client.
- *
- * @var bool
- */
- protected $passthrough = false;
- /**
- * Do the HTTP request.
- *
- * @abstract
- *
- * @return array
- */
- abstract public function doRequest();
- /**
- * Get client instance: curl or stream driver.
- *
- * @static
- *
- * @return \PicoFeed\Client\Client
- */
- public static function getInstance()
- {
- if (function_exists('curl_init')) {
- return new Curl();
- } elseif (ini_get('allow_url_fopen')) {
- return new Stream();
- }
- throw new LogicException('You must have "allow_url_fopen=1" or curl extension installed');
- }
- /**
- * Add HTTP Header to the request.
- *
- * @param array $headers
- */
- public function setHeaders($headers)
- {
- $this->request_headers = $headers;
- }
- /**
- * Perform the HTTP request.
- *
- * @param string $url URL
- *
- * @return Client
- */
- public function execute($url = '')
- {
- if ($url !== '') {
- $this->url = $url;
- }
- Logger::setMessage(get_called_class().' Fetch URL: '.$this->url);
- Logger::setMessage(get_called_class().' Etag provided: '.$this->etag);
- Logger::setMessage(get_called_class().' Last-Modified provided: '.$this->last_modified);
- $response = $this->doRequest();
- $this->status_code = $response['status'];
- $this->handleNotModifiedResponse($response);
- $this->handleErrorResponse($response);
- $this->handleNormalResponse($response);
- $this->expiration = $this->parseExpiration($response['headers']);
- Logger::setMessage(get_called_class().' Expiration: '.$this->expiration->format(DATE_ISO8601));
- return $this;
- }
- /**
- * Handle not modified response.
- *
- * @param array $response Client response
- */
- protected function handleNotModifiedResponse(array $response)
- {
- if ($response['status'] == 304) {
- $this->is_modified = false;
- } elseif ($response['status'] == 200) {
- $this->is_modified = $this->hasBeenModified($response, $this->etag, $this->last_modified);
- $this->etag = $this->getHeader($response, 'ETag');
- $this->last_modified = $this->getHeader($response, 'Last-Modified');
- }
- if ($this->is_modified === false) {
- Logger::setMessage(get_called_class().' Resource not modified');
- }
- }
- /**
- * Handle Http Error codes
- *
- * @param array $response Client response
- * @throws ForbiddenException
- * @throws InvalidUrlException
- * @throws UnauthorizedException
- */
- protected function handleErrorResponse(array $response)
- {
- $status = $response['status'];
- if ($status == 401) {
- throw new UnauthorizedException('Wrong or missing credentials');
- } else if ($status == 403) {
- throw new ForbiddenException('Not allowed to access resource');
- } else if ($status == 404) {
- throw new InvalidUrlException('Resource not found');
- }
- }
- /**
- * Handle normal response.
- *
- * @param array $response Client response
- */
- protected function handleNormalResponse(array $response)
- {
- if ($response['status'] == 200) {
- $this->content = $response['body'];
- $this->content_type = $this->findContentType($response);
- $this->encoding = $this->findCharset();
- }
- }
- /**
- * Check if a request has been modified according to the parameters.
- *
- * @param array $response
- * @param string $etag
- * @param string $lastModified
- *
- * @return bool
- */
- private function hasBeenModified($response, $etag, $lastModified)
- {
- $headers = array(
- 'Etag' => $etag,
- 'Last-Modified' => $lastModified,
- );
- // Compare the values for each header that is present
- $presentCacheHeaderCount = 0;
- foreach ($headers as $key => $value) {
- if (isset($response['headers'][$key])) {
- if ($response['headers'][$key] !== $value) {
- return true;
- }
- ++$presentCacheHeaderCount;
- }
- }
- // If at least one header is present and the values match, the response
- // was not modified
- if ($presentCacheHeaderCount > 0) {
- return false;
- }
- return true;
- }
- /**
- * Find content type from response headers.
- *
- * @param array $response Client response
- * @return string
- */
- public function findContentType(array $response)
- {
- return strtolower($this->getHeader($response, 'Content-Type'));
- }
- /**
- * Find charset from response headers.
- *
- * @return string
- */
- public function findCharset()
- {
- $result = explode('charset=', $this->content_type);
- return isset($result[1]) ? $result[1] : '';
- }
- /**
- * Get header value from a client response.
- *
- * @param array $response Client response
- * @param string $header Header name
- * @return string
- */
- public function getHeader(array $response, $header)
- {
- return isset($response['headers'][$header]) ? $response['headers'][$header] : '';
- }
- /**
- * Set the Last-Modified HTTP header.
- *
- * @param string $last_modified Header value
- * @return $this
- */
- public function setLastModified($last_modified)
- {
- $this->last_modified = $last_modified;
- return $this;
- }
- /**
- * Get the value of the Last-Modified HTTP header.
- *
- * @return string
- */
- public function getLastModified()
- {
- return $this->last_modified;
- }
- /**
- * Set the value of the Etag HTTP header.
- *
- * @param string $etag Etag HTTP header value
- * @return $this
- */
- public function setEtag($etag)
- {
- $this->etag = $etag;
- return $this;
- }
- /**
- * Get the Etag HTTP header value.
- *
- * @return string
- */
- public function getEtag()
- {
- return $this->etag;
- }
- /**
- * Get the final url value.
- *
- * @return string
- */
- public function getUrl()
- {
- return $this->url;
- }
- /**
- * Set the url.
- *
- * @param $url
- * @return string
- */
- public function setUrl($url)
- {
- $this->url = $url;
- return $this;
- }
- /**
- * Get the HTTP response status code.
- *
- * @return int
- */
- public function getStatusCode()
- {
- return $this->status_code;
- }
- /**
- * Get the body of the HTTP response.
- *
- * @return string
- */
- public function getContent()
- {
- return $this->content;
- }
- /**
- * Get the content type value from HTTP headers.
- *
- * @return string
- */
- public function getContentType()
- {
- return $this->content_type;
- }
- /**
- * Get the encoding value from HTTP headers.
- *
- * @return string
- */
- public function getEncoding()
- {
- return $this->encoding;
- }
- /**
- * Return true if the remote resource has changed.
- *
- * @return bool
- */
- public function isModified()
- {
- return $this->is_modified;
- }
- /**
- * return true if passthrough mode is enabled.
- *
- * @return bool
- */
- public function isPassthroughEnabled()
- {
- return $this->passthrough;
- }
- /**
- * Set connection timeout.
- *
- * @param int $timeout Connection timeout
- * @return $this
- */
- public function setTimeout($timeout)
- {
- $this->timeout = $timeout ?: $this->timeout;
- return $this;
- }
- /**
- * Set a custom user agent.
- *
- * @param string $user_agent User Agent
- * @return $this
- */
- public function setUserAgent($user_agent)
- {
- $this->user_agent = $user_agent ?: $this->user_agent;
- return $this;
- }
- /**
- * Set the maximum number of HTTP redirections.
- *
- * @param int $max Maximum
- * @return $this
- */
- public function setMaxRedirections($max)
- {
- $this->max_redirects = $max ?: $this->max_redirects;
- return $this;
- }
- /**
- * Set the maximum size of the HTTP body.
- *
- * @param int $max Maximum
- * @return $this
- */
- public function setMaxBodySize($max)
- {
- $this->max_body_size = $max ?: $this->max_body_size;
- return $this;
- }
- /**
- * Set the proxy hostname.
- *
- * @param string $hostname Proxy hostname
- * @return $this
- */
- public function setProxyHostname($hostname)
- {
- $this->proxy_hostname = $hostname ?: $this->proxy_hostname;
- return $this;
- }
- /**
- * Set the proxy port.
- *
- * @param int $port Proxy port
- * @return $this
- */
- public function setProxyPort($port)
- {
- $this->proxy_port = $port ?: $this->proxy_port;
- return $this;
- }
- /**
- * Set the proxy username.
- *
- * @param string $username Proxy username
- * @return $this
- */
- public function setProxyUsername($username)
- {
- $this->proxy_username = $username ?: $this->proxy_username;
- return $this;
- }
- /**
- * Set the proxy password.
- *
- * @param string $password Password
- * @return $this
- */
- public function setProxyPassword($password)
- {
- $this->proxy_password = $password ?: $this->proxy_password;
- return $this;
- }
- /**
- * Set the username.
- *
- * @param string $username Basic Auth username
- *
- * @return $this
- */
- public function setUsername($username)
- {
- $this->username = $username ?: $this->username;
- return $this;
- }
- /**
- * Set the password.
- *
- * @param string $password Basic Auth Password
- *
- * @return $this
- */
- public function setPassword($password)
- {
- $this->password = $password ?: $this->password;
- return $this;
- }
- /**
- * Set the CURL options.
- *
- * @param array $options
- * @return $this
- */
- public function setAdditionalCurlOptions(array $options)
- {
- $this->additional_curl_options = $options ?: $this->additional_curl_options;
- return $this;
- }
- /**
- * Enable the passthrough mode.
- *
- * @return $this
- */
- public function enablePassthroughMode()
- {
- $this->passthrough = true;
- return $this;
- }
- /**
- * Disable the passthrough mode.
- *
- * @return $this
- */
- public function disablePassthroughMode()
- {
- $this->passthrough = false;
- return $this;
- }
- /**
- * Set config object.
- *
- * @param \PicoFeed\Config\Config $config Config instance
- * @return $this
- */
- public function setConfig(Config $config)
- {
- if ($config !== null) {
- $this->setTimeout($config->getClientTimeout());
- $this->setUserAgent($config->getClientUserAgent());
- $this->setMaxRedirections($config->getMaxRedirections());
- $this->setMaxBodySize($config->getMaxBodySize());
- $this->setProxyHostname($config->getProxyHostname());
- $this->setProxyPort($config->getProxyPort());
- $this->setProxyUsername($config->getProxyUsername());
- $this->setProxyPassword($config->getProxyPassword());
- $this->setAdditionalCurlOptions($config->getAdditionalCurlOptions() ?: array());
- }
- return $this;
- }
- /**
- * Return true if the HTTP status code is a redirection
- *
- * @access protected
- * @param integer $code
- * @return boolean
- */
- public function isRedirection($code)
- {
- return $code == 301 || $code == 302 || $code == 303 || $code == 307;
- }
- public function parseExpiration(HttpHeaders $headers)
- {
- try {
- if (isset($headers['Cache-Control'])) {
- if (preg_match('/s-maxage=(\d+)/', $headers['Cache-Control'], $matches)) {
- return new DateTime('+' . $matches[1] . ' seconds');
- } else if (preg_match('/max-age=(\d+)/', $headers['Cache-Control'], $matches)) {
- return new DateTime('+' . $matches[1] . ' seconds');
- }
- }
- if (! empty($headers['Expires'])) {
- return new DateTime($headers['Expires']);
- }
- } catch (Exception $e) {
- Logger::setMessage('Unable to parse expiration date: '.$e->getMessage());
- }
- return new DateTime();
- }
- /**
- * Get expiration date time from "Expires" or "Cache-Control" headers
- *
- * @return DateTime
- */
- public function getExpiration()
- {
- return $this->expiration ?: new DateTime();
- }
- }
|