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}