Completed
Push — master ( 0fab66...c72de8 )
by Alberto
13s
created

I18nMiddleware::changeLangAndRedirect()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 2
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2018 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
namespace BEdita\I18n\Middleware;
14
15
use Cake\Core\Configure;
16
use Cake\Core\InstanceConfigTrait;
17
use Cake\Http\ServerRequest;
18
use Cake\I18n\I18n;
19
use Cake\Network\Exception\BadRequestException;
20
use Cake\Utility\Hash;
21
use Psr\Http\Message\ResponseInterface;
22
use Zend\Diactoros\Response\RedirectResponse;
23
24
/**
25
 * i18n middleware.
26
 *
27
 * It is responsible to setup the right locale based on URI path as `/:lang/page/to/reach`.
28
 * It is configurable to redirect URI matches some rules using `:lang` prefix.
29
 */
30
class I18nMiddleware
31
{
32
    use InstanceConfigTrait;
33
34
    /**
35
     * Define when I18n rules are applied with `/:lang` prefix:
36
     *  - 'match': array of URL paths, if there's an exact match rule is applied
37
     *  - 'startWith': array of URL paths, if current URL path starts with one of these rule is applied
38
     *  - 'switchLangUrl': reserved URL (for example `/lang`) used to switch language and redirect to referer URL.
39
     *                     Disabled by default.
40
     *  - 'cookie': array for cookie that keeps the locale value. By default no cookie is used.
41
     *      - 'name': cookie name
42
     *      - 'create': set to `true` if the middleware is responsible of cookie creation
43
     *      - 'expire': used when `create` is `true` to define when the cookie must expire
44
     *
45
     * @var array
46
     */
47
    protected $_defaultConfig = [
48
        'match' => [],
49
        'startWith' => [],
50
        'switchLangUrl' => null,
51
        'cookie' => [
52
            'name' => null,
53
            'create' => false,
54
            'expire' => '+1 year',
55
        ],
56
    ];
57
58
    /**
59
     * Middleware constructor.
60
     *
61
     * @param array $config Configuration.
62
     */
63
    public function __construct(array $config = [])
64
    {
65
        $this->setConfig($config);
66
    }
67
68
    /**
69
     * Setup used language code and locale from URL prefix `/:lang`
70
     *
71
     * Add `/:lang` (language code) prefix to a URL if a match is found
72
     * using `match` and `startWith` configurations.
73
     *
74
     * At the moment only primary languages are correctly handled as language codes to be used as URL prefix.
75
     *
76
     * @param \Cake\Http\ServerRequest $request The request.
77
     * @param \Psr\Http\Message\ResponseInterface $response The response.
78
     * @param callable $next Callback to invoke the next middleware.
79
     *
80
     * @return \Psr\Http\Message\ResponseInterface A response
81
     */
82
    public function __invoke(ServerRequest $request, ResponseInterface $response, $next) : ResponseInterface
83
    {
84
        $path = $request->getUri()->getPath();
85
86
        if ($path === (string)$this->getConfig('switchLangUrl') && $this->getConfig('cookie.name')) {
87
            return $this->changeLangAndRedirect($request, $response);
88
        }
89
90
        $redir = false;
91
        foreach ($this->getConfig('startWith') as $needle) {
92
            if (stripos($path, $needle) === 0) {
93
                $redir = true;
94
            }
95
        }
96
97
        $locale = $this->detectLocale($request);
98
99
        if (!$redir && !in_array($path, $this->getConfig('match'))) {
100
            $this->setupLocale($locale);
101
            $response = $this->getResponseWithCookie($response, I18n::getLocale());
102
103
            return $next($request, $response);
104
        }
105
106
        $lang = Configure::read('I18n.default');
107
        if ($locale) {
108
            $localeLang = Configure::read(sprintf('I18n.locales.%s', $locale));
109
            if ($localeLang) {
110
                $lang = $localeLang;
111
            } else {
112
                // try with primary language
113
                $primary = \Locale::getPrimaryLanguage($locale);
114
                if (Configure::read(sprintf('I18n.languages.%s', $primary))) {
115
                    $lang = $primary;
116
                }
117
            }
118
        }
119
        $statusCode = 301;
120
121
        $uri = $request->getUri()->withPath(sprintf('%s%s', $lang, $path));
122
123
        return new RedirectResponse($uri, $statusCode);
124
    }
125
126
    /**
127
     * Detect locale following the rules:
128
     *
129
     * 1. first try to detect from url path
130
     * 2. then try to detect from cookie
131
     * 3. finally try to detect it from HTTP Accept-Language header
132
     *
133
     * @param ServerRequest $request The request.
134
     * @return string
135
     */
136
    protected function detectLocale(ServerRequest $request) : string
137
    {
138
        $path = $request->getUri()->getPath();
139
        $urlLang = (string)Hash::get(explode('/', $path), '1');
140
        $locale = array_search($urlLang, (array)Configure::read('I18n.locales'));
141
        if ($locale !== false) {
142
            return $locale;
143
        }
144
145
        $locale = (string)$request->getCookie($this->config('cookie.name'));
0 ignored issues
show
Deprecated Code introduced by
The function BEdita\I18n\Middleware\I18nMiddleware::config() has been deprecated: 3.4.0 use setConfig()/getConfig() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

145
        $locale = (string)$request->getCookie(/** @scrutinizer ignore-deprecated */ $this->config('cookie.name'));

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
Bug introduced by
It seems like $this->config('cookie.name') can also be of type BEdita\I18n\Middleware\I18nMiddleware; however, parameter $key of Cake\Http\ServerRequest::getCookie() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

145
        $locale = (string)$request->getCookie(/** @scrutinizer ignore-type */ $this->config('cookie.name'));
Loading history...
146
        if (!empty($locale)) {
147
            return $locale;
148
        }
149
150
        return \Locale::acceptFromHttp($request->getHeaderLine('Accept-Language'));
151
    }
152
153
    /**
154
     * Setup locale and language code from passed `$locale`.
155
     * If `$locale` is not found in configuraion then use the default.
156
     *
157
     * @param string $locale Detected HTTP locale.
158
     * @return void
159
     */
160
    protected function setupLocale(?string $locale) : void
161
    {
162
        $i18nConf = Configure::read('I18n', []);
163
        $lang = Hash::get($i18nConf, sprintf('locales.%s', (string)$locale));
164
        if ($lang === null) {
165
            $lang = Hash::get($i18nConf, 'default');
166
            $locale = array_search($lang, (array)Hash::get($i18nConf, 'locales', []));
167
        }
168
169
        Configure::write('I18n.lang', $lang);
170
        I18n::setLocale($locale);
0 ignored issues
show
Bug introduced by
It seems like $locale can also be of type false; however, parameter $locale of Cake\I18n\I18n::setLocale() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

170
        I18n::setLocale(/** @scrutinizer ignore-type */ $locale);
Loading history...
171
    }
172
173
    /**
174
     * Return a response object with the locale cookie set or updated.
175
     *
176
     * The cookie is added only if the middleware is configured to create cookie.
177
     *
178
     * @param ResponseInterface $response The response.
179
     * @param string $locale The locale string to set in cookie.
180
     * @return \Psr\Http\Message\ResponseInterface
181
     */
182
    protected function getResponseWithCookie(ResponseInterface $response, string $locale) : ResponseInterface
183
    {
184
        $name = $this->getConfig('cookie.name');
185
        $create = $this->getConfig('cookie.create', false);
186
        if ($create !== true || empty($name)) {
187
            return $response;
188
        }
189
190
        return $response->withCookie($name, [
0 ignored issues
show
Bug introduced by
The method withCookie() does not exist on Psr\Http\Message\ResponseInterface. It seems like you code against a sub-type of Psr\Http\Message\ResponseInterface such as Cake\Http\Response. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

190
        return $response->/** @scrutinizer ignore-call */ withCookie($name, [
Loading history...
191
            'value' => $locale,
192
            'expire' => strtotime($this->getConfig('cookie.expire', '+1 year')),
193
        ]);
194
    }
195
196
    /**
197
     * Change lang and redirect to referer.
198
     *
199
     * Require query string `new` and `redirect`
200
     *
201
     * @param ServerRequest $request The request
202
     * @param \Psr\Http\Message\ResponseInterface $response The response.
203
     * @return ResponseInterface
204
     * @throws BadRequestException When missing required query string or unsupported language
205
     */
206
    protected function changeLangAndRedirect(ServerRequest $request, ResponseInterface $response) : ResponseInterface
207
    {
208
        $new = (string)$request->getQuery('new');
209
        if (empty($new)) {
210
            throw new BadRequestException(__('Missing required "new" query string'));
211
        }
212
213
        $locale = array_search($new, (array)Configure::read('I18n.locales'));
214
        if ($locale === false) {
215
            throw new BadRequestException(__('Lang "{0}" not supported', [$new]));
216
        }
217
        $response = $this->getResponseWithCookie($response, $locale);
218
219
        return $response->withLocation($request->referer())->withDisabledCache()->withStatus(302);
0 ignored issues
show
Bug introduced by
The method withLocation() does not exist on Psr\Http\Message\ResponseInterface. It seems like you code against a sub-type of Psr\Http\Message\ResponseInterface such as Cake\Http\Response. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

219
        return $response->/** @scrutinizer ignore-call */ withLocation($request->referer())->withDisabledCache()->withStatus(302);
Loading history...
220
    }
221
}
222