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')); |
|
|
|
|
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); |
|
|
|
|
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, [ |
|
|
|
|
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); |
|
|
|
|
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
|
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.