Completed
Push — master ( 4b1474...809083 )
by Stefano
12s
created

I18nMiddleware::detectLocale()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 1
dl 0
loc 15
rs 9.9666
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\Utility\Hash;
20
use Psr\Http\Message\ResponseInterface;
21
use Zend\Diactoros\Response\RedirectResponse;
22
23
/**
24
 * i18n middleware.
25
 *
26
 * It is responsible to setup the right locale based on URI path as `/:lang/page/to/reach`.
27
 * It is configurable to redirect URI matches some rules using `:lang` prefix.
28
 */
29
class I18nMiddleware
30
{
31
    use InstanceConfigTrait;
32
33
    /**
34
     * Define when I18n rules are applied with `/:lang` prefix:
35
     *  - 'match': array of URL paths, if there's an exact match rule is applied
36
     *  - 'startWith': array of URL paths, if current URL path starts with one of these rule is applied
37
     *  - 'cookie': array for cookie that keeps the locale value. By default no cookie is used.
38
     *      - 'name': cookie name
39
     *      - 'create': set to `true` if the middleware is responsible of cookie creation
40
     *      - 'expire': used when `create` is `true` to define when the cookie must expire
41
     *
42
     * @var array
43
     */
44
    protected $_defaultConfig = [
45
        'match' => [],
46
        'startWith' => [],
47
        'cookie' => [
48
            'name' => null,
49
            'create' => false,
50
            'expire' => '+1 year',
51
        ],
52
    ];
53
54
    /**
55
     * Middleware constructor.
56
     *
57
     * @param array $config Configuration.
58
     */
59
    public function __construct(array $config = [])
60
    {
61
        $this->setConfig($config);
62
    }
63
64
    /**
65
     * Setup used language code and locale from URL prefix `/:lang`
66
     *
67
     * Add `/:lang` (language code) prefix to a URL if a match is found
68
     * using `match` and `startWith` configurations.
69
     *
70
     * At the moment only primary languages are correctly handled as language codes to be used as URL prefix.
71
     *
72
     * @param \Cake\Http\ServerRequest $request The request.
73
     * @param \Psr\Http\Message\ResponseInterface $response The response.
74
     * @param callable $next Callback to invoke the next middleware.
75
     *
76
     * @return \Psr\Http\Message\ResponseInterface A response
77
     */
78
    public function __invoke(ServerRequest $request, ResponseInterface $response, $next) : ResponseInterface
79
    {
80
        $path = $request->getUri()->getPath();
81
82
        $redir = false;
83
        foreach ($this->getConfig('startWith') as $needle) {
84
            if (stripos($path, $needle) === 0) {
85
                $redir = true;
86
            }
87
        }
88
89
        $locale = $this->detectLocale($request);
90
91
        if (!$redir && !in_array($path, $this->getConfig('match'))) {
92
            $this->setupLocale($locale);
93
            $response = $this->getResponseWithCookie($request, $response, I18n::getLocale());
94
95
            return $next($request, $response);
96
        }
97
98
        $lang = Configure::read('I18n.default');
99
        if ($locale) {
100
            $localeLang = Configure::read(sprintf('I18n.locales.%s', $locale));
101
            if ($localeLang) {
102
                $lang = $localeLang;
103
            } else {
104
                // try with primary language
105
                $primary = \Locale::getPrimaryLanguage($locale);
106
                if (Configure::read(sprintf('I18n.languages.%s', $primary))) {
107
                    $lang = $primary;
108
                }
109
            }
110
        }
111
        $statusCode = 301;
112
113
        $uri = $request->getUri()->withPath(sprintf('%s%s', $lang, $path));
114
115
        return new RedirectResponse($uri, $statusCode);
116
    }
117
118
    /**
119
     * Detect locale following the rules:
120
     *
121
     * 1. first try to detect from url path
122
     * 2. then try to detect from cookie
123
     * 3. finally try to detect it from HTTP Accept-Language header
124
     *
125
     * @param ServerRequest $request The request.
126
     * @return string
127
     */
128
    protected function detectLocale(ServerRequest $request) : string
129
    {
130
        $path = $request->getUri()->getPath();
131
        $urlLang = (string)Hash::get(explode('/', $path), '1');
132
        $locale = array_search($urlLang, (array)Configure::read('I18n.locales'));
133
        if ($locale !== false) {
134
            return $locale;
135
        }
136
137
        $locale = $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

137
        $locale = $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

137
        $locale = $request->getCookie(/** @scrutinizer ignore-type */ $this->config('cookie.name'));
Loading history...
138
        if ($locale !== null) {
139
            return $locale;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $locale could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
140
        }
141
142
        return \Locale::acceptFromHttp($request->getHeaderLine('Accept-Language'));
143
    }
144
145
    /**
146
     * Setup locale and language code from passed `$locale`.
147
     * If `$locale` is not found in configuraion then use the default.
148
     *
149
     * @param string $locale Detected HTTP locale.
150
     * @return void
151
     */
152
    protected function setupLocale(?string $locale) : void
153
    {
154
        $i18nConf = Configure::read('I18n', []);
155
        $lang = Hash::get($i18nConf, sprintf('locales.%s', (string)$locale));
156
        if ($lang === null) {
157
            $lang = Hash::get($i18nConf, 'default');
158
            $locale = array_search($lang, (array)Hash::get($i18nConf, 'locales', []));
159
        }
160
161
        Configure::write('I18n.lang', $lang);
162
        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

162
        I18n::setLocale(/** @scrutinizer ignore-type */ $locale);
Loading history...
163
    }
164
165
    /**
166
     * Return a response object with the locale cookie set or updated.
167
     *
168
     * The cookie is added only if the middleware is configured to create cookie.
169
     *
170
     * @param ServerRequest $request The request.
171
     * @param ResponseInterface $response The response.
172
     * @param string $locale The locale string to set in cookie.
173
     * @return \Psr\Http\Message\ResponseInterface
174
     */
175
    protected function getResponseWithCookie(ServerRequest $request, ResponseInterface $response, string $locale) : ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

175
    protected function getResponseWithCookie(/** @scrutinizer ignore-unused */ ServerRequest $request, ResponseInterface $response, string $locale) : ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
176
    {
177
        $name = $this->getConfig('cookie.name');
178
        $create = $this->getConfig('cookie.create', false);
179
        if ($create !== true || empty($name)) {
180
            return $response;
181
        }
182
183
        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

183
        return $response->/** @scrutinizer ignore-call */ withCookie($name, [
Loading history...
184
            'value' => $locale,
185
            'expire' => strtotime($this->getConfig('cookie.expire', '+1 year')),
186
        ]);
187
    }
188
}
189