aa7a75e0b009bd01ec39063a8b3b8a459b18fb60
[yaffs-website] / vendor / symfony / routing / RouteCompiler.php
1 <?php
2
3 /*
4  * This file is part of the Symfony package.
5  *
6  * (c) Fabien Potencier <fabien@symfony.com>
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Symfony\Component\Routing;
13
14 /**
15  * RouteCompiler compiles Route instances to CompiledRoute instances.
16  *
17  * @author Fabien Potencier <fabien@symfony.com>
18  * @author Tobias Schultze <http://tobion.de>
19  */
20 class RouteCompiler implements RouteCompilerInterface
21 {
22     const REGEX_DELIMITER = '#';
23
24     /**
25      * This string defines the characters that are automatically considered separators in front of
26      * optional placeholders (with default and no static text following). Such a single separator
27      * can be left out together with the optional placeholder from matching and generating URLs.
28      */
29     const SEPARATORS = '/,;.:-_~+*=@|';
30
31     /**
32      * The maximum supported length of a PCRE subpattern name
33      * http://pcre.org/current/doc/html/pcre2pattern.html#SEC16.
34      *
35      * @internal
36      */
37     const VARIABLE_MAXIMUM_LENGTH = 32;
38
39     /**
40      * {@inheritdoc}
41      *
42      * @throws \InvalidArgumentException If a path variable is named _fragment
43      * @throws \LogicException           If a variable is referenced more than once
44      * @throws \DomainException          If a variable name starts with a digit or if it is too long to be successfully used as
45      *                                   a PCRE subpattern.
46      */
47     public static function compile(Route $route)
48     {
49         $hostVariables = array();
50         $variables = array();
51         $hostRegex = null;
52         $hostTokens = array();
53
54         if ('' !== $host = $route->getHost()) {
55             $result = self::compilePattern($route, $host, true);
56
57             $hostVariables = $result['variables'];
58             $variables = $hostVariables;
59
60             $hostTokens = $result['tokens'];
61             $hostRegex = $result['regex'];
62         }
63
64         $path = $route->getPath();
65
66         $result = self::compilePattern($route, $path, false);
67
68         $staticPrefix = $result['staticPrefix'];
69
70         $pathVariables = $result['variables'];
71
72         foreach ($pathVariables as $pathParam) {
73             if ('_fragment' === $pathParam) {
74                 throw new \InvalidArgumentException(sprintf('Route pattern "%s" cannot contain "_fragment" as a path parameter.', $route->getPath()));
75             }
76         }
77
78         $variables = array_merge($variables, $pathVariables);
79
80         $tokens = $result['tokens'];
81         $regex = $result['regex'];
82
83         return new CompiledRoute(
84             $staticPrefix,
85             $regex,
86             $tokens,
87             $pathVariables,
88             $hostRegex,
89             $hostTokens,
90             $hostVariables,
91             array_unique($variables)
92         );
93     }
94
95     private static function compilePattern(Route $route, $pattern, $isHost)
96     {
97         $tokens = array();
98         $variables = array();
99         $matches = array();
100         $pos = 0;
101         $defaultSeparator = $isHost ? '.' : '/';
102         $useUtf8 = preg_match('//u', $pattern);
103         $needsUtf8 = $route->getOption('utf8');
104
105         if (!$needsUtf8 && $useUtf8 && preg_match('/[\x80-\xFF]/', $pattern)) {
106             $needsUtf8 = true;
107             @trigger_error(sprintf('Using UTF-8 route patterns without setting the "utf8" option is deprecated since Symfony 3.2 and will throw a LogicException in 4.0. Turn on the "utf8" route option for pattern "%s".', $pattern), E_USER_DEPRECATED);
108         }
109         if (!$useUtf8 && $needsUtf8) {
110             throw new \LogicException(sprintf('Cannot mix UTF-8 requirements with non-UTF-8 pattern "%s".', $pattern));
111         }
112
113         // Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable
114         // in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself.
115         preg_match_all('#\{\w+\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
116         foreach ($matches as $match) {
117             $varName = substr($match[0][0], 1, -1);
118             // get all static text preceding the current variable
119             $precedingText = substr($pattern, $pos, $match[0][1] - $pos);
120             $pos = $match[0][1] + strlen($match[0][0]);
121
122             if (!strlen($precedingText)) {
123                 $precedingChar = '';
124             } elseif ($useUtf8) {
125                 preg_match('/.$/u', $precedingText, $precedingChar);
126                 $precedingChar = $precedingChar[0];
127             } else {
128                 $precedingChar = substr($precedingText, -1);
129             }
130             $isSeparator = '' !== $precedingChar && false !== strpos(static::SEPARATORS, $precedingChar);
131
132             // A PCRE subpattern name must start with a non-digit. Also a PHP variable cannot start with a digit so the
133             // variable would not be usable as a Controller action argument.
134             if (preg_match('/^\d/', $varName)) {
135                 throw new \DomainException(sprintf('Variable name "%s" cannot start with a digit in route pattern "%s". Please use a different name.', $varName, $pattern));
136             }
137             if (in_array($varName, $variables)) {
138                 throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $pattern, $varName));
139             }
140
141             if (strlen($varName) > self::VARIABLE_MAXIMUM_LENGTH) {
142                 throw new \DomainException(sprintf('Variable name "%s" cannot be longer than %s characters in route pattern "%s". Please use a shorter name.', $varName, self::VARIABLE_MAXIMUM_LENGTH, $pattern));
143             }
144
145             if ($isSeparator && $precedingText !== $precedingChar) {
146                 $tokens[] = array('text', substr($precedingText, 0, -strlen($precedingChar)));
147             } elseif (!$isSeparator && strlen($precedingText) > 0) {
148                 $tokens[] = array('text', $precedingText);
149             }
150
151             $regexp = $route->getRequirement($varName);
152             if (null === $regexp) {
153                 $followingPattern = (string) substr($pattern, $pos);
154                 // Find the next static character after the variable that functions as a separator. By default, this separator and '/'
155                 // are disallowed for the variable. This default requirement makes sure that optional variables can be matched at all
156                 // and that the generating-matching-combination of URLs unambiguous, i.e. the params used for generating the URL are
157                 // the same that will be matched. Example: new Route('/{page}.{_format}', array('_format' => 'html'))
158                 // If {page} would also match the separating dot, {_format} would never match as {page} will eagerly consume everything.
159                 // Also even if {_format} was not optional the requirement prevents that {page} matches something that was originally
160                 // part of {_format} when generating the URL, e.g. _format = 'mobile.html'.
161                 $nextSeparator = self::findNextSeparator($followingPattern, $useUtf8);
162                 $regexp = sprintf(
163                     '[^%s%s]+',
164                     preg_quote($defaultSeparator, self::REGEX_DELIMITER),
165                     $defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator, self::REGEX_DELIMITER) : ''
166                 );
167                 if (('' !== $nextSeparator && !preg_match('#^\{\w+\}#', $followingPattern)) || '' === $followingPattern) {
168                     // When we have a separator, which is disallowed for the variable, we can optimize the regex with a possessive
169                     // quantifier. This prevents useless backtracking of PCRE and improves performance by 20% for matching those patterns.
170                     // Given the above example, there is no point in backtracking into {page} (that forbids the dot) when a dot must follow
171                     // after it. This optimization cannot be applied when the next char is no real separator or when the next variable is
172                     // directly adjacent, e.g. '/{x}{y}'.
173                     $regexp .= '+';
174                 }
175             } else {
176                 if (!preg_match('//u', $regexp)) {
177                     $useUtf8 = false;
178                 } elseif (!$needsUtf8 && preg_match('/[\x80-\xFF]|(?<!\\\\)\\\\(?:\\\\\\\\)*+(?-i:X|[pP][\{CLMNPSZ]|x\{[A-Fa-f0-9]{3})/', $regexp)) {
179                     $needsUtf8 = true;
180                     @trigger_error(sprintf('Using UTF-8 route requirements without setting the "utf8" option is deprecated since Symfony 3.2 and will throw a LogicException in 4.0. Turn on the "utf8" route option for variable "%s" in pattern "%s".', $varName, $pattern), E_USER_DEPRECATED);
181                 }
182                 if (!$useUtf8 && $needsUtf8) {
183                     throw new \LogicException(sprintf('Cannot mix UTF-8 requirement with non-UTF-8 charset for variable "%s" in pattern "%s".', $varName, $pattern));
184                 }
185             }
186
187             $tokens[] = array('variable', $isSeparator ? $precedingChar : '', $regexp, $varName);
188             $variables[] = $varName;
189         }
190
191         if ($pos < strlen($pattern)) {
192             $tokens[] = array('text', substr($pattern, $pos));
193         }
194
195         // find the first optional token
196         $firstOptional = PHP_INT_MAX;
197         if (!$isHost) {
198             for ($i = count($tokens) - 1; $i >= 0; --$i) {
199                 $token = $tokens[$i];
200                 if ('variable' === $token[0] && $route->hasDefault($token[3])) {
201                     $firstOptional = $i;
202                 } else {
203                     break;
204                 }
205             }
206         }
207
208         // compute the matching regexp
209         $regexp = '';
210         for ($i = 0, $nbToken = count($tokens); $i < $nbToken; ++$i) {
211             $regexp .= self::computeRegexp($tokens, $i, $firstOptional);
212         }
213         $regexp = self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s'.($isHost ? 'i' : '');
214
215         // enable Utf8 matching if really required
216         if ($needsUtf8) {
217             $regexp .= 'u';
218             for ($i = 0, $nbToken = count($tokens); $i < $nbToken; ++$i) {
219                 if ('variable' === $tokens[$i][0]) {
220                     $tokens[$i][] = true;
221                 }
222             }
223         }
224
225         return array(
226             'staticPrefix' => 'text' === $tokens[0][0] ? $tokens[0][1] : '',
227             'regex' => $regexp,
228             'tokens' => array_reverse($tokens),
229             'variables' => $variables,
230         );
231     }
232
233     /**
234      * Returns the next static character in the Route pattern that will serve as a separator.
235      *
236      * @param string $pattern The route pattern
237      * @param bool   $useUtf8 Whether the character is encoded in UTF-8 or not
238      *
239      * @return string The next static character that functions as separator (or empty string when none available)
240      */
241     private static function findNextSeparator($pattern, $useUtf8)
242     {
243         if ('' == $pattern) {
244             // return empty string if pattern is empty or false (false which can be returned by substr)
245             return '';
246         }
247         // first remove all placeholders from the pattern so we can find the next real static character
248         if ('' === $pattern = preg_replace('#\{\w+\}#', '', $pattern)) {
249             return '';
250         }
251         if ($useUtf8) {
252             preg_match('/^./u', $pattern, $pattern);
253         }
254
255         return false !== strpos(static::SEPARATORS, $pattern[0]) ? $pattern[0] : '';
256     }
257
258     /**
259      * Computes the regexp used to match a specific token. It can be static text or a subpattern.
260      *
261      * @param array $tokens        The route tokens
262      * @param int   $index         The index of the current token
263      * @param int   $firstOptional The index of the first optional token
264      *
265      * @return string The regexp pattern for a single token
266      */
267     private static function computeRegexp(array $tokens, $index, $firstOptional)
268     {
269         $token = $tokens[$index];
270         if ('text' === $token[0]) {
271             // Text tokens
272             return preg_quote($token[1], self::REGEX_DELIMITER);
273         } else {
274             // Variable tokens
275             if (0 === $index && 0 === $firstOptional) {
276                 // When the only token is an optional variable token, the separator is required
277                 return sprintf('%s(?P<%s>%s)?', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
278             } else {
279                 $regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
280                 if ($index >= $firstOptional) {
281                     // Enclose each optional token in a subpattern to make it optional.
282                     // "?:" means it is non-capturing, i.e. the portion of the subject string that
283                     // matched the optional subpattern is not passed back.
284                     $regexp = "(?:$regexp";
285                     $nbTokens = count($tokens);
286                     if ($nbTokens - 1 == $index) {
287                         // Close the optional subpatterns
288                         $regexp .= str_repeat(')?', $nbTokens - $firstOptional - (0 === $firstOptional ? 1 : 0));
289                     }
290                 }
291
292                 return $regexp;
293             }
294         }
295     }
296 }