Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_AI_Experiments
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 5
272
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 is_enabled
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 is_ai_bot
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 is_ai_bot_fallback
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
 add_ai_bot_header
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * AI Experiments orchestrator.
4 *
5 * Manages experiment toggles and delegates to feature classes.
6 * Each experiment is gated by a filter: jetpack_ai_experiments_{$slug}.
7 *
8 * @package automattic/jetpack
9 */
10
11use Automattic\Jetpack\Device_Detection\User_Agent_Info;
12
13/**
14 * Orchestrator for the AI Experiments module.
15 */
16class Jetpack_AI_Experiments {
17
18    /**
19     * Registered experiments: slug => default enabled state.
20     *
21     * @var array<string, bool>
22     */
23    private static $experiments = array(
24        'ai_bot_header' => true,
25    );
26
27    /**
28     * Initialize the module. Iterates experiments and wires enabled ones.
29     */
30    public static function init() {
31        foreach ( self::$experiments as $slug => $default ) {
32            if ( ! self::is_enabled( $slug ) ) {
33                continue;
34            }
35
36            switch ( $slug ) {
37                case 'ai_bot_header':
38                    add_action( 'send_headers', array( __CLASS__, 'add_ai_bot_header' ) );
39                    break;
40            }
41        }
42    }
43
44    /**
45     * Check whether an experiment is enabled.
46     *
47     * @param string $experiment Experiment slug.
48     * @return bool
49     */
50    public static function is_enabled( $experiment ) {
51        $default = isset( self::$experiments[ $experiment ] ) ? self::$experiments[ $experiment ] : false;
52
53        /**
54         * Filter to enable or disable an AI experiment.
55         *
56         * The dynamic portion of the hook name, `$experiment`, refers to the experiment slug.
57         *
58         * @param bool $enabled Whether the experiment is enabled.
59         */
60        return (bool) apply_filters( "jetpack_ai_experiments_{$experiment}", $default );
61    }
62
63    /**
64     * Detect whether the current request is from an AI bot/agent.
65     *
66     * Delegates to User_Agent_Info::is_agent() from the device-detection package
67     * when available, with a hardcoded fallback for environments without it.
68     *
69     * @return bool
70     */
71    public static function is_ai_bot() {
72        if ( class_exists( User_Agent_Info::class ) && method_exists( User_Agent_Info::class, 'is_agent' ) ) {
73            return User_Agent_Info::is_agent();
74        }
75
76        return self::is_ai_bot_fallback();
77    }
78
79    /**
80     * Fallback AI bot detection for environments without the device-detection package.
81     *
82     * @return bool
83     */
84    private static function is_ai_bot_fallback() {
85        if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
86            return false;
87        }
88
89        $ua       = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) );
90        $patterns = array(
91            'claudebot',
92            'claude-web',
93            'anthropic-ai',
94            'chatgpt-user',
95            'gptbot',
96            'oai-searchbot',
97            'google-extended',
98            'gemini',
99            'perplexitybot',
100            'cohere-ai',
101            'bytespider',
102            'ccbot',
103        );
104
105        $ua_lower = strtolower( $ua );
106        foreach ( $patterns as $pattern ) {
107            if ( false !== strpos( $ua_lower, $pattern ) ) {
108                return true;
109            }
110        }
111
112        return false;
113    }
114
115    /**
116     * Send X-AI-Bot response header indicating whether the request is from an AI agent.
117     */
118    public static function add_ai_bot_header() {
119        if ( headers_sent() ) {
120            return;
121        }
122
123        header( 'X-AI-Bot: ' . ( self::is_ai_bot() ? 'true' : 'false' ) );
124    }
125}