Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.71% covered (success)
94.71%
197 / 208
82.35% covered (warning)
82.35%
14 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialPlatformRouteDispatcher
94.69% covered (success)
94.69%
196 / 207
82.35% covered (warning)
82.35%
14 / 17
79.94
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canDispatch
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
18
 dispatch
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
24
 dispatchMentionsWebRoute
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 dispatchAccessWebRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchAccessRulesDrawerRoute
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 dispatchImpersonationPanelRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 dispatchImpersonationSearchUsersRoute
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 dispatchImpersonationSaveRoute
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
3.00
 dispatchImpersonateUserRoute
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 dispatchImpersonateRevertRoute
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 dispatchLogout
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 dispatchLoginRoute
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
5
 dispatchForgotPasswordRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchResetPasswordRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchMfaVerifyRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 dispatchDavRoutes
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Bootstrap\Dispatcher;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Application\Security\PermissionContextFactory;
12use App\Core\Security\Middleware\CsrfMiddleware;
13use App\Core\Access\Presentation\Web\AccessWebController;
14use App\Modules\Comments\Presentation\Web\MentionsWebController;
15use App\Modules\Dav\Presentation\Web\DavSetupWebController;
16use App\Modules\User\Infrastructure\Repository\SqlUserRepository;
17use App\Modules\User\Presentation\Middleware\AuthenticatedOnlyMiddleware;
18use App\Modules\User\Presentation\Middleware\GuestOnlyMiddleware;
19use App\Modules\User\Presentation\Web\AuthController;
20use App\Modules\User\Presentation\Web\Identity;
21use App\Modules\User\Presentation\Web\ImpersonationWebController;
22use Nyholm\Psr7\Factory\Psr17Factory;
23use Psr\Http\Message\ResponseInterface;
24use Psr\Http\Message\ServerRequestInterface;
25use Psr\Http\Server\RequestHandlerInterface;
26use Yiisoft\Session\SessionInterface;
27use Yiisoft\User\CurrentUser;
28
29/**
30 * Modular route dispatcher for Platform, Authentication, User Impersonation, Access & DAV.
31 */
32final readonly class SpecialPlatformRouteDispatcher implements ModuleRouteDispatcherInterface
33{
34    public function __construct(
35        private ?MentionsWebController $mentionsWebController,
36        private ?AccessWebController $accessWebController,
37        private ?ImpersonationWebController $impersonationWebController,
38        private AuthController $authController,
39        private ?DavSetupWebController $davSetupWebController,
40        private SqlUserRepository $userRepository,
41        private CurrentUser $currentUser,
42        private SessionInterface $session,
43        private PermissionContextFactory $contextFactory,
44        private AuthenticatedOnlyMiddleware $authMiddleware,
45        private GuestOnlyMiddleware $guestMiddleware,
46        private CsrfMiddleware $csrfMiddleware,
47        private Psr17Factory $psr17Factory,
48        private string $appProfile = 'admin'
49    ) {
50    }
51
52    public function canDispatch(string $path): bool
53    {
54        return $path === '/mentions'
55            || $path === '/mentions/'
56            || $path === '/api/mentions/search'
57            || $path === '/settings/access'
58            || $path === '/settings/access/admin'
59            || $path === '/settings/access/client'
60            || $path === '/settings/access/rules/drawer'
61            || $path === '/settings/access/rules-drawer'
62            || $path === '/system/access/rules-drawer'
63            || str_starts_with($path, '/impersonation/')
64            || str_starts_with($path, '/auth/impersonate/')
65            || $path === '/auth/impersonate/revert'
66            || $path === '/login'
67            || $path === '/logout'
68            || $path === '/forgot-password'
69            || $path === '/reset-password'
70            || $path === '/mfa/verify'
71            || str_starts_with($path, '/dav/');
72    }
73
74    public function dispatch(string $path, ServerRequestInterface $request): ?ResponseInterface
75    {
76        return match (true) {
77            $path === '/mentions' || $path === '/mentions/' || $path === '/api/mentions/search' =>
78                $this->dispatchMentionsWebRoute($request),
79            $path === '/settings/access'
80            || $path === '/settings/access/admin'
81            || $path === '/settings/access/client' => $this->dispatchAccessWebRoute($request),
82            $path === '/settings/access/rules/drawer'
83            || $path === '/settings/access/rules-drawer'
84            || $path === '/system/access/rules-drawer' => $this->dispatchAccessRulesDrawerRoute($request),
85            $path === '/impersonation/panel' => $this->dispatchImpersonationPanelRoute($request),
86            $path === '/impersonation/search' => $this->dispatchImpersonationSearchUsersRoute($request),
87            $path === '/impersonation/save' && $request->getMethod() === 'POST' =>
88                $this->dispatchImpersonationSaveRoute($request),
89            (bool) preg_match('#^/(?:impersonation/user|auth/impersonate)/(\d+)$#', $path, $m) =>
90                $this->dispatchImpersonateUserRoute((int) $m[1], $request),
91            ($path === '/auth/impersonate/revert' || $path === '/impersonation/revert') =>
92                $this->dispatchImpersonateRevertRoute($request),
93            $path === '/logout' => $this->dispatchLogout(),
94            $path === '/login' => $this->dispatchLoginRoute($request),
95            $path === '/forgot-password' => $this->dispatchForgotPasswordRoute($request),
96            $path === '/reset-password' => $this->dispatchResetPasswordRoute($request),
97            $path === '/mfa/verify' => $this->dispatchMfaVerifyRoute($request),
98            str_starts_with($path, '/dav/') => $this->dispatchDavRoutes($path),
99            default => null,
100        };
101    }
102
103    private function dispatchMentionsWebRoute(ServerRequestInterface $request): ResponseInterface
104    {
105        if ($this->mentionsWebController === null) {
106            return $this->psr17Factory->createResponse(404);
107        }
108
109        return $this->authMiddleware->process(
110            $request,
111            new class($this->mentionsWebController, $this->contextFactory) implements RequestHandlerInterface {
112                public function __construct(
113                    private MentionsWebController $controller,
114                    private PermissionContextFactory $contextFactory
115                ) {
116                }
117
118                public function handle(ServerRequestInterface $req): ResponseInterface
119                {
120                    $ctx = $this->contextFactory->createFromRequest($req);
121                    return $this->controller->actionIndex($req, $ctx);
122                }
123            }
124        );
125    }
126
127    private function dispatchAccessWebRoute(ServerRequestInterface $request): ResponseInterface
128    {
129        if ($this->accessWebController === null) {
130            return $this->psr17Factory->createResponse(404);
131        }
132
133        return $this->authMiddleware->process(
134            $request,
135            new class($this->accessWebController) implements RequestHandlerInterface {
136                public function __construct(private AccessWebController $controller)
137                {
138                }
139
140                public function handle(ServerRequestInterface $req): ResponseInterface
141                {
142                    return $this->controller->index($req);
143                }
144            }
145        );
146    }
147
148    private function dispatchAccessRulesDrawerRoute(ServerRequestInterface $request): ResponseInterface
149    {
150        if ($this->accessWebController === null) {
151            return $this->psr17Factory->createResponse(404);
152        }
153
154        return $this->authMiddleware->process(
155            $request,
156            new class($this->accessWebController) implements RequestHandlerInterface {
157                public function __construct(private AccessWebController $controller)
158                {
159                }
160
161                public function handle(ServerRequestInterface $req): ResponseInterface
162                {
163                    $params = $req->getQueryParams();
164                    $module = (string) ($params['module'] ?? '');
165                    return $this->controller->rulesDrawer($req, $module);
166                }
167            }
168        );
169    }
170
171    private function dispatchImpersonationPanelRoute(ServerRequestInterface $request): ResponseInterface
172    {
173        if ($this->impersonationWebController === null || $this->appProfile === 'client') {
174            return $this->psr17Factory->createResponse(404);
175        }
176
177        return $this->authMiddleware->process(
178            $request,
179            new class($this->impersonationWebController) implements RequestHandlerInterface {
180                public function __construct(private ImpersonationWebController $controller)
181                {
182                }
183
184                public function handle(ServerRequestInterface $req): ResponseInterface
185                {
186                    return $this->controller->panel($req);
187                }
188            }
189        );
190    }
191
192    private function dispatchImpersonationSearchUsersRoute(ServerRequestInterface $request): ResponseInterface
193    {
194        if ($this->impersonationWebController === null || $this->appProfile === 'client') {
195            return $this->psr17Factory->createResponse(404);
196        }
197
198        return $this->authMiddleware->process(
199            $request,
200            new class($this->impersonationWebController) implements RequestHandlerInterface {
201                public function __construct(private ImpersonationWebController $controller)
202                {
203                }
204
205                public function handle(ServerRequestInterface $req): ResponseInterface
206                {
207                    return $this->controller->searchUsers($req);
208                }
209            }
210        );
211    }
212
213    private function dispatchImpersonationSaveRoute(ServerRequestInterface $request): ResponseInterface
214    {
215        if ($this->impersonationWebController === null || $this->appProfile === 'client') {
216            return $this->psr17Factory->createResponse(404);
217        }
218
219        $ctrl = $this->impersonationWebController;
220        $inner = new class($ctrl) implements RequestHandlerInterface {
221            public function __construct(private ImpersonationWebController $c)
222            {
223            }
224
225            public function handle(ServerRequestInterface $req): ResponseInterface
226            {
227                return $this->c->saveGrants($req);
228            }
229        };
230
231        return $this->csrfMiddleware->process(
232            $request,
233            new class($this->authMiddleware, $inner) implements RequestHandlerInterface {
234                public function __construct(
235                    private AuthenticatedOnlyMiddleware $auth,
236                    private RequestHandlerInterface $next
237                ) {
238                }
239
240                public function handle(ServerRequestInterface $req): ResponseInterface
241                {
242                    return $this->auth->process($req, $this->next);
243                }
244            }
245        );
246    }
247
248    private function dispatchImpersonateUserRoute(int $id, ServerRequestInterface $request): ResponseInterface
249    {
250        if ($this->impersonationWebController === null) {
251            return $this->psr17Factory->createResponse(404);
252        }
253
254        $ctrl = $this->impersonationWebController;
255        $inner = new class($ctrl, $id) implements RequestHandlerInterface {
256            public function __construct(private ImpersonationWebController $c, private int $id)
257            {
258            }
259
260            public function handle(ServerRequestInterface $req): ResponseInterface
261            {
262                return $this->c->impersonate($req, $this->id);
263            }
264        };
265
266        return $this->csrfMiddleware->process(
267            $request,
268            new class($this->authMiddleware, $inner) implements RequestHandlerInterface {
269                public function __construct(
270                    private AuthenticatedOnlyMiddleware $auth,
271                    private RequestHandlerInterface $next
272                ) {
273                }
274
275                public function handle(ServerRequestInterface $req): ResponseInterface
276                {
277                    return $this->auth->process($req, $this->next);
278                }
279            }
280        );
281    }
282
283    private function dispatchImpersonateRevertRoute(ServerRequestInterface $request): ResponseInterface
284    {
285        if ($this->impersonationWebController === null) {
286            return $this->psr17Factory->createResponse(404);
287        }
288
289        $ctrl = $this->impersonationWebController;
290        $inner = new class($ctrl) implements RequestHandlerInterface {
291            public function __construct(private ImpersonationWebController $c)
292            {
293            }
294
295            public function handle(ServerRequestInterface $req): ResponseInterface
296            {
297                return $this->c->revert($req);
298            }
299        };
300
301        return $this->csrfMiddleware->process(
302            $request,
303            new class($this->authMiddleware, $inner) implements RequestHandlerInterface {
304                public function __construct(
305                    private AuthenticatedOnlyMiddleware $auth,
306                    private RequestHandlerInterface $next
307                ) {
308                }
309
310                public function handle(ServerRequestInterface $req): ResponseInterface
311                {
312                    return $this->auth->process($req, $this->next);
313                }
314            }
315        );
316    }
317
318    private function dispatchLogout(): ResponseInterface
319    {
320        $this->session->destroy();
321        unset($_SESSION['user'], $_SESSION['user_id']);
322        return $this->authController->logout();
323    }
324
325    private function dispatchLoginRoute(ServerRequestInterface $request): ResponseInterface
326    {
327        if ($request->getMethod() !== 'POST') {
328            return $this->guestMiddleware->process(
329                $request,
330                new class($this->authController) implements RequestHandlerInterface {
331                    public function __construct(private AuthController $controller)
332                    {
333                    }
334
335                    public function handle(ServerRequestInterface $req): ResponseInterface
336                    {
337                        return $this->controller->loginForm();
338                    }
339                }
340            );
341        }
342
343        return $this->csrfMiddleware->process(
344            $request,
345            new class($this->authController, $this->userRepository, $this->session, $this->currentUser)
346                implements RequestHandlerInterface {
347                public function __construct(
348                    private AuthController $controller,
349                    private SqlUserRepository $userRepository,
350                    private SessionInterface $session,
351                    private CurrentUser $currentUser
352                ) {
353                }
354
355                public function handle(ServerRequestInterface $req): ResponseInterface
356                {
357                    $response = $this->controller->loginSubmit($req);
358                    if ($response->getStatusCode() === 302) {
359                        $this->syncUserSession();
360                    }
361                    return $response;
362                }
363
364                private function syncUserSession(): void
365                {
366                    $userId = (int) ($this->currentUser->getId() ?? ($_SESSION['user_id'] ?? 0));
367                    $userData = $userId > 0 ? $this->userRepository->findById($userId) : null;
368                    if ($userData !== null) {
369                        $userPayload = [
370                            'id' => $userData->getId(),
371                            'username' => $userData->getUsername(),
372                            'email' => $userData->getEmail(),
373                            'is_superuser' => $userData->isSuperuser(),
374                            'is_active' => $userData->isActive(),
375                        ];
376                        $this->session->set('user', $userPayload);
377                        $this->session->set('user_id', $userData->getId());
378                        $_SESSION['user'] = $userPayload;
379                        $_SESSION['user_id'] = $userData->getId();
380                        $this->currentUser->login(new Identity((int) $userData->getId(), $userData->getLocale()));
381                    }
382                }
383            }
384        );
385    }
386
387    private function dispatchForgotPasswordRoute(ServerRequestInterface $request): ResponseInterface
388    {
389        if ($request->getMethod() !== 'POST') {
390            return $this->authController->forgotPasswordForm();
391        }
392
393        return $this->csrfMiddleware->process(
394            $request,
395            new class($this->authController) implements RequestHandlerInterface {
396                public function __construct(private AuthController $controller)
397                {
398                }
399
400                public function handle(ServerRequestInterface $req): ResponseInterface
401                {
402                    return $this->controller->forgotPasswordSubmit($req);
403                }
404            }
405        );
406    }
407
408    private function dispatchResetPasswordRoute(ServerRequestInterface $request): ResponseInterface
409    {
410        if ($request->getMethod() !== 'POST') {
411            return $this->authController->resetPasswordForm($request);
412        }
413
414        return $this->csrfMiddleware->process(
415            $request,
416            new class($this->authController) implements RequestHandlerInterface {
417                public function __construct(private AuthController $controller)
418                {
419                }
420
421                public function handle(ServerRequestInterface $req): ResponseInterface
422                {
423                    return $this->controller->resetPasswordSubmit($req);
424                }
425            }
426        );
427    }
428
429    private function dispatchMfaVerifyRoute(ServerRequestInterface $request): ResponseInterface
430    {
431        if ($request->getMethod() !== 'POST') {
432            return $this->authController->mfaVerifyForm($request);
433        }
434
435        return $this->csrfMiddleware->process(
436            $request,
437            new class($this->authController) implements RequestHandlerInterface {
438                public function __construct(private AuthController $controller)
439                {
440                }
441
442                public function handle(ServerRequestInterface $req): ResponseInterface
443                {
444                    return $this->controller->mfaVerifySubmit($req);
445                }
446            }
447        );
448    }
449
450    private function dispatchDavRoutes(string $path): ?ResponseInterface
451    {
452        if (preg_match('#^/dav/setup/([\w-]+)$#', $path, $m)) {
453            return $this->davSetupWebController !== null
454                ? $this->davSetupWebController->setupPage($m[1])
455                : $this->psr17Factory->createResponse(404);
456        }
457
458        if (preg_match('#^/dav/profile/([\w-]+)\.mobileconfig$#', $path, $m)) {
459            return $this->davSetupWebController !== null
460                ? $this->davSetupWebController->downloadAppleProfile($m[1])
461                : $this->psr17Factory->createResponse(404);
462        }
463
464        return null;
465    }
466}