Passed
Pull Request — master (#27)
by Alberto
18:23
created

I18nMiddleware::process()   B

Complexity

Conditions 11
Paths 32

Size

Total Lines 44
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 25
c 0
b 0
f 0
dl 0
loc 44
rs 7.3166
cc 11
nc 32
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2018 ChannelWeb Srl, Chialab Srl
7
 *
8
 * This file is part of BEdita: you can redistribute it and/or modify
9
 * it under the terms of the GNU Lesser General Public License as published
10
 * by the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14
 */
15
namespace BEdita\I18n\Middleware;
16
17
use BEdita\I18n\Core\I18nTrait;
18
use Cake\Core\Configure;
19
use Cake\Core\InstanceConfigTrait;
20
use Cake\Http\Cookie\Cookie;
21
use Cake\Http\Exception\BadRequestException;
22
use Cake\Http\Response;
23
use Cake\Http\ServerRequest;
24
use Cake\I18n\I18n;
25
use Cake\I18n\Time;
26
use Cake\Utility\Hash;
27
use Laminas\Diactoros\Response\RedirectResponse;
28
use Psr\Http\Message\ResponseInterface;
29
use Psr\Http\Message\ServerRequestInterface;
30
use Psr\Http\Server\MiddlewareInterface;
31
use Psr\Http\Server\RequestHandlerInterface;
32
33
/**
34
 * i18n middleware.
35
 *
36
 * It is responsible to setup the right locale based on URI path as `/:lang/page/to/reach`.
37
 * It is configurable to redirect URI matches some rules using `:lang` prefix.
38
 */
39
class I18nMiddleware implements MiddlewareInterface
40
{
41
    use I18nTrait;
42
    use InstanceConfigTrait;
43
44
    /**
45
     * Define when I18n rules are applied with `/:lang` prefix:
46
     *  - 'match': array of URL paths, if there's an exact match rule is applied
47
     *  - 'startWith': array of URL paths, if current URL path starts with one of these rule is applied
48
     *  - 'switchLangUrl': reserved URL (for example `/lang`) used to switch language and redirect to referer URL.
49
     *                     Disabled by default.
50
     *  - 'cookie': array for cookie that keeps the locale value. By default no cookie is used.
51
     *      - 'name': cookie name
52
     *      - 'create': set to `true` if the middleware is responsible of cookie creation
53
     *      - 'expire': used when `create` is `true` to define when the cookie must expire
54
     *
55
     * @var array
56
     */
57
    protected $_defaultConfig = [
58
        'match' => [],
59
        'startWith' => [],
60
        'switchLangUrl' => null,
61
        'cookie' => [
62
            'name' => null,
63
            'create' => false,
64
            'expire' => '+1 year',
65
        ],
66
    ];
67
68
    /**
69
     * Middleware constructor.
70
     *
71
     * @param array $config Configuration.
72
     */
73
    public function __construct(array $config = [])
74
    {
75
        $this->setConfig($config);
76
    }
77
78
    /**
79
     * Setup used language code and locale from URL prefix `/:lang`
80
     *
81
     * Add `/:lang` (language code) prefix to a URL if a match is found
82
     * using `match` and `startWith` configurations.
83
     *
84
     * At the moment only primary languages are correctly handled as language codes to be used as URL prefix.
85
     *
86
     * @param \Psr\Http\Message\ServerRequestInterface $request The request.
87
     * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
88
     * @return \Psr\Http\Message\ResponseInterface A response.
89
     */
90
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
91
    {
92
        $path = $request->getUri()->getPath();
93
        if ($path !== '/') {
94
            $path = rtrim($path, '/'); // remove trailing slashes
95
        }
96
97
        if ($path === (string)$this->getConfig('switchLangUrl') && $this->getConfig('cookie.name')) {
98
            return $this->changeLangAndRedirect($request);
99
        }
100
101
        $redir = false;
102
        foreach ($this->getConfig('startWith') as $needle) {
103
            if (stripos($path, $needle) === 0) {
104
                $redir = true;
105
            }
106
        }
107
108
        $locale = $this->detectLocale($request);
109
110
        if (!$redir && !in_array($path, $this->getConfig('match'))) {
111
            $this->setupLocale($locale);
112
            $response = $handler->handle($request);
113
114
            return $this->getResponseWithCookie($response, $this->getLocale());
115
        }
116
117
        $lang = $this->getDefaultLang();
118
        if ($locale) {
119
            $localeLang = Hash::get($this->getLocales(), $locale);
120
            if ($localeLang) {
121
                $lang = $localeLang;
122
            } else {
123
                // try with primary language
124
                $primary = \Locale::getPrimaryLanguage($locale);
125
                if (Hash::get($this->getLanguages(), $primary)) {
126
                    $lang = $primary;
127
                }
128
            }
129
        }
130
131
        $uri = $request->getUri()->withPath(sprintf('%s%s', $lang, rtrim($path, '/')));
132
133
        return new RedirectResponse($uri);
134
    }
135
136
    /**
137
     * Detect locale following the rules:
138
     *
139
     * 1. first try to detect from url path
140
     * 2. then try to detect from cookie
141
     * 3. finally try to detect it from HTTP Accept-Language header
142
     *
143
     * @param \Cake\Http\ServerRequest $request The request.
144
     * @return string
145
     */
146
    protected function detectLocale(ServerRequest $request): string
147
    {
148
        $path = $request->getUri()->getPath();
149
        $urlLang = (string)Hash::get(explode('/', $path), '1');
150
        $locale = array_search($urlLang, $this->getLocales());
151
        if ($locale !== false) {
152
            return (string)$locale;
153
        }
154
155
        $locale = (string)$request->getCookie($this->getConfig('cookie.name', ''));
156
        if (!empty($locale)) {
157
            return $locale;
158
        }
159
160
        return (string)\Locale::acceptFromHttp($request->getHeaderLine('Accept-Language'));
161
    }
162
163
    /**
164
     * Setup locale and language code from passed `$locale`.
165
     * If `$locale` is not found in configuraion then use the default.
166
     *
167
     * @param string $locale Detected HTTP locale.
168
     * @return void
169
     */
170
    protected function setupLocale(?string $locale): void
171
    {
172
        $locales = $this->getLocales();
173
        $lang = Hash::get($locales, (string)$locale);
174
        if ($lang === null) {
175
            $lang = $this->getDefaultLang();
176
            $locale = array_search($lang, $locales);
177
        }
178
179
        Configure::write('I18n.lang', $lang);
180
        I18n::setLocale($locale);
181
    }
182
183
    /**
184
     * Return a response object with the locale cookie set or updated.
185
     *
186
     * The cookie is added only if the middleware is configured to create cookie.
187
     *
188
     * @param \Psr\Http\Message\ResponseInterface $response The response.
189
     * @param string $locale The locale string to set in cookie.
190
     * @return \Psr\Http\Message\ResponseInterface
191
     */
192
    protected function getResponseWithCookie(ResponseInterface $response, string $locale): ResponseInterface
193
    {
194
        $name = $this->getConfig('cookie.name');
195
        $create = $this->getConfig('cookie.create', false);
196
        if ($create !== true || empty($name)) {
197
            return $response;
198
        }
199
200
        $expire = Time::createFromTimestamp(strtotime($this->getConfig('cookie.expire', '+1 year')));
201
202
        return $response->withCookie(new Cookie($name, $locale, $expire));
203
    }
204
205
    /**
206
     * Change lang and redirect to referer.
207
     *
208
     * Require query string `new` and `redirect`
209
     *
210
     * @param \Cake\Http\ServerRequest $request The request
211
     * @return \Psr\Http\Message\ResponseInterface
212
     * @throws \Cake\Http\Exception\BadRequestException When missing required query string or unsupported language
213
     */
214
    protected function changeLangAndRedirect(ServerRequest $request): ResponseInterface
215
    {
216
        $new = (string)$request->getQuery('new');
217
        if (empty($new)) {
218
            throw new BadRequestException(__('Missing required "new" query string'));
219
        }
220
221
        $locale = array_search($new, $this->getLocales());
222
        if ($locale === false) {
223
            throw new BadRequestException(__('Lang "{0}" not supported', [$new]));
224
        }
225
226
        $response = (new Response())
227
            ->withLocation((string)$request->referer())
228
            ->withDisabledCache()
229
            ->withStatus(302);
230
231
        return $this->getResponseWithCookie($response, $locale);
232
    }
233
}
234