Completed
Push — master ( b4d6a5...352f6d )
by Basil
02:42
created

UrlManager::parseRequest()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 47
rs 8.223
c 0
b 0
f 0
cc 7
nc 5
nop 1
1
<?php
2
3
namespace luya\web;
4
5
use Yii;
6
7
use yii\web\BadRequestHttpException;
8
use yii\web\NotFoundHttpException;
9
10
/**
11
 * Extended LUYA UrlManager.
12
 *
13
 * UrlManger extends the Yii2 Url Manager by resolving composition informations while parseRequest and provides other helper methods.
14
 *
15
 * @property boolean|\luya\cms\Menu $menu The menu componet if registered.
16
 * @property \luya\web\Composition $composition The composition component if registered.
17
 *
18
 * @author Basil Suter <[email protected]>
19
 * @since 1.0.0
20
 */
21
class UrlManager extends \yii\web\UrlManager
22
{
23
    /**
24
     * @var boolean Pretty urls are enabled by default and can not be turned off in luya cms context.
25
     */
26
    public $enablePrettyUrl = true;
27
28
    /**
29
     * @var boolean As mod rewrite is required by a LUYA cms instance the script name must be turned off by default.
30
     */
31
    public $showScriptName = false;
32
33
    /**
34
     * @var array The default url rule configuration uses the {{\luya\web\UrlRule}} class.
35
     */
36
    public $ruleConfig = ['class' => 'luya\web\UrlRule'];
37
38
    /**
39
     * @var integer In order to build urls, the nav item id from cms module can be stored in the UrlManager as `$contextNavItemId`.
40
     *
41
     * This context setter is called in {{luya\cms\frontend\base\Controller::renderItem()}} method and is used when calling {{\luya\web\UrlManager::createUrl}} method.
42
     */
43
    public $contextNavItemId;
44
45
    private $_menu;
46
47
    private $_composition;
48
49
    /**
50
     * Ensure whether a route starts with a language short key or not.
51
     *
52
     * @param string $route The route to check `en/module/controller/action` or without `module/controller/action`
53
     * @param string $language The language to check whether it exists or not `en`.
54
     * @return boolean
55
     */
56
    public function routeHasLanguageCompositionPrefix($route, $language)
57
    {
58
        $parts = explode("/", $route);
59
        if (isset($parts[0]) && $parts[0] == $language) {
60
            return true;
61
        }
62
        
63
        return false;
64
    }
65
    
66
    /**
67
     * Extend functionality of parent::parseRequest() by verify and resolve the composition informations.
68
     *
69
     * @inheritDoc
70
     *
71
     * @see \yii\web\UrlManager::parseRequest()
72
     * @param \luya\web\Request $request The request component.
73
     */
74
    public function parseRequest($request)
75
    {
76
        // extra data from request to composition, which changes the pathInfo of the Request-Object.
77
        $resolver = $this->getComposition()->getResolvedPathInfo($request);
78
79
        try {
80
            $request->setPathInfo($resolver->resolvedPath);
81
        } catch (NotFoundHttpException $error) {
82
            // the resolver has thrown an 404 excpetion, stop parsing request and return false (which is: page not found)
83
            return false;
84
        }
85
        
86
        $parsedRequest = parent::parseRequest($request);
87
88
        // if [[enablePrettyUrl]] is `false`. `false` is returned if the current request cannot be successfully parsed.
89
        if ($parsedRequest === false) {
90
            return false;
91
        }
92
93
        // ensure if the parsed route first match equals the composition pattern.
94
        // This can be the case when composition is hidden, but not default language is loaded and a
95
        // url composition route is loaded!
96
        // @see https://github.com/luyadev/luya/issues/1146
97
        $res = $this->routeHasLanguageCompositionPrefix($parsedRequest[0], $resolver->getResolvedKeyValue(Composition::VAR_LANG_SHORT_CODE));
98
        
99
        // set the application language based from the parsed composition request:
100
        Yii::$app->setLocale($this->composition->langShortCode);
101
        Yii::$app->language = $this->composition->langShortCode;
102
        
103
        // if enableStrictParsing is enabled and the route is not found, $parsedRequest will return `false`.
104
        if ($res === false && ($this->composition->hidden || $parsedRequest === false)) {
105
            return $parsedRequest;
106
        }
107
        
108
        $composition = $this->composition->createRoute();
109
        $length = strlen($composition);
110
        $route = $parsedRequest[0];
111
        
112
        if (substr($route, 0, $length+1) == $composition.'/') {
113
            $parsedRequest[0] = substr($parsedRequest[0], $length);
114
        }
115
        
116
        // remove start trailing slashes from route.
117
        $parsedRequest[0] = ltrim($parsedRequest[0], '/');
118
        
119
        return $parsedRequest;
120
    }
121
    
122
    /**
123
     * Extend functionality of parent::addRules by the ability to add composition routes.
124
     *
125
     * @see \yii\web\UrlManager::addRules()
126
     * @param array $rules An array wil rules
127
     * @param boolean $append Append to the end of the rules or not.
128
     */
129
    public function addRules($rules, $append = true)
130
    {
131
        foreach ($rules as $key => $rule) {
132
            if (is_array($rule) && isset($rule['composition'])) {
133
                foreach ($rule['composition'] as $composition => $pattern) {
134
                    $rules[] = [
135
                        'pattern' => $pattern,
136
                        'route' => $composition.'/'.$rule['route'],
137
                    ];
138
                }
139
            }
140
        }
141
142
        return parent::addRules($rules, $append);
143
    }
144
145
    /**
146
     * Get the menu component if its registered in the current applications.
147
     *
148
     * The menu component is only registered when the cms module is registered.
149
     *
150
     * @return boolean|\luya\cms\Menu The menu component object or false if not available.
151
     */
152
    public function getMenu()
153
    {
154
        if ($this->_menu === null) {
155
            $menu = Yii::$app->get('menu', false);
156
            if ($menu) {
157
                $this->_menu = $menu;
158
            } else {
159
                $this->_menu = false;
160
            }
161
        }
162
163
        return $this->_menu;
164
    }
165
166
    /**
167
     * Setter method for the composition component.
168
     *
169
     * @param \luya\web\Composition $composition
170
     */
171
    public function setComposition(Composition $composition)
172
    {
173
        $this->_composition = $composition;
174
    }
175
    
176
    /**
177
     * Get the composition component
178
     *
179
     * @return \luya\web\Composition Get the composition component to resolve multi lingual handling.
180
     */
181
    public function getComposition()
182
    {
183
        if ($this->_composition === null) {
184
            $this->_composition = Yii::$app->get('composition');
185
        }
186
187
        return $this->_composition;
188
    }
189
190
    /**
191
     * Prepand the base url to an existing route
192
     *
193
     * @param string $route The route where the base url should be prepend to.
194
     * @return string The route with prepanded baseUrl.
195
     */
196
    public function prependBaseUrl($route)
197
    {
198
        return rtrim($this->baseUrl, '/').'/'.ltrim($route, '/');
199
    }
200
201
    /**
202
     * Remove the base url from a route
203
     *
204
     * @param string $route The route where the baseUrl should be removed from.
205
     * @return mixed
206
     */
207
    public function removeBaseUrl($route)
208
    {
209
        return preg_replace('#'.preg_quote($this->baseUrl, '#').'#', '', $route, 1);
210
    }
211
212
    /**
213
     * Extend createUrl method by verify its context implementation to add cms urls prepand to the requested createurl params.
214
     *
215
     * From the original create url function of Yii:
216
     *
217
     * You may specify the route as a string, e.g., `site/index`. You may also use an array
218
     * if you want to specify additional query parameters for the URL being created. The
219
     * array format must be:
220
     *
221
     * ```php
222
     * // generates: /index.php?r=site%2Findex&param1=value1&param2=value2
223
     * ['site/index', 'param1' => 'value1', 'param2' => 'value2']
224
     * ```
225
     *
226
     * If you want to create a URL with an anchor, you can use the array format with a `#` parameter.
227
     * For example,
228
     *
229
     * ```php
230
     * // generates: /index.php?r=site%2Findex&param1=value1#name
231
     * ['site/index', 'param1' => 'value1', '#' => 'name']
232
     * ```
233
     *
234
     * The URL created is a relative one. Use [[createAbsoluteUrl()]] to create an absolute URL.
235
     *
236
     * Note that unlike {{luya\helpers\Url::toRoute()}}, this method always treats the given route
237
     * as an absolute route.
238
     *
239
     * @see \yii\web\UrlManager::createUrl()
240
     * @param string|array $params use a string to represent a route (e.g. `site/index`),
241
     * or an array to represent a route with query parameters (e.g. `['site/index', 'param1' => 'value1']`).
242
     * @return string the created URL.
243
     */
244
    public function createUrl($params)
245
    {
246
        $response = $this->internalCreateUrl($params);
247
248
        if ($this->contextNavItemId) {
249
            return $this->urlReplaceModule($response, $this->contextNavItemId, $this->getComposition());
250
        }
251
252
        return $response;
253
    }
254
255
    /**
256
     * Create an url for a menu item.
257
     *
258
     * @param string|array $params Use a string to represent a route (e.g. `site/index`), or an array to represent a route with query parameters (e.g. `['site/index', 'param1' => 'value1']`).
259
     * @param integer $navItemId The nav item Id
260
     * @param null|\luya\web\Composition $composition Optional other composition config instead of using the default composition
261
     * @return string
262
     */
263
    public function createMenuItemUrl($params, $navItemId, $composition = null)
264
    {
265
        $composition = empty($composition) ? $this->getComposition() : $composition;
266
        $url = $this->internalCreateUrl($params, $composition);
267
268
        if (!$this->menu) {
269
            return $url;
270
        }
271
272
        return $this->urlReplaceModule($url, $navItemId, $composition);
273
    }
274
275
    /**
276
     * Yii2 createUrl base implementation extends the prepand of the comosition
277
     *
278
     * @param string|array $params An array with params or not (e.g. `['module/controller/action', 'param1' => 'value1']`)
279
     * @param null|\luya\web\Composition $composition Composition instance to change the route behavior
280
     * @return string
281
     */
282
    public function internalCreateUrl($params, $composition = null)
283
    {
284
        $params = (array) $params;
285
        
286
        $composition = empty($composition) ? $this->getComposition() : $composition;
287
        
288
        $originalParams = $params;
289
        
290
        // prepand the original route, whether is hidden or not!
291
        // https://github.com/luyadev/luya/issues/1146
292
        $params[0] = $composition->prependTo($params[0], $composition->createRoute());
293
        
294
        $response = parent::createUrl($params);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (createUrl() instead of internalCreateUrl()). Are you sure this is correct? If so, you might want to change this to $this->createUrl().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
295
296
        // Check if the parsed route with the prepand composition has been found or not.
297
        if (strpos($response, rtrim($params[0], '/')) !== false) {
298
            // we got back the same url from the createUrl, no match against composition route.
299
            $response = parent::createUrl($originalParams);
300
        }
301
302
        $response = $this->removeBaseUrl($response);
303
        $response = $composition->prependTo($response);
304
305
        return $this->prependBaseUrl($response);
306
    }
307
    
308
    /**
309
     * Create absolute url from the given route params.
310
     *
311
     * @param string|array $params The see createUrl
312
     * @param boolean $scheme Whether to use absolute scheme path or not.
313
     * @return string The created url
314
     */
315
    public function internalCreateAbsoluteUrl($params, $scheme = null)
316
    {
317
        $params = (array) $params;
318
        $url = $this->internalCreateUrl($params);
319
        if (strpos($url, '://') === false) {
320
            $url = $this->getHostInfo() . $url;
321
        }
322
        if (is_string($scheme) && ($pos = strpos($url, '://')) !== false) {
323
            $url = $scheme . substr($url, $pos);
324
        }
325
        return $url;
326
    }
327
    
328
    /**
329
     * See if the module of a provided route exists in the luya application list.
330
     *
331
     * The module to test must be an instance of `luya\base\Module`.
332
     *
333
     * @param string $route
334
     * @return boolean|string
335
     */
336
    private function findModuleInRoute($route)
337
    {
338
        $route = parse_url($route, PHP_URL_PATH);
339
        
340
        $parts = array_values(array_filter(explode('/', $route)));
341
        
342
        if (isset($parts[0]) && array_key_exists($parts[0], Yii::$app->getApplicationModules())) {
343
            return $parts[0];
344
        }
345
    
346
        return false;
347
    }
348
    
349
    /**
350
     * Replace the url with the current module context.
351
     *
352
     * @param string $url The url to replace
353
     * @param integer $navItemId The navigation item where the context url to be found.
354
     * @param \luya\web\Composition $composition Composition component object to resolve language context.
355
     * @throws \yii\web\BadRequestHttpException
356
     * @return string The replaced string.
357
     */
358
    private function urlReplaceModule($url, $navItemId, Composition $composition)
359
    {
360
        $route = $composition->removeFrom($this->removeBaseUrl($url));
361
        $moduleName = $this->findModuleInRoute($route);
362
    
363
        if ($moduleName === false || $this->menu === false) {
364
            return $url;
365
        }
366
    
367
        $item = $this->menu->find()->where(['id' => $navItemId])->with('hidden')->lang($composition[Composition::VAR_LANG_SHORT_CODE])->one();
368
    
369
        if (!$item) {
370
            throw new BadRequestHttpException("Unable to find nav_item_id '$navItemId' to generate the module link for url '$url'.");
371
        }
372
    
373
        $isOutgoingModulePage = $item->type == 2 && $moduleName !== $item->moduleName;
374
        
375
        // 1. if the current page is a module and the requested url is not the same module, its an outgoing link to
376
        // another module which should not be modificated.
377
        // 2. If the current page (nav) context is the homepage, we have to keep the original link as it wont work because the homepage
378
        // does not have a route prefix.
379
        if ($isOutgoingModulePage || $item->isHome) {
380
            return $url;
381
        }
382
    
383
        // 1. if the current page is a module and the requested url is not the same module, its an outgoing link to
384
        // another module and ...
385
        // 2. if current controller context has an other module as the requested url, its an outgoing link to another module which should not be modificated.
386
        if ($isOutgoingModulePage && $moduleName !== Yii::$app->controller->module->id) {
387
            return $url;
388
        }
389
390
        return preg_replace("/$moduleName/", rtrim($item->link, '/'), ltrim($route, '/'), 1);
391
    }
392
}
393