Utils.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. <?php
  2. namespace GuzzleHttp\Psr7;
  3. use Psr\Http\Message\RequestInterface;
  4. use Psr\Http\Message\ServerRequestInterface;
  5. use Psr\Http\Message\StreamInterface;
  6. use Psr\Http\Message\UriInterface;
  7. final class Utils
  8. {
  9. /**
  10. * Remove the items given by the keys, case insensitively from the data.
  11. *
  12. * @param iterable<string> $keys
  13. *
  14. * @return array
  15. */
  16. public static function caselessRemove($keys, array $data)
  17. {
  18. $result = [];
  19. foreach ($keys as &$key) {
  20. $key = strtolower($key);
  21. }
  22. foreach ($data as $k => $v) {
  23. if (!in_array(strtolower($k), $keys)) {
  24. $result[$k] = $v;
  25. }
  26. }
  27. return $result;
  28. }
  29. /**
  30. * Copy the contents of a stream into another stream until the given number
  31. * of bytes have been read.
  32. *
  33. * @param StreamInterface $source Stream to read from
  34. * @param StreamInterface $dest Stream to write to
  35. * @param int $maxLen Maximum number of bytes to read. Pass -1
  36. * to read the entire stream.
  37. *
  38. * @throws \RuntimeException on error.
  39. */
  40. public static function copyToStream(StreamInterface $source, StreamInterface $dest, $maxLen = -1)
  41. {
  42. $bufferSize = 8192;
  43. if ($maxLen === -1) {
  44. while (!$source->eof()) {
  45. if (!$dest->write($source->read($bufferSize))) {
  46. break;
  47. }
  48. }
  49. } else {
  50. $remaining = $maxLen;
  51. while ($remaining > 0 && !$source->eof()) {
  52. $buf = $source->read(min($bufferSize, $remaining));
  53. $len = strlen($buf);
  54. if (!$len) {
  55. break;
  56. }
  57. $remaining -= $len;
  58. $dest->write($buf);
  59. }
  60. }
  61. }
  62. /**
  63. * Copy the contents of a stream into a string until the given number of
  64. * bytes have been read.
  65. *
  66. * @param StreamInterface $stream Stream to read
  67. * @param int $maxLen Maximum number of bytes to read. Pass -1
  68. * to read the entire stream.
  69. * @return string
  70. *
  71. * @throws \RuntimeException on error.
  72. */
  73. public static function copyToString(StreamInterface $stream, $maxLen = -1)
  74. {
  75. $buffer = '';
  76. if ($maxLen === -1) {
  77. while (!$stream->eof()) {
  78. $buf = $stream->read(1048576);
  79. // Using a loose equality here to match on '' and false.
  80. if ($buf == null) {
  81. break;
  82. }
  83. $buffer .= $buf;
  84. }
  85. return $buffer;
  86. }
  87. $len = 0;
  88. while (!$stream->eof() && $len < $maxLen) {
  89. $buf = $stream->read($maxLen - $len);
  90. // Using a loose equality here to match on '' and false.
  91. if ($buf == null) {
  92. break;
  93. }
  94. $buffer .= $buf;
  95. $len = strlen($buffer);
  96. }
  97. return $buffer;
  98. }
  99. /**
  100. * Calculate a hash of a stream.
  101. *
  102. * This method reads the entire stream to calculate a rolling hash, based
  103. * on PHP's `hash_init` functions.
  104. *
  105. * @param StreamInterface $stream Stream to calculate the hash for
  106. * @param string $algo Hash algorithm (e.g. md5, crc32, etc)
  107. * @param bool $rawOutput Whether or not to use raw output
  108. *
  109. * @return string Returns the hash of the stream
  110. *
  111. * @throws \RuntimeException on error.
  112. */
  113. public static function hash(StreamInterface $stream, $algo, $rawOutput = false)
  114. {
  115. $pos = $stream->tell();
  116. if ($pos > 0) {
  117. $stream->rewind();
  118. }
  119. $ctx = hash_init($algo);
  120. while (!$stream->eof()) {
  121. hash_update($ctx, $stream->read(1048576));
  122. }
  123. $out = hash_final($ctx, (bool) $rawOutput);
  124. $stream->seek($pos);
  125. return $out;
  126. }
  127. /**
  128. * Clone and modify a request with the given changes.
  129. *
  130. * This method is useful for reducing the number of clones needed to mutate
  131. * a message.
  132. *
  133. * The changes can be one of:
  134. * - method: (string) Changes the HTTP method.
  135. * - set_headers: (array) Sets the given headers.
  136. * - remove_headers: (array) Remove the given headers.
  137. * - body: (mixed) Sets the given body.
  138. * - uri: (UriInterface) Set the URI.
  139. * - query: (string) Set the query string value of the URI.
  140. * - version: (string) Set the protocol version.
  141. *
  142. * @param RequestInterface $request Request to clone and modify.
  143. * @param array $changes Changes to apply.
  144. *
  145. * @return RequestInterface
  146. */
  147. public static function modifyRequest(RequestInterface $request, array $changes)
  148. {
  149. if (!$changes) {
  150. return $request;
  151. }
  152. $headers = $request->getHeaders();
  153. if (!isset($changes['uri'])) {
  154. $uri = $request->getUri();
  155. } else {
  156. // Remove the host header if one is on the URI
  157. if ($host = $changes['uri']->getHost()) {
  158. $changes['set_headers']['Host'] = $host;
  159. if ($port = $changes['uri']->getPort()) {
  160. $standardPorts = ['http' => 80, 'https' => 443];
  161. $scheme = $changes['uri']->getScheme();
  162. if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) {
  163. $changes['set_headers']['Host'] .= ':'.$port;
  164. }
  165. }
  166. }
  167. $uri = $changes['uri'];
  168. }
  169. if (!empty($changes['remove_headers'])) {
  170. $headers = self::caselessRemove($changes['remove_headers'], $headers);
  171. }
  172. if (!empty($changes['set_headers'])) {
  173. $headers = self::caselessRemove(array_keys($changes['set_headers']), $headers);
  174. $headers = $changes['set_headers'] + $headers;
  175. }
  176. if (isset($changes['query'])) {
  177. $uri = $uri->withQuery($changes['query']);
  178. }
  179. if ($request instanceof ServerRequestInterface) {
  180. return (new ServerRequest(
  181. isset($changes['method']) ? $changes['method'] : $request->getMethod(),
  182. $uri,
  183. $headers,
  184. isset($changes['body']) ? $changes['body'] : $request->getBody(),
  185. isset($changes['version'])
  186. ? $changes['version']
  187. : $request->getProtocolVersion(),
  188. $request->getServerParams()
  189. ))
  190. ->withParsedBody($request->getParsedBody())
  191. ->withQueryParams($request->getQueryParams())
  192. ->withCookieParams($request->getCookieParams())
  193. ->withUploadedFiles($request->getUploadedFiles());
  194. }
  195. return new Request(
  196. isset($changes['method']) ? $changes['method'] : $request->getMethod(),
  197. $uri,
  198. $headers,
  199. isset($changes['body']) ? $changes['body'] : $request->getBody(),
  200. isset($changes['version'])
  201. ? $changes['version']
  202. : $request->getProtocolVersion()
  203. );
  204. }
  205. /**
  206. * Read a line from the stream up to the maximum allowed buffer length.
  207. *
  208. * @param StreamInterface $stream Stream to read from
  209. * @param int|null $maxLength Maximum buffer length
  210. *
  211. * @return string
  212. */
  213. public static function readLine(StreamInterface $stream, $maxLength = null)
  214. {
  215. $buffer = '';
  216. $size = 0;
  217. while (!$stream->eof()) {
  218. // Using a loose equality here to match on '' and false.
  219. if (null == ($byte = $stream->read(1))) {
  220. return $buffer;
  221. }
  222. $buffer .= $byte;
  223. // Break when a new line is found or the max length - 1 is reached
  224. if ($byte === "\n" || ++$size === $maxLength - 1) {
  225. break;
  226. }
  227. }
  228. return $buffer;
  229. }
  230. /**
  231. * Create a new stream based on the input type.
  232. *
  233. * Options is an associative array that can contain the following keys:
  234. * - metadata: Array of custom metadata.
  235. * - size: Size of the stream.
  236. *
  237. * This method accepts the following `$resource` types:
  238. * - `Psr\Http\Message\StreamInterface`: Returns the value as-is.
  239. * - `string`: Creates a stream object that uses the given string as the contents.
  240. * - `resource`: Creates a stream object that wraps the given PHP stream resource.
  241. * - `Iterator`: If the provided value implements `Iterator`, then a read-only
  242. * stream object will be created that wraps the given iterable. Each time the
  243. * stream is read from, data from the iterator will fill a buffer and will be
  244. * continuously called until the buffer is equal to the requested read size.
  245. * Subsequent read calls will first read from the buffer and then call `next`
  246. * on the underlying iterator until it is exhausted.
  247. * - `object` with `__toString()`: If the object has the `__toString()` method,
  248. * the object will be cast to a string and then a stream will be returned that
  249. * uses the string value.
  250. * - `NULL`: When `null` is passed, an empty stream object is returned.
  251. * - `callable` When a callable is passed, a read-only stream object will be
  252. * created that invokes the given callable. The callable is invoked with the
  253. * number of suggested bytes to read. The callable can return any number of
  254. * bytes, but MUST return `false` when there is no more data to return. The
  255. * stream object that wraps the callable will invoke the callable until the
  256. * number of requested bytes are available. Any additional bytes will be
  257. * buffered and used in subsequent reads.
  258. *
  259. * @param resource|string|null|int|float|bool|StreamInterface|callable|\Iterator $resource Entity body data
  260. * @param array $options Additional options
  261. *
  262. * @return StreamInterface
  263. *
  264. * @throws \InvalidArgumentException if the $resource arg is not valid.
  265. */
  266. public static function streamFor($resource = '', array $options = [])
  267. {
  268. if (is_scalar($resource)) {
  269. $stream = fopen('php://temp', 'r+');
  270. if ($resource !== '') {
  271. fwrite($stream, $resource);
  272. fseek($stream, 0);
  273. }
  274. return new Stream($stream, $options);
  275. }
  276. switch (gettype($resource)) {
  277. case 'resource':
  278. return new Stream($resource, $options);
  279. case 'object':
  280. if ($resource instanceof StreamInterface) {
  281. return $resource;
  282. } elseif ($resource instanceof \Iterator) {
  283. return new PumpStream(function () use ($resource) {
  284. if (!$resource->valid()) {
  285. return false;
  286. }
  287. $result = $resource->current();
  288. $resource->next();
  289. return $result;
  290. }, $options);
  291. } elseif (method_exists($resource, '__toString')) {
  292. return Utils::streamFor((string) $resource, $options);
  293. }
  294. break;
  295. case 'NULL':
  296. return new Stream(fopen('php://temp', 'r+'), $options);
  297. }
  298. if (is_callable($resource)) {
  299. return new PumpStream($resource, $options);
  300. }
  301. throw new \InvalidArgumentException('Invalid resource type: ' . gettype($resource));
  302. }
  303. /**
  304. * Safely opens a PHP stream resource using a filename.
  305. *
  306. * When fopen fails, PHP normally raises a warning. This function adds an
  307. * error handler that checks for errors and throws an exception instead.
  308. *
  309. * @param string $filename File to open
  310. * @param string $mode Mode used to open the file
  311. *
  312. * @return resource
  313. *
  314. * @throws \RuntimeException if the file cannot be opened
  315. */
  316. public static function tryFopen($filename, $mode)
  317. {
  318. $ex = null;
  319. set_error_handler(function () use ($filename, $mode, &$ex) {
  320. $ex = new \RuntimeException(sprintf(
  321. 'Unable to open %s using mode %s: %s',
  322. $filename,
  323. $mode,
  324. func_get_args()[1]
  325. ));
  326. });
  327. $handle = fopen($filename, $mode);
  328. restore_error_handler();
  329. if ($ex) {
  330. /** @var $ex \RuntimeException */
  331. throw $ex;
  332. }
  333. return $handle;
  334. }
  335. /**
  336. * Returns a UriInterface for the given value.
  337. *
  338. * This function accepts a string or UriInterface and returns a
  339. * UriInterface for the given value. If the value is already a
  340. * UriInterface, it is returned as-is.
  341. *
  342. * @param string|UriInterface $uri
  343. *
  344. * @return UriInterface
  345. *
  346. * @throws \InvalidArgumentException
  347. */
  348. public static function uriFor($uri)
  349. {
  350. if ($uri instanceof UriInterface) {
  351. return $uri;
  352. }
  353. if (is_string($uri)) {
  354. return new Uri($uri);
  355. }
  356. throw new \InvalidArgumentException('URI must be a string or UriInterface');
  357. }
  358. }