friendship ended with social-app. php is my new best friend
1<?php
2/**
3 * Class ServerUtil
4 *
5 * @created 29.03.2021
6 * @author smiley <smiley@chillerlan.net>
7 * @copyright 2021 smiley
8 * @license MIT
9 */
10declare(strict_types=1);
11
12namespace chillerlan\HTTP\Utils;
13
14use Psr\Http\Message\{ServerRequestFactoryInterface, ServerRequestInterface, StreamFactoryInterface, StreamInterface,
15 UploadedFileFactoryInterface, UploadedFileInterface, UriFactoryInterface, UriInterface};
16use InvalidArgumentException;
17use function array_keys, explode, function_exists, is_array, is_file, substr;
18
19/**
20 * @phpstan-type File array{tmp_name: string, size: int, error: int, name: string, type: string}
21 * @phpstan-type FileSpec array{tmp_name: string[], size: int[], error: int[], name: string[], type: string[]}
22 */
23final class ServerUtil{
24
25 protected ServerRequestFactoryInterface $serverRequestFactory;
26 protected UriFactoryInterface $uriFactory;
27 protected UploadedFileFactoryInterface $uploadedFileFactory;
28 protected StreamFactoryInterface $streamFactory;
29
30 public function __construct(
31 ServerRequestFactoryInterface $serverRequestFactory,
32 UriFactoryInterface $uriFactory,
33 UploadedFileFactoryInterface $uploadedFileFactory,
34 StreamFactoryInterface $streamFactory,
35 ){
36 $this->serverRequestFactory = $serverRequestFactory;
37 $this->uriFactory = $uriFactory;
38 $this->uploadedFileFactory = $uploadedFileFactory;
39 $this->streamFactory = $streamFactory;
40 }
41
42 /**
43 * Returns a ServerRequest populated with superglobals:
44 * - $_GET
45 * - $_POST
46 * - $_COOKIE
47 * - $_FILES
48 * - $_SERVER
49 */
50 public function createServerRequestFromGlobals():ServerRequestInterface{
51
52 $serverRequest = $this->serverRequestFactory->createServerRequest(
53 ($_SERVER['REQUEST_METHOD'] ?? 'GET'),
54 $this->createUriFromGlobals(),
55 $_SERVER,
56 );
57
58 if(function_exists('getallheaders')){
59 $allHeaders = getallheaders();
60
61 if(is_array($allHeaders)){
62 foreach($allHeaders as $name => $value){
63 $serverRequest = $serverRequest->withHeader($name, $value);
64 }
65 }
66 }
67
68 $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? substr($_SERVER['SERVER_PROTOCOL'], 5) : '1.1';
69
70 return $serverRequest
71 ->withProtocolVersion($protocol)
72 ->withCookieParams($_COOKIE)
73 ->withQueryParams($_GET)
74 ->withParsedBody($_POST)
75 ->withUploadedFiles($this->normalizeFiles($_FILES))
76 ;
77 }
78
79 /**
80 * Creates a UriInterface populated with values from $_SERVER.
81 */
82 public function createUriFromGlobals():UriInterface{
83 $hasPort = false;
84 $hasQuery = false;
85
86 $uri = $this->uriFactory
87 ->createUri()
88 ->withScheme((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http')
89 ;
90
91 if(isset($_SERVER['HTTP_HOST'])){
92 $hostHeaderParts = explode(':', $_SERVER['HTTP_HOST']);
93 $uri = $uri->withHost($hostHeaderParts[0]);
94
95 if(isset($hostHeaderParts[1])){
96 $hasPort = true;
97 $uri = $uri->withPort((int)$hostHeaderParts[1]);
98 }
99 }
100 elseif(isset($_SERVER['SERVER_NAME'])){
101 $uri = $uri->withHost($_SERVER['SERVER_NAME']);
102 }
103 elseif(isset($_SERVER['SERVER_ADDR'])){
104 $uri = $uri->withHost($_SERVER['SERVER_ADDR']);
105 }
106
107 if(!$hasPort && isset($_SERVER['SERVER_PORT'])){
108 $uri = $uri->withPort($_SERVER['SERVER_PORT']);
109 }
110
111 if(isset($_SERVER['REQUEST_URI'])){
112 $requestUriParts = explode('?', $_SERVER['REQUEST_URI']);
113 $uri = $uri->withPath($requestUriParts[0]);
114
115 if(isset($requestUriParts[1])){
116 $hasQuery = true;
117 $uri = $uri->withQuery($requestUriParts[1]);
118 }
119 }
120
121 if(!$hasQuery && isset($_SERVER['QUERY_STRING'])){
122 $uri = $uri->withQuery($_SERVER['QUERY_STRING']);
123 }
124
125 return $uri;
126 }
127
128
129 /**
130 * Returns an UploadedFile instance array.
131 *
132 * @param array<string, string> $files An array which respects $_FILES structure
133 *
134 * @return array<string, \Psr\Http\Message\UploadedFileInterface>
135 * @throws \InvalidArgumentException for unrecognized values
136 */
137 public function normalizeFiles(iterable $files):array{
138 $normalized = [];
139
140 foreach($files as $key => $value){
141
142 if($value instanceof UploadedFileInterface){
143 $normalized[$key] = $value;
144 }
145 elseif(is_array($value) && isset($value['tmp_name'])){
146 $normalized[$key] = $this->createUploadedFileFromSpec($value);
147 }
148 elseif(is_array($value)){
149 // recursion
150 $normalized[$key] = $this->normalizeFiles($value);
151 }
152 else{
153 throw new InvalidArgumentException('Invalid value in files specification');
154 }
155
156 }
157
158 return $normalized;
159 }
160
161 /**
162 * Creates an UploadedFile instance from a $_FILES specification.
163 *
164 * If the specification represents an array of values, this method will
165 * delegate to normalizeNestedFileSpec() and return that return value.
166 *
167 * @phpstan-param (File|FileSpec) $value
168 *
169 * @return \Psr\Http\Message\UploadedFileInterface|\Psr\Http\Message\UploadedFileInterface[]
170 */
171 public function createUploadedFileFromSpec(array $value):UploadedFileInterface|array{
172
173 if(is_array($value['tmp_name'])){
174 return self::normalizeNestedFileSpec($value);
175 }
176
177 $stream = $this->createStreamFromFile($value['tmp_name']);
178
179 return $this->uploadedFileFactory
180 ->createUploadedFile($stream, (int)$value['size'], (int)$value['error'], $value['name'], $value['type']);
181 }
182
183 /** @codeCoverageIgnore */
184 private function createStreamFromFile(string $file):StreamInterface{
185
186 if(is_file($file)){
187 return $this->streamFactory->createStreamFromFile($file);
188 }
189
190 return $this->streamFactory->createStream($file);
191 }
192
193 /**
194 * Normalizes an array of file specifications.
195 *
196 * Loops through all nested files and returns a normalized array of
197 * UploadedFileInterface instances.
198 *
199 * @phpstan-param FileSpec $files
200 *
201 * @return \Psr\Http\Message\UploadedFileInterface[]
202 */
203 public function normalizeNestedFileSpec(array $files):array{
204 $normalized = [];
205
206 foreach(array_keys($files['tmp_name']) as $key){
207 $spec = [
208 'tmp_name' => $files['tmp_name'][$key],
209 'size' => $files['size'][$key],
210 'error' => $files['error'][$key],
211 'name' => $files['name'][$key],
212 'type' => $files['type'][$key],
213 ];
214
215 $normalized[$key] = self::createUploadedFileFromSpec($spec);
216 }
217
218 return $normalized;
219 }
220
221}