1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Http; |
4: | |
5: | use Psr\Http\Message\MessageInterface; |
6: | use Psr\Http\Message\RequestInterface; |
7: | use Psr\Http\Message\StreamInterface; |
8: | use Psr\Http\Message\UriInterface as PsrUriInterface; |
9: | use Salient\Contract\Core\Arrayable; |
10: | use Salient\Contract\Http\HttpHeader; |
11: | use Salient\Contract\Http\HttpRequestInterface; |
12: | use Salient\Contract\Http\HttpRequestMethod as Method; |
13: | use Salient\Contract\Http\MimeType; |
14: | use Salient\Core\Concern\ImmutableTrait; |
15: | use Salient\Utility\Exception\InvalidArgumentTypeException; |
16: | use Salient\Utility\Regex; |
17: | use InvalidArgumentException; |
18: | use Stringable; |
19: | |
20: | |
21: | |
22: | |
23: | |
24: | |
25: | class HttpRequest extends AbstractHttpMessage implements HttpRequestInterface |
26: | { |
27: | use ImmutableTrait; |
28: | |
29: | private const TOKEN = '/^[-0-9a-z!#$%&\'*+.^_`|~]++$/iD'; |
30: | |
31: | protected string $Method; |
32: | protected ?string $RequestTarget; |
33: | protected Uri $Uri; |
34: | |
35: | |
36: | |
37: | |
38: | |
39: | |
40: | public function __construct( |
41: | string $method, |
42: | $uri, |
43: | $body = null, |
44: | $headers = null, |
45: | ?string $requestTarget = null, |
46: | string $version = '1.1' |
47: | ) { |
48: | $this->Method = $this->filterMethod($method); |
49: | $this->RequestTarget = $this->filterRequestTarget($requestTarget); |
50: | $this->Uri = $this->filterUri($uri); |
51: | |
52: | parent::__construct($body, $headers, $version); |
53: | |
54: | if ($this->Headers->getHeaderLine(HttpHeader::HOST) !== '') { |
55: | return; |
56: | } |
57: | $host = $this->getUriHost(); |
58: | if ($host === '') { |
59: | return; |
60: | } |
61: | $this->Headers = $this->Headers->set(HttpHeader::HOST, $host); |
62: | } |
63: | |
64: | |
65: | |
66: | |
67: | public static function fromPsr7(MessageInterface $message): HttpRequest |
68: | { |
69: | if ($message instanceof HttpRequest) { |
70: | return $message; |
71: | } |
72: | |
73: | if (!$message instanceof RequestInterface) { |
74: | throw new InvalidArgumentTypeException(1, 'message', RequestInterface::class, $message); |
75: | } |
76: | |
77: | return new self( |
78: | $message->getMethod(), |
79: | $message->getUri(), |
80: | $message->getBody(), |
81: | $message->getHeaders(), |
82: | $message->getRequestTarget(), |
83: | $message->getProtocolVersion(), |
84: | ); |
85: | } |
86: | |
87: | |
88: | |
89: | |
90: | public function getMethod(): string |
91: | { |
92: | return $this->Method; |
93: | } |
94: | |
95: | |
96: | |
97: | |
98: | public function getRequestTarget(): string |
99: | { |
100: | if ($this->RequestTarget !== null) { |
101: | return $this->RequestTarget; |
102: | } |
103: | |
104: | $target = $this->Uri->getPath(); |
105: | if ($target === '') { |
106: | $target = '/'; |
107: | } |
108: | |
109: | $query = $this->Uri->toParts()['query'] ?? null; |
110: | if ($query !== null) { |
111: | return "{$target}?{$query}"; |
112: | } |
113: | |
114: | return $target; |
115: | } |
116: | |
117: | |
118: | |
119: | |
120: | public function getUri(): Uri |
121: | { |
122: | return $this->Uri; |
123: | } |
124: | |
125: | |
126: | |
127: | |
128: | public function withRequestTarget(string $requestTarget): RequestInterface |
129: | { |
130: | return $this->with('RequestTarget', $this->filterRequestTarget($requestTarget)); |
131: | } |
132: | |
133: | |
134: | |
135: | |
136: | public function withMethod(string $method): RequestInterface |
137: | { |
138: | return $this->with('Method', $this->filterMethod($method)); |
139: | } |
140: | |
141: | |
142: | |
143: | |
144: | public function withUri(PsrUriInterface $uri, bool $preserveHost = false): RequestInterface |
145: | { |
146: | if ((string) $uri === (string) $this->Uri) { |
147: | $instance = $this; |
148: | } else { |
149: | $instance = $this->with('Uri', $this->filterUri($uri)); |
150: | } |
151: | |
152: | if ( |
153: | $preserveHost |
154: | && $instance->Headers->getHeaderLine(HttpHeader::HOST) !== '' |
155: | ) { |
156: | return $instance; |
157: | } |
158: | |
159: | $host = $instance->getUriHost(); |
160: | if ($host === '') { |
161: | return $instance; |
162: | } |
163: | return $instance->withHeader(HttpHeader::HOST, $host); |
164: | } |
165: | |
166: | |
167: | |
168: | |
169: | public function jsonSerialize(): array |
170: | { |
171: | $request = parent::jsonSerialize(); |
172: | |
173: | if ( |
174: | $request['bodySize'] === -1 |
175: | || $request['bodySize'] > 0 |
176: | || ([ |
177: | Method::POST => true, |
178: | Method::PUT => true, |
179: | Method::PATCH => true, |
180: | Method::DELETE => true, |
181: | ][$this->Method] ?? false) |
182: | ) { |
183: | $mediaType = $this->Headers->getHeaderValues(HttpHeader::CONTENT_TYPE); |
184: | $mediaType = count($mediaType) === 1 ? $mediaType[0] : ''; |
185: | $body = (string) $this->Body; |
186: | $postData = [ |
187: | 'postData' => [ |
188: | 'mimeType' => $mediaType, |
189: | 'params' => HttpUtil::mediaTypeIs($mediaType, MimeType::FORM) |
190: | ? $this->splitQuery($body) |
191: | : [], |
192: | 'text' => $body, |
193: | ], |
194: | ]; |
195: | } else { |
196: | $postData = []; |
197: | } |
198: | |
199: | return [ |
200: | 'method' => $this->Method, |
201: | 'url' => (string) $this->Uri, |
202: | 'httpVersion' => $request['httpVersion'], |
203: | 'cookies' => $request['cookies'], |
204: | 'headers' => $request['headers'], |
205: | 'queryString' => $this->splitQuery($this->Uri->getQuery()), |
206: | ] + $postData + $request; |
207: | } |
208: | |
209: | |
210: | |
211: | |
212: | protected function getStartLine(): string |
213: | { |
214: | return sprintf( |
215: | '%s %s HTTP/%s', |
216: | $this->Method, |
217: | $this->getRequestTarget(), |
218: | $this->ProtocolVersion, |
219: | ); |
220: | } |
221: | |
222: | private function getUriHost(): string |
223: | { |
224: | $host = $this->Uri->getHost(); |
225: | if ($host === '') { |
226: | return ''; |
227: | } |
228: | |
229: | $port = $this->Uri->getPort(); |
230: | if ($port !== null) { |
231: | $host .= ':' . $port; |
232: | } |
233: | |
234: | return $host; |
235: | } |
236: | |
237: | private function filterMethod(string $method): string |
238: | { |
239: | if (!Regex::match(self::TOKEN, $method)) { |
240: | throw new InvalidArgumentException( |
241: | sprintf('Invalid HTTP method: %s', $method) |
242: | ); |
243: | } |
244: | return $method; |
245: | } |
246: | |
247: | |
248: | |
249: | |
250: | private function filterRequestTarget(?string $requestTarget): ?string |
251: | { |
252: | if ($requestTarget === null || $requestTarget === '') { |
253: | return null; |
254: | } |
255: | |
256: | |
257: | if ($requestTarget === '*') { |
258: | return $requestTarget; |
259: | } |
260: | |
261: | |
262: | if (Uri::isAuthorityForm($requestTarget)) { |
263: | return $requestTarget; |
264: | } |
265: | |
266: | $parts = Uri::parse($requestTarget); |
267: | if ($parts === false) { |
268: | throw new InvalidArgumentException( |
269: | sprintf('Invalid request target: %s', $requestTarget) |
270: | ); |
271: | } |
272: | |
273: | |
274: | if (isset($parts['scheme'])) { |
275: | return $requestTarget; |
276: | } |
277: | |
278: | |
279: | $invalid = array_diff_key($parts, array_flip(['path', 'query'])); |
280: | if (!$invalid) { |
281: | return $requestTarget; |
282: | } |
283: | throw new InvalidArgumentException( |
284: | sprintf('origin-form of request-target cannot have URI components other than path, query: %s', $requestTarget) |
285: | ); |
286: | } |
287: | |
288: | |
289: | |
290: | |
291: | private function filterUri($uri): Uri |
292: | { |
293: | |
294: | |
295: | |
296: | |
297: | |
298: | return Uri::from($uri); |
299: | } |
300: | |
301: | |
302: | |
303: | |
304: | private function splitQuery(string $query): array |
305: | { |
306: | if ($query === '') { |
307: | return []; |
308: | } |
309: | foreach (explode('&', $query) as $param) { |
310: | $param = explode('=', $param, 2); |
311: | $params[] = [ |
312: | 'name' => $param[0], |
313: | 'value' => $param[1] ?? '', |
314: | ]; |
315: | } |
316: | return $params; |
317: | } |
318: | } |
319: | |