ServerRequestFactory.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.3.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Http;
  17. use Cake\Core\Configure;
  18. use Cake\Http\Uri as CakeUri;
  19. use Cake\Utility\Hash;
  20. use Psr\Http\Message\ServerRequestFactoryInterface;
  21. use Psr\Http\Message\ServerRequestInterface;
  22. use Psr\Http\Message\UriInterface;
  23. use function Laminas\Diactoros\marshalHeadersFromSapi;
  24. use function Laminas\Diactoros\marshalUriFromSapi;
  25. use function Laminas\Diactoros\normalizeServer;
  26. use function Laminas\Diactoros\normalizeUploadedFiles;
  27. /**
  28. * Factory for making ServerRequest instances.
  29. *
  30. * This subclass adds in CakePHP specific behavior to populate
  31. * the basePath and webroot attributes. Furthermore the Uri's path
  32. * is corrected to only contain the 'virtual' path for the request.
  33. */
  34. abstract class ServerRequestFactory implements ServerRequestFactoryInterface
  35. {
  36. /**
  37. * Create a request from the supplied superglobal values.
  38. *
  39. * If any argument is not supplied, the corresponding superglobal value will
  40. * be used.
  41. *
  42. * The ServerRequest created is then passed to the fromServer() method in
  43. * order to marshal the request URI and headers.
  44. *
  45. * @see fromServer()
  46. * @param array|null $server $_SERVER superglobal
  47. * @param array|null $query $_GET superglobal
  48. * @param array|null $parsedBody $_POST superglobal
  49. * @param array|null $cookies $_COOKIE superglobal
  50. * @param array|null $files $_FILES superglobal
  51. * @return \Cake\Http\ServerRequest
  52. * @throws \InvalidArgumentException for invalid file values
  53. */
  54. public static function fromGlobals(
  55. ?array $server = null,
  56. ?array $query = null,
  57. ?array $parsedBody = null,
  58. ?array $cookies = null,
  59. ?array $files = null
  60. ): ServerRequest {
  61. $server = normalizeServer($server ?: $_SERVER);
  62. $uri = static::createUri($server);
  63. $webroot = '';
  64. $base = '';
  65. if ($uri instanceof CakeUri) {
  66. // Unwrap our shim for base and webroot.
  67. // For 5.x we should change the interface on createUri() to return a
  68. // tuple of [$uri, $base, $webroot] and remove the wrapper.
  69. $webroot = $uri->getWebroot();
  70. $base = $uri->getBase();
  71. $uri->getUri();
  72. }
  73. /** @psalm-suppress NoInterfaceProperties */
  74. $sessionConfig = (array)Configure::read('Session') + [
  75. 'defaults' => 'php',
  76. 'cookiePath' => $webroot,
  77. ];
  78. $session = Session::create($sessionConfig);
  79. $request = new ServerRequest([
  80. 'environment' => $server,
  81. 'uri' => $uri,
  82. 'cookies' => $cookies ?: $_COOKIE,
  83. 'query' => $query ?: $_GET,
  84. 'webroot' => $webroot,
  85. 'base' => $base,
  86. 'session' => $session,
  87. 'input' => $server['CAKEPHP_INPUT'] ?? null,
  88. ]);
  89. $request = static::marshalBodyAndRequestMethod($parsedBody ?? $_POST, $request);
  90. $request = static::marshalFiles($files ?? $_FILES, $request);
  91. return $request;
  92. }
  93. /**
  94. * Sets the REQUEST_METHOD environment variable based on the simulated _method
  95. * HTTP override value. The 'ORIGINAL_REQUEST_METHOD' is also preserved, if you
  96. * want the read the non-simulated HTTP method the client used.
  97. *
  98. * Request body of content type "application/x-www-form-urlencoded" is parsed
  99. * into array for PUT/PATCH/DELETE requests.
  100. *
  101. * @param array $parsedBody Parsed body.
  102. * @param \Cake\Http\ServerRequest $request Request instance.
  103. * @return \Cake\Http\ServerRequest
  104. */
  105. protected static function marshalBodyAndRequestMethod(array $parsedBody, ServerRequest $request): ServerRequest
  106. {
  107. $method = $request->getMethod();
  108. $override = false;
  109. if (
  110. in_array($method, ['PUT', 'DELETE', 'PATCH'], true) &&
  111. strpos((string)$request->contentType(), 'application/x-www-form-urlencoded') === 0
  112. ) {
  113. $data = (string)$request->getBody();
  114. parse_str($data, $parsedBody);
  115. }
  116. if ($request->hasHeader('X-Http-Method-Override')) {
  117. $parsedBody['_method'] = $request->getHeaderLine('X-Http-Method-Override');
  118. $override = true;
  119. }
  120. $request = $request->withEnv('ORIGINAL_REQUEST_METHOD', $method);
  121. if (isset($parsedBody['_method'])) {
  122. $request = $request->withEnv('REQUEST_METHOD', $parsedBody['_method']);
  123. unset($parsedBody['_method']);
  124. $override = true;
  125. }
  126. if (
  127. $override &&
  128. !in_array($request->getMethod(), ['PUT', 'POST', 'DELETE', 'PATCH'], true)
  129. ) {
  130. $parsedBody = [];
  131. }
  132. return $request->withParsedBody($parsedBody);
  133. }
  134. /**
  135. * Process uploaded files and move things onto the parsed body.
  136. *
  137. * @param array $files Files array for normalization and merging in parsed body.
  138. * @param \Cake\Http\ServerRequest $request Request instance.
  139. * @return \Cake\Http\ServerRequest
  140. */
  141. protected static function marshalFiles(array $files, ServerRequest $request): ServerRequest
  142. {
  143. $files = normalizeUploadedFiles($files);
  144. $request = $request->withUploadedFiles($files);
  145. $parsedBody = $request->getParsedBody();
  146. if (!is_array($parsedBody)) {
  147. return $request;
  148. }
  149. if (Configure::read('App.uploadedFilesAsObjects', true)) {
  150. $parsedBody = Hash::merge($parsedBody, $files);
  151. } else {
  152. // Make a flat map that can be inserted into body for BC.
  153. $fileMap = Hash::flatten($files);
  154. foreach ($fileMap as $key => $file) {
  155. $error = $file->getError();
  156. $tmpName = '';
  157. if ($error === UPLOAD_ERR_OK) {
  158. $tmpName = $file->getStream()->getMetadata('uri');
  159. }
  160. $parsedBody = Hash::insert($parsedBody, (string)$key, [
  161. 'tmp_name' => $tmpName,
  162. 'error' => $error,
  163. 'name' => $file->getClientFilename(),
  164. 'type' => $file->getClientMediaType(),
  165. 'size' => $file->getSize(),
  166. ]);
  167. }
  168. }
  169. return $request->withParsedBody($parsedBody);
  170. }
  171. /**
  172. * Create a new server request.
  173. *
  174. * Note that server-params are taken precisely as given - no parsing/processing
  175. * of the given values is performed, and, in particular, no attempt is made to
  176. * determine the HTTP method or URI, which must be provided explicitly.
  177. *
  178. * @param string $method The HTTP method associated with the request.
  179. * @param \Psr\Http\Message\UriInterface|string $uri The URI associated with the request. If
  180. * the value is a string, the factory MUST create a UriInterface
  181. * instance based on it.
  182. * @param array $serverParams Array of SAPI parameters with which to seed
  183. * the generated request instance.
  184. * @return \Psr\Http\Message\ServerRequestInterface
  185. */
  186. public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
  187. {
  188. $serverParams['REQUEST_METHOD'] = $method;
  189. $options = ['environment' => $serverParams];
  190. if ($uri instanceof UriInterface) {
  191. $options['uri'] = $uri;
  192. } else {
  193. $options['url'] = $uri;
  194. }
  195. return new ServerRequest($options);
  196. }
  197. /**
  198. * Create a new Uri instance from the provided server data.
  199. *
  200. * @param array $server Array of server data to build the Uri from.
  201. * $_SERVER will be added into the $server parameter.
  202. * @return \Psr\Http\Message\UriInterface New instance.
  203. */
  204. public static function createUri(array $server = []): UriInterface
  205. {
  206. $server += $_SERVER;
  207. $server = normalizeServer($server);
  208. $headers = marshalHeadersFromSapi($server);
  209. return static::marshalUriFromSapi($server, $headers);
  210. }
  211. /**
  212. * Build a UriInterface object.
  213. *
  214. * Add in some CakePHP specific logic/properties that help
  215. * preserve backwards compatibility.
  216. *
  217. * @param array $server The server parameters.
  218. * @param array $headers The normalized headers
  219. * @return \Cake\Http\Uri A constructed Uri
  220. */
  221. protected static function marshalUriFromSapi(array $server, array $headers): UriInterface
  222. {
  223. /** @psalm-suppress DeprecatedFunction */
  224. $uri = marshalUriFromSapi($server, $headers);
  225. [$base, $webroot] = static::getBase($uri, $server);
  226. // Look in PATH_INFO first, as this is the exact value we need prepared
  227. // by PHP.
  228. $pathInfo = Hash::get($server, 'PATH_INFO');
  229. if ($pathInfo) {
  230. $uri = $uri->withPath($pathInfo);
  231. } else {
  232. $uri = static::updatePath($base, $uri);
  233. }
  234. if (!$uri->getHost()) {
  235. $uri = $uri->withHost('localhost');
  236. }
  237. return new CakeUri($uri, $base, $webroot);
  238. }
  239. /**
  240. * Updates the request URI to remove the base directory.
  241. *
  242. * @param string $base The base path to remove.
  243. * @param \Psr\Http\Message\UriInterface $uri The uri to update.
  244. * @return \Psr\Http\Message\UriInterface The modified Uri instance.
  245. */
  246. protected static function updatePath(string $base, UriInterface $uri): UriInterface
  247. {
  248. $path = $uri->getPath();
  249. if ($base !== '' && strpos($path, $base) === 0) {
  250. $path = substr($path, strlen($base));
  251. }
  252. if ($path === '/index.php' && $uri->getQuery()) {
  253. $path = $uri->getQuery();
  254. }
  255. if (empty($path) || $path === '/' || $path === '//' || $path === '/index.php') {
  256. $path = '/';
  257. }
  258. $endsWithIndex = '/' . (Configure::read('App.webroot') ?: 'webroot') . '/index.php';
  259. $endsWithLength = strlen($endsWithIndex);
  260. if (
  261. strlen($path) >= $endsWithLength &&
  262. substr($path, -$endsWithLength) === $endsWithIndex
  263. ) {
  264. $path = '/';
  265. }
  266. return $uri->withPath($path);
  267. }
  268. /**
  269. * Calculate the base directory and webroot directory.
  270. *
  271. * @param \Psr\Http\Message\UriInterface $uri The Uri instance.
  272. * @param array $server The SERVER data to use.
  273. * @return array An array containing the [baseDir, webroot]
  274. */
  275. protected static function getBase(UriInterface $uri, array $server): array
  276. {
  277. $config = (array)Configure::read('App') + [
  278. 'base' => null,
  279. 'webroot' => null,
  280. 'baseUrl' => null,
  281. ];
  282. $base = $config['base'];
  283. $baseUrl = $config['baseUrl'];
  284. $webroot = $config['webroot'];
  285. if ($base !== false && $base !== null) {
  286. return [$base, $base . '/'];
  287. }
  288. if (!$baseUrl) {
  289. $base = dirname(Hash::get($server, 'PHP_SELF'));
  290. // Clean up additional / which cause following code to fail..
  291. $base = preg_replace('#/+#', '/', $base);
  292. $indexPos = strpos($base, '/' . $webroot . '/index.php');
  293. if ($indexPos !== false) {
  294. $base = substr($base, 0, $indexPos) . '/' . $webroot;
  295. }
  296. if ($webroot === basename($base)) {
  297. $base = dirname($base);
  298. }
  299. if ($base === DIRECTORY_SEPARATOR || $base === '.') {
  300. $base = '';
  301. }
  302. $base = implode('/', array_map('rawurlencode', explode('/', $base)));
  303. return [$base, $base . '/'];
  304. }
  305. $file = '/' . basename($baseUrl);
  306. $base = dirname($baseUrl);
  307. if ($base === DIRECTORY_SEPARATOR || $base === '.') {
  308. $base = '';
  309. }
  310. $webrootDir = $base . '/';
  311. $docRoot = Hash::get($server, 'DOCUMENT_ROOT');
  312. $docRootContainsWebroot = strpos($docRoot, $webroot);
  313. if (!empty($base) || !$docRootContainsWebroot) {
  314. if (strpos($webrootDir, '/' . $webroot . '/') === false) {
  315. $webrootDir .= $webroot . '/';
  316. }
  317. }
  318. return [$base . $file, $webrootDir];
  319. }
  320. }