Completed
Push — master ( 5b1779...94d8df )
by Basil
02:43
created

Composition::resolveHostInfo()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 4
nc 8
nop 1
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
use luya\helpers\ArrayHelper;
11
12
/**
13
 * Composition parseRequest Handler.
14
 *
15
 * The composition is run for every {{luya\web\UrlManager::parseRequest()}} call in order to determine
16
 * the language of the application based on {{luya\web\Composition::$hostInfoMapping}} or from the given
17
 * {{luya\web\Request::$hostInfo}}.
18
 *
19
 * It also provides common functions in order to make complex regional language detection for urls
20
 * like `https://example.com/fr/ch`, its possible to set {{luya\web\Composition::$pattern}} and retrieve
21
 * the value later inside your application. The `langShortCode` must be provided as long as working with the
22
 * cms, as its bound to the administration language `admin_lang` database table.
23
 *
24
 * It also provides security check options:
25
 *
26
 * + {{luya\web\Composition::$allowedHosts}}
27
 *
28
 * The Composition component is registered by the {{luya\base\Boot}} object and is therefore accessible
29
 * trough `Yii::$app->composition` as "singleton" instance.
30
 *
31
 * @property string $prefixPath Return the current composition prefix path for the request based on request input and hidden option.
32
 * @property array $keys Return an array with key and value of all resolve composition values for the current request.
33
 * @property string $defaultLangShortCode Return default defined language shord code
34
 * @property string $langShortCode Return wrapper of getKey('langShortCode')
35
 *
36
 * @author Basil Suter <[email protected]>
37
 * @since 1.0.0
38
 */
39
class Composition extends Component implements \ArrayAccess
40
{
41
    const VAR_LANG_SHORT_CODE = 'langShortCode';
42
    
43
    /**
44
     * @var string The Regular-Expression matching the var finder inside the url parts
45
     */
46
    const VAR_MATCH_REGEX = '/<(\w+):?([^>]+)?>/';
47
    
48
    /**
49
     * @deprecated Deprecated in 1.0.5 remove in 1.1.x
50
     */
51
    const EVENT_AFTER_SET = 'EVENT_AFTER_SET';
52
53
    /**
54
     * @var \yii\web\Request Request-Object container from DI
55
     */
56
    public $request;
57
58
    /**
59
     * @var boolean Enable or disable composition prefix out when using `prefixPath`. If disabled the response of prefixPath would be empty, otherwhise it
60
     * returns the full prefix composition pattern based url.
61
     */
62
    public $hidden = true;
63
64
    /**
65
     * @var boolean Disable composition prefixes in URLs only for default language. Takes effect only when `hidden` option is disabled.
66
     * @since 1.0.10
67
     */
68
    public $hideDefaultPrefixOnly = false;
69
70
    /**
71
     * @var string Url matching prefix, which is used for all the modules (e.g. an e-store requireds a language
72
     * as the cms needs this informations too). After proccessing this informations, they will be removed
73
     * from the url for further proccessing.
74
     *
75
     * Examples of how to use patterns:
76
     *
77
     * ```php
78
     * 'pattern' => '<langShortCode:[a-z]{2}>/<countryShortCode:[a-z]{2}>', // de/ch; fr/ch
79
     * ```
80
     */
81
    public $pattern = "<".self::VAR_LANG_SHORT_CODE.":[a-z]{2}>";
82
83
    /**
84
     * @var array Default value if there is no composition provided in the url. The default value must match the url.
85
     */
86
    public $default = [self::VAR_LANG_SHORT_CODE => 'en'];
87
88
    /**
89
     * @var array Define the default behavior for differnet host info schemas, if the host info is not found
90
     * the default behvaior via `$default` will be used.
91
     * An array where the key is the host info and value the array with the default configuration .e.g.
92
     *
93
     * ```php
94
     * 'hostInfoMapping' => [
95
     *     'http://mydomain.com' => ['langShortCode' => 'en'],
96
     *     'http://meinedomain.de' => ['langShortCode' => 'de'],
97
     * ],
98
     * ```
99
     *
100
     * The above configuration must be defined in your compostion componeont configuration in your config file.
101
     */
102
    public $hostInfoMapping = [];
103
    
104
    /**
105
     *
106
     * @var array|string An array with all valid hosts in order to ensure the request host is equals to valid hosts.
107
     * This filter provides protection against ['host header' attacks](https://www.acunetix.com/vulnerabilities/web/host-header-attack).
108
     *
109
     * ```php
110
     * 'allowedHosts' => [
111
     *     'example.com',
112
     *     '*.example.com',
113
     * ]
114
     * ```
115
     *
116
     * If null is defined, the allow host filtering is disabled, default value.
117
     * @since 1.0.5
118
     */
119
    public $allowedHosts = null;
120
    
121
    /**
122
     * A list of values which are valid for every pattern. If set and a value is provided which is not inside this property
123
     * an http not found exception is thrown.
124
     *
125
     * Every value must be set for the given pattern name:
126
     *
127
     * ```php
128
     * 'expectedValues' => [
129
     *     'langShortCode' => ['en', 'de'], // langShortCode pattern is required
130
     *     'countryShortCode' => ['ch', 'fr', 'de', 'uk'], // additional patterns if configured
131
     * ],
132
     * ```
133
     *
134
     * > This configuration is usual only used in MVC applications without CMS module, as the cms module throws an
135
     * > exception if the requested language is not available.
136
     *
137
     * @var array An array where key is the pattern and value an array of possible values for this pattern.
138
     * @since 1.0.15
139
     */
140
    public $expectedValues = [];
141
142
    /**
143
     * Class constructor, to get data from DiContainer.
144
     *
145
     * @param \luya\web\Request $request Request componet resolved from Depency Manager
146
     * @param array $config The object configuration array
147
     */
148
    public function __construct(Request $request, array $config = [])
149
    {
150
        $this->request = $request;
151
        parent::__construct($config);
152
    }
153
154
    /**
155
     * @inheritdoc
156
     */
157
    public function init()
158
    {
159
        parent::init();
160
161
        // check if the required key langShortCode exist in the default array.
162
        if (!array_key_exists(self::VAR_LANG_SHORT_CODE, $this->default)) {
163
            throw new Exception("The composition default rule must contain a langShortCode.");
164
        }
165
166
        if ($this->allowedHosts !== null && !$this->isHostAllowed($this->allowedHosts)) {
167
            throw new ForbiddenHttpException("The current host '{$this->request->hostName}' is not in the list valid of hosts.");
168
        }
169
        
170
        if (array_key_exists($this->request->hostInfo, $this->hostInfoMapping)) {
171
            $this->default = $this->hostInfoMapping[$this->request->hostInfo];
172
        }
173
    }
174
    
175
    /**
176
     * Checks if the current request name against the allowedHosts list.
177
     *
178
     * @return boolean Whether the current hostName is allowed or not.
179
     * @since 1.0.5
180
     */
181
    public function isHostAllowed($allowedHosts)
182
    {
183
        $currentHost = $this->request->hostName;
184
        
185
        $rules = (array) $allowedHosts;
186
        
187
        foreach ($rules as $allowedHost) {
188
            if (StringHelper::matchWildcard($allowedHost, $currentHost)) {
189
                return true;
190
            }
191
        }
192
        
193
        return false;
194
    }
195
196
    /**
197
     * Find the host for a given definition based on the {{Composition::$hostInfoMapping}} definition.
198
     * 
199
     * Find the host info mapping (if existing) for a lang short code:
200
     * 
201
     * ```php
202
     * $host = $composition->resolveHostInfo('en');
203
     * ```
204
     * 
205
     * Or resolve by provide full host info mapping defintion:
206
     * 
207
     * ```php
208
     * $host = $composition->resolveHostInfo([
209
     *     'langShortCode' => 'de'
210
     *     'countryShortCode' => 'ch',
211
     * ]);
212
     * ```
213
     * 
214
     * > Keep in mind that when {{Composition::$hostInfoMapping}} is empty (no defintion), false is returned.
215
     * 
216
     * @param string|array $defintion The hostinfo mapping config containing an array with full defintion of different keys or a string
217
     * which will only resolved based on langShortCode identifier.
218
     * @return string|boolean Returns the host name from the host info maping otherwise false if not found.
219
     * @since 1.0.18
220
     */
221
    public function resolveHostInfo($defintion)
222
    {
223
        // if its a scalar value, we assume the user wants to find host info based on languageShortCode
224
        if (is_scalar($defintion)) {
225
            $defintion = [self::VAR_LANG_SHORT_CODE => $defintion];
226
        }
227
228
        $results = $this->hostInfoMapping;
229
        foreach ($defintion as $key => $value) {
230
            $results = ArrayHelper::searchColumns($results, $key, $value);
231
        }
232
233
        return empty($results) ? false : key($results);;
234
    }
235
236
    private $_keys;
237
    
238
    /**
239
     * Resolves the current key and value objects based on the current pathInto and pattern from Request component.
240
     *
241
     * @return array An array with key values like `['langShortCode' => 'en']`.
242
     * @since 1.0.5
243
     */
244
    public function getKeys()
245
    {
246
        if ($this->_keys === null) {
247
            $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...
248
        }
249
        
250
        return $this->_keys;
251
    }
252
    
253
    /**
254
     * Resolve the current url request and retun an array contain resolved route and the resolved values.
255
     *
256
     * @param Request $request
257
     * @return \luya\web\CompositionResolver
258
     */
259
    public function getResolvedPathInfo(Request $request)
260
    {
261
        return new CompositionResolver($request, $this);
262
    }
263
264
    /**
265
     * Set a new composition key and value in composition array. If the key already exists, it will
266
     * be overwritten.
267
     *
268
     * @param string $key The key in the array, e.g. langShortCode
269
     * @param string $value The value coresponding to the key e.g. de
270
     */
271
    public function setKey($key, $value)
272
    {
273
        $this->_keys[$key] = $value;
274
    }
275
276
    /**
277
     * Get value from the composition array for the provided key, if the key does not existing the default value
278
     * will be return. The standard value of the defaultValue is false, so if nothing defined and the could not
279
     * be found, the return value is `false`.
280
     *
281
     * @param string $key The key to find in the composition array e.g. langShortCode
282
     * @param string $defaultValue The default value if they could not be found
283
     * @return string|bool
284
     */
285
    public function getKey($key, $defaultValue = false)
286
    {
287
        $this->getKeys();
288
        
289
        return isset($this->_keys[$key]) ? $this->_keys[$key] : $defaultValue;
290
    }
291
    
292
    /**
293
     * Get the composition prefix path based on current provided request.
294
     *
295
     * An example response could be `de` or with other composition keys and patterns `de/ch` or `de-CH`.
296
     *
297
     * @return string A prefix path like `de/ch`
298
     * @since 1.0.5
299
     */
300
    public function getPrefixPath()
301
    {
302
        return $this->createRouteEnsure();
303
    }
304
305
    /**
306
     * Create a route but ensures if composition is hidden anyhow.
307
     *
308
     * @param array $overrideKeys
309
     * @return string
310
     */
311
    public function createRouteEnsure(array $overrideKeys = [])
312
    {
313
        if (isset($overrideKeys[self::VAR_LANG_SHORT_CODE])) {
314
            $langShortCode = $overrideKeys[self::VAR_LANG_SHORT_CODE];
315
        } else {
316
            $langShortCode = $this->langShortCode;
317
        }
318
        return $this->hidden || (!$this->hidden && $langShortCode == $this->defaultLangShortCode && $this->hideDefaultPrefixOnly) ? '' : $this->createRoute($overrideKeys);
319
    }
320
321
    /**
322
     * Create compositon route based on the provided keys (to override), if no keys provided
323
     * all the default values will be used.
324
     *
325
     * @param array $overrideKeys
326
     * @return string
327
     */
328
    public function createRoute(array $overrideKeys = [])
329
    {
330
        $composition = $this->getKeys();
331
        
332
        foreach ($overrideKeys as $key => $value) {
333
            if (array_key_exists($key, $composition)) {
334
                $composition[$key] = $value;
335
            }
336
        }
337
        return implode('/', $composition);
338
    }
339
340
    /**
341
     * Prepend to current composition (or to provided composition prefix-route) to a given route.
342
     *
343
     * Assuming the current composition returns `en/gb` and the route is `foo/bar` the return
344
     * value would be `en/gb/foo/bar`.
345
     *
346
     * If a trailing slash is provided from a route, this will be returned as well, assuming:
347
     *
348
     * ```php
349
     * echo prependTo('/foobar', 'en/gb'); // ouput: /en/gb/foobar
350
     * echo prependTo('foobar', 'en/gb'); // output: en/gb/foobar
351
     * ```
352
     *
353
     * @param string $route The route where the composition prefix should be prepended.
354
     * @param null|string $prefix Define the value you want to prepend to the route or not.
355
     * @return string
356
     */
357
    public function prependTo($route, $prefix = null)
358
    {
359
        if ($prefix === null) {
360
            $prefix = $this->getPrefixPath();
361
        }
362
363
        if (empty($prefix)) {
364
            return $route;
365
        }
366
367
        $prepend = '';
368
369
        // if it contains a prepend slash, we keep this, as long as the route is also longer then just a slash.
370
        if (substr($route, 0, 1) == '/' && strlen($route) > 1) {
371
            $prepend = '/';
372
        }
373
374
        return $prepend.$prefix.'/'.ltrim($route, '/');
375
    }
376
377
    /**
378
     * Remove the composition full parterns from a given route
379
     *
380
     * @param string $route
381
     * @return string route cleanup from the compositon pattern (without).
382
     */
383
    public function removeFrom($route)
384
    {
385
        $pattern = preg_quote($this->prefixPath.'/', '#');
386
387
        return preg_replace("#$pattern#", '', $route, 1);
388
    }
389
    
390
    /**
391
     * Wrapper for `getKey('langShortCode')` to load language to set php env settings.
392
     *
393
     * @return string|boolean Get the language value from the langShortCode key, false if not set.
394
     */
395
    public function getLangShortCode()
396
    {
397
        return $this->getKey(self::VAR_LANG_SHORT_CODE);
398
    }
399
    
400
    /**
401
     * Return the the default langt short code.
402
     *
403
     * @return string
404
     */
405
    public function getDefaultLangShortCode()
406
    {
407
        return $this->default[self::VAR_LANG_SHORT_CODE];
408
    }
409
410
    /**
411
     * ArrayAccess offset exists.
412
     *
413
     * @see \ArrayAccess::offsetExists()
414
     * @return boolean
415
     */
416
    public function offsetExists($offset)
417
    {
418
        return isset($this->_keys[$offset]);
419
    }
420
421
    /**
422
     * ArrayAccess set value to array.
423
     *
424
     * @see \ArrayAccess::offsetSet()
425
     * @param string $offset The key of the array
426
     * @param mixed $value The value for the offset key.
427
     * @throws \luya\Exception
428
     */
429
    public function offsetSet($offset, $value)
430
    {
431
        $this->setKey($offset, $value);
432
    }
433
434
    /**
435
     * ArrayAccess get the value for a key.
436
     *
437
     * @see \ArrayAccess::offsetGet()
438
     * @param string $offset The key to get from the array.
439
     * @return mixed The value for the offset key from the array.
440
     */
441
    public function offsetGet($offset)
442
    {
443
        return $this->getKey($offset, null);
444
    }
445
446
    /**
447
     * ArrayAccess unset key.
448
     *
449
     * Unsetting data via array access is not allowed.
450
     *
451
     * @see \ArrayAccess::offsetUnset()
452
     * @param string $offset The key to unset from the array.
453
     * @throws \luya\Exception
454
     */
455
    public function offsetUnset($offset)
456
    {
457
        throw new Exception('Deleting keys in Composition is not allowed.');
458
    }
459
    
460
    // Deprecated methods
461
    
462
    /**
463
     * Wrapper for `getKey('langShortCode')` to load language to set php env settings.
464
     *
465
     * @return string|boolean Get the language value from the langShortCode key, false if not set.
466
     * @deprecated in 1.1.0 use `getLangShortCode()` instead.
467
     */
468
    public function getLanguage()
469
    {
470
        return $this->getKey(self::VAR_LANG_SHORT_CODE);
471
    }
472
    
473
    /**
474
     * Return the whole composition array.
475
     *
476
     * @return array
477
     * @deprecated Remove in 1.1.0 use `getKeys()` instead.
478
     */
479
    public function get()
480
    {
481
        return $this->_keys;
482
    }
483
    
484
    /**
485
     * Return a path like string with all composition with trailing slash e.g. us/e.
486
     *
487
     * @return string
488
     * @deprecated Remove in 1.1.0 use `getPrefixPath()` instead.
489
     */
490
    public function getFull()
491
    {
492
        return $this->createRouteEnsure();
493
    }
494
}
495