Drupal investigation

ApacheMatcherDumper.php 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Routing\Matcher\Dumper;
  11. @trigger_error('The '.__NAMESPACE__.'\ApacheMatcherDumper class is deprecated since version 2.5 and will be removed in 3.0. It\'s hard to replicate the behaviour of the PHP implementation and the performance gains are minimal.', E_USER_DEPRECATED);
  12. use Symfony\Component\Routing\Route;
  13. /**
  14. * Dumps a set of Apache mod_rewrite rules.
  15. *
  16. * @deprecated since version 2.5, to be removed in 3.0.
  17. * The performance gains are minimal and it's very hard to replicate
  18. * the behavior of PHP implementation.
  19. *
  20. * @author Fabien Potencier <fabien@symfony.com>
  21. * @author Kris Wallsmith <kris@symfony.com>
  22. */
  23. class ApacheMatcherDumper extends MatcherDumper
  24. {
  25. /**
  26. * Dumps a set of Apache mod_rewrite rules.
  27. *
  28. * Available options:
  29. *
  30. * * script_name: The script name (app.php by default)
  31. * * base_uri: The base URI ("" by default)
  32. *
  33. * @param array $options An array of options
  34. *
  35. * @return string A string to be used as Apache rewrite rules
  36. *
  37. * @throws \LogicException When the route regex is invalid
  38. */
  39. public function dump(array $options = array())
  40. {
  41. $options = array_merge(array(
  42. 'script_name' => 'app.php',
  43. 'base_uri' => '',
  44. ), $options);
  45. $options['script_name'] = self::escape($options['script_name'], ' ', '\\');
  46. $rules = array("# skip \"real\" requests\nRewriteCond %{REQUEST_FILENAME} -f\nRewriteRule .* - [QSA,L]");
  47. $methodVars = array();
  48. $hostRegexUnique = 0;
  49. $prevHostRegex = '';
  50. foreach ($this->getRoutes()->all() as $name => $route) {
  51. if ($route->getCondition()) {
  52. throw new \LogicException(sprintf('Unable to dump the routes for Apache as route "%s" has a condition.', $name));
  53. }
  54. $compiledRoute = $route->compile();
  55. $hostRegex = $compiledRoute->getHostRegex();
  56. if (null !== $hostRegex && $prevHostRegex !== $hostRegex) {
  57. $prevHostRegex = $hostRegex;
  58. ++$hostRegexUnique;
  59. $rule = array();
  60. $regex = $this->regexToApacheRegex($hostRegex);
  61. $regex = self::escape($regex, ' ', '\\');
  62. $rule[] = sprintf('RewriteCond %%{HTTP:Host} %s', $regex);
  63. $variables = array();
  64. $variables[] = sprintf('E=__ROUTING_host_%s:1', $hostRegexUnique);
  65. foreach ($compiledRoute->getHostVariables() as $i => $variable) {
  66. $variables[] = sprintf('E=__ROUTING_host_%s_%s:%%%d', $hostRegexUnique, $variable, $i + 1);
  67. }
  68. $variables = implode(',', $variables);
  69. $rule[] = sprintf('RewriteRule .? - [%s]', $variables);
  70. $rules[] = implode("\n", $rule);
  71. }
  72. $rules[] = $this->dumpRoute($name, $route, $options, $hostRegexUnique);
  73. $methodVars = array_merge($methodVars, $route->getMethods());
  74. }
  75. if (0 < count($methodVars)) {
  76. $rule = array('# 405 Method Not Allowed');
  77. $methodVars = array_values(array_unique($methodVars));
  78. if (in_array('GET', $methodVars) && !in_array('HEAD', $methodVars)) {
  79. $methodVars[] = 'HEAD';
  80. }
  81. foreach ($methodVars as $i => $methodVar) {
  82. $rule[] = sprintf('RewriteCond %%{ENV:_ROUTING__allow_%s} =1%s', $methodVar, isset($methodVars[$i + 1]) ? ' [OR]' : '');
  83. }
  84. $rule[] = sprintf('RewriteRule .* %s [QSA,L]', $options['script_name']);
  85. $rules[] = implode("\n", $rule);
  86. }
  87. return implode("\n\n", $rules)."\n";
  88. }
  89. /**
  90. * Dumps a single route.
  91. *
  92. * @param string $name Route name
  93. * @param Route $route The route
  94. * @param array $options Options
  95. * @param bool $hostRegexUnique Unique identifier for the host regex
  96. *
  97. * @return string The compiled route
  98. */
  99. private function dumpRoute($name, $route, array $options, $hostRegexUnique)
  100. {
  101. $compiledRoute = $route->compile();
  102. // prepare the apache regex
  103. $regex = $this->regexToApacheRegex($compiledRoute->getRegex());
  104. $regex = '^'.self::escape(preg_quote($options['base_uri']).substr($regex, 1), ' ', '\\');
  105. $methods = $this->getRouteMethods($route);
  106. $hasTrailingSlash = (!$methods || in_array('HEAD', $methods)) && '/$' === substr($regex, -2) && '^/$' !== $regex;
  107. $variables = array('E=_ROUTING_route:'.$name);
  108. foreach ($compiledRoute->getHostVariables() as $variable) {
  109. $variables[] = sprintf('E=_ROUTING_param_%s:%%{ENV:__ROUTING_host_%s_%s}', $variable, $hostRegexUnique, $variable);
  110. }
  111. foreach ($compiledRoute->getPathVariables() as $i => $variable) {
  112. $variables[] = 'E=_ROUTING_param_'.$variable.':%'.($i + 1);
  113. }
  114. foreach ($this->normalizeValues($route->getDefaults()) as $key => $value) {
  115. $variables[] = 'E=_ROUTING_default_'.$key.':'.strtr($value, array(
  116. ':' => '\\:',
  117. '=' => '\\=',
  118. '\\' => '\\\\',
  119. ' ' => '\\ ',
  120. ));
  121. }
  122. $variables = implode(',', $variables);
  123. $rule = array("# $name");
  124. // method mismatch
  125. if (0 < count($methods)) {
  126. $allow = array();
  127. foreach ($methods as $method) {
  128. $allow[] = 'E=_ROUTING_allow_'.$method.':1';
  129. }
  130. if ($compiledRoute->getHostRegex()) {
  131. $rule[] = sprintf('RewriteCond %%{ENV:__ROUTING_host_%s} =1', $hostRegexUnique);
  132. }
  133. $rule[] = "RewriteCond %{REQUEST_URI} $regex";
  134. $rule[] = sprintf('RewriteCond %%{REQUEST_METHOD} !^(%s)$ [NC]', implode('|', $methods));
  135. $rule[] = sprintf('RewriteRule .* - [S=%d,%s]', $hasTrailingSlash ? 2 : 1, implode(',', $allow));
  136. }
  137. // redirect with trailing slash appended
  138. if ($hasTrailingSlash) {
  139. if ($compiledRoute->getHostRegex()) {
  140. $rule[] = sprintf('RewriteCond %%{ENV:__ROUTING_host_%s} =1', $hostRegexUnique);
  141. }
  142. $rule[] = 'RewriteCond %{REQUEST_URI} '.substr($regex, 0, -2).'$';
  143. $rule[] = 'RewriteRule .* $0/ [QSA,L,R=301]';
  144. }
  145. // the main rule
  146. if ($compiledRoute->getHostRegex()) {
  147. $rule[] = sprintf('RewriteCond %%{ENV:__ROUTING_host_%s} =1', $hostRegexUnique);
  148. }
  149. $rule[] = "RewriteCond %{REQUEST_URI} $regex";
  150. $rule[] = "RewriteRule .* {$options['script_name']} [QSA,L,$variables]";
  151. return implode("\n", $rule);
  152. }
  153. /**
  154. * Returns methods allowed for a route.
  155. *
  156. * @param Route $route The route
  157. *
  158. * @return array The methods
  159. */
  160. private function getRouteMethods(Route $route)
  161. {
  162. $methods = $route->getMethods();
  163. // GET and HEAD are equivalent
  164. if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
  165. $methods[] = 'HEAD';
  166. }
  167. return $methods;
  168. }
  169. /**
  170. * Converts a regex to make it suitable for mod_rewrite.
  171. *
  172. * @param string $regex The regex
  173. *
  174. * @return string The converted regex
  175. */
  176. private function regexToApacheRegex($regex)
  177. {
  178. $regexPatternEnd = strrpos($regex, $regex[0]);
  179. return preg_replace('/\?P<.+?>/', '', substr($regex, 1, $regexPatternEnd - 1));
  180. }
  181. /**
  182. * Escapes a string.
  183. *
  184. * @param string $string The string to be escaped
  185. * @param string $char The character to be escaped
  186. * @param string $with The character to be used for escaping
  187. *
  188. * @return string The escaped string
  189. */
  190. private static function escape($string, $char, $with)
  191. {
  192. $escaped = false;
  193. $output = '';
  194. foreach (str_split($string) as $symbol) {
  195. if ($escaped) {
  196. $output .= $symbol;
  197. $escaped = false;
  198. continue;
  199. }
  200. if ($symbol === $char) {
  201. $output .= $with.$char;
  202. continue;
  203. }
  204. if ($symbol === $with) {
  205. $escaped = true;
  206. }
  207. $output .= $symbol;
  208. }
  209. return $output;
  210. }
  211. /**
  212. * Normalizes an array of values.
  213. *
  214. * @param array $values
  215. *
  216. * @return string[]
  217. */
  218. private function normalizeValues(array $values)
  219. {
  220. $normalizedValues = array();
  221. foreach ($values as $key => $value) {
  222. if (is_array($value)) {
  223. foreach ($value as $index => $bit) {
  224. $normalizedValues[sprintf('%s[%s]', $key, $index)] = $bit;
  225. }
  226. } else {
  227. $normalizedValues[$key] = (string) $value;
  228. }
  229. }
  230. return $normalizedValues;
  231. }
  232. }