-
-
Notifications
You must be signed in to change notification settings - Fork 999
Expand file tree
/
Copy pathFrontcontroller.php
More file actions
490 lines (397 loc) · 17.4 KB
/
Copy pathFrontcontroller.php
File metadata and controls
490 lines (397 loc) · 17.4 KB
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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
<?php
namespace Leantime\Core\Controller;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Leantime\Core\Auth\Permissions\PermissionEnforcer;
use Leantime\Core\Configuration\Environment;
use Leantime\Core\Events\DispatchesEvents;
use Leantime\Core\Http\HtmxRequest;
use Leantime\Core\Http\IncomingRequest;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
/**
* Frontcontroller class
*/
class Frontcontroller
{
use DispatchesEvents;
/**
* @var string - last action that was fired
*/
private string $lastAction = '';
/**
* @var string - fully parsed action
*/
private string $fullAction = '';
private IncomingRequest $incomingRequest;
/**
* @var array - valid status codes
*/
private array $validStatusCodes = ['100', '101', '200', '201', '202', '203', '204', '205', '206', '300', '301', '302', '303', '304', '305', '306', '307', '400', '401', '402', '403', '404', '405', '406', '407', '408', '409', '410', '411', '412', '413', '414', '415', '416', '417', '500', '501', '502', '503', '504', '505'];
protected $defaultRoute = 'dashboard.home';
protected Environment $config;
/**
* __construct - Set the rootpath of the server
*
* @return void
*/
public function __construct(IncomingRequest $request, private PermissionEnforcer $permissionEnforcer)
{
$this->incomingRequest = $request;
$this->config = app(Environment::class);
}
/**
* run - executes the action depending on Request or firstAction
*
*
* @throws BindingResolutionException
*/
public function dispatch(IncomingRequest $request): Response
{
$this->incomingRequest = $request;
[$moduleName, $controllerType, $controllerName, $method] = $this->parseRequestParts($request);
$this->dispatchEvent('execute_action_start', ['action' => $controllerName, 'module' => $moduleName]);
$routeParts = $this->getValidControllerCall($moduleName, $controllerName, $method, $controllerType);
// Setting default response code to 200, can be changed in controller
$this->setResponseCode(200);
$this->lastAction = $moduleName.'.'.$controllerName.'.'.$method;
$this->dispatchEvent('execute_action_end', ['action' => $controllerName, 'module' => $moduleName]);
// execute action
return $this->executeAction($routeParts['class'], $routeParts['method']);
}
public static function dispatch_request(IncomingRequest $request): Response
{
// Resolve through the container so constructor dependencies (e.g. PermissionEnforcer)
// are injected; dispatch() sets the active request explicitly.
$frontcontroller = app()->make(self::class);
return $frontcontroller->dispatch($request);
}
/**
* parseRequestParts - Parses the request segments and sets the necessary values in the IncomingRequest object.
*
* @param IncomingRequest $request The incoming request object.
* @return array An array containing the controller name, action name, and method.
*/
public function parseRequestParts(IncomingRequest $request)
{
$id = null;
$segments = $request->segments();
$method = strtolower($this->incomingRequest->getMethod());
if (count($segments) == 0) {
$segments = explode('.', $this->defaultRoute);
}
// First part is hx tells us this is a htmx controller request
$controllerType = 'Controllers';
if ($segments[0] == 'hx') {
array_shift($segments);
$controllerType = 'Hxcontrollers';
}
// If only one segment part was given the url is mean to be an index placeholder
if (count($segments) == 1) {
$segments[] = 'index';
}
// First segment is always module
$moduleName = $segments[0] ?? '';
// Second is action
$controllerName = $segments[1] ?? '';
// third is either id or method
// we can say that a numeric value always represents an id
if (isset($segments[2]) &&
(is_numeric($segments[2]) || Str::isUuid($segments[2]))
) {
$id = $segments[2];
}
// If not numeric, it's quite likely this is a method name
// But it needs to be double checked.
if (isset($segments[2]) &&
! (is_numeric($segments[2]) || Str::isUuid($segments[2]))
) {
$method = $segments[2];
}
// If a third segment is set it is the id
if (isset($segments[3])) {
$id = $segments[3];
$method = $segments[2];
$request_parts = implode('.', array_slice($segments, 3));
$this->incomingRequest->query->set('request_parts', $request_parts);
}
$this->incomingRequest->query->set('act', $moduleName.'.'.$controllerName.'.'.$method);
$this->incomingRequest->setCurrentRoute($moduleName.'.'.$controllerName);
if ($id === '0' || ! empty($id)) {
$this->incomingRequest->query->set('id', $id);
}
// need to update all controllers to stop using global get and post methods.
// In the meantime we are setting it again.
$this->incomingRequest->overrideGlobals();
return [$moduleName, $controllerType, $controllerName, $method];
}
/**
* executeAction - includes the class in includes/modules by the Request
*
* @param string $controller actionname.filename
*
* @throws BindingResolutionException
*/
public function executeAction(string $controller, string $method): Response
{
$parameters = $this->incomingRequest->getRequestParams();
// Enforce #[RequiresPermission] on the resolved action before instantiating the
// controller. This is the single chokepoint for every convention-routed controller,
// regardless of which base class (if any) it extends.
$this->permissionEnforcer->enforce($controller, $method, is_array($parameters) ? $parameters : []);
$controllerClass = app()->make($controller);
$response = $controllerClass->callAction($method, $parameters);
// A controller may return a Symfony Response directly, a Responsable (e.g. an
// ImageResponse / JsonRpcResponse — now honored on this legacy dispatch path the same
// way Laravel's router and the ExceptionHandler already do), or a string fragment key
// handled by the controller's own getResponse().
return match (true) {
$response instanceof Response => $response,
$response instanceof Responsable => $response->toResponse($this->incomingRequest),
default => $controllerClass->getResponse($response),
};
}
/**
* Retrieves the type of controller based on the incoming request.
*
* @return string The type of controller. Possible values are 'Controllers' or 'Hxcontrollers'.
*/
protected function getControllerType(): string
{
$controllerType = 'Controllers';
if (
($this->incomingRequest instanceof HtmxRequest) &&
$this->incomingRequest->header('is-modal') == false &&
$this->incomingRequest->header('hx-boosted') == false
) {
$controllerType = 'Hxcontrollers';
}
return $controllerType;
}
/**
* Retrieves the valid controller call based on the module name, action name, and method name.
*
* @param string $moduleName The name of the module.
* @param string $actionName The name of the action.
* @param string $methodName The name of the method.
* @return array The valid controller call in the form of an associative array. The "class" key represents the class path of the controller,
* and the "method" key represents the method name of the controller.
*/
public function getValidControllerCall(string $moduleName, string $actionName, string $methodName, string $controllerType): array
{
$moduleName = Str::studly($moduleName);
$actionName = Str::studly($actionName);
$methodNameLower = Str::lower($methodName);
$routepath = $moduleName.'.'.$controllerType.'.'.$actionName;
$actionPath = $moduleName.'\\'.$controllerType.'\\'.$actionName;
if ($this->config->debug == false) {
$cachedRoute = Cache::store('installation')->get('routes.'.$routepath.'.'.$methodNameLower);
// Cached routes can outlive a deploy (e.g. a controller's run() replaced by get()/post()).
// Only trust the cache if the class and method still exist; otherwise drop it and re-resolve.
if (
is_array($cachedRoute)
&& isset($cachedRoute['class'], $cachedRoute['method'])
&& class_exists($cachedRoute['class'])
&& method_exists($cachedRoute['class'], $cachedRoute['method'])
) {
return $cachedRoute;
}
if ($cachedRoute !== null) {
Cache::store('installation')->forget('routes.'.$routepath.'.'.$methodNameLower);
}
}
$classPath = $this->getClassPath($controllerType, $moduleName, $actionName);
if ($classPath === false) {
throw new NotFoundHttpException("Can't find a valid controller for ".strip_tags($moduleName).'/'.strip_tags($actionName));
}
$classMethod = $this->getValidControllerMethod($classPath, $methodName);
Cache::store('installation')->set('routes.'.$routepath.'.'.($classMethod == 'run' ? $methodNameLower : $classMethod), ['class' => $classPath, 'method' => $classMethod]);
return ['class' => $classPath, 'method' => $classMethod];
}
/**
* Retrieves the class path of a controller based on the provided controller type, module name, and action name.
*
* @param string $controllerType The type of controller. Possible values are 'Controllers' or 'Hxcontrollers'.
**/
public function getClassPath(string $controllerType, string $moduleName, string $actionName): string|false
{
$controllerNs = 'Domain';
$classname = 'Leantime\\Domain\\'.$moduleName.'\\'.$controllerType.'\\'.$actionName;
if (class_exists($classname)) {
return $classname;
}
// Check if hxcontroller exists
$classname = 'Leantime\\Domain\\'.$moduleName.'\\Hxcontrollers\\'.$actionName;
if (class_exists($classname)) {
return $classname;
}
$classname = 'Leantime\\Plugins\\'.$moduleName.'\\'.$controllerType.'\\'.$actionName;
$enabledPlugins = app()->make(\Leantime\Domain\Plugins\Services\Plugins::class)->getEnabledPlugins();
$pluginEnabled = false;
foreach ($enabledPlugins as $key => $obj) {
if (strtolower($obj->foldername) !== strtolower($moduleName)) {
continue;
}
$pluginEnabled = true;
break;
}
if (! $pluginEnabled) {
return false;
}
if (class_exists($classname)) {
return $classname;
}
$classname = 'Leantime\\Plugins\\'.$moduleName.'\\Hxcontrollers\\'.$actionName;
if (class_exists($classname)) {
return $classname;
}
return false;
}
/**
* Retrieves a valid controller method based on the given controller class and method.
*
* @param string $controllerClass The fully qualified class name of the controller.
* @param string $method The method name to check for validity.
* @return string The valid controller method name. If the given method is "head",
* it will be converted to "get". If the given method exists in the controller
* class, it will be returned. Otherwise, if the "run" method exists in the
* controller class, it will be returned. If no valid method is found, a
* RouteNotFoundException will be thrown.
*
* @throws RouteNotFoundException If no valid method is found for the given route.
*/
public function getValidControllerMethod(string $controllerClass, string $method): string
{
$methodFormatted = Str::camel($method);
$httpMethod = Str::lower($this->incomingRequest->getMethod());
if (Str::lower($method) == 'head') {
$method = 'get';
}
// First check if the given method exists.
if (method_exists($controllerClass, $methodFormatted)) {
return $methodFormatted;
// Then check if the http method exists as verb
} elseif (method_exists($controllerClass, $httpMethod)) {
// If this was the case our first assumption around $method was wrong and $method is actually a
// id/slug. Let's set id to that slug.
$this->incomingRequest->query->set('id', $method);
return $httpMethod;
// Just for backwards compatibility, let's also check if run exists.
} elseif (method_exists($controllerClass, 'run')) {
return 'run';
}
throw new NotFoundHttpException("Can't find valid method for ".strip_tags($method).' in '.strip_tags($controllerClass));
}
/**
* getActionName - split string to get actionName
*
* @throws BindingResolutionException
*/
public static function getActionName(?string $completeName = null): string
{
$completeName ??= currentRoute();
$actionParts = explode('.', empty($completeName) ? currentRoute() : $completeName);
// If not action name was given, call index controller
if (is_array($actionParts) && count($actionParts) == 1) {
return 'index';
} elseif (is_array($actionParts) && count($actionParts) >= 2) {
return $actionParts[1];
}
return '';
}
/**
* Retrieves the method name based on the complete name of a route.
*
* @param string|null $completeName The complete name of the route. Defaults to the current route if not provided.
* @return string The method name. If the route name consists of two parts (e.g. "controllers.index"), the method name will be the lowercase representation of the current request method
*. If the route name consists of three parts (e.g. "controllers.update"), the method name will be the second part of the route name. Otherwise, an empty string is returned.
*
* @deprecated
**/
public static function getMethodName(?string $completeName = null): string
{
$completeName ??= currentRoute();
$actionParts = explode('.', empty($completeName) ? currentRoute() : $completeName);
// If not action name was given, call index controller
if (is_array($actionParts) && count($actionParts) == 2) {
return strtolower(app('request')->getMethod());
} elseif (is_array($actionParts) && count($actionParts) == 3) {
return $actionParts[2];
}
return '';
}
/**
* getModuleName - split string to get modulename
*
* @throws BindingResolutionException
*/
public static function getModuleName(?string $completeName = null): string
{
$completeName ??= currentRoute();
$actionParts = explode('.', empty($completeName) ? currentRoute() : $completeName);
if (is_array($actionParts)) {
return $actionParts[0];
}
return '';
}
/**
* redirect - redirects to a given url
*/
public static function redirect(string $url, int $http_response_code = 303, $headers = []): RedirectResponse
{
if (app('request')->headers->get('is-modal')) {
Frontcontroller::redirectHtmx($url, $headers);
}
return new RedirectResponse(
trim(preg_replace('/\s\s+/', '', strip_tags($url))),
$http_response_code,
$headers
);
}
/**
* redirect - redirects an htmx page.
*
* @param array $headers
*/
public static function redirectHtmx(string $url, $headers = []): Response
{
// modal redirect
if (Str::start($url, '#')) {
$hxCurrentUrl = app('request')->headers->get('hx-current-url');
$mainPageUrl = Str::before($hxCurrentUrl, '#');
$url = $mainPageUrl.''.$url;
}
$headers['HX-Redirect'] = $url;
// $headers["hx-push-url"] = $url;
// $headers["hx-replace-url"] = $url;
// $headers["HX-Refresh"] = true;
// this redirect is actually handled on the client side.
// We'll just return an empty response with a few headers
return new Response(
'redirecting...',
200, // Anything else than 200 will fail.
);
}
/**
* getCurrentRoute - gets current route
*
* @deprecated use request class to get current route
*
* @return string
*/
public static function getCurrentRoute()
{
return app('request')->getCurrentRoute();
}
/**
* setResponseCode - sets the response code
*/
public function setResponseCode(int $responseCode): void
{
http_response_code($responseCode);
}
}