Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.89% covered (success)
91.89%
34 / 37
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Preferences
91.89% covered (success)
91.89%
34 / 37
85.71% covered (warning)
85.71%
6 / 7
20.21
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
 locale
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 guestLocale
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 available
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 refererPath
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 localeCookie
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 redirect
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
2.21
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Controller\Panel;
6
7use Celema\Container\Container;
8use Celema\Core\Factory\Factory;
9use Celema\Core\Request;
10use Celema\Core\Response;
11use Cosray\Auth as CmsAuth;
12use Cosray\Config;
13use Cosray\Locales;
14use Cosray\Middleware\PanelLocale;
15use Cosray\Users;
16
17final class Preferences extends Panel
18{
19    private const int YEAR = 60 * 60 * 24 * 365;
20
21    public function __construct(
22        Config $config,
23        Container $container,
24        Request $request,
25        private readonly CmsAuth $auth,
26        private readonly Users $users,
27    ) {
28        parent::__construct($config, $container, $request);
29    }
30
31    /**
32     * Persists the user's panel UI language. A value outside the selectable
33     * set resets the preference to NULL (inherit the negotiated default).
34     */
35    public function locale(Factory $factory): Response
36    {
37        $user = $this->auth->user();
38
39        if ($user !== null) {
40            $locale = $this->formData()['locale'] ?? null;
41
42            $this->users->savePanelLocale(
43                $user->id,
44                is_string($locale) && in_array($locale, $this->available(), true) ? $locale : null,
45            );
46        }
47
48        return $this->redirect($factory, $this->refererPath());
49    }
50
51    /**
52     * Keeps the language chosen on the sign-in pages in a cookie, as there
53     * is no user to store it with yet. Any other value forgets the choice.
54     */
55    public function guestLocale(Factory $factory): Response
56    {
57        $locale = $this->formData()['locale'] ?? null;
58        $locale = is_string($locale) && in_array($locale, $this->available(), true) ? $locale : null;
59
60        return $this->redirect($factory, $this->refererPath())->header('Set-Cookie', $this->localeCookie($locale));
61    }
62
63    /** @return list<string> */
64    private function available(): array
65    {
66        $locales = $this->request->get('locales', null);
67
68        return $locales instanceof Locales ? $locales->panelLocales() : [];
69    }
70
71    /**
72     * The panel page the switcher was used on, so the reload lands where
73     * the user was. Anything outside the panel falls back to the index.
74     */
75    private function refererPath(): string
76    {
77        $referer = $this->request->header('Referer');
78        $path = parse_url($referer, PHP_URL_PATH);
79        $query = parse_url($referer, PHP_URL_QUERY);
80
81        if (is_string($path) && str_starts_with($path, $this->panelPath())) {
82            return is_string($query) && $query !== '' ? $path . '?' . $query : $path;
83        }
84
85        return $this->panelPath();
86    }
87
88    private function localeCookie(?string $locale): string
89    {
90        $options = $this->config->session->options;
91
92        return implode('; ', array_filter([
93            PanelLocale::cookie($this->config) . '=' . ($locale ?? ''),
94            'Path=' . (string) ($options['cookie_path'] ?? '/'),
95            'Max-Age=' . ($locale === null ? 0 : self::YEAR),
96            'SameSite=Lax',
97            'HttpOnly',
98            $options['cookie_secure'] ?? true ? 'Secure' : '',
99        ]));
100    }
101
102    private function redirect(Factory $factory, string $target): Response
103    {
104        $response = Response::create($factory);
105
106        if ($this->request->hasHeader('HX-Request')) {
107            return $response
108                ->status(200)
109                ->header('HX-Redirect', $target);
110        }
111
112        return $response
113            ->status(303)
114            ->header('Location', $target);
115    }
116}