<?php
/**
* Crawler / bot detection helper.
*
* @package Automattic\WooCommerce\Pinterest
*/
declare( strict_types=1 );
namespace Automattic\WooCommerce\Pinterest\Utilities;
use Automattic\Jetpack\Device_Detection\User_Agent_Info;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Detects whether the current HTTP request appears to come from a crawler/bot.
*
* Primary detection is delegated to Jetpack's
* `Automattic\Jetpack\Device_Detection\User_Agent_Info::is_bot_user_agent()`,
* which ships an actively-maintained list of search-engine bots, AI crawlers,
* social-network preview fetchers and other indexers. A small supplementary
* regex catches programmatic / headless HTTP clients (curl, wget, Feedly,
* PhantomJS, HeadlessChrome) that will never execute the browser-side
* Pinterest Tag JS and are therefore still relevant to CAPI inflation, but
* which Jetpack does not flag as bots.
*
* Tracking-domain code (Conversions API in particular) uses this to avoid
* dispatching server-side events for requests that will never fire the
* browser-side Pinterest Tag, which would otherwise inflate CAPI counts
* relative to Tag counts.
*
* @since 1.4.27
*/
class CrawlerDetector {
/**
* Supplementary User-Agent regex for programmatic / headless clients that
* Jetpack's bot list intentionally does not cover. These clients will not
* execute the browser-side Pinterest Tag JS, so any CAPI dispatch from
* them is by definition CAPI-only and inflates the CAPI vs Tag ratio.
*
* @var string
*/
const PROGRAMMATIC_CLIENT_REGEX = '/curl|wget|feed|phantom|headless/i';
/**
* Returns true when the current request looks like a crawler/bot.
*
* Detection is intentionally NOT memoized: the underlying checks and the
* filter call are cheap, and re-evaluating per call keeps the
* `pinterest_for_woocommerce_is_crawler_request` filter responsive to
* runtime changes (added/removed hooks, test fixtures) without requiring
* callers to remember to reset static state.
*
* @since 1.4.27
*
* @return bool
*/
public static function is_crawler_request(): bool {
// Unslashed raw value for the filter, so consumers see exactly what the
// client sent. The sanitized copy below is only used internally for the
// detection checks.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$raw_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : '';
$user_agent = '' === $raw_user_agent ? '' : sanitize_text_field( $raw_user_agent );
$is_crawler = '' !== $user_agent
&& (
User_Agent_Info::is_bot_user_agent( $user_agent )
|| 1 === preg_match( self::PROGRAMMATIC_CLIENT_REGEX, $user_agent )
);
/**
* Filters whether the current request is treated as a crawler.
*
* When true, server-side tracking events (Conversions API) are not
* dispatched for the request. Browser-side rendering (Pinterest Tag
* JS) is intentionally NOT suppressed, so full-page caches that omit
* `Vary: User-Agent` do not serve bot-rendered HTML (missing Tag JS)
* to real users.
*
* @since 1.4.27
*
* @param bool $is_crawler Whether the request looks like a crawler.
* @param string $user_agent The raw (unslashed, unsanitized) User-Agent header value.
*/
return (bool) apply_filters(
'pinterest_for_woocommerce_is_crawler_request',
$is_crawler,
$raw_user_agent
);
}
}