StreamWrapper.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpClient\Response;
  11. use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
  12. use Symfony\Contracts\HttpClient\HttpClientInterface;
  13. use Symfony\Contracts\HttpClient\ResponseInterface;
  14. /**
  15. * Allows turning ResponseInterface instances to PHP streams.
  16. *
  17. * @author Nicolas Grekas <p@tchwork.com>
  18. */
  19. class StreamWrapper
  20. {
  21. /** @var resource|null */
  22. public $context;
  23. /** @var HttpClientInterface */
  24. private $client;
  25. /** @var ResponseInterface */
  26. private $response;
  27. /** @var resource|string|null */
  28. private $content;
  29. /** @var resource|null */
  30. private $handle;
  31. private $blocking = true;
  32. private $timeout;
  33. private $eof = false;
  34. private $offset = 0;
  35. /**
  36. * Creates a PHP stream resource from a ResponseInterface.
  37. *
  38. * @return resource
  39. */
  40. public static function createResource(ResponseInterface $response, HttpClientInterface $client = null)
  41. {
  42. if ($response instanceof StreamableInterface) {
  43. $stack = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  44. if ($response !== ($stack[1]['object'] ?? null)) {
  45. return $response->toStream(false);
  46. }
  47. }
  48. if (null === $client && !method_exists($response, 'stream')) {
  49. throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__));
  50. }
  51. static $registered = false;
  52. if (!$registered = $registered || stream_wrapper_register(strtr(__CLASS__, '\\', '-'), __CLASS__)) {
  53. throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.');
  54. }
  55. $context = [
  56. 'client' => $client ?? $response,
  57. 'response' => $response,
  58. ];
  59. return fopen(strtr(__CLASS__, '\\', '-').'://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context]));
  60. }
  61. public function getResponse(): ResponseInterface
  62. {
  63. return $this->response;
  64. }
  65. /**
  66. * @param resource|callable|null $handle The resource handle that should be monitored when
  67. * stream_select() is used on the created stream
  68. * @param resource|null $content The seekable resource where the response body is buffered
  69. */
  70. public function bindHandles(&$handle, &$content): void
  71. {
  72. $this->handle = &$handle;
  73. $this->content = &$content;
  74. $this->offset = null;
  75. }
  76. public function stream_open(string $path, string $mode, int $options): bool
  77. {
  78. if ('r' !== $mode) {
  79. if ($options & \STREAM_REPORT_ERRORS) {
  80. trigger_error(sprintf('Invalid mode "%s": only "r" is supported.', $mode), \E_USER_WARNING);
  81. }
  82. return false;
  83. }
  84. $context = stream_context_get_options($this->context)['symfony'] ?? null;
  85. $this->client = $context['client'] ?? null;
  86. $this->response = $context['response'] ?? null;
  87. $this->context = null;
  88. if (null !== $this->client && null !== $this->response) {
  89. return true;
  90. }
  91. if ($options & \STREAM_REPORT_ERRORS) {
  92. trigger_error('Missing options "client" or "response" in "symfony" stream context.', \E_USER_WARNING);
  93. }
  94. return false;
  95. }
  96. public function stream_read(int $count)
  97. {
  98. if (\is_resource($this->content)) {
  99. // Empty the internal activity list
  100. foreach ($this->client->stream([$this->response], 0) as $chunk) {
  101. try {
  102. if (!$chunk->isTimeout() && $chunk->isFirst()) {
  103. $this->response->getStatusCode(); // ignore 3/4/5xx
  104. }
  105. } catch (ExceptionInterface $e) {
  106. trigger_error($e->getMessage(), \E_USER_WARNING);
  107. return false;
  108. }
  109. }
  110. if (0 !== fseek($this->content, $this->offset ?? 0)) {
  111. return false;
  112. }
  113. if ('' !== $data = fread($this->content, $count)) {
  114. fseek($this->content, 0, \SEEK_END);
  115. $this->offset += \strlen($data);
  116. return $data;
  117. }
  118. }
  119. if (\is_string($this->content)) {
  120. if (\strlen($this->content) <= $count) {
  121. $data = $this->content;
  122. $this->content = null;
  123. } else {
  124. $data = substr($this->content, 0, $count);
  125. $this->content = substr($this->content, $count);
  126. }
  127. $this->offset += \strlen($data);
  128. return $data;
  129. }
  130. foreach ($this->client->stream([$this->response], $this->blocking ? $this->timeout : 0) as $chunk) {
  131. try {
  132. $this->eof = true;
  133. $this->eof = !$chunk->isTimeout();
  134. if (!$this->eof && !$this->blocking) {
  135. return '';
  136. }
  137. $this->eof = $chunk->isLast();
  138. if ($chunk->isFirst()) {
  139. $this->response->getStatusCode(); // ignore 3/4/5xx
  140. }
  141. if ('' !== $data = $chunk->getContent()) {
  142. if (\strlen($data) > $count) {
  143. if (null === $this->content) {
  144. $this->content = substr($data, $count);
  145. }
  146. $data = substr($data, 0, $count);
  147. }
  148. $this->offset += \strlen($data);
  149. return $data;
  150. }
  151. } catch (ExceptionInterface $e) {
  152. trigger_error($e->getMessage(), \E_USER_WARNING);
  153. return false;
  154. }
  155. }
  156. return '';
  157. }
  158. public function stream_set_option(int $option, int $arg1, ?int $arg2): bool
  159. {
  160. if (\STREAM_OPTION_BLOCKING === $option) {
  161. $this->blocking = (bool) $arg1;
  162. } elseif (\STREAM_OPTION_READ_TIMEOUT === $option) {
  163. $this->timeout = $arg1 + $arg2 / 1e6;
  164. } else {
  165. return false;
  166. }
  167. return true;
  168. }
  169. public function stream_tell(): int
  170. {
  171. return $this->offset ?? 0;
  172. }
  173. public function stream_eof(): bool
  174. {
  175. return $this->eof && !\is_string($this->content);
  176. }
  177. public function stream_seek(int $offset, int $whence = \SEEK_SET): bool
  178. {
  179. if (null === $this->content && null === $this->offset) {
  180. $this->response->getStatusCode();
  181. $this->offset = 0;
  182. }
  183. if (!\is_resource($this->content) || 0 !== fseek($this->content, 0, \SEEK_END)) {
  184. return false;
  185. }
  186. $size = ftell($this->content);
  187. if (\SEEK_CUR === $whence) {
  188. $offset += $this->offset ?? 0;
  189. }
  190. if (\SEEK_END === $whence || $size < $offset) {
  191. foreach ($this->client->stream([$this->response]) as $chunk) {
  192. try {
  193. if ($chunk->isFirst()) {
  194. $this->response->getStatusCode(); // ignore 3/4/5xx
  195. }
  196. // Chunks are buffered in $this->content already
  197. $size += \strlen($chunk->getContent());
  198. if (\SEEK_END !== $whence && $offset <= $size) {
  199. break;
  200. }
  201. } catch (ExceptionInterface $e) {
  202. trigger_error($e->getMessage(), \E_USER_WARNING);
  203. return false;
  204. }
  205. }
  206. if (\SEEK_END === $whence) {
  207. $offset += $size;
  208. }
  209. }
  210. if (0 <= $offset && $offset <= $size) {
  211. $this->eof = false;
  212. $this->offset = $offset;
  213. return true;
  214. }
  215. return false;
  216. }
  217. public function stream_cast(int $castAs)
  218. {
  219. if (\STREAM_CAST_FOR_SELECT === $castAs) {
  220. $this->response->getHeaders(false);
  221. return (\is_callable($this->handle) ? ($this->handle)() : $this->handle) ?? false;
  222. }
  223. return false;
  224. }
  225. public function stream_stat(): array
  226. {
  227. try {
  228. $headers = $this->response->getHeaders(false);
  229. } catch (ExceptionInterface $e) {
  230. trigger_error($e->getMessage(), \E_USER_WARNING);
  231. $headers = [];
  232. }
  233. return [
  234. 'dev' => 0,
  235. 'ino' => 0,
  236. 'mode' => 33060,
  237. 'nlink' => 0,
  238. 'uid' => 0,
  239. 'gid' => 0,
  240. 'rdev' => 0,
  241. 'size' => (int) ($headers['content-length'][0] ?? -1),
  242. 'atime' => 0,
  243. 'mtime' => strtotime($headers['last-modified'][0] ?? '') ?: 0,
  244. 'ctime' => 0,
  245. 'blksize' => 0,
  246. 'blocks' => 0,
  247. ];
  248. }
  249. private function __construct()
  250. {
  251. }
  252. }