PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.0
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.0
2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 All 139 releases
← All changes | otto/class-metasync-otto-excluded-urls-database.php +146 -36 2.6.212.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