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