← All changes
|
otto/class-metasync-otto-excluded-urls-database.php
+146
-36
2.6.21
→
2.7.0
View file →
| @@ -200,52 +200,162 @@ | ||
| 200 | 200 | $url = trim($url); |
| 201 | 201 | $url = rtrim($url, '/'); |
| 202 | 202 | |
| 203 | 203 | foreach ($excluded_urls as $excluded) { |
| 204 | - $pattern = trim($excluded->url_pattern); | |
| 205 | - $pattern = rtrim($pattern, '/'); | |
| 206 | - $pattern_type = $excluded->pattern_type; | |
| 204 | + if (self::pattern_matches($url, $excluded->url_pattern, $excluded->pattern_type)) { | |
| 205 | + return true; | |
| 206 | + } | |
| 207 | + } | |
| 207 | 208 | |
| 208 | - switch ($pattern_type) { | |
| 209 | - case 'exact': | |
| 210 | - if ($url === $pattern) { | |
| 211 | - return true; | |
| 212 | - } | |
| 213 | - break; | |
| 209 | + return false; | |
| 210 | + } | |
| 214 | 211 | |
| 215 | - case 'contain': | |
| 216 | - if (strpos($url, $pattern) !== false) { | |
| 217 | - return true; | |
| 218 | - } | |
| 219 | - break; | |
| 212 | + /** | |
| 213 | + * Match a URL against a single exclusion pattern. | |
| 214 | + * | |
| 215 | + * Single source of truth for exclusion matching. Both the queue/SEO-write path | |
| 216 | + * (self::is_url_excluded()) and the visitor render gate | |
| 217 | + * (metasync_is_otto_url_manually_excluded() in otto/otto_pixel.php) call this, so the | |
| 218 | + * set of supported pattern types cannot drift apart between the two paths again. | |
| 219 | + * | |
| 220 | + * Failure is deliberately fail-open (return false = not excluded = OTTO still renders): | |
| 221 | + * an unevaluable rule must not take the front end down. Regex rules that cannot be | |
| 222 | + * compiled are logged once per request so a broken rule is diagnosable rather than | |
| 223 | + * invisible. | |
| 224 | + * | |
| 225 | + * @param string $url URL to test. Callers pass it trimmed with no trailing slash. | |
| 226 | + * @param string $pattern Raw url_pattern column value. Normalized here, per type. | |
| 227 | + * @param string $pattern_type One of exact|contain|start|end|regex. | |
| 228 | + * @return bool True when the URL matches the pattern. | |
| 229 | + */ | |
| 230 | + public static function pattern_matches($url, $pattern, $pattern_type) | |
| 231 | + { | |
| 232 | + // Literal patterns are compared against a URL the caller already stripped of its | |
| 233 | + // trailing slash, so strip the pattern the same way. A regex must NOT be stripped: | |
| 234 | + // in a `/.../`-delimited pattern the trailing slash is the closing delimiter, and | |
| 235 | + // removing it yields an uncompilable pattern (`/\/blog\//` becomes `/\/blog\`) that | |
| 236 | + // silently matches nothing. | |
| 237 | + $pattern = ($pattern_type === 'regex') ? trim($pattern) : rtrim(trim($pattern), '/'); | |
| 220 | 238 | |
| 221 | - case 'start': | |
| 222 | - if (strpos($url, $pattern) === 0) { | |
| 223 | - return true; | |
| 224 | - } | |
| 225 | - break; | |
| 239 | + switch ($pattern_type) { | |
| 240 | + case 'exact': | |
| 241 | + return $url === $pattern; | |
| 226 | 242 | |
| 227 | - case 'end': | |
| 228 | - if (substr($url, -strlen($pattern)) === $pattern) { | |
| 229 | - return true; | |
| 230 | - } | |
| 231 | - break; | |
| 243 | + case 'contain': | |
| 244 | + return strpos($url, $pattern) !== false; | |
| 232 | 245 | |
| 233 | - case 'regex': | |
| 234 | - // Normalize with delimiters | |
| 235 | - $test_pattern = Metasync_Redirection::normalize_regex_pattern($pattern); | |
| 236 | - $prev_limit = ini_get('pcre.backtrack_limit'); | |
| 237 | - ini_set('pcre.backtrack_limit', 10000); | |
| 238 | - $match = @preg_match($test_pattern, $url); | |
| 239 | - ini_set('pcre.backtrack_limit', $prev_limit); | |
| 240 | - if ($match) { | |
| 241 | - return true; | |
| 242 | - } | |
| 243 | - break; | |
| 246 | + case 'start': | |
| 247 | + return strpos($url, $pattern) === 0; | |
| 248 | + | |
| 249 | + case 'end': | |
| 250 | + return substr($url, -strlen($pattern)) === $pattern; | |
| 251 | + | |
| 252 | + case 'regex': | |
| 253 | + return self::regex_matches($url, $pattern); | |
| 254 | + } | |
| 255 | + | |
| 256 | + return false; | |
| 257 | + } | |
| 258 | + | |
| 259 | + /** | |
| 260 | + * Evaluate a single regex exclusion pattern against a URL. | |
| 261 | + * | |
| 262 | + * @param string $url URL to test. | |
| 263 | + * @param string $pattern Regex pattern, with or without delimiters. | |
| 264 | + * @return bool True on a match. False on no match, or on any evaluation failure. | |
| 265 | + */ | |
| 266 | + private static function regex_matches($url, $pattern) | |
| 267 | + { | |
| 268 | + if ($pattern === '') { | |
| 269 | + return false; | |
| 270 | + } | |
| 271 | + | |
| 272 | + if (!self::regex_normalizer_available()) { | |
| 273 | + return false; | |
| 274 | + } | |
| 275 | + | |
| 276 | + // Normalize with delimiters | |
| 277 | + $test_pattern = Metasync_Redirection::normalize_regex_pattern($pattern); | |
| 278 | + | |
| 279 | + // Cap backtracking so a pathological pattern cannot burn CPU on a visitor request. | |
| 280 | + // ini_set() sits in disable_functions on some hardened hosts and calling a disabled | |
| 281 | + // function raises an Error, not an Exception - which the callers' catch (Exception) | |
| 282 | + // would not contain, white-screening the front end. Only restore the limit if it was | |
| 283 | + // actually read and changed. | |
| 284 | + $prev_limit = function_exists('ini_get') ? ini_get('pcre.backtrack_limit') : false; | |
| 285 | + $capped = ($prev_limit !== false && function_exists('ini_set') | |
| 286 | + && ini_set('pcre.backtrack_limit', 10000) !== false); | |
| 287 | + | |
| 288 | + $match = @preg_match($test_pattern, $url); | |
| 289 | + | |
| 290 | + if ($capped) { | |
| 291 | + ini_set('pcre.backtrack_limit', $prev_limit); | |
| 292 | + } | |
| 293 | + | |
| 294 | + if ($match === false) { | |
| 295 | + self::log_unusable_regex($pattern, $test_pattern); | |
| 296 | + } | |
| 297 | + | |
| 298 | + return $match === 1; | |
| 299 | + } | |
| 300 | + | |
| 301 | + /** | |
| 302 | + * Load the class that owns the regex normalizer, once per request. | |
| 303 | + * | |
| 304 | + * normalize_regex_pattern() is a static on Metasync_Redirection. It normally resolves via | |
| 305 | + * the Composer classmap registered in metasync.php, but release packages have shipped with | |
| 306 | + * an incomplete classmap before (see docs/wiki/release-package-classmaps.md), so the file | |
| 307 | + * is included explicitly rather than trusting the autoloader. If it is missing the caller | |
| 308 | + * must not attempt the static call at all: that raises an Error, not an Exception, so the | |
| 309 | + * callers' try/catch would not contain it and the front end would white-screen. | |
| 310 | + * | |
| 311 | + * Memoized because this runs on every front-end request - without it there would be one | |
| 312 | + * filesystem stat per regex exclusion row per page load. | |
| 313 | + * | |
| 314 | + * @return bool True when the normalizer can be called. | |
| 315 | + */ | |
| 316 | + private static function regex_normalizer_available() | |
| 317 | + { | |
| 318 | + static $available = null; | |
| 319 | + | |
| 320 | + if ($available === null) { | |
| 321 | + $class_path = dirname(__FILE__, 2) . '/redirections/class-metasync-redirection.php'; | |
| 322 | + $available = file_exists($class_path); | |
| 323 | + | |
| 324 | + if ($available) { | |
| 325 | + require_once $class_path; | |
| 244 | 326 | } |
| 245 | 327 | } |
| 246 | 328 | |
| 247 | - return false; | |
| 329 | + return $available; | |
| 330 | + } | |
| 331 | + | |
| 332 | + /** | |
| 333 | + * Log an exclusion regex that could not be evaluated, once per pattern per request. | |
| 334 | + * | |
| 335 | + * Without this the rule is skipped in total silence: the admin sees an active exclusion | |
| 336 | + * and a page that is still being rewritten, with nothing to go on. | |
| 337 | + * | |
| 338 | + * @param string $pattern The stored pattern. | |
| 339 | + * @param string $test_pattern The pattern as handed to preg_match(). | |
| 340 | + * @return void | |
| 341 | + */ | |
| 342 | + private static function log_unusable_regex($pattern, $test_pattern) | |
| 343 | + { | |
| 344 | + static $logged = []; | |
| 345 | + | |
| 346 | + $key = md5($pattern); | |
| 347 | + if (isset($logged[$key])) { | |
| 348 | + return; | |
| 349 | + } | |
| 350 | + $logged[$key] = true; | |
| 351 | + | |
| 352 | + error_log(sprintf( | |
| 353 | + 'MetaSync OTTO: skipped an exclusion rule whose regex could not be evaluated - stored "%s", compiled "%s" (%s)', | |
| 354 | + $pattern, | |
| 355 | + $test_pattern, | |
| 356 | + preg_last_error_msg() | |
| 357 | + )); | |
| 248 | 358 | } |
| 249 | 359 | |
| 250 | 360 | /** |
| 251 | 361 | * Add a new excluded URL |