Sapi.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <?php
  2. declare(strict_types=1);
  3. namespace Sabre\HTTP;
  4. /**
  5. * PHP SAPI.
  6. *
  7. * This object is responsible for:
  8. * 1. Constructing a Request object based on the current HTTP request sent to
  9. * the PHP process.
  10. * 2. Sending the Response object back to the client.
  11. *
  12. * It could be said that this class provides a mapping between the Request and
  13. * Response objects, and php's:
  14. *
  15. * * $_SERVER
  16. * * $_POST
  17. * * $_FILES
  18. * * php://input
  19. * * echo()
  20. * * header()
  21. * * php://output
  22. *
  23. * You can choose to either call all these methods statically, but you can also
  24. * instantiate this as an object to allow for polymorphism.
  25. *
  26. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  27. * @author Evert Pot (http://evertpot.com/)
  28. * @license http://sabre.io/license/ Modified BSD License
  29. */
  30. class Sapi
  31. {
  32. /**
  33. * This static method will create a new Request object, based on the
  34. * current PHP request.
  35. */
  36. public static function getRequest(): Request
  37. {
  38. $serverArr = $_SERVER;
  39. if ('cli' === PHP_SAPI) {
  40. // If we're running off the CLI, we're going to set some default
  41. // settings.
  42. $serverArr['REQUEST_URI'] = $_SERVER['REQUEST_URI'] ?? '/';
  43. $serverArr['REQUEST_METHOD'] = $_SERVER['REQUEST_METHOD'] ?? 'CLI';
  44. }
  45. $r = self::createFromServerArray($serverArr);
  46. $r->setBody(fopen('php://input', 'r'));
  47. $r->setPostData($_POST);
  48. return $r;
  49. }
  50. /**
  51. * Sends the HTTP response back to a HTTP client.
  52. *
  53. * This calls php's header() function and streams the body to php://output.
  54. */
  55. public static function sendResponse(ResponseInterface $response)
  56. {
  57. header('HTTP/'.$response->getHttpVersion().' '.$response->getStatus().' '.$response->getStatusText());
  58. foreach ($response->getHeaders() as $key => $value) {
  59. foreach ($value as $k => $v) {
  60. if (0 === $k) {
  61. header($key.': '.$v);
  62. } else {
  63. header($key.': '.$v, false);
  64. }
  65. }
  66. }
  67. $body = $response->getBody();
  68. if (null === $body) {
  69. return;
  70. }
  71. if (is_callable($body)) {
  72. $body();
  73. return;
  74. }
  75. $contentLength = $response->getHeader('Content-Length');
  76. if (null !== $contentLength) {
  77. $output = fopen('php://output', 'wb');
  78. if (is_resource($body) && 'stream' == get_resource_type($body)) {
  79. // a workaround to make PHP more possible to use mmap based copy, see https://github.com/sabre-io/http/pull/119
  80. $left = (int) $contentLength;
  81. // copy with 4MiB chunks
  82. $chunk_size = 4 * 1024 * 1024;
  83. stream_set_chunk_size($output, $chunk_size);
  84. // If this is a partial response, flush the beginning bytes until the first position that is a multiple of the page size.
  85. $contentRange = $response->getHeader('Content-Range');
  86. // Matching "Content-Range: bytes 1234-5678/7890"
  87. if (null !== $contentRange && preg_match('/^bytes\s([0-9]+)-([0-9]+)\//i', $contentRange, $matches)) {
  88. // 4kB should be the default page size on most architectures
  89. $pageSize = 4096;
  90. $offset = (int) $matches[1];
  91. $delta = ($offset % $pageSize) > 0 ? ($pageSize - $offset % $pageSize) : 0;
  92. if ($delta > 0) {
  93. $left -= stream_copy_to_stream($body, $output, min($delta, $left));
  94. }
  95. }
  96. while ($left > 0) {
  97. $copied = stream_copy_to_stream($body, $output, min($left, $chunk_size));
  98. // stream_copy_to_stream($src, $dest, $maxLength) must return the number of bytes copied or false in case of failure
  99. // But when the $maxLength is greater than the total number of bytes remaining in the stream,
  100. // It returns the negative number of bytes copied
  101. // So break the loop in such cases.
  102. if ($copied <= 0) {
  103. break;
  104. }
  105. // Abort on client disconnect.
  106. // With ignore_user_abort(true), the script is not aborted on client disconnect.
  107. // To avoid reading the entire stream and dismissing the data afterward, check between the chunks if the client is still there.
  108. if (1 === ignore_user_abort() && 1 === connection_aborted()) {
  109. break;
  110. }
  111. $left -= $copied;
  112. }
  113. } else {
  114. fwrite($output, $body, (int) $contentLength);
  115. }
  116. } else {
  117. file_put_contents('php://output', $body);
  118. }
  119. if (is_resource($body)) {
  120. fclose($body);
  121. }
  122. }
  123. /**
  124. * This static method will create a new Request object, based on a PHP
  125. * $_SERVER array.
  126. *
  127. * REQUEST_URI and REQUEST_METHOD are required.
  128. */
  129. public static function createFromServerArray(array $serverArray): Request
  130. {
  131. $headers = [];
  132. $method = null;
  133. $url = null;
  134. $httpVersion = '1.1';
  135. $protocol = 'http';
  136. $hostName = 'localhost';
  137. foreach ($serverArray as $key => $value) {
  138. $key = (string) $key;
  139. switch ($key) {
  140. case 'SERVER_PROTOCOL':
  141. if ('HTTP/1.0' === $value) {
  142. $httpVersion = '1.0';
  143. } elseif ('HTTP/2.0' === $value) {
  144. $httpVersion = '2.0';
  145. }
  146. break;
  147. case 'REQUEST_METHOD':
  148. $method = $value;
  149. break;
  150. case 'REQUEST_URI':
  151. $url = $value;
  152. break;
  153. // These sometimes show up without a HTTP_ prefix
  154. case 'CONTENT_TYPE':
  155. $headers['Content-Type'] = $value;
  156. break;
  157. case 'CONTENT_LENGTH':
  158. $headers['Content-Length'] = $value;
  159. break;
  160. // mod_php on apache will put credentials in these variables.
  161. // (fast)cgi does not usually do this, however.
  162. case 'PHP_AUTH_USER':
  163. if (isset($serverArray['PHP_AUTH_PW'])) {
  164. $headers['Authorization'] = 'Basic '.base64_encode($value.':'.$serverArray['PHP_AUTH_PW']);
  165. }
  166. break;
  167. // Similarly, mod_php may also screw around with digest auth.
  168. case 'PHP_AUTH_DIGEST':
  169. $headers['Authorization'] = 'Digest '.$value;
  170. break;
  171. // Apache may prefix the HTTP_AUTHORIZATION header with
  172. // REDIRECT_, if mod_rewrite was used.
  173. case 'REDIRECT_HTTP_AUTHORIZATION':
  174. $headers['Authorization'] = $value;
  175. break;
  176. case 'HTTP_HOST':
  177. $hostName = $value;
  178. $headers['Host'] = $value;
  179. break;
  180. case 'HTTPS':
  181. if (!empty($value) && 'off' !== $value) {
  182. $protocol = 'https';
  183. }
  184. break;
  185. default:
  186. if ('HTTP_' === substr($key, 0, 5)) {
  187. // It's a HTTP header
  188. // Normalizing it to be prettier
  189. $header = strtolower(substr($key, 5));
  190. // Transforming dashes into spaces, and upper-casing
  191. // every first letter.
  192. $header = ucwords(str_replace('_', ' ', $header));
  193. // Turning spaces into dashes.
  194. $header = str_replace(' ', '-', $header);
  195. $headers[$header] = $value;
  196. }
  197. break;
  198. }
  199. }
  200. if (null === $url) {
  201. throw new \InvalidArgumentException('The _SERVER array must have a REQUEST_URI key');
  202. }
  203. if (null === $method) {
  204. throw new \InvalidArgumentException('The _SERVER array must have a REQUEST_METHOD key');
  205. }
  206. $r = new Request($method, $url, $headers);
  207. $r->setHttpVersion($httpVersion);
  208. $r->setRawServerData($serverArray);
  209. $r->setAbsoluteUrl($protocol.'://'.$hostName.$url);
  210. return $r;
  211. }
  212. }