1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
|
<?php
/// CRIMSON --- A simple PHP framework.
/// Copyright © 2024 Freya Murphy <contact@freyacat.org>
///
/// This file is part of CRIMSON.
///
/// CRIMSON is free software; you can redistribute it and/or modify it
/// under the terms of the GNU General Public License as published by
/// the Free Software Foundation; either version 3 of the License, or (at
/// your option) any later version.
///
/// CRIMSON is distributed in the hope that it will be useful, but
/// WITHOUT ANY WARRANTY; without even the implied warranty of
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/// GNU General Public License for more details.
///
/// You should have received a copy of the GNU General Public License
/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>.
class Router extends Base {
// if the website is ready to accept requests
// (protects against users requesting unready resources)
private bool $ready;
// if the router is currently in an error handler
private bool $in_err;
// The parsed request. Contains the following fields.
//
// NAME | TYPE | DEFAULT | DESCRIPTION
// ------------------------------------------------------------
// app | string | 'index' | The first part of the uri, selects which
// | | | controller will be called.
// slug | string | 'index' | The second part of the uri, slects
// | | | which method will be called.
// args | array | [] | Rest of uri parts. Arguments passed to
// | | | the method.
// uri | array? | | From PHP's parse_url($uri_str).
// uri_str | string | | The URI given in the HTML request.
// method | string | | The HTTP method.
// ip | string | | The requesting IP.
public final array $req;
/**
* Creates the crimson router.
*
* => Checks if crimson is ready to route requests.
* => Defines self as ROUTER.
*/
function __construct()
{
// are we ready?
$this->ready = $this->ready_check();
// get request
$this->in_err = FALSE;
$this->req = $this->get_req();
// ROUTER is used by Controller in error($code) function
define('ROUTER', $this);
// fail if URI is invalid (get_req does not check this)
if (!$this->req['uri'])
$this->handle_error(400); // does not return!
}
/**
* Cheks if crimson is ready to route requests.
* => Checks if the database is ready (if enabled).
*/
private function ready_check(): bool {
// assume we are ready unless told otherwise
$ready = TRUE;
// check db
if ($ready && getenv('POSTGRES_ENABLED') === 'true') {
$ready = file_exists('/var/run/crimson/db_ready');
}
// return result
return $ready;
}
/**
* @param string $path - the current request path
* Gets the current route
* @return array<string,mixed>
*/
private function get_req_route($path): array
{
// trim the path
$path = trim($path);
// remove first '/'
$path = substr($path, 1);
// get modified route
$routes = CONFIG['routes'];
foreach ($routes as $key => $value) {
$key = "/^{$key}$/";
if (!preg_match($key, $path, $matches))
continue;
$path = $value;
for ($i = 1; $i < count($matches); $i++) {
$path = str_replace(
"\\{$i}",
$matches[$i],
$path);
}
break;
}
// get path parts
$parts = explode('/', $path);
if ($path == '')
$parts = [];
// get the length
$len = count($parts);
// get route info
$route = array();
$route['app'] = $len > 0 ? $parts[0] : 'index';
$route['slug'] = $len > 1 ? $parts[1] : 'index';
$route['args'] = array_slice($parts, 2);
return $route;
}
/**
* Gets the users ip
*/
private function get_ip(): ?string
{
$headers = array (
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'HTTP_X_REAL_IP',
'REMOTE_ADDR'
);
foreach ($headers as $header) {
if (isset($_SERVER[$header]))
return $_SERVER[$header];
}
return NULL;
}
/**
* Gets the curret request info.
* Does not fail in invalid uri. Must be handeled by
* caller function.
* @return array<string,mixed>
*/
private function get_req(): array
{
$method = $_SERVER['REQUEST_METHOD'];
$uri_str = $_SERVER['REQUEST_URI'];
$uri = parse_url($uri_str);
$path = '';
if ($uri && array_key_exists('path', $uri))
$path = $uri['path'];
return array_merge(
array(
'uri' => $uri,
'uri_str' => $uri_str,
'method' => $method,
'ip' => $this->get_ip()
),
$this->get_req_route($path),
);
}
/**
* Handles a router error code
* @param int $code - the http error code
*/
public function handle_error(int $code): never
{
// if in_err is set RIGHT NOW, this means the user specified
// error hook errored itself. To prevent error recursion we do
// not want to run it again!
$force_builtin = $this->in_err;
// Sets the in_err catch to true, read comment above to why.
$this->in_err = TRUE;
CRIMSON_HOOK('error', [$this->req, $code], $force_builtin);
// error hook is type never, but CRIMSON_HOOK is type void
CRIMSON_DIE();
}
/**
* @param array $req
* @param array<int,mixed> $req
*/
public function handle_req(): never
{
// block requests if we are not ready
if ($this->ready === FALSE)
$this->handle_error(503);
// run pre route hook
CRIMSON_HOOK('pre_route', [$this]);
// load the controller
$controller = $this->load_controller($this->req['app']);
if (!$controller)
$this->handle_error(404);
// find the function that matches our request
// format: /controller/fn_name/fn_arg1/fn_arg2/.../fn_argN
$handler = NULL;
try {
$cls = new ReflectionClass($controller);
$mds = $cls->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($mds as $md) {
if ($md->name !== $this->req['slug'])
continue;
if (count($md->getParameters()) !=
count($this->req['args']))
continue;
$handler = $md;
break;
}
} catch (Exception $_e) {}
// return 404 if no handler found
if (!$handler)
$this->handle_error(404);
try {
$handler->invokeArgs($controller, $this->req['args']);
} catch (Exception $_e) {};
// sanity check
CRIMSON_DIE();
}
}
|