1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
|
3
|
|
|
use Koded\Framework\{HTTPBadRequest, HTTPMethodNotAllowed, HTTPNotFound, HTTPNotImplemented}; |
4
|
|
|
use Koded\Http\{HttpFactory, ServerResponse}; |
5
|
|
|
use Koded\Http\Client\ClientFactory; |
6
|
|
|
use Koded\Http\Interfaces\{HttpStatus, Request}; |
7
|
|
|
use Psr\Http\Message\ResponseInterface; |
8
|
|
|
use function Koded\Http\create_stream; |
9
|
|
|
|
10
|
|
|
|
11
|
|
|
function path_not_found(string $path): callable |
12
|
|
|
{ |
13
|
|
|
throw new HTTPNotFound(instance: $path); |
14
|
|
|
} |
15
|
|
|
|
16
|
|
|
|
17
|
|
|
function bad_request(...$args): callable |
18
|
|
|
{ |
19
|
|
|
throw new HTTPBadRequest(...$args); |
20
|
|
|
} |
21
|
|
|
|
22
|
|
|
|
23
|
|
|
function method_not_allowed(array $allowed): callable |
24
|
|
|
{ |
25
|
1 |
|
throw new HTTPMethodNotAllowed($allowed); |
26
|
|
|
} |
27
|
|
|
|
28
|
|
|
|
29
|
|
|
function no_app_routes(): callable |
30
|
|
|
{ |
31
|
1 |
|
throw (new HTTPNotImplemented( |
32
|
|
|
title: 'No Routes', |
33
|
|
|
detail: 'No routes are defined in your application', |
34
|
|
|
type: 'https://kodeart.github.io/koded/routing/' |
35
|
|
|
)) |
36
|
1 |
|
->setMember('framework', 'Koded Framework') |
37
|
1 |
|
->setMember('version', get_version()); |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Creates a responder for HTTP HEAD method. |
42
|
|
|
* |
43
|
|
|
* @param string $uri |
44
|
|
|
* @param array $methods |
45
|
|
|
* @return callable |
46
|
|
|
*/ |
47
|
|
|
function head_response(string $uri, array $methods): callable |
48
|
|
|
{ |
49
|
|
|
return function() use ($uri, $methods): ResponseInterface { |
50
|
|
|
$methods || $methods = [Request::HEAD, Request::OPTIONS]; |
|
|
|
|
51
|
|
|
if (false === \in_array(Request::GET, $methods)) { |
52
|
|
|
return (new ServerResponse)->withHeader('Allow', $methods); |
53
|
|
|
} |
54
|
|
|
$get = (new ClientFactory( |
55
|
|
|
\function_exists('curl_init') |
56
|
|
|
? ClientFactory::CURL |
57
|
|
|
: ClientFactory::PHP |
58
|
|
|
)) |
59
|
|
|
->get($uri, ['Connection' => 'close']) |
60
|
|
|
->timeout(5) |
61
|
|
|
->read() |
62
|
|
|
->withoutHeader('Set-Cookie') |
63
|
|
|
->withHeader('Allow', \join(',', $methods)); |
64
|
|
|
|
65
|
|
|
if ($get->getStatusCode() < HttpStatus::BAD_REQUEST) { |
66
|
|
|
return $get; |
67
|
|
|
} |
68
|
|
|
// If GET request fails, it returns the Allow header |
69
|
|
|
// with the failure reason in the "X-Error-*" headers |
70
|
|
|
\error_log($get->getBody()->getContents()); |
71
|
|
|
return $get |
72
|
|
|
->withHeader('X-Error-Status', \join(' ', [$get->getStatusCode(), $get->getReasonPhrase()])) |
73
|
|
|
->withHeader('X-Error-Message', \str_replace(["\n", "\r", "\t"], ' ', $get->getBody())) |
74
|
|
|
->withStatus(HttpStatus::OK) |
75
|
|
|
->withBody(create_stream('')); |
76
|
|
|
}; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Creates a responder for HTTP OPTIONS method. |
81
|
|
|
* This method does not return the Origin header, because it may |
82
|
|
|
* be a CORS request which should be handled by the middleware. |
83
|
|
|
* |
84
|
|
|
* @param array $methods Supported HTTP methods for the URI in question |
85
|
|
|
* @return callable The responder for the OPTIONS method |
86
|
|
|
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS |
87
|
|
|
*/ |
88
|
|
|
function create_options_response(array $methods): callable |
89
|
|
|
{ |
90
|
6 |
|
return fn(): ResponseInterface => (new HttpFactory) |
91
|
6 |
|
->createResponse(HttpStatus::NO_CONTENT) |
92
|
6 |
|
->withHeader('Cache-Control', 'no-cache, max-age=0') |
93
|
6 |
|
->withHeader('Allow', \join(',', $methods)) |
94
|
6 |
|
->withHeader('Content-Type', 'text/plain'); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Maps the HTTP methods to responder (public) methods. |
99
|
|
|
* |
100
|
|
|
* @param callable|object|string $resource |
101
|
|
|
* @return array|callable[]|object[]|string[] |
102
|
|
|
*/ |
103
|
|
|
function map_http_methods(callable|object|string $resource): array |
104
|
|
|
{ |
105
|
23 |
|
$map = [ |
106
|
|
|
Request::HEAD => 'head', |
107
|
|
|
Request::OPTIONS => 'options' |
108
|
|
|
]; |
109
|
|
|
foreach ([ |
110
|
|
|
Request::GET, |
111
|
|
|
Request::POST, |
112
|
|
|
Request::PUT, |
113
|
|
|
Request::PATCH, |
114
|
|
|
Request::DELETE |
115
|
|
|
] as $method) { |
116
|
23 |
|
if (\method_exists($resource, $method)) { |
117
|
18 |
|
$map = [$method => \strtolower($method)] + $map; |
118
|
23 |
|
} elseif (\is_callable($resource)) { |
119
|
3 |
|
$map = [$method => $resource] + $map; |
120
|
|
|
} |
121
|
|
|
} |
122
|
23 |
|
return $map; |
123
|
|
|
} |
124
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.