Completed
Push — master ( 86f7cc...7c1ca4 )
by Basil
07:22
created

Composition::getLangShortCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace luya\web;
4
5
use Yii;
6
use yii\base\Component;
7
use yii\web\ForbiddenHttpException;
8
use luya\Exception;
9
use luya\helpers\StringHelper;
10
11
/**
12
 * Composition parseRequest Handler.
13
 * 
14
 * The composition is run for every {{luya\web\UrlManager::parseRequest()} call in order to determine
15
 * the language of the application based on {{luya\web\Composition::$hostInfoMapping}} or from the given
16
 * {{luya\web\Request::$hostInfo}}.
17
 * 
18
 * It also provides common functions in order to make complex regional language detection for urls
19
 * like `https://example.com/fr/ch`, its possible to set {{luya\web\Composition::$pattern}} and retrieve
20
 * the value later inside your application. The `langShortCode` must be provided as long as working with the 
21
 * cms, as its bound to the administration are language database table.
22
 * 
23
 * It also provides security checks like
24
 * 
25
 * + {{luya\web\Composition::$allowedHosts}}
26
 * 
27
 * The Composition component is registered by the {{luya\base\Boot}} object and can therefore always access
28
 * trough `Yii::$app->composition` as "singleton" instance.
29
 * 
30
 * @property string $prefixPath Return the current composition prefix path for the request based on request input and hidden option.
31
 * @property array $keys Return an array with key and value of all resolve composition values for the current request.
32
 * @property string $defaultLangShortCode Return default defined language shord code
33
 * @property string $langShortCode Return wrapper of getKey('langShortCode')
34
 *
35
 * @author Basil Suter <[email protected]>
36
 * @since 1.0.0
37
 */
38
class Composition extends Component implements \ArrayAccess
39
{
40
    /**
41
     * @var string The Regular-Expression matching the var finder inside the url parts
42
     */
43
    const VAR_MATCH_REGEX = '/<(\w+):?([^>]+)?>/';
44
45
    /**
46
     * @var \yii\web\Request Request-Object container from DI
47
     */
48
    public $request;
49
50
    /**
51
     * @var boolean Enable or disable composition prefix out when using `prefixPath`. If disabled the response of prefixPath would be empty, otherwhise it
52
     * returns the full prefix composition pattern based url.
53
     */
54
    public $hidden = true;
55
56
    /**
57
     * @var string Url matching prefix, which is used for all the modules (e.g. an e-store requireds a language
58
     * as the cms needs this informations too). After proccessing this informations, they will be removed
59
     * from the url for further proccessing.
60
     *
61
     * Examples of how to use patterns:
62
     *
63
     * ```php
64
     * 'pattern' => '<langShortCode:[a-z]{2}>/<countryShortCode:[a-z]{2}>', // de/ch; fr/ch
65
     * ```
66
     */
67
    public $pattern = '<langShortCode:[a-z]{2}>';
68
69
    /**
70
     * @var array Default value if there is no composition provided in the url. The default value must match the url.
71
     */
72
    public $default = ['langShortCode' => 'en'];
73
74
    /**
75
     * @var array Define the default behavior for differnet host info schemas, if the host info is not found
76
     * the default behvaior via `$default` will be used.
77
     * An array where the key is the host info and value the array with the default configuration .e.g.
78
     *
79
     * ```php
80
     * 'hostInfoMapping' => [
81
     *     'http://mydomain.com' => ['langShortCode' => 'en'],
82
     *     'http://meinedomain.de' => ['langShortCode' => 'de'],
83
     * ],
84
     * ```
85
     *
86
     * The above configuration must be defined in your compostion componeont configuration in your config file.
87
     */
88
    public $hostInfoMapping = [];
89
    
90
    /**
91
     * 
92
     * @var array|string An array with all valid hosts in order to ensure the request host is equals to valid hosts.
93
     * This filter provides protection against ['host header' attacks](https://www.acunetix.com/vulnerabilities/web/host-header-attack).
94
     * 
95
     * ```php
96
     * 'allowedHosts' => [
97
     *     'example.com',
98
     *     '*.example.com',
99
     * ]
100
     * ```
101
     * 
102
     * If null is defined, the allow host filtering is disabled, default value.
103
     * @since 1.0.5
104
     */
105
    public $allowedHosts = null;
106
107
    /**
108
     * Class constructor, to get data from DiContainer.
109
     *
110
     * @param \luya\web\Request $request Request componet resolved from Depency Manager
111
     * @param array $config The object configuration array
112
     */
113
    public function __construct(Request $request, array $config = [])
114
    {
115
        $this->request = $request;
116
        parent::__construct($config);
117
    }
118
119
    /**
120
     * @inheritdoc
121
     */
122
    public function init()
123
    {
124
        parent::init();
125
126
        // check if the required key langShortCode exist in the default array.
127
        if (!array_key_exists('langShortCode', $this->default)) {
128
            throw new Exception("The composition default rule must contain a langShortCode.");
129
        }
130
131
        if ($this->allowedHosts !== null && !$this->isHostAllowed($this->allowedHosts)) {
132
            throw new ForbiddenHttpException("The current host '{$this->request->hostName}' is not in the list valid of hosts.");
133
        }
134
        
135
        if (array_key_exists($this->request->hostInfo, $this->hostInfoMapping)) {
136
            $this->default = $this->hostInfoMapping[$this->request->hostInfo];
137
        }
138
    }
139
    
140
    /**
141
     * Checks if the current request name against the allowedHosts list.
142
     *
143
     * @return boolean Whether the current hostName is allowed or not.
144
     * @since 1.0.5
145
     */
146
    public function isHostAllowed($allowedHosts)
147
    {
148
        $currentHost = $this->request->hostName;
149
        
150
        $rules = (array) $allowedHosts;
151
        
152
        foreach ($rules as $allowedHost) {
153
            if (StringHelper::matchWildcard($allowedHost, $currentHost)) {
154
                return true;
155
            }
156
        }
157
        
158
        return false;
159
    }
160
161
    private $_keys;
162
    
163
    /**
164
     * Resolves the current key and value objects based on the current pathInto and pattern from Request component.
165
     * 
166
     * @return array An array with key values like `['langShortCode' => 'en']`.
167
     * @since 1.0.5
168
     */
169
    public function getKeys()
170
    {
171
        if ($this->_keys === null) {
172
            $this->_keys = $this->getResolvedPathInfo($this->request)->resolvedValues;
0 ignored issues
show
Compatibility introduced by
$this->request of type object<yii\web\Request> is not a sub-type of object<luya\web\Request>. It seems like you assume a child class of the class yii\web\Request to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
173
        }
174
        
175
        return $this->_keys;
176
    }
177
    
178
    /**
179
     * Resolve the current url request and retun an array contain resolved route and the resolved values.
180
     * 
181
     * @param Request $request
182
     * @return \luya\web\CompositionResolver
183
     */
184
    public function getResolvedPathInfo(Request $request)
185
    {
186
        return new CompositionResolver($request, ['pattern' => $this->pattern, 'defaultValues' => $this->default]);
187
    }
188
189
    /**
190
     * Set a new composition key and value in composition array. If the key already exists, it will
191
     * be overwritten.
192
     *
193
     * @param string $key The key in the array, e.g. langShortCode
194
     * @param string $value The value coresponding to the key e.g. de
195
     */
196
    public function setKey($key, $value)
197
    {
198
        $this->_keys[$key] = $value;
199
    }
200
201
    /**
202
     * Get value from the composition array for the provided key, if the key does not existing the default value
203
     * will be return. The standard value of the defaultValue is false, so if nothing defined and the could not
204
     * be found, the return value is `false`.
205
     *
206
     * @param string $key The key to find in the composition array e.g. langShortCode
207
     * @param string $defaultValue The default value if they could not be found
208
     * @return string|bool
209
     */
210
    public function getKey($key, $defaultValue = false)
211
    {
212
        $this->getKeys();
213
        
214
        return isset($this->_keys[$key]) ? $this->_keys[$key] : $defaultValue;
215
    }
216
    
217
    /**
218
     * Get the composition prefix path based on current provided request.
219
     * 
220
     * An example response could be `de` or with other composition keys and patters `de/ch` or `de-CH`.
221
     * 
222
     * @return string
223
     * @since 1.0.5
224
     */
225
    public function getPrefixPath()
226
    {
227
        return $this->createRouteEnsure();
228
    }
229
230
    /**
231
     * Create a route but ensures if composition is hidden anyhow.
232
     *
233
     * @param array $overrideKeys
234
     * @return string
235
     */
236
    public function createRouteEnsure(array $overrideKeys = [])
237
    {
238
        return $this->hidden ? '' : $this->createRoute($overrideKeys);
239
    }
240
    
241
    /**
242
     * Create compositon route based on the provided keys (to override), if no keys provided
243
     * all the default values will be used.
244
     *
245
     * @param array $overrideKeys
246
     * @return string
247
     */
248
    public function createRoute(array $overrideKeys = [])
249
    {
250
        $composition = $this->getKeys();
251
        
252
        foreach ($overrideKeys as $key => $value) {
253
            if (isset($key, $composition)) {
254
                $composition[$key] = $value;
255
            }
256
        }
257
        return implode('/', $composition);
258
    }
259
260
    /**
261
     * Prepend to current composition (or to provided composition prefix-route) to a given route.
262
     *
263
     * @param string $route The route where the composition prefix should be prepended.
264
     * @param null|string $prefix Define the value you want to prepend to the route or not.
265
     * @return string
266
     */
267
    public function prependTo($route, $prefix = null)
268
    {
269
        if ($prefix === null) {
270
            $prefix = $this->getPrefixPath();
271
        }
272
273
        if (empty($prefix)) {
274
            return $route;
275
        }
276
277
        $prepend = '';
278
279
        if (substr($route, 0, 1) == '/') {
280
            $prepend = '/';
281
        }
282
283
        return $prepend.$prefix.'/'.ltrim($route, '/');
284
    }
285
286
    /**
287
     * Remove the composition full parterns from a given route
288
     *
289
     * @param string $route
290
     * @return string route cleanup from the compositon pattern (without).
291
     */
292
    public function removeFrom($route)
293
    {
294
        $pattern = preg_quote($this->prefixPath.'/', '#');
295
296
        return preg_replace("#$pattern#", '', $route, 1);
297
    }
298
    
299
    /**
300
     * Wrapper for `getKey('langShortCode')` to load language to set php env settings.
301
     *
302
     * @return string|boolean Get the language value from the langShortCode key, false if not set.
303
     */
304
    public function getLangShortCode()
305
    {
306
        return $this->getKey('langShortCode');
307
    }
308
    
309
    /**
310
     * Return the the default langt short code.
311
     *
312
     * @return string
313
     */
314
    public function getDefaultLangShortCode()
315
    {
316
        return $this->default['langShortCode'];
317
    }
318
319
    /**
320
     * ArrayAccess offset exists.
321
     *
322
     * @see \ArrayAccess::offsetExists()
323
     * @return boolean
324
     */
325
    public function offsetExists($offset)
326
    {
327
        return isset($this->_keys[$offset]);
328
    }
329
330
    /**
331
     * ArrayAccess set value to array.
332
     *
333
     * @see \ArrayAccess::offsetSet()
334
     * @param string $offset The key of the array
335
     * @param mixed $value The value for the offset key.
336
     * @throws \luya\Exception
337
     */
338
    public function offsetSet($offset, $value)
339
    {
340
        $this->setKey($offset, $value);
341
    }
342
343
    /**
344
     * ArrayAccess get the value for a key.
345
     *
346
     * @see \ArrayAccess::offsetGet()
347
     * @param string $offset The key to get from the array.
348
     * @return mixed The value for the offset key from the array.
349
     */
350
    public function offsetGet($offset)
351
    {
352
        return $this->getKey($offset, null);
353
    }
354
355
    /**
356
     * ArrayAccess unset key.
357
     *
358
     * Unsetting data via array access is not allowed.
359
     *
360
     * @see \ArrayAccess::offsetUnset()
361
     * @param string $offset The key to unset from the array.
362
     * @throws \luya\Exception
363
     */
364
    public function offsetUnset($offset)
365
    {
366
        throw new Exception('Deleting keys in Composition is not allowed.');
367
    }
368
    
369
    // Deprecated methods
370
    
371
    /**
372
     * Wrapper for `getKey('langShortCode')` to load language to set php env settings.
373
     *
374
     * @return string|boolean Get the language value from the langShortCode key, false if not set.
375
     * @deprecated in 1.1.0 use `getLangShortCode()` instead.
376
     */
377
    public function getLanguage()
378
    {
379
        return $this->getKey('langShortCode');
380
    }
381
    
382
    /**
383
     * Return the whole composition array.
384
     *
385
     * @return array
386
     * @deprecated Remove in 1.1.0 use `getKeys()` instead.
387
     */
388
    public function get()
389
    {
390
        return $this->_keys;
391
    }
392
    
393
    /**
394
     * Return a path like string with all composition with trailing slash e.g. us/e.
395
     *
396
     * @return string
397
     * @deprecated Remove in 1.1.0 use `getPrefixPath()` instead.
398
     */
399
    public function getFull()
400
    {
401
        return $this->createRouteEnsure();
402
    }
403
}
404