Completed
Push — master ( 0595da...cb5a6f )
by Basil
06:40
created

Composition::isHostAllowed()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 1
1
<?php
2
3
namespace luya\web;
4
5
use Yii;
6
use yii\base\Component;
7
use luya\Exception;
8
use luya\helpers\StringHelper;
9
use yii\web\ForbiddenHttpException;
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 $full Return `getFull()` method represents full composition
31
 * @property string $defaultLangShortCode Return default defined language shord code
32
 * @property string $language Return wrapper of getKey('langShortCode')
33
 *
34
 * @author Basil Suter <[email protected]>
35
 * @since 1.0.0
36
 */
37
class Composition extends Component implements \ArrayAccess
38
{
39
    /**
40
     * @var string This event will method will triggere after setKey method is proccessed
41
     */
42
    const EVENT_AFTER_SET = 'EVENT_AFTER_SET';
43
44
    /**
45
     * @var string The Regular-Expression matching the var finder inside the url parts
46
     */
47
    const VAR_MATCH_REGEX = '/<(\w+):?([^>]+)?>/';
48
49
    /**
50
     * @var \yii\web\Request Request-Object container from DI
51
     */
52
    public $request;
53
54
    /**
55
     * @var bool Enable or disable the->getFull() prefix. If disabled the response of getFull() would be empty, otherwhise it
56
     * returns the full prefix composition pattern based url.
57
     */
58
    public $hidden = true;
59
60
    /**
61
     * @var string Url matching prefix, which is used for all the modules (e.g. an e-store requireds a language
62
     * as the cms needs this informations too). After proccessing this informations, they will be removed
63
     * from the url for further proccessing.
64
     *
65
     * The fullqualified composer key will be stored in `$request->get('urlPrefixCompositionKey')`.
66
     *
67
     * Examples of how to use urlPrefixComposition
68
     *
69
     * ```php
70
     * $urlPrefixComposition = '<langShortCode:[a-z]{2}>/<countryShortCode:[a-z]{2}>'; // de/ch; fr/ch
71
     * ```
72
     */
73
    public $pattern = '<langShortCode:[a-z]{2}>';
74
75
    /**
76
     * @var array Default value if there is no composition provided in the url. The default value must match the url.
77
     */
78
    public $default = ['langShortCode' => 'en'];
79
80
    /**
81
     * @var array Define the default behavior for differnet host info schemas, if the host info is not found
82
     * the default behvaior via `$default` will be used.
83
     * An array where the key is the host info and value the array with the default configuration .e.g.
84
     *
85
     * ```
86
     * 'hostInfoMapping' => [
87
     *     'http://mydomain.com' => ['langShortCode' => 'en'],
88
     *     'http://meinedomain.de' => ['langShortCode' => 'de'],
89
     * ],
90
     * ```
91
     *
92
     * The above configuration must be defined in your compostion componeont configuration in your config file.
93
     */
94
    public $hostInfoMapping = [];
95
    
96
    /**
97
     * 
98
     * @var array|string An array with all valid hosts in order to ensure the request host is equals to valid hosts.
99
     * This filter provides protection against ['host header' attacks](https://www.acunetix.com/vulnerabilities/web/host-header-attack).
100
     * 
101
     * ```php
102
     * 'allowedHosts' => [
103
     *     'example.com',
104
     *     '*.example.com',
105
     * ]
106
     * ```
107
     * 
108
     * If null is defined, the allow host filtering is disabled, default value.
109
     * @since 1.0.5
110
     */
111
    public $allowedHosts = null;
112
113
    /**
114
     * Class constructor, to get data from DiContainer.
115
     *
116
     * @param \luya\web\Request $request Request componet resolved from Depency Manager
117
     * @param array $config The object configuration array
118
     */
119
    public function __construct(Request $request, array $config = [])
120
    {
121
        $this->request = $request;
122
        parent::__construct($config);
123
    }
124
125
    /**
126
     * Return the the default langt short code.
127
     *
128
     * @return string
129
     */
130
    public function getDefaultLangShortCode()
131
    {
132
        return $this->default['langShortCode'];
133
    }
134
135
    private $_compositionKeys = [];
136
    
137
    /**
138
     * Resolve the the composition on init.
139
     */
140
    public function init()
141
    {
142
        parent::init();
143
144
        // check if the required key langShortCode exist in the default array.
145
        if (!array_key_exists('langShortCode', $this->default)) {
146
            throw new Exception("The composition default rule must contain a langShortCode.");
147
        }
148
149
        if ($this->allowedHosts !== null && !$this->isHostAllowed($this->allowedHosts)) {
150
            throw new ForbiddenHttpException("The current host '{$this->request->hostName}' is not in the list valid of hosts.");
151
        }
152
        
153
        if (array_key_exists($this->request->hostInfo, $this->hostInfoMapping)) {
154
            $this->default = $this->hostInfoMapping[$this->request->hostInfo];
155
        }
156
157
        // atach event to component
158
        $this->on(self::EVENT_AFTER_SET, [$this, 'eventAfterSet']);
159
        // resolved data
160
        $resolve = $this->getResolvedPathInfo($this->request);
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...
161
        // set initializer comosition
162
        foreach ($resolve['compositionKeys'] as $key => $value) {
163
            $this->setKey($key, $value);
164
        }
165
        $this->_compositionKeys = $resolve['keys'];
166
    }
167
    
168
    /**
169
     * Checks if the current request name against the allowedHosts list.
170
     *
171
     * If the list of 
172
     *
173
     * @return boolean Whether the current hostName is allowed or not.
174
     * @since 1.0.5
175
     */
176
    public function isHostAllowed($allowedHosts)
177
    {
178
        $currentHost = $this->request->hostName;
179
        
180
        $rules = (array) $allowedHosts;
181
        
182
        foreach ($rules as $allowedHost) {
183
            if (StringHelper::matchWildcard($allowedHost, $currentHost)) {
184
                return true;
185
            }
186
        }
187
        
188
        return false;
189
    }
190
191
    /**
192
     * This event will method will trigge after setKey method is proccessed. The
193
     * main purpose of this function to change the localisation based on the required
194
     * key 'langShortCode'.
195
     *
196
     * @param \luya\web\CompositionAfterSetEvent $event The event object.
197
     */
198
    public function eventAfterSet($event)
199
    {
200
        if ($event->key == 'langShortCode') {
201
            Yii::$app->setLocale($event->value);
202
        }
203
    }
204
205
    /**
206
     * Resolve the current url request and retun an array contain resolved route and the resolved values.
207
     *
208
     * @param \luya\web\Request $request
209
     * @return array An array containing the route and the resolvedValues. Example array output when request path is `de/hello/world`:
210
     *
211
     * ```php
212
     * [
213
     *     'route' => 'hello/world',
214
     *     'resolvedValues' => [
215
     *         0 => 'de'
216
     *     ],
217
     * ]
218
     * ```
219
     */
220
    public function getResolvedPathInfo(Request $request)
221
    {
222
        // contains all resolved values
223
        $resolvedValues = [];
224
        $foundKeys = [];
225
        // array with all url parts, seperated by slash
226
        $requestUrlParts = explode('/', $request->pathInfo);
227
        // catch all results matching the var match regular expression
228
        preg_match_all(static::VAR_MATCH_REGEX, $this->pattern, $matches, PREG_SET_ORDER);
229
        // get all matches
230
        foreach ($matches as $index => $match) {
231
            $foundKeys[] = $match[1];
232
            // check if the index of the match is existing in the requestUrlParts array as the match always
233
            // starts from the begin of a string.
234
            if (isset($requestUrlParts[$index])) {
235
                $requestUrlValue = $requestUrlParts[$index];
236
                // check if the value of the requeste url value matches the regex of the compositoin
237
                if (preg_match('/^'.$match[2].'$/', $requestUrlValue)) {
238
                    $resolvedValues[$match[1]] = $requestUrlValue;
239
                    // cause the part is matched by the composition, we have to unset the key from the array.
240
                    unset($requestUrlParts[$index]);
241
                }
242
            }
243
        }
244
        // get default values if nothing have been resolved
245
        if (count($resolvedValues) == 0) {
246
            $keys = $this->default;
247
        } else {
248
            $keys = $resolvedValues;
249
        }
250
        // return array with route and resolvedValues
251
        return ['route' => implode('/', $requestUrlParts), 'resolvedValues' => $resolvedValues, 'compositionKeys' => $keys, 'keys' => $foundKeys];
252
    }
253
    
254
    private $_composition = [];
255
256
    /**
257
     * Set a new composition key and value in composition array. If the key already exists, it will
258
     * be overwritten. The setKey method triggers the CompositionAfterSetEvent class.
259
     *
260
     * @param string $key The key in the array, e.g. langShortCode
261
     * @param string $value The value coresponding to the key e.g. de
262
     */
263
    public function setKey($key, $value)
264
    {
265
        // set and override composition
266
        $this->_composition[$key] = $value;
267
        // trigger event
268
        $event = new CompositionAfterSetEvent();
269
        $event->key = $key;
270
        $event->value = $value;
271
        $this->trigger(self::EVENT_AFTER_SET, $event);
272
    }
273
274
    /**
275
     * Get value from the composition array for the provided key, if the key does not existing the default value
276
     * will be return. The standard value of the defaultValue is false, so if nothing defined and the could not
277
     * be found, the return value is `false`.
278
     *
279
     * @param string $key          The key to find in the composition array e.g. langShortCode
280
     * @param string $defaultValue The default value if they could not be found
281
     * @return string|bool
282
     */
283
    public function getKey($key, $defaultValue = false)
284
    {
285
        return (isset($this->_composition[$key])) ? $this->_composition[$key] : $defaultValue;
286
    }
287
288
    /**
289
     * Return the whole composition array.
290
     *
291
     * @return array
292
     */
293
    public function get()
294
    {
295
        return $this->_composition;
296
    }
297
298
    /**
299
     * Return a path like string with all composition with trailing slash e.g. us/e.
300
     *
301
     * @return string
302
     */
303
    public function getFull()
304
    {
305
        return $this->createRouteEnsure();
306
    }
307
308
    /**
309
     * create a route but ensures if composition is hidden anywho.
310
     *
311
     * @param array $overrideKeys
312
     * @return string
313
     */
314
    public function createRouteEnsure(array $overrideKeys = [])
315
    {
316
        return $this->hidden ? '' : $this->createRoute($overrideKeys);
317
    }
318
319
    /**
320
     * Create compositon route based on the provided keys (to override), if no keys provided
321
     * all the default values will be used.
322
     *
323
     * @param array $overrideKeys
324
     * @return string
325
     */
326
    public function createRoute(array $overrideKeys = [])
327
    {
328
        $composition = $this->_composition;
329
330
        foreach ($overrideKeys as $key => $value) {
331
            if (in_array($key, $this->_compositionKeys)) {
332
                $composition[$key] = $value;
333
            }
334
        }
335
336
        return implode('/', $composition);
337
    }
338
339
    /**
340
     * Prepend to current composition (or to provided composition prefix-route) to a given route.
341
     *
342
     * @param string $route The route where the composition prefix should be prepended.
343
     * @param null|string $prefix Define the value you want to prepend to the route or not.
344
     * @return string
345
     */
346
    public function prependTo($route, $prefix = null)
347
    {
348
        if ($prefix === null) {
349
            $prefix = $this->getFull();
350
        }
351
352
        if (empty($prefix)) {
353
            return $route;
354
        }
355
356
        $prepend = '';
357
358
        if (substr($route, 0, 1) == '/') {
359
            $prepend = '/';
360
        }
361
362
        return $prepend.$prefix.'/'.ltrim($route, '/');
363
    }
364
365
    /**
366
     * Remove the composition full parterns from a given route
367
     *
368
     * @param string $route
369
     * @return string route cleanup from the compositon pattern (without).
370
     */
371
    public function removeFrom($route)
372
    {
373
        $pattern = preg_quote($this->getFull().'/', '#');
374
375
        return preg_replace("#$pattern#", '', $route, 1);
376
    }
377
378
    /**
379
     * Wrapper for `getKey('langShortCode')` to load language to set php env settings.
380
     *
381
     * @return string|boolean Get the language value from the langShortCode key, false if not set.
382
     */
383
    public function getLanguage()
384
    {
385
        return $this->getKey('langShortCode');
386
    }
387
388
    /**
389
     * ArrayAccess offset exists.
390
     *
391
     * @see \ArrayAccess::offsetExists()
392
     * @return boolean
393
     */
394
    public function offsetExists($offset)
395
    {
396
        return isset($this->_composition[$offset]);
397
    }
398
399
    /**
400
     * ArrayAccess set value to array.
401
     *
402
     * @see \ArrayAccess::offsetSet()
403
     * @param string $offset The key of the array
404
     * @param mixed $value The value for the offset key.
405
     * @throws \luya\Exception
406
     */
407
    public function offsetSet($offset, $value)
408
    {
409
        if (is_null($offset)) {
410
            throw new Exception('Unable to set array value without key. Empty keys not allowed.');
411
        }
412
        $this->setKey($offset, $value);
413
    }
414
415
    /**
416
     * ArrayAccess get the value for a key.
417
     *
418
     * @see \ArrayAccess::offsetGet()
419
     * @param string $offset The key to get from the array.
420
     * @return mixed The value for the offset key from the array.
421
     */
422
    public function offsetGet($offset)
423
    {
424
        return $this->getKey($offset, null);
425
    }
426
427
    /**
428
     * ArrayAccess unset key.
429
     *
430
     * Unsetting data via array access is not allowed.
431
     *
432
     * @see \ArrayAccess::offsetUnset()
433
     * @param string $offset The key to unset from the array.
434
     * @throws \luya\Exception
435
     */
436
    public function offsetUnset($offset)
437
    {
438
        throw new Exception('Deleting keys in Composition is not allowed.');
439
    }
440
}
441