Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

397 lines
14KB

  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. namespace yii\web;
  8. use Yii;
  9. use yii\base\Object;
  10. use yii\base\InvalidConfigException;
  11. /**
  12. * UrlRule represents a rule used by [[UrlManager]] for parsing and generating URLs.
  13. *
  14. * To define your own URL parsing and creation logic you can extend from this class
  15. * and add it to [[UrlManager::rules]] like this:
  16. *
  17. * ```php
  18. * 'rules' => [
  19. * ['class' => 'MyUrlRule', 'pattern' => '...', 'route' => 'site/index', ...],
  20. * // ...
  21. * ]
  22. * ```
  23. *
  24. * @author Qiang Xue <qiang.xue@gmail.com>
  25. * @since 2.0
  26. */
  27. class UrlRule extends Object implements UrlRuleInterface
  28. {
  29. /**
  30. * Set [[mode]] with this value to mark that this rule is for URL parsing only
  31. */
  32. const PARSING_ONLY = 1;
  33. /**
  34. * Set [[mode]] with this value to mark that this rule is for URL creation only
  35. */
  36. const CREATION_ONLY = 2;
  37. /**
  38. * @var string the name of this rule. If not set, it will use [[pattern]] as the name.
  39. */
  40. public $name;
  41. /**
  42. * On the rule initialization, the [[pattern]] matching parameters names will be replaced with [[placeholders]].
  43. * @var string the pattern used to parse and create the path info part of a URL.
  44. * @see host
  45. * @see placeholders
  46. */
  47. public $pattern;
  48. /**
  49. * @var string the pattern used to parse and create the host info part of a URL (e.g. `http://example.com`).
  50. * @see pattern
  51. */
  52. public $host;
  53. /**
  54. * @var string the route to the controller action
  55. */
  56. public $route;
  57. /**
  58. * @var array the default GET parameters (name => value) that this rule provides.
  59. * When this rule is used to parse the incoming request, the values declared in this property
  60. * will be injected into $_GET.
  61. */
  62. public $defaults = [];
  63. /**
  64. * @var string the URL suffix used for this rule.
  65. * For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
  66. * If not, the value of [[UrlManager::suffix]] will be used.
  67. */
  68. public $suffix;
  69. /**
  70. * @var string|array the HTTP verb (e.g. GET, POST, DELETE) that this rule should match.
  71. * Use array to represent multiple verbs that this rule may match.
  72. * If this property is not set, the rule can match any verb.
  73. * Note that this property is only used when parsing a request. It is ignored for URL creation.
  74. */
  75. public $verb;
  76. /**
  77. * @var integer a value indicating if this rule should be used for both request parsing and URL creation,
  78. * parsing only, or creation only.
  79. * If not set or 0, it means the rule is both request parsing and URL creation.
  80. * If it is [[PARSING_ONLY]], the rule is for request parsing only.
  81. * If it is [[CREATION_ONLY]], the rule is for URL creation only.
  82. */
  83. public $mode;
  84. /**
  85. * @var boolean a value indicating if parameters should be url encoded.
  86. */
  87. public $encodeParams = true;
  88. /**
  89. * @var array list of placeholders for matching parameters names. Used in [[parseRequest()]], [[createUrl()]].
  90. * On the rule initialization, the [[pattern]] parameters names will be replaced with placeholders.
  91. * This array contains relations between the original parameters names and their placeholders.
  92. * The array keys are the placeholders and the values are the original names.
  93. *
  94. * @see parseRequest()
  95. * @see createUrl()
  96. * @since 2.0.7
  97. */
  98. protected $placeholders = [];
  99. /**
  100. * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL.
  101. */
  102. private $_template;
  103. /**
  104. * @var string the regex for matching the route part. This is used in generating URL.
  105. */
  106. private $_routeRule;
  107. /**
  108. * @var array list of regex for matching parameters. This is used in generating URL.
  109. */
  110. private $_paramRules = [];
  111. /**
  112. * @var array list of parameters used in the route.
  113. */
  114. private $_routeParams = [];
  115. /**
  116. * Initializes this rule.
  117. */
  118. public function init()
  119. {
  120. if ($this->pattern === null) {
  121. throw new InvalidConfigException('UrlRule::pattern must be set.');
  122. }
  123. if ($this->route === null) {
  124. throw new InvalidConfigException('UrlRule::route must be set.');
  125. }
  126. if ($this->verb !== null) {
  127. if (is_array($this->verb)) {
  128. foreach ($this->verb as $i => $verb) {
  129. $this->verb[$i] = strtoupper($verb);
  130. }
  131. } else {
  132. $this->verb = [strtoupper($this->verb)];
  133. }
  134. }
  135. if ($this->name === null) {
  136. $this->name = $this->pattern;
  137. }
  138. $this->pattern = trim($this->pattern, '/');
  139. $this->route = trim($this->route, '/');
  140. if ($this->host !== null) {
  141. $this->host = rtrim($this->host, '/');
  142. $this->pattern = rtrim($this->host . '/' . $this->pattern, '/');
  143. } elseif ($this->pattern === '') {
  144. $this->_template = '';
  145. $this->pattern = '#^$#u';
  146. return;
  147. } elseif (($pos = strpos($this->pattern, '://')) !== false) {
  148. if (($pos2 = strpos($this->pattern, '/', $pos + 3)) !== false) {
  149. $this->host = substr($this->pattern, 0, $pos2);
  150. } else {
  151. $this->host = $this->pattern;
  152. }
  153. } else {
  154. $this->pattern = '/' . $this->pattern . '/';
  155. }
  156. if (strpos($this->route, '<') !== false && preg_match_all('/<([\w._-]+)>/', $this->route, $matches)) {
  157. foreach ($matches[1] as $name) {
  158. $this->_routeParams[$name] = "<$name>";
  159. }
  160. }
  161. $tr = [
  162. '.' => '\\.',
  163. '*' => '\\*',
  164. '$' => '\\$',
  165. '[' => '\\[',
  166. ']' => '\\]',
  167. '(' => '\\(',
  168. ')' => '\\)',
  169. ];
  170. $tr2 = [];
  171. if (preg_match_all('/<([\w._-]+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
  172. foreach ($matches as $match) {
  173. $name = $match[1][0];
  174. $pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+';
  175. $placeholder = 'a' . hash('crc32b', $name); // placeholder must begin with a letter
  176. $this->placeholders[$placeholder] = $name;
  177. if (array_key_exists($name, $this->defaults)) {
  178. $length = strlen($match[0][0]);
  179. $offset = $match[0][1];
  180. if ($offset > 1 && $this->pattern[$offset - 1] === '/' && (!isset($this->pattern[$offset + $length]) || $this->pattern[$offset + $length] === '/')) {
  181. $tr["/<$name>"] = "(/(?P<$placeholder>$pattern))?";
  182. } else {
  183. $tr["<$name>"] = "(?P<$placeholder>$pattern)?";
  184. }
  185. } else {
  186. $tr["<$name>"] = "(?P<$placeholder>$pattern)";
  187. }
  188. if (isset($this->_routeParams[$name])) {
  189. $tr2["<$name>"] = "(?P<$placeholder>$pattern)";
  190. } else {
  191. $this->_paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#u";
  192. }
  193. }
  194. }
  195. $this->_template = preg_replace('/<([\w._-]+):?([^>]+)?>/', '<$1>', $this->pattern);
  196. $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u';
  197. if (!empty($this->_routeParams)) {
  198. $this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u';
  199. }
  200. }
  201. /**
  202. * Parses the given request and returns the corresponding route and parameters.
  203. * @param UrlManager $manager the URL manager
  204. * @param Request $request the request component
  205. * @return array|boolean the parsing result. The route and the parameters are returned as an array.
  206. * If false, it means this rule cannot be used to parse this path info.
  207. */
  208. public function parseRequest($manager, $request)
  209. {
  210. if ($this->mode === self::CREATION_ONLY) {
  211. return false;
  212. }
  213. if (!empty($this->verb) && !in_array($request->getMethod(), $this->verb, true)) {
  214. return false;
  215. }
  216. $pathInfo = $request->getPathInfo();
  217. $suffix = (string)($this->suffix === null ? $manager->suffix : $this->suffix);
  218. if ($suffix !== '' && $pathInfo !== '') {
  219. $n = strlen($suffix);
  220. if (substr_compare($pathInfo, $suffix, -$n, $n) === 0) {
  221. $pathInfo = substr($pathInfo, 0, -$n);
  222. if ($pathInfo === '') {
  223. // suffix alone is not allowed
  224. return false;
  225. }
  226. } else {
  227. return false;
  228. }
  229. }
  230. if ($this->host !== null) {
  231. $pathInfo = strtolower($request->getHostInfo()) . ($pathInfo === '' ? '' : '/' . $pathInfo);
  232. }
  233. if (!preg_match($this->pattern, $pathInfo, $matches)) {
  234. return false;
  235. }
  236. $matches = $this->substitutePlaceholderNames($matches);
  237. foreach ($this->defaults as $name => $value) {
  238. if (!isset($matches[$name]) || $matches[$name] === '') {
  239. $matches[$name] = $value;
  240. }
  241. }
  242. $params = $this->defaults;
  243. $tr = [];
  244. foreach ($matches as $name => $value) {
  245. if (isset($this->_routeParams[$name])) {
  246. $tr[$this->_routeParams[$name]] = $value;
  247. unset($params[$name]);
  248. } elseif (isset($this->_paramRules[$name])) {
  249. $params[$name] = $value;
  250. }
  251. }
  252. if ($this->_routeRule !== null) {
  253. $route = strtr($this->route, $tr);
  254. } else {
  255. $route = $this->route;
  256. }
  257. Yii::trace("Request parsed with URL rule: {$this->name}", __METHOD__);
  258. return [$route, $params];
  259. }
  260. /**
  261. * Creates a URL according to the given route and parameters.
  262. * @param UrlManager $manager the URL manager
  263. * @param string $route the route. It should not have slashes at the beginning or the end.
  264. * @param array $params the parameters
  265. * @return string|boolean the created URL, or false if this rule cannot be used for creating this URL.
  266. */
  267. public function createUrl($manager, $route, $params)
  268. {
  269. if ($this->mode === self::PARSING_ONLY) {
  270. return false;
  271. }
  272. $tr = [];
  273. // match the route part first
  274. if ($route !== $this->route) {
  275. if ($this->_routeRule !== null && preg_match($this->_routeRule, $route, $matches)) {
  276. $matches = $this->substitutePlaceholderNames($matches);
  277. foreach ($this->_routeParams as $name => $token) {
  278. if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) {
  279. $tr[$token] = '';
  280. } else {
  281. $tr[$token] = $matches[$name];
  282. }
  283. }
  284. } else {
  285. return false;
  286. }
  287. }
  288. // match default params
  289. // if a default param is not in the route pattern, its value must also be matched
  290. foreach ($this->defaults as $name => $value) {
  291. if (isset($this->_routeParams[$name])) {
  292. continue;
  293. }
  294. if (!isset($params[$name])) {
  295. return false;
  296. } elseif (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically
  297. unset($params[$name]);
  298. if (isset($this->_paramRules[$name])) {
  299. $tr["<$name>"] = '';
  300. }
  301. } elseif (!isset($this->_paramRules[$name])) {
  302. return false;
  303. }
  304. }
  305. // match params in the pattern
  306. foreach ($this->_paramRules as $name => $rule) {
  307. if (isset($params[$name]) && !is_array($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) {
  308. $tr["<$name>"] = $this->encodeParams ? urlencode($params[$name]) : $params[$name];
  309. unset($params[$name]);
  310. } elseif (!isset($this->defaults[$name]) || isset($params[$name])) {
  311. return false;
  312. }
  313. }
  314. $url = trim(strtr($this->_template, $tr), '/');
  315. if ($this->host !== null) {
  316. $pos = strpos($url, '/', 8);
  317. if ($pos !== false) {
  318. $url = substr($url, 0, $pos) . preg_replace('#/+#', '/', substr($url, $pos));
  319. }
  320. } elseif (strpos($url, '//') !== false) {
  321. $url = preg_replace('#/+#', '/', $url);
  322. }
  323. if ($url !== '') {
  324. $url .= ($this->suffix === null ? $manager->suffix : $this->suffix);
  325. }
  326. if (!empty($params) && ($query = http_build_query($params)) !== '') {
  327. $url .= '?' . $query;
  328. }
  329. return $url;
  330. }
  331. /**
  332. * Returns list of regex for matching parameter.
  333. * @return array parameter keys and regexp rules.
  334. *
  335. * @since 2.0.6
  336. */
  337. protected function getParamRules()
  338. {
  339. return $this->_paramRules;
  340. }
  341. /**
  342. * Iterates over [[placeholders]] and checks whether each placeholder exists as a key in $matches array.
  343. * When found - replaces this placeholder key with a appropriate name of matching parameter.
  344. * Used in [[parseRequest()]], [[createUrl()]].
  345. *
  346. * @param array $matches result of `preg_match()` call
  347. * @return array input array with replaced placeholder keys
  348. * @see placeholders
  349. * @since 2.0.7
  350. */
  351. protected function substitutePlaceholderNames(array $matches)
  352. {
  353. foreach ($this->placeholders as $placeholder => $name) {
  354. if (isset($matches[$placeholder])) {
  355. $matches[$name] = $matches[$placeholder];
  356. unset($matches[$placeholder]);
  357. }
  358. }
  359. return $matches;
  360. }
  361. }