ServerRequestFactory.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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\Utility\Hash;
  19. use Psr\Http\Message\ServerRequestFactoryInterface;
  20. use Psr\Http\Message\ServerRequestInterface;
  21. use Psr\Http\Message\UriInterface;
  22. use function Zend\Diactoros\marshalHeadersFromSapi;
  23. use function Zend\Diactoros\marshalUriFromSapi;
  24. use function Zend\Diactoros\normalizeServer;
  25. /**
  26. * Factory for making ServerRequest instances.
  27. *
  28. * This subclass adds in CakePHP specific behavior to populate
  29. * the basePath and webroot attributes. Furthermore the Uri's path
  30. * is corrected to only contain the 'virtual' path for the request.
  31. */
  32. abstract class ServerRequestFactory implements ServerRequestFactoryInterface
  33. {
  34. /**
  35. * Create a request from the supplied superglobal values.
  36. *
  37. * If any argument is not supplied, the corresponding superglobal value will
  38. * be used.
  39. *
  40. * The ServerRequest created is then passed to the fromServer() method in
  41. * order to marshal the request URI and headers.
  42. *
  43. * @see fromServer()
  44. * @param array $server $_SERVER superglobal
  45. * @param array $query $_GET superglobal
  46. * @param array $body $_POST superglobal
  47. * @param array $cookies $_COOKIE superglobal
  48. * @param array $files $_FILES superglobal
  49. * @return \Cake\Http\ServerRequest
  50. * @throws \InvalidArgumentException for invalid file values
  51. */
  52. public static function fromGlobals(
  53. ?array $server = null,
  54. ?array $query = null,
  55. ?array $body = null,
  56. ?array $cookies = null,
  57. ?array $files = null
  58. ): ServerRequest {
  59. $server = normalizeServer($server ?: $_SERVER);
  60. $uri = static::createUri($server);
  61. /** @psalm-suppress NoInterfaceProperties */
  62. $sessionConfig = (array)Configure::read('Session') + [
  63. 'defaults' => 'php',
  64. 'cookiePath' => $uri->webroot,
  65. ];
  66. $session = Session::create($sessionConfig);
  67. /** @psalm-suppress NoInterfaceProperties */
  68. $request = new ServerRequest([
  69. 'environment' => $server,
  70. 'uri' => $uri,
  71. 'files' => $files ?: $_FILES,
  72. 'cookies' => $cookies ?: $_COOKIE,
  73. 'query' => $query ?: $_GET,
  74. 'post' => $body ?: $_POST,
  75. 'webroot' => $uri->webroot,
  76. 'base' => $uri->base,
  77. 'session' => $session,
  78. 'mergeFilesAsObjects' => Configure::read('App.uploadedFilesAsObjects', true),
  79. ]);
  80. return $request;
  81. }
  82. /**
  83. * Create a new server request.
  84. *
  85. * Note that server-params are taken precisely as given - no parsing/processing
  86. * of the given values is performed, and, in particular, no attempt is made to
  87. * determine the HTTP method or URI, which must be provided explicitly.
  88. *
  89. * @param string $method The HTTP method associated with the request.
  90. * @param \Psr\Http\Message\UriInterface|string $uri The URI associated with the request. If
  91. * the value is a string, the factory MUST create a UriInterface
  92. * instance based on it.
  93. * @param array $serverParams Array of SAPI parameters with which to seed
  94. * the generated request instance.
  95. *
  96. * @return \Psr\Http\Message\ServerRequestInterface
  97. */
  98. public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
  99. {
  100. $serverParams['REQUEST_METHOD'] = $method;
  101. $options = ['environment' => $serverParams];
  102. if ($uri instanceof UriInterface) {
  103. $options['uri'] = $uri;
  104. } else {
  105. $options['url'] = $uri;
  106. }
  107. return new ServerRequest($options);
  108. }
  109. /**
  110. * Create a new Uri instance from the provided server data.
  111. *
  112. * @param array $server Array of server data to build the Uri from.
  113. * $_SERVER will be added into the $server parameter.
  114. * @return \Psr\Http\Message\UriInterface New instance.
  115. */
  116. public static function createUri(array $server = []): UriInterface
  117. {
  118. $server += $_SERVER;
  119. $server = normalizeServer($server);
  120. $headers = marshalHeadersFromSapi($server);
  121. return static::marshalUriFromSapi($server, $headers);
  122. }
  123. /**
  124. * Build a UriInterface object.
  125. *
  126. * Add in some CakePHP specific logic/properties that help
  127. * preserve backwards compatibility.
  128. *
  129. * @param array $server The server parameters.
  130. * @param array $headers The normalized headers
  131. * @return \Psr\Http\Message\UriInterface a constructed Uri
  132. */
  133. protected static function marshalUriFromSapi(array $server, array $headers): UriInterface
  134. {
  135. $uri = marshalUriFromSapi($server, $headers);
  136. [$base, $webroot] = static::getBase($uri, $server);
  137. // Look in PATH_INFO first, as this is the exact value we need prepared
  138. // by PHP.
  139. $pathInfo = Hash::get($server, 'PATH_INFO');
  140. if ($pathInfo) {
  141. $uri = $uri->withPath($pathInfo);
  142. } else {
  143. $uri = static::updatePath($base, $uri);
  144. }
  145. if (!$uri->getHost()) {
  146. $uri = $uri->withHost('localhost');
  147. }
  148. // Splat on some extra attributes to save
  149. // some method calls.
  150. /** @psalm-suppress NoInterfaceProperties */
  151. $uri->base = $base;
  152. /** @psalm-suppress NoInterfaceProperties */
  153. $uri->webroot = $webroot;
  154. return $uri;
  155. }
  156. /**
  157. * Updates the request URI to remove the base directory.
  158. *
  159. * @param string $base The base path to remove.
  160. * @param \Psr\Http\Message\UriInterface $uri The uri to update.
  161. * @return \Psr\Http\Message\UriInterface The modified Uri instance.
  162. */
  163. protected static function updatePath(string $base, UriInterface $uri): UriInterface
  164. {
  165. $path = $uri->getPath();
  166. if (strlen($base) > 0 && strpos($path, $base) === 0) {
  167. $path = substr($path, strlen($base));
  168. }
  169. if ($path === '/index.php' && $uri->getQuery()) {
  170. $path = $uri->getQuery();
  171. }
  172. if (empty($path) || $path === '/' || $path === '//' || $path === '/index.php') {
  173. $path = '/';
  174. }
  175. $endsWithIndex = '/' . (Configure::read('App.webroot') ?: 'webroot') . '/index.php';
  176. $endsWithLength = strlen($endsWithIndex);
  177. if (strlen($path) >= $endsWithLength &&
  178. substr($path, -$endsWithLength) === $endsWithIndex
  179. ) {
  180. $path = '/';
  181. }
  182. return $uri->withPath($path);
  183. }
  184. /**
  185. * Calculate the base directory and webroot directory.
  186. *
  187. * @param \Psr\Http\Message\UriInterface $uri The Uri instance.
  188. * @param array $server The SERVER data to use.
  189. * @return array An array containing the [baseDir, webroot]
  190. */
  191. protected static function getBase(UriInterface $uri, array $server): array
  192. {
  193. $config = (array)Configure::read('App') + [
  194. 'base' => null,
  195. 'webroot' => null,
  196. 'baseUrl' => null,
  197. ];
  198. $base = $config['base'];
  199. $baseUrl = $config['baseUrl'];
  200. $webroot = $config['webroot'];
  201. if ($base !== false && $base !== null) {
  202. return [$base, $base . '/'];
  203. }
  204. if (!$baseUrl) {
  205. $base = dirname(Hash::get($server, 'PHP_SELF'));
  206. // Clean up additional / which cause following code to fail..
  207. $base = preg_replace('#/+#', '/', $base);
  208. $indexPos = strpos($base, '/' . $webroot . '/index.php');
  209. if ($indexPos !== false) {
  210. $base = substr($base, 0, $indexPos) . '/' . $webroot;
  211. }
  212. if ($webroot === basename($base)) {
  213. $base = dirname($base);
  214. }
  215. if ($base === DIRECTORY_SEPARATOR || $base === '.') {
  216. $base = '';
  217. }
  218. $base = implode('/', array_map('rawurlencode', explode('/', $base)));
  219. return [$base, $base . '/'];
  220. }
  221. $file = '/' . basename($baseUrl);
  222. $base = dirname($baseUrl);
  223. if ($base === DIRECTORY_SEPARATOR || $base === '.') {
  224. $base = '';
  225. }
  226. $webrootDir = $base . '/';
  227. $docRoot = Hash::get($server, 'DOCUMENT_ROOT');
  228. $docRootContainsWebroot = strpos($docRoot, $webroot);
  229. if (!empty($base) || !$docRootContainsWebroot) {
  230. if (strpos($webrootDir, '/' . $webroot . '/') === false) {
  231. $webrootDir .= $webroot . '/';
  232. }
  233. }
  234. return [$base . $file, $webrootDir];
  235. }
  236. }