Completed
Push — master ( 0f62d2...1e4484 )
by Carsten
13:18
created

UrlManager::createAbsoluteUrl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 2
crap 2
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\web;
9
10
use Yii;
11
use yii\base\Component;
12
use yii\base\InvalidConfigException;
13
use yii\caching\Cache;
14
use yii\helpers\Url;
15
16
/**
17
 * UrlManager handles HTTP request parsing and creation of URLs based on a set of rules.
18
 *
19
 * UrlManager is configured as an application component in [[\yii\base\Application]] by default.
20
 * You can access that instance via `Yii::$app->urlManager`.
21
 *
22
 * You can modify its configuration by adding an array to your application config under `components`
23
 * as it is shown in the following example:
24
 *
25
 * ```php
26
 * 'urlManager' => [
27
 *     'enablePrettyUrl' => true,
28
 *     'rules' => [
29
 *         // your rules go here
30
 *     ],
31
 *     // ...
32
 * ]
33
 * ```
34
 *
35
 * Rules are classes implementing the [[UrlRuleInterface]], by default that is [[UrlRule]].
36
 * For nesting rules, there is also a [[GroupUrlRule]] class.
37
 *
38
 * For more details and usage information on UrlManager, see the [guide article on routing](guide:runtime-routing).
39
 *
40
 * @property string $baseUrl The base URL that is used by [[createUrl()]] to prepend to created URLs.
41
 * @property string $hostInfo The host info (e.g. "http://www.example.com") that is used by
42
 * [[createAbsoluteUrl()]] to prepend to created URLs.
43
 * @property string $scriptUrl The entry script URL that is used by [[createUrl()]] to prepend to created
44
 * URLs.
45
 *
46
 * @author Qiang Xue <[email protected]>
47
 * @since 2.0
48
 */
49
class UrlManager extends Component
50
{
51
    /**
52
     * @var bool whether to enable pretty URLs. Instead of putting all parameters in the query
53
     * string part of a URL, pretty URLs allow using path info to represent some of the parameters
54
     * and can thus produce more user-friendly URLs, such as "/news/Yii-is-released", instead of
55
     * "/index.php?r=news%2Fview&id=100".
56
     */
57
    public $enablePrettyUrl = false;
58
    /**
59
     * @var bool whether to enable strict parsing. If strict parsing is enabled, the incoming
60
     * requested URL must match at least one of the [[rules]] in order to be treated as a valid request.
61
     * Otherwise, the path info part of the request will be treated as the requested route.
62
     * This property is used only when [[enablePrettyUrl]] is `true`.
63
     */
64
    public $enableStrictParsing = false;
65
    /**
66
     * @var array the rules for creating and parsing URLs when [[enablePrettyUrl]] is `true`.
67
     * This property is used only if [[enablePrettyUrl]] is `true`. Each element in the array
68
     * is the configuration array for creating a single URL rule. The configuration will
69
     * be merged with [[ruleConfig]] first before it is used for creating the rule object.
70
     *
71
     * A special shortcut format can be used if a rule only specifies [[UrlRule::pattern|pattern]]
72
     * and [[UrlRule::route|route]]: `'pattern' => 'route'`. That is, instead of using a configuration
73
     * array, one can use the key to represent the pattern and the value the corresponding route.
74
     * For example, `'post/<id:\d+>' => 'post/view'`.
75
     *
76
     * For RESTful routing the mentioned shortcut format also allows you to specify the
77
     * [[UrlRule::verb|HTTP verb]] that the rule should apply for.
78
     * You can do that  by prepending it to the pattern, separated by space.
79
     * For example, `'PUT post/<id:\d+>' => 'post/update'`.
80
     * You may specify multiple verbs by separating them with comma
81
     * like this: `'POST,PUT post/index' => 'post/create'`.
82
     * The supported verbs in the shortcut format are: GET, HEAD, POST, PUT, PATCH and DELETE.
83
     * Note that [[UrlRule::mode|mode]] will be set to PARSING_ONLY when specifying verb in this way
84
     * so you normally would not specify a verb for normal GET request.
85
     *
86
     * Here is an example configuration for RESTful CRUD controller:
87
     *
88
     * ```php
89
     * [
90
     *     'dashboard' => 'site/index',
91
     *
92
     *     'POST <controller:[\w-]+>s' => '<controller>/create',
93
     *     '<controller:[\w-]+>s' => '<controller>/index',
94
     *
95
     *     'PUT <controller:[\w-]+>/<id:\d+>'    => '<controller>/update',
96
     *     'DELETE <controller:[\w-]+>/<id:\d+>' => '<controller>/delete',
97
     *     '<controller:[\w-]+>/<id:\d+>'        => '<controller>/view',
98
     * ];
99
     * ```
100
     *
101
     * Note that if you modify this property after the UrlManager object is created, make sure
102
     * you populate the array with rule objects instead of rule configurations.
103
     */
104
    public $rules = [];
105
    /**
106
     * @var string the URL suffix used when [[enablePrettyUrl]] is `true`.
107
     * For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
108
     * This property is used only if [[enablePrettyUrl]] is `true`.
109
     */
110
    public $suffix;
111
    /**
112
     * @var bool whether to show entry script name in the constructed URL. Defaults to `true`.
113
     * This property is used only if [[enablePrettyUrl]] is `true`.
114
     */
115
    public $showScriptName = true;
116
    /**
117
     * @var string the GET parameter name for route. This property is used only if [[enablePrettyUrl]] is `false`.
118
     */
119
    public $routeParam = 'r';
120
    /**
121
     * @var Cache|string the cache object or the application component ID of the cache object.
122
     * Compiled URL rules will be cached through this cache object, if it is available.
123
     *
124
     * After the UrlManager object is created, if you want to change this property,
125
     * you should only assign it with a cache object.
126
     * Set this property to `false` if you do not want to cache the URL rules.
127
     */
128
    public $cache = 'cache';
129
    /**
130
     * @var array the default configuration of URL rules. Individual rule configurations
131
     * specified via [[rules]] will take precedence when the same property of the rule is configured.
132
     */
133
    public $ruleConfig = ['class' => 'yii\web\UrlRule'];
134
    /**
135
     * @var UrlNormalizer|array|string|false the configuration for [[UrlNormalizer]] used by this UrlManager.
136
     * The default value is `false`, which means normalization will be skipped.
137
     * If you wish to enable URL normalization, you should configure this property manually.
138
     * For example:
139
     *
140
     * ```php
141
     * [
142
     *     'class' => 'yii\web\UrlNormalizer',
143
     *     'collapseSlashes' => true,
144
     *     'normalizeTrailingSlash' => true,
145
     * ]
146
     * ```
147
     *
148
     * @since 2.0.10
149
     */
150
    public $normalizer = false;
151
152
    /**
153
     * @var string the cache key for cached rules
154
     * @since 2.0.8
155
     */
156
    protected $cacheKey = __CLASS__;
157
158
    private $_baseUrl;
159
    private $_scriptUrl;
160
    private $_hostInfo;
161
    private $_ruleCache;
162
163
164
    /**
165
     * Initializes UrlManager.
166
     */
167 153
    public function init()
168
    {
169 153
        parent::init();
170
171 153
        if ($this->normalizer !== false) {
172 3
            $this->normalizer = Yii::createObject($this->normalizer);
173 3
            if (!$this->normalizer instanceof UrlNormalizer) {
174
                throw new InvalidConfigException('`' . get_class($this) . '::normalizer` should be an instance of `' . UrlNormalizer::className() . '` or its DI compatible configuration.');
175
            }
176 3
        }
177
178 153
        if (!$this->enablePrettyUrl || empty($this->rules)) {
179 88
            return;
180
        }
181 66
        if (is_string($this->cache)) {
182
            $this->cache = Yii::$app->get($this->cache, false);
183
        }
184 66
        if ($this->cache instanceof Cache) {
185
            $cacheKey = $this->cacheKey;
186
            $hash = md5(json_encode($this->rules));
187
            if (($data = $this->cache->get($cacheKey)) !== false && isset($data[1]) && $data[1] === $hash) {
188
                $this->rules = $data[0];
189
            } else {
190
                $this->rules = $this->buildRules($this->rules);
191
                $this->cache->set($cacheKey, [$this->rules, $hash]);
192
            }
193
        } else {
194 66
            $this->rules = $this->buildRules($this->rules);
195
        }
196 66
    }
197
198
    /**
199
     * Adds additional URL rules.
200
     *
201
     * This method will call [[buildRules()]] to parse the given rule declarations and then append or insert
202
     * them to the existing [[rules]].
203
     *
204
     * Note that if [[enablePrettyUrl]] is `false`, this method will do nothing.
205
     *
206
     * @param array $rules the new rules to be added. Each array element represents a single rule declaration.
207
     * Please refer to [[rules]] for the acceptable rule format.
208
     * @param bool $append whether to add the new rules by appending them to the end of the existing rules.
209
     */
210
    public function addRules($rules, $append = true)
211
    {
212
        if (!$this->enablePrettyUrl) {
213
            return;
214
        }
215
        $rules = $this->buildRules($rules);
216
        if ($append) {
217
            $this->rules = array_merge($this->rules, $rules);
218
        } else {
219
            $this->rules = array_merge($rules, $this->rules);
220
        }
221
    }
222
223
    /**
224
     * Builds URL rule objects from the given rule declarations.
225
     * @param array $rules the rule declarations. Each array element represents a single rule declaration.
226
     * Please refer to [[rules]] for the acceptable rule formats.
227
     * @return UrlRuleInterface[] the rule objects built from the given rule declarations
228
     * @throws InvalidConfigException if a rule declaration is invalid
229
     */
230 66
    protected function buildRules($rules)
231
    {
232 66
        $compiledRules = [];
233 66
        $verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS';
234 66
        foreach ($rules as $key => $rule) {
235 66
            if (is_string($rule)) {
236 61
                $rule = ['route' => $rule];
237 61
                if (preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches)) {
238 1
                    $rule['verb'] = explode(',', $matches[1]);
239
                    // rules that do not apply for GET requests should not be use to create urls
240 1
                    if (!in_array('GET', $rule['verb'])) {
241 1
                        $rule['mode'] = UrlRule::PARSING_ONLY;
242 1
                    }
243 1
                    $key = $matches[4];
244 1
                }
245 61
                $rule['pattern'] = $key;
246 61
            }
247 66
            if (is_array($rule)) {
248 66
                $rule = Yii::createObject(array_merge($this->ruleConfig, $rule));
249 66
            }
250 66
            if (!$rule instanceof UrlRuleInterface) {
251
                throw new InvalidConfigException('URL rule class must implement UrlRuleInterface.');
252
            }
253 66
            $compiledRules[] = $rule;
254 66
        }
255 66
        return $compiledRules;
256
    }
257
258
    /**
259
     * Parses the user request.
260
     * @param Request $request the request component
261
     * @return array|bool the route and the associated parameters. The latter is always empty
262
     * if [[enablePrettyUrl]] is `false`. `false` is returned if the current request cannot be successfully parsed.
263
     */
264 16
    public function parseRequest($request)
265
    {
266 16
        if ($this->enablePrettyUrl) {
267
            /* @var $rule UrlRule */
268 13
            foreach ($this->rules as $rule) {
269 9
                $result = $rule->parseRequest($this, $request);
270 9
                if (YII_DEBUG) {
271 9
                    Yii::trace([
272 9
                        'rule' => method_exists($rule, '__toString') ? $rule->__toString() : get_class($rule),
273 9
                        'match' => $result !== false,
274
                        'parent' => null
275 9
                    ], __METHOD__);
276 9
                }
277 9
                if ($result !== false) {
278 9
                    return $result;
279
                }
280 13
            }
281
282 11
            if ($this->enableStrictParsing) {
283 4
                return false;
284
            }
285
286 7
            Yii::trace('No matching URL rules. Using default URL parsing logic.', __METHOD__);
287
288 7
            $suffix = (string) $this->suffix;
289 7
            $pathInfo = $request->getPathInfo();
290 7
            $normalized = false;
291 7
            if ($this->normalizer !== false) {
292 1
                $pathInfo = $this->normalizer->normalizePathInfo($pathInfo, $suffix, $normalized);
293 1
            }
294 7
            if ($suffix !== '' && $pathInfo !== '') {
295 4
                $n = strlen($this->suffix);
296 4
                if (substr_compare($pathInfo, $this->suffix, -$n, $n) === 0) {
297 4
                    $pathInfo = substr($pathInfo, 0, -$n);
298 4
                    if ($pathInfo === '') {
299
                        // suffix alone is not allowed
300
                        return false;
301
                    }
302 4
                } else {
303
                    // suffix doesn't match
304 4
                    return false;
305
                }
306 4
            }
307
308 7
            if ($normalized) {
309
                // pathInfo was changed by normalizer - we need also normalize route
310 1
                return $this->normalizer->normalizeRoute([$pathInfo, []]);
311
            } else {
312 6
                return [$pathInfo, []];
313
            }
314
        } else {
315 3
            Yii::trace('Pretty URL not enabled. Using default URL parsing logic.', __METHOD__);
316 3
            $route = $request->getQueryParam($this->routeParam, '');
317 3
            if (is_array($route)) {
318 3
                $route = '';
319 3
            }
320
321 3
            return [(string) $route, []];
322
        }
323
    }
324
325
    /**
326
     * Creates a URL using the given route and query parameters.
327
     *
328
     * You may specify the route as a string, e.g., `site/index`. You may also use an array
329
     * if you want to specify additional query parameters for the URL being created. The
330
     * array format must be:
331
     *
332
     * ```php
333
     * // generates: /index.php?r=site%2Findex&param1=value1&param2=value2
334
     * ['site/index', 'param1' => 'value1', 'param2' => 'value2']
335
     * ```
336
     *
337
     * If you want to create a URL with an anchor, you can use the array format with a `#` parameter.
338
     * For example,
339
     *
340
     * ```php
341
     * // generates: /index.php?r=site%2Findex&param1=value1#name
342
     * ['site/index', 'param1' => 'value1', '#' => 'name']
343
     * ```
344
     *
345
     * The URL created is a relative one. Use [[createAbsoluteUrl()]] to create an absolute URL.
346
     *
347
     * Note that unlike [[\yii\helpers\Url::toRoute()]], this method always treats the given route
348
     * as an absolute route.
349
     *
350
     * @param string|array $params use a string to represent a route (e.g. `site/index`),
351
     * or an array to represent a route with query parameters (e.g. `['site/index', 'param1' => 'value1']`).
352
     * @return string the created URL
353
     */
354 120
    public function createUrl($params)
355
    {
356 120
        $params = (array) $params;
357 120
        $anchor = isset($params['#']) ? '#' . $params['#'] : '';
358 120
        unset($params['#'], $params[$this->routeParam]);
359
360 120
        $route = trim($params[0], '/');
361 120
        unset($params[0]);
362
363 120
        $baseUrl = $this->showScriptName || !$this->enablePrettyUrl ? $this->getScriptUrl() : $this->getBaseUrl();
364
365 120
        if ($this->enablePrettyUrl) {
366 74
            $cacheKey = $route . '?';
367 74
            foreach ($params as $key => $value) {
368 74
                if ($value !== null) {
369 74
                    $cacheKey .= $key . '&';
370 74
                }
371 74
            }
372
373 74
            $url = $this->getUrlFromCache($cacheKey, $route, $params);
374
375 74
            if ($url === false) {
376 74
                $cacheable = true;
377 74
                foreach ($this->rules as $rule) {
378
                    /* @var $rule UrlRule */
379 58
                    if (!empty($rule->defaults) && $rule->mode !== UrlRule::PARSING_ONLY) {
380
                        // if there is a rule with default values involved, the matching result may not be cached
381 12
                        $cacheable = false;
382 12
                    }
383 58
                    if (($url = $rule->createUrl($this, $route, $params)) !== false) {
384 57
                        if ($cacheable) {
385 45
                            $this->setRuleToCache($cacheKey, $rule);
386 45
                        }
387 57
                        break;
388
                    }
389 74
                }
390 74
            }
391
392 74
            if ($url !== false) {
393 57
                if (strpos($url, '://') !== false) {
394 8
                    if ($baseUrl !== '' && ($pos = strpos($url, '/', 8)) !== false) {
395 3
                        return substr($url, 0, $pos) . $baseUrl . substr($url, $pos) . $anchor;
396
                    } else {
397 5
                        return $url . $baseUrl . $anchor;
398
                    }
399
                } else {
400 49
                    return "$baseUrl/{$url}{$anchor}";
401
                }
402
            }
403
404 53
            if ($this->suffix !== null) {
405 16
                $route .= $this->suffix;
406 16
            }
407 53
            if (!empty($params) && ($query = http_build_query($params)) !== '') {
408 45
                $route .= '?' . $query;
409 45
            }
410
411 53
            return "$baseUrl/{$route}{$anchor}";
412
        } else {
413 46
            $url = "$baseUrl?{$this->routeParam}=" . urlencode($route);
414 46
            if (!empty($params) && ($query = http_build_query($params)) !== '') {
415 36
                $url .= '&' . $query;
416 36
            }
417
418 46
            return $url . $anchor;
419
        }
420
    }
421
422
    /**
423
     * Get URL from internal cache if exists
424
     * @param string $cacheKey generated cache key to store data.
425
     * @param string $route the route (e.g. `site/index`).
426
     * @param array $params rule params.
427
     * @return bool|string the created URL
428
     * @see createUrl()
429
     * @since 2.0.8
430
     */
431 74
    protected function getUrlFromCache($cacheKey, $route, $params)
432
    {
433 74
        if (!empty($this->_ruleCache[$cacheKey])) {
434 45
            foreach ($this->_ruleCache[$cacheKey] as $rule) {
435
                /* @var $rule UrlRule */
436 45
                if (($url = $rule->createUrl($this, $route, $params)) !== false) {
437 45
                    return $url;
438
                }
439
            }
440
        } else {
441 74
            $this->_ruleCache[$cacheKey] = [];
442
        }
443 74
        return false;
444
    }
445
446
    /**
447
     * Store rule (e.g. [[UrlRule]]) to internal cache
448
     * @param $cacheKey
449
     * @param UrlRuleInterface $rule
450
     * @since 2.0.8
451
     */
452 45
    protected function setRuleToCache($cacheKey, UrlRuleInterface $rule)
453
    {
454 45
        $this->_ruleCache[$cacheKey][] = $rule;
455 45
    }
456
457
    /**
458
     * Creates an absolute URL using the given route and query parameters.
459
     *
460
     * This method prepends the URL created by [[createUrl()]] with the [[hostInfo]].
461
     *
462
     * Note that unlike [[\yii\helpers\Url::toRoute()]], this method always treats the given route
463
     * as an absolute route.
464
     *
465
     * @param string|array $params use a string to represent a route (e.g. `site/index`),
466
     * or an array to represent a route with query parameters (e.g. `['site/index', 'param1' => 'value1']`).
467
     * @param string|null $scheme the scheme to use for the URL (either `http`, `https` or empty string
468
     * for protocol-relative URL).
469
     * If not specified the scheme of the current request will be used.
470
     * @return string the created URL
471
     * @see createUrl()
472
     */
473 54
    public function createAbsoluteUrl($params, $scheme = null)
474
    {
475 54
        $params = (array) $params;
476 54
        $url = $this->createUrl($params);
477 54
        if (strpos($url, '://') === false) {
478 50
            $url = $this->getHostInfo() . $url;
479 50
        }
480
481 54
        return Url::ensureScheme($url, $scheme);
482
    }
483
484
    /**
485
     * Returns the base URL that is used by [[createUrl()]] to prepend to created URLs.
486
     * It defaults to [[Request::baseUrl]].
487
     * This is mainly used when [[enablePrettyUrl]] is `true` and [[showScriptName]] is `false`.
488
     * @return string the base URL that is used by [[createUrl()]] to prepend to created URLs.
489
     * @throws InvalidConfigException if running in console application and [[baseUrl]] is not configured.
490
     */
491 37
    public function getBaseUrl()
492
    {
493 37
        if ($this->_baseUrl === null) {
494 1
            $request = Yii::$app->getRequest();
495 1
            if ($request instanceof Request) {
496 1
                $this->_baseUrl = $request->getBaseUrl();
497 1
            } else {
498
                throw new InvalidConfigException('Please configure UrlManager::baseUrl correctly as you are running a console application.');
499
            }
500 1
        }
501
502 37
        return $this->_baseUrl;
503
    }
504
505
    /**
506
     * Sets the base URL that is used by [[createUrl()]] to prepend to created URLs.
507
     * This is mainly used when [[enablePrettyUrl]] is `true` and [[showScriptName]] is `false`.
508
     * @param string $value the base URL that is used by [[createUrl()]] to prepend to created URLs.
509
     */
510 104
    public function setBaseUrl($value)
511
    {
512 104
        $this->_baseUrl = $value === null ? null : rtrim($value, '/');
513 104
    }
514
515
    /**
516
     * Returns the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
517
     * It defaults to [[Request::scriptUrl]].
518
     * This is mainly used when [[enablePrettyUrl]] is `false` or [[showScriptName]] is `true`.
519
     * @return string the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
520
     * @throws InvalidConfigException if running in console application and [[scriptUrl]] is not configured.
521
     */
522 84
    public function getScriptUrl()
523
    {
524 84
        if ($this->_scriptUrl === null) {
525 12
            $request = Yii::$app->getRequest();
526 12
            if ($request instanceof Request) {
527 12
                $this->_scriptUrl = $request->getScriptUrl();
528 12
            } else {
529
                throw new InvalidConfigException('Please configure UrlManager::scriptUrl correctly as you are running a console application.');
530
            }
531 12
        }
532
533 84
        return $this->_scriptUrl;
534
    }
535
536
    /**
537
     * Sets the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
538
     * This is mainly used when [[enablePrettyUrl]] is `false` or [[showScriptName]] is `true`.
539
     * @param string $value the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
540
     */
541 112
    public function setScriptUrl($value)
542
    {
543 112
        $this->_scriptUrl = $value;
544 112
    }
545
546
    /**
547
     * Returns the host info that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
548
     * @return string the host info (e.g. `http://www.example.com`) that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
549
     * @throws InvalidConfigException if running in console application and [[hostInfo]] is not configured.
550
     */
551 52
    public function getHostInfo()
552
    {
553 52
        if ($this->_hostInfo === null) {
554 7
            $request = Yii::$app->getRequest();
555 7
            if ($request instanceof \yii\web\Request) {
556 7
                $this->_hostInfo = $request->getHostInfo();
557 7
            } else {
558
                throw new InvalidConfigException('Please configure UrlManager::hostInfo correctly as you are running a console application.');
559
            }
560 7
        }
561
562 52
        return $this->_hostInfo;
563
    }
564
565
    /**
566
     * Sets the host info that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
567
     * @param string $value the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
568
     */
569 101
    public function setHostInfo($value)
570
    {
571 101
        $this->_hostInfo = $value === null ? null : rtrim($value, '/');
572 101
    }
573
}
574