Stream.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. <?php
  2. namespace PicoFeed\Client;
  3. use PicoFeed\Logging\Logger;
  4. /**
  5. * Stream context HTTP client.
  6. *
  7. * @author Frederic Guillot
  8. */
  9. class Stream extends Client
  10. {
  11. /**
  12. * Prepare HTTP headers.
  13. *
  14. * @return string[]
  15. */
  16. private function prepareHeaders()
  17. {
  18. $headers = array(
  19. 'Connection: close',
  20. 'User-Agent: '.$this->user_agent,
  21. );
  22. // disable compression in passthrough mode. It could result in double
  23. // compressed content which isn't decodeable by browsers
  24. if (function_exists('gzdecode') && !$this->isPassthroughEnabled()) {
  25. $headers[] = 'Accept-Encoding: gzip';
  26. }
  27. if ($this->etag) {
  28. $headers[] = 'If-None-Match: '.$this->etag;
  29. $headers[] = 'A-IM: feed';
  30. }
  31. if ($this->last_modified) {
  32. $headers[] = 'If-Modified-Since: '.$this->last_modified;
  33. }
  34. if ($this->proxy_username) {
  35. $headers[] = 'Proxy-Authorization: Basic '.base64_encode($this->proxy_username.':'.$this->proxy_password);
  36. }
  37. if ($this->username && $this->password) {
  38. $headers[] = 'Authorization: Basic '.base64_encode($this->username.':'.$this->password);
  39. }
  40. $headers = array_merge($headers, $this->request_headers);
  41. return $headers;
  42. }
  43. /**
  44. * Construct the final URL from location headers.
  45. *
  46. * @param array $headers List of HTTP response header
  47. */
  48. private function setEffectiveUrl($headers)
  49. {
  50. foreach ($headers as $header) {
  51. if (stripos($header, 'Location') === 0) {
  52. list(, $value) = explode(': ', $header);
  53. $this->url = Url::resolve($value, $this->url);
  54. }
  55. }
  56. }
  57. /**
  58. * Prepare stream context.
  59. *
  60. * @return array
  61. */
  62. private function prepareContext()
  63. {
  64. $context = array(
  65. 'http' => array(
  66. 'method' => 'GET',
  67. 'protocol_version' => 1.1,
  68. 'timeout' => $this->timeout,
  69. 'max_redirects' => $this->max_redirects,
  70. ),
  71. );
  72. if ($this->proxy_hostname) {
  73. Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port);
  74. $context['http']['proxy'] = 'tcp://'.$this->proxy_hostname.':'.$this->proxy_port;
  75. $context['http']['request_fulluri'] = true;
  76. if ($this->proxy_username) {
  77. Logger::setMessage(get_called_class().' Proxy credentials: Yes');
  78. } else {
  79. Logger::setMessage(get_called_class().' Proxy credentials: No');
  80. }
  81. }
  82. $context['http']['header'] = implode("\r\n", $this->prepareHeaders());
  83. return $context;
  84. }
  85. /**
  86. * Do the HTTP request.
  87. *
  88. * @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...]
  89. * @throws InvalidUrlException
  90. * @throws MaxSizeException
  91. * @throws TimeoutException
  92. */
  93. public function doRequest()
  94. {
  95. $body = '';
  96. // Create context
  97. $context = stream_context_create($this->prepareContext());
  98. // Make HTTP request
  99. $stream = @fopen($this->url, 'r', false, $context);
  100. if (!is_resource($stream)) {
  101. throw new InvalidUrlException('Unable to establish a connection');
  102. }
  103. // Get HTTP headers response
  104. $metadata = stream_get_meta_data($stream);
  105. list($status, $headers) = HttpHeaders::parse($metadata['wrapper_data']);
  106. if ($this->isPassthroughEnabled()) {
  107. header(':', true, $status);
  108. if (isset($headers['Content-Type'])) {
  109. header('Content-Type: '.$headers['Content-Type']);
  110. }
  111. fpassthru($stream);
  112. } else {
  113. // Get the entire body until the max size
  114. $body = stream_get_contents($stream, $this->max_body_size + 1);
  115. // If the body size is too large abort everything
  116. if (strlen($body) > $this->max_body_size) {
  117. throw new MaxSizeException('Content size too large');
  118. }
  119. if ($metadata['timed_out']) {
  120. throw new TimeoutException('Operation timeout');
  121. }
  122. }
  123. fclose($stream);
  124. $this->setEffectiveUrl($metadata['wrapper_data']);
  125. return array(
  126. 'status' => $status,
  127. 'body' => $this->decodeBody($body, $headers),
  128. 'headers' => $headers,
  129. );
  130. }
  131. /**
  132. * Decode body response according to the HTTP headers.
  133. *
  134. * @param string $body Raw body
  135. * @param HttpHeaders $headers HTTP headers
  136. *
  137. * @return string
  138. */
  139. public function decodeBody($body, HttpHeaders $headers)
  140. {
  141. if (isset($headers['Transfer-Encoding']) && $headers['Transfer-Encoding'] === 'chunked') {
  142. $body = $this->decodeChunked($body);
  143. }
  144. if (isset($headers['Content-Encoding']) && $headers['Content-Encoding'] === 'gzip') {
  145. $body = gzdecode($body);
  146. }
  147. return $body;
  148. }
  149. /**
  150. * Decode a chunked body.
  151. *
  152. * @param string $str Raw body
  153. *
  154. * @return string Decoded body
  155. */
  156. public function decodeChunked($str)
  157. {
  158. for ($result = ''; !empty($str); $str = trim($str)) {
  159. // Get the chunk length
  160. $pos = strpos($str, "\r\n");
  161. $len = hexdec(substr($str, 0, $pos));
  162. // Append the chunk to the result
  163. $result .= substr($str, $pos + 2, $len);
  164. $str = substr($str, $pos + 2 + $len);
  165. }
  166. return $result;
  167. }
  168. }