Issues (62)

Security/Http/RouteMatcher.php (2 issues)

1
<?php
2
3
/*
4
 *
5
 * (c) Yaroslav Honcharuk <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Yarhon\RouteGuardBundle\Security\Http;
12
13
use Symfony\Component\Routing\Route;
14
15
/**
16
 * RouteMatcher checks if Route would always/possibly/never match a RequestConstraint.
17
 *
18
 * @author Yaroslav Honcharuk <[email protected]>
19
 */
20
class RouteMatcher
21
{
22
    /**
23
     * @var string
24
     */
25
    private $defaultHost;
26
27
    /**
28
     * @var RegexParser
29
     */
30
    private $regexParser;
31
32
    /**
33
     * @param string|null      $defaultHost
34
     * @param RegexParser|null $regexParser
35
     */
36 49
    public function __construct($defaultHost = null, RegexParser $regexParser = null)
37
    {
38 49
        $this->defaultHost = $defaultHost;
39 49
        $this->regexParser = $regexParser ?: new RegexParser();
40 49
    }
41
42
    /**
43
     * @param Route             $route
44
     * @param RequestConstraint $constraint
45
     *
46
     * @return bool|RequestConstraint Boolean true/false if route would always/never match RequestConstraint
47
     *                                A fresh RequestConstraint instance if route would possibly match RequestConstraint
48
     */
49 49
    public function matches(Route $route, RequestConstraint $constraint)
50
    {
51
        // Note: order of parameters should be equal to the order of RequestConstraint constructor arguments.
52
        $originalParameters = [
53 49
            'pathPattern' => $constraint->getPathPattern(),
54 49
            'hostPattern' => $constraint->getHostPattern(),
55 49
            'methods' => $constraint->getMethods(),
56 49
            'ips' => $constraint->getIps(),
57
        ];
58
59
        // All parameters equal to false (like nulls, empty strings and arrays) would be filtered out.
60 49
        $parameters = array_filter($originalParameters);
61
62 49
        if (0 === count($parameters)) {
63
            // If all parameters are empty, route would always match
64 1
            return true;
65
        }
66
67 48
        $matchResults = [];
68
69 48
        foreach ($parameters as $parameter => $value) {
70 48
            $matcher = 'match'.ucfirst($parameter);
71 48
            $matchResults[$parameter] = method_exists($this, $matcher) ? $this->$matcher($route, $value) : 0;
72
        }
73
74 48
        if (in_array(-1, $matchResults, true)) {
75
            // One of the parameters would never match
76 15
            return false;
77
        }
78
79 41
        if ([1] === array_unique(array_values($matchResults))) {
80
            // All parameters would always match
81 26
            return true;
82
        }
83
84 23
        $parameters = $originalParameters;
85
86
        // Set always matching parameters to null to avoid theirs further unnecessary matching.
87 23
        foreach (array_keys($matchResults, 1, true) as $parameter) {
88 2
            $parameters[$parameter] = null;
89
        }
90
91 23
        return new RequestConstraint(...array_values($parameters));
0 ignored issues
show
array_values($parameters) is expanded, but the parameter $pathPattern of Yarhon\RouteGuardBundle\...nstraint::__construct() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

91
        return new RequestConstraint(/** @scrutinizer ignore-type */ ...array_values($parameters));
Loading history...
92
    }
93
94
    /**
95
     * @param Route  $route
96
     * @param string $pattern
97
     *
98
     * @return int
99
     */
100 31
    private function matchPathPattern(Route $route, $pattern)
101
    {
102 31
        $compiledRoute = $route->compile();
103 31
        $staticPrefix = $compiledRoute->getStaticPrefix();
104
105
        // If route is static (no path variables), static prefix would be equal to the resulting url for the route.
106 31
        if (!$compiledRoute->getPathVariables()) {
107
            // Note: It's important to use the same regexp delimiters ("{}") used in \Symfony\Component\HttpFoundation\RequestMatcher::matches.
108 18
            return preg_match('{'.$pattern.'}', $staticPrefix) ? 1 : -1;
109
        }
110
111 21
        if ('' === $staticPrefix) {
112 1
            return 0;
113
        }
114
115 20
        return $this->matchStaticPrefixToPattern($staticPrefix, $pattern);
116
    }
117
118
    /**
119
     * @param Route  $route
120
     * @param string $pattern
121
     *
122
     * @return int
123
     */
124 13
    private function matchHostPattern(Route $route, $pattern)
125
    {
126 13
        $compiledRoute = $route->compile();
127
128 13
        $host = $route->getHost() ?: $this->defaultHost;
129
130 13
        if (!$host) {
131 2
            return 0;
132
        }
133
134 11
        if (!$compiledRoute->getHostVariables()) {
135
            // Note: It's important to use the same regexp delimiters ("{}") used in \Symfony\Component\HttpFoundation\RequestMatcher::matches.
136 7
            return preg_match('{'.$pattern.'}i', $host) ? 1 : -1;
137
        }
138
139 4
        $staticPrefix = strstr($host, '{', true);
140
141 4
        if ('' === $staticPrefix) {
142 1
            return 0;
143
        }
144
145 3
        return $this->matchStaticPrefixToPattern($staticPrefix, $pattern, false);
146
    }
147
148
    /**
149
     * @param Route $route
150
     * @param array $methods
151
     *
152
     * @return int
153
     */
154 6
    private function matchMethods(Route $route, array $methods)
155
    {
156 6
        if (!$route->getMethods()) {
157 2
            return 0;
158
        }
159
160 4
        $matchingMethods = array_intersect($route->getMethods(), $methods);
161
162 4
        if (!$matchingMethods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matchingMethods of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
163 1
            return -1;
164
        }
165
166 3
        if ($matchingMethods == $route->getMethods()) {
167 2
            return 1;
168
        }
169
170 1
        return 0;
171
    }
172
173
    /**
174
     * @param string $staticPrefix
175
     * @param string $pattern
176
     * @param bool   $caseSensitive
177
     *
178
     * @return int
179
     */
180 23
    private function matchStaticPrefixToPattern($staticPrefix, $pattern, $caseSensitive = true)
181
    {
182 23
        $parsedPattern = $this->regexParser->parse($pattern);
183 23
        $patternStaticPrefix = $parsedPattern['staticPrefix'];
184
185 23
        if ('' === $patternStaticPrefix) {
186 2
            return 0;
187
        }
188
189 21
        if ($parsedPattern['hasStringStartAssert']) {
190 15
            $compareLength = min(strlen($staticPrefix), strlen($patternStaticPrefix));
191 15
            $compareFunction = $caseSensitive ? 'strncmp' : 'strncasecmp';
192 15
            $compareResult = $compareFunction($staticPrefix, $patternStaticPrefix, $compareLength);
193
194 15
            if (0 !== $compareResult) {
195 9
                return -1;
196
            }
197
198 14
            if ($parsedPattern['dynamicPartIsWildcard'] && strlen($patternStaticPrefix) <= strlen($staticPrefix)) {
199 3
                return 1;
200
            }
201
        } else {
202 14
            $searchFunction = $caseSensitive ? 'strpos' : 'stripos';
203 14
            if ($parsedPattern['dynamicPartIsWildcard'] && false !== $searchFunction($staticPrefix, $patternStaticPrefix)) {
204 4
                return 1;
205
            }
206
        }
207
208 13
        return 0;
209
    }
210
}
211