Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.60% covered (success)
91.60%
218 / 238
68.00% covered (warning)
68.00%
17 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 1
Agents_Manager
91.60% covered (success)
91.60%
218 / 238
68.00% covered (warning)
68.00%
17 / 25
114.92
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 get_ai_icon
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 add_ai_chat_button
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 get_active_context
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 add_admin_bar_nodes
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
8
 enqueue_scripts
96.67% covered (success)
96.67%
29 / 30
0.00% covered (danger)
0.00%
0 / 1
5
 get_active_variant
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_plugin_information_iframe
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 get_variant
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
7
 is_enabled
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 passes_admin_checks
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
 enqueue_script
97.14% covered (success)
97.14%
34 / 35
0.00% covered (danger)
0.00%
0 / 1
8
 determine_iso_639_locale
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 get_assets_json
59.09% covered (warning)
59.09%
13 / 22
0.00% covered (danger)
0.00%
0 / 1
14.55
 init
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 get_instance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_proxied
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 is_tracking_automattician
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
4
 is_dev_mode
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
12
 register_rest_api
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 is_block_editor
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 is_admin_bar_in_editor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_jetpack_disconnected
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 get_current_user_data
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
4.03
 get_current_site
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Agents manager
4 *
5 * @package automattic/jetpack-agents-manager
6 */
7
8namespace Automattic\Jetpack\Agents_Manager;
9
10use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11use Automattic\Jetpack\Connection\REST_Jetpack_AI_JWT;
12use Automattic\Jetpack\Constants;
13
14/**
15 * Class Agents_Manager
16 */
17class Agents_Manager {
18    /**
19     * The package version of the Agents Manager package.
20     *
21     * @var string
22     */
23    const PACKAGE_VERSION = '0.12.8';
24
25    /**
26     * Class instance.
27     *
28     * @var Agents_Manager
29     */
30    private static $instance = null;
31
32    /**
33     * Agents_Manager constructor.
34     */
35    private function __construct() {
36        add_action( 'rest_api_init', array( $this, 'register_rest_api' ) );
37
38        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ), 101 );
39        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ), 101 );
40
41        add_action( 'admin_bar_menu', array( $this, 'add_admin_bar_nodes' ), 100 );
42
43        Sidebar_Open_Preservation::init();
44    }
45
46    /**
47     * Get the SVG icon markup for the AI chat button.
48     *
49     * @return string The SVG markup.
50     */
51    private function get_ai_icon() {
52        // Not cached: the sparkle icon's aria-label is translated, and the locale can switch mid-request.
53        return '<svg class="ab-icon" role="img" aria-label="' . esc_attr__( 'Agent', 'jetpack-agents-manager' ) . '" width="24" height="24" viewBox="-45 -45 490 490" xmlns="http://www.w3.org/2000/svg">
54                                <path fill="currentColor" d="M391.528 188.061L309.455 159.75C276.997 148.597 251.403 123.003 240.25 90.5451L211.939 8.47185C208.079 -2.82395 191.921 -2.82395 188.061 8.47185L159.75 90.5451C148.597 123.003 123.003 148.597 90.5451 159.75L8.47185 188.061C-2.82395 191.921 -2.82395 208.079 8.47185 211.939L90.5451 240.25C123.003 251.403 148.597 276.997 159.75 309.455L188.061 391.528C191.921 402.824 208.079 402.824 211.939 391.528L240.25 309.455C251.403 276.997 276.997 251.403 309.455 240.25L391.528 211.939C402.824 208.079 402.824 191.921 391.528 188.061ZM295.728 206.077L254.692 220.232C238.391 225.809 225.666 238.677 220.089 254.835L205.934 295.871C203.932 301.591 195.925 301.591 193.923 295.871L179.768 254.835C174.191 238.534 161.323 225.809 145.165 220.232L104.129 206.077C98.4093 204.075 98.4093 196.068 104.129 194.066L145.165 179.911C161.466 174.334 174.191 161.466 179.768 145.308L193.923 104.272C195.925 98.5523 203.932 98.5523 205.934 104.272L220.089 145.308C225.666 161.609 238.534 174.334 254.692 179.911L295.728 194.066C301.448 196.068 301.448 204.075 295.728 206.077Z" />
55                            </svg>';
56    }
57
58    /**
59     * Add the standalone AI chat button to the admin bar.
60     *
61     * @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
62     */
63    public function add_ai_chat_button( $wp_admin_bar ) {
64        $meta = array(
65            'menu_title' => __( 'Agent', 'jetpack-agents-manager' ),
66            'icon'       => 'sparkle',
67            // The wp-admin bundle mounts the chat into this div.
68            'html'       => '<div id="agents-manager-masterbar"></div>',
69        );
70
71        // The "Agent" label shows while the chat is hidden (closed or minimized).
72        // Pre-hide it when the chat will restore visible; the bundle keeps the
73        // class in step afterwards.
74        $state = Open_State_Store::get_cached();
75        if ( ! empty( $state['agents_manager_open'] ) && empty( $state['agents_manager_minimized'] ) ) {
76            $meta['class'] = 'is-chat-visible';
77        }
78
79        $wp_admin_bar->add_menu(
80            array(
81                'id'     => 'agents-manager-ai-chat',
82                'parent' => 'top-secondary',
83                'title'  => '<span title="' . esc_attr__( 'Agent', 'jetpack-agents-manager' ) . '">' . $this->get_ai_icon() . '</span>'
84                    . '<span class="agents-manager-ai-chat-label" aria-hidden="true"><span>' . esc_html__( 'Agent', 'jetpack-agents-manager' ) . '</span></span>',
85                'meta'   => $meta,
86            )
87        );
88    }
89
90    /**
91     * The Agents Manager context for this request, or null when it should not load.
92     *
93     * Shared by `add_admin_bar_nodes()` and `enqueue_scripts()`.
94     *
95     * @return array{variant: string, disconnected: bool, gutenberg: bool, enabled: bool}|null
96     */
97    private function get_active_context() {
98        // P2 frontends get neither the admin bar entry points nor the app.
99        $stylesheet = get_stylesheet();
100        $is_p2      = str_contains( $stylesheet, 'pub/p2' ) || function_exists( '\WPForTeams\is_wpforteams_site' ) && \WPForTeams\is_wpforteams_site( get_current_blog_id() );
101
102        if ( ! is_admin() && $is_p2 ) {
103            return null;
104        }
105
106        // Determine which variant to load (null = don't load).
107        $variant = self::get_active_variant();
108        if ( null === $variant ) {
109            return null;
110        }
111
112        return array(
113            'variant'      => $variant,
114            'disconnected' => str_contains( $variant, 'disconnected' ),
115            'gutenberg'    => $this->is_block_editor(),
116            'enabled'      => self::is_enabled(),
117        );
118    }
119
120    /**
121     * Add the Agents Manager entry points to the admin bar.
122     *
123     * Hooked unconditionally, with eligibility resolved here rather than at registration, so the
124     * admin-bar REST endpoints â€” which fire `admin_bar_menu` with no enqueue hook â€” get the same
125     * nodes as a page load.
126     *
127     * @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
128     */
129    public function add_admin_bar_nodes( $wp_admin_bar ) {
130        $context = $this->get_active_context();
131        if ( null === $context ) {
132            return;
133        }
134
135        // Gutenberg uses JS when no admin bar is visible and the server-side branch below when one is available.
136        if ( ! $context['gutenberg'] ) {
137            // Standalone AI chat button, shown whenever the full Agents Manager app is enabled.
138            if ( ! $context['disconnected'] && $context['enabled'] ) {
139                $this->add_ai_chat_button( $wp_admin_bar );
140            }
141        }
142
143        // When the block editor exposes the WordPress admin bar, add the entry point there.
144        if ( ! $context['disconnected'] && self::is_admin_bar_in_editor() ) {
145            if ( $context['enabled'] ) {
146                $this->add_ai_chat_button( $wp_admin_bar );
147            }
148        }
149    }
150
151    /**
152     * Enqueue Agents Manager scripts and add inline script data.
153     */
154    public function enqueue_scripts() {
155        $context = $this->get_active_context();
156        if ( null === $context ) {
157            return;
158        }
159
160        $variant = $context['variant'];
161
162        /**
163         * Filter to register agent provider modules for the Agents Manager.
164         *
165         * Plugins can hook into this filter to register script module IDs that export
166         * toolProvider and/or contextProvider. The Agents Manager JS will dynamically
167         * import these modules and merge their providers.
168         *
169         * @param array $providers Array of provider script module IDs.
170         */
171        $agent_providers = apply_filters( 'agents_manager_agent_providers', array() );
172
173        /**
174         * Filter the default agent ID for the Agents Manager.
175         *
176         * Allows host applications (e.g., WooCommerce AI) to specify a custom
177         * workflow agent instead of the default orchestrator. The value is passed to
178         * the frontend as `agentsManagerData.agentId` and consumed by `useAgentConfig()`.
179         *
180         * @param string|null $agent_id The agent ID to use, or null for default behavior.
181         */
182        $agent_id = apply_filters( 'agents_manager_agent_id', null );
183
184        $script_version = $this->enqueue_script( $variant );
185
186        $inline_data = array(
187            'agentProviders'  => $agent_providers,
188            'isDevMode'       => self::is_dev_mode(),
189            'isA11n'          => self::is_tracking_automattician(),
190            'isWpcomPlatform' => ( new \Automattic\Jetpack\Status\Host() )->is_wpcom_platform(),
191            'sectionName'     => apply_filters( 'agents_manager_section_name', $variant ),
192            'currentUser'     => $this->get_current_user_data(),
193            'site'            => $this->get_current_site(),
194        );
195
196        if ( null !== $script_version ) {
197            $inline_data['version'] = $variant . ':' . $script_version;
198        }
199
200        if ( $agent_id ) {
201            $inline_data['agentId'] = $agent_id;
202        }
203
204        /**
205         * Filter the data exposed to the Agents Manager frontend.
206         *
207         * @param array $inline_data Data encoded into `agentsManagerData`.
208         */
209        $filtered    = apply_filters( 'jetpack_ai_sidebar_agents_manager_data', $inline_data );
210        $inline_data = is_array( $filtered ) ? $filtered : $inline_data;
211
212        wp_add_inline_script(
213            'agents-manager',
214            'const agentsManagerData = ' . wp_json_encode(
215                $inline_data,
216                JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
217            ) . ';',
218            'before'
219        );
220    }
221
222    /**
223     * The script variant active for this request, or null if none.
224     *
225     * Single source of truth for "is the Agents Manager app loaded on this
226     * request?". Used both to enqueue the app and to gate the server-side
227     * sidebar pre-render, so the pre-rendered shell can never appear on a page
228     * where the app won't mount to reconcile it.
229     *
230     * @return string|null The variant name, or null if scripts should not be loaded.
231     */
232    public static function get_active_variant() {
233        if ( self::is_plugin_information_iframe() ) {
234            return null;
235        }
236
237        /**
238         * Filter the script variant the Agents Manager loads for this request.
239         *
240         * @since 0.1.0
241         *
242         * @param string|null $variant The resolved variant, or null to not load.
243         */
244        return apply_filters( 'agents_manager_variant', self::get_variant() );
245    }
246
247    /**
248     * Whether the current request renders the plugin information iframe.
249     *
250     * The parent plugin screen may load Agents Manager, but the iframe must not
251     * bootstrap a second copy of the app.
252     *
253     * @return bool
254     */
255    private static function is_plugin_information_iframe() {
256        global $current_screen;
257
258        if ( ! $current_screen || 'plugin-install' !== $current_screen->id ) {
259            return false;
260        }
261
262        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a request context check, not a form submission.
263        return isset( $_GET['tab'] ) && 'plugin-information' === sanitize_text_field( wp_unslash( $_GET['tab'] ) );
264    }
265
266    /**
267     * Determine which script variant to load, or null if none should be loaded.
268     *
269     * Combines the gating logic (should we load at all?) with variant selection
270     * (which build to use?) into a single method so the two cannot get out of sync.
271     *
272     * @return string|null The variant name, or null if scripts should not be loaded.
273     */
274    private static function get_variant() {
275        // Agents Manager does not render on the frontend.
276        if ( ! is_admin() ) {
277            return null;
278        }
279
280        // Apply wp-admin exclusions (customizer, asset, and preview contexts).
281        if ( ! self::passes_admin_checks() ) {
282            return null;
283        }
284
285        if ( ! self::is_enabled() ) {
286            return null;
287        }
288
289        $disconnected = self::is_jetpack_disconnected();
290
291        if ( self::is_block_editor() ) {
292            return $disconnected ? 'gutenberg-disconnected' : 'gutenberg';
293        }
294
295        return $disconnected ? null : 'wp-admin';
296    }
297
298    /**
299     * Returns true if the Agents Manager should be loaded in the current context.
300     *
301     * @return bool
302     */
303    public static function is_enabled() {
304        $enabled = false;
305
306        if ( self::is_block_editor() && apply_filters( 'agents_manager_enabled_in_block_editor', false ) ) {
307            // Block editor only. Hooked by hosts such as jetpack-mu-wpcom and the Jetpack AI Sidebar.
308            $enabled = true;
309        }
310
311        /**
312         * Filters whether an integration requests the Agents Manager shell on this request.
313         *
314         * Providers should preserve an existing true value so multiple integrations can
315         * request the shared shell independently.
316         *
317         * @since 0.9.1
318         *
319         * @param bool $should_load Whether another integration already requested the shell.
320         */
321        $should_load = (bool) apply_filters( 'agents_manager_should_load', false );
322
323        return $enabled || $should_load;
324    }
325
326    /**
327     * Returns true if the current wp-admin context passes all exclusion checks.
328     *
329     * Excludes customizer previews, Gutenberg asset requests, and preview query
330     * param contexts.
331     *
332     * @return bool
333     */
334    private static function passes_admin_checks() {
335        // Don't load in customizer preview iframe.
336        if ( is_customize_preview() ) {
337            return false;
338        }
339
340        // Don't load during Gutenberg asset requests or preview contexts.
341        $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
342        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission.
343        $is_preview = isset( $_GET['preview'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['preview'] ) );
344        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission.
345        $is_preview_overlay = isset( $_GET['preview_overlay'] );
346        if ( str_contains( $request_uri, 'wp-content/plugins/gutenberg-core' ) || $is_preview || $is_preview_overlay ) {
347            return false;
348        }
349
350        return true;
351    }
352
353    /**
354     * Enqueue Agents Manager script based on context.
355     *
356     * @param string $variant The variant of the asset file to get.
357     * @return string|null The deployed build version from the asset file, or null when unavailable.
358     */
359    private function enqueue_script( $variant ) {
360        $cache_key  = 'agents-manager-asset-' . $variant . '.asset.json';
361        $asset_file = get_transient( $cache_key );
362
363        if ( ! $asset_file ) {
364            $asset_file = self::get_assets_json( 'widgets.wp.com/agents-manager/agents-manager-' . $variant . '.asset.json' );
365            if ( ! $asset_file ) {
366                return null;
367            }
368            set_transient( $cache_key, $asset_file, HOUR_IN_SECONDS );
369        }
370
371        // When the request is dev mode, use a random cache buster as the version for easier debugging.
372        $version = self::is_dev_mode() ? wp_rand() : $asset_file['version'];
373
374        $script_dependencies = $asset_file['dependencies'] ?? array();
375
376        // Load translations for connected variants from widgets.wp.com.
377        // Disconnected variants have no translatable UI, so skip them (as Help
378        // Center does). English needs no translation file.
379        if ( ! str_contains( $variant, 'disconnected' ) ) {
380            $locale = self::determine_iso_639_locale();
381
382            if ( 'en' !== $locale ) {
383                wp_enqueue_script(
384                    'agents-manager-translations',
385                    'https://widgets.wp.com/agents-manager/languages/' . $locale . '-v1.js',
386                    array( 'wp-i18n' ),
387                    $version,
388                    true
389                );
390
391                $script_dependencies[] = 'agents-manager-translations';
392            }
393        }
394
395        wp_enqueue_script(
396            'agents-manager',
397            'https://widgets.wp.com/agents-manager/agents-manager-' . $variant . '.min.js',
398            $script_dependencies,
399            $version,
400            /**
401             * Filter the strategy to use when enqueuing the script.
402             *
403             * @param array|bool $args The arguments to pass to wp_enqueue_script. Default is true.
404             * @param string $handle The handle of the script.
405             */
406            apply_filters( 'agents_manager_enqueue_script_strategy', true, 'agents-manager' )
407        );
408
409        if ( 'gutenberg-disconnected' !== $variant ) {
410            wp_enqueue_style(
411                'agents-manager-style',
412                'https://widgets.wp.com/agents-manager/agents-manager-' . $variant . ( is_rtl() ? '.rtl.css' : '.css' ),
413                array(),
414                $version
415            );
416        }
417
418        return (string) $asset_file['version'];
419    }
420
421    /**
422     * Returns the ISO 639 conforming locale string for the current user.
423     *
424     * Normalizes the WordPress user locale to match the widgets.wp.com translation
425     * file naming at languages/{code}-v1.js. Preserves the region for the few locales
426     * where it is meaningful (pt-br, zh-tw, zh-cn); strips the region for all others;
427     * falls back to 'en' when the locale is empty.
428     *
429     * @return string The ISO 639 locale string, e.g. "en".
430     */
431    private static function determine_iso_639_locale() {
432        $language = get_user_locale();
433        $language = strtolower( $language );
434
435        if ( in_array( $language, array( 'pt_br', 'pt-br', 'zh_tw', 'zh-tw', 'zh_cn', 'zh-cn' ), true ) ) {
436            $language = str_replace( '_', '-', $language );
437        } else {
438            $language = preg_replace( '/([-_].*)$/i', '', $language );
439        }
440
441        if ( empty( $language ) ) {
442            return 'en';
443        }
444
445        return $language;
446    }
447
448    /**
449     * Get the asset via file-system on wpcom and via network on Atomic sites.
450     *
451     * @param string $filepath The URL to download the asset file from.
452     * @return array|null The asset file data or null on failure.
453     */
454    private static function get_assets_json( $filepath ) {
455        $accessible_directly = file_exists( ABSPATH . $filepath );
456
457        if ( $accessible_directly ) {
458            $file_contents = file_get_contents( ABSPATH . $filepath );
459
460            if ( false === $file_contents ) {
461                return null;
462            }
463
464            return json_decode( $file_contents, true );
465        }
466
467        $request = wp_remote_get( 'https://' . $filepath );
468
469        if ( is_wp_error( $request ) ) {
470            return null;
471        }
472
473        $response_code = wp_remote_retrieve_response_code( $request );
474        if ( 200 !== $response_code ) {
475            return null;
476        }
477
478        $content_type = wp_remote_retrieve_header( $request, 'content-type' );
479        if ( is_string( $content_type ) && false === strpos( $content_type, 'json' ) ) {
480            return null;
481        }
482
483        $body = wp_remote_retrieve_body( $request );
484        if ( '' === $body ) {
485            return null;
486        }
487
488        $decoded = json_decode( $body, true );
489        if ( json_last_error() !== JSON_ERROR_NONE ) {
490            return null;
491        }
492
493        return $decoded;
494    }
495
496    /**
497     * Creates instance.
498     *
499     * @return Agents_Manager
500     */
501    public static function init() {
502        if ( did_action( 'jetpack_agents_manager_initialized' ) ) {
503            return self::get_instance();
504        }
505
506        self::$instance = new self();
507
508        /**
509         * Fires once the Agents Manager class has been instantiated.
510         *
511         * @since 0.5.0
512         */
513        do_action( 'jetpack_agents_manager_initialized' );
514
515        return self::$instance;
516    }
517
518    /**
519     * Returns the instance of the Agents Manager class.
520     *
521     * @return Agents_Manager
522     */
523    public static function get_instance() {
524        return self::$instance;
525    }
526
527    /**
528     * Returns whether the current request is coming from the A8C proxy.
529     *
530     * @return bool
531     */
532    private static function is_proxied() {
533        // On Simple sites, use the wpcom function if available.
534        if ( function_exists( 'wpcom_is_proxied_request' ) ) {
535            return wpcom_is_proxied_request();
536        }
537
538        // On WoA/Garden sites, check server variable or constant.
539        return isset( $_SERVER['A8C_PROXIED_REQUEST'] )
540            ? (bool) sanitize_text_field( wp_unslash( $_SERVER['A8C_PROXIED_REQUEST'] ) )
541            : Constants::is_true( 'A8C_PROXIED_REQUEST' );
542    }
543
544    /**
545     * Returns whether the current visitor should be marked as an Automattician in tracking.
546     *
547     * @return bool
548     */
549    private static function is_tracking_automattician() {
550        $is_automattician = function_exists( 'is_automattician' ) && (bool) is_automattician();
551
552        return $is_automattician || self::is_proxied() || Constants::is_true( 'AT_PROXIED_REQUEST' );
553    }
554
555    /**
556     * Enables "Development" features that should be accessible only for admins.
557     */
558    private static function is_dev_mode() {
559        // Known local environments.
560        $domain = wp_parse_url( get_site_url(), PHP_URL_HOST );
561        if (
562            $domain === 'localhost' ||
563            '.jurassic.tube' === stristr( $domain, '.jurassic.tube' ) ||
564            '.jurassic.ninja' === stristr( $domain, '.jurassic.ninja' )
565        ) {
566            return true;
567        }
568
569        // A8C development.
570        if ( self::is_proxied() ) {
571            return true;
572        }
573
574        if ( Constants::is_true( 'AT_PROXIED_REQUEST' ) && Constants::is_defined( 'ATOMIC_CLIENT_ID' ) ) {
575            switch ( Constants::get_constant( 'ATOMIC_CLIENT_ID' ) ) {
576                case 1:
577                case 2:
578                case 3: // Pressable
579                case 32:
580                case 118: // Commerce garden client.
581                    return true;
582            }
583        }
584
585        return false;
586    }
587
588    /**
589     * Register the Agents Manager endpoints.
590     */
591    public function register_rest_api() {
592        ( new WP_REST_Agents_Manager_Persisted_Open_State() )->register_rest_route();
593        ( new REST_Jetpack_AI_JWT() )->register_rest_route();
594    }
595
596    /**
597     * Returns true if the current screen is the block editor.
598     *
599     * @return bool True if the current screen is the block editor.
600     */
601    private static function is_block_editor() {
602        if ( ! function_exists( 'get_current_screen' ) ) {
603            return false;
604        }
605
606        $current_screen = get_current_screen();
607        // The widgets screen has the block editor but no Gutenberg top bar.
608        return $current_screen && $current_screen->is_block_editor() && $current_screen->id !== 'widgets';
609    }
610
611    /**
612     * Returns true when the WordPress admin bar is available in the block editor.
613     *
614     * The frontend uses the visible admin bar as its signal to move editor entry points out of the
615     * Gutenberg toolbar. Use WordPress's matching server-side signal so a classic editor admin bar
616     * receives those entry points too, not only Gutenberg's experimental omnibar.
617     *
618     * @return bool
619     */
620    private static function is_admin_bar_in_editor() {
621        return self::is_block_editor() && is_admin_bar_showing();
622    }
623
624    /**
625     * Returns true if the current user is NOT connected through Jetpack.
626     *
627     * Mirrors the logic from Help_Center::is_jetpack_disconnected().
628     *
629     * @return bool True if the site uses Jetpack but the current user is not connected.
630     */
631    private static function is_jetpack_disconnected() {
632        $user_id = get_current_user_id();
633        $blog_id = get_current_blog_id();
634
635        if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) {
636            return ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $user_id );
637        }
638
639        if ( true === apply_filters( 'is_jetpack_site', false, $blog_id ) ) {
640            return ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $user_id );
641        }
642
643        return false;
644    }
645
646    /**
647     * Get current user data for the agents manager.
648     *
649     * Mirrors the user data structure from Help Center's helpCenterData.
650     *
651     * @return array|null User data array or null if not logged in.
652     */
653    private function get_current_user_data() {
654        $user_id = get_current_user_id();
655        if ( ! $user_id ) {
656            return null;
657        }
658
659        $user_data = get_userdata( $user_id );
660        if ( ! $user_data ) {
661            return null;
662        }
663
664        $user_email = $user_data->user_email;
665
666        // Use wpcom_get_avatar_url on Simple sites, fall back to get_avatar_url elsewhere.
667        if ( function_exists( 'wpcom_get_avatar_url' ) ) {
668            $avatar_url = wpcom_get_avatar_url( $user_email, 64, '', true )[0];
669        } else {
670            $avatar_url = get_avatar_url( $user_id );
671        }
672
673        return array(
674            'ID'           => $user_id,
675            'username'     => $user_data->user_login,
676            'display_name' => $user_data->display_name,
677            'avatar_URL'   => $avatar_url,
678            'email'        => $user_email,
679        );
680    }
681
682    /**
683     * Get current site data for the agents manager.
684     *
685     * Returns minimal site data needed by AgentsManager (ID and domain only).
686     * Uses jetpack_options['id'] on Atomic sites for the wpcom blog ID.
687     *
688     * @return array Site data with ID and domain.
689     */
690    private function get_current_site() {
691        /*
692         * Atomic sites have the WP.com blog ID stored as a Jetpack option.
693         * This code deliberately doesn't use `Jetpack_Options::get_option`
694         * so it works even when Jetpack has not been loaded.
695         */
696        $jetpack_options = get_option( 'jetpack_options' );
697        if ( is_array( $jetpack_options ) && isset( $jetpack_options['id'] ) ) {
698            $site_id = (int) $jetpack_options['id'];
699        } else {
700            $site_id = get_current_blog_id();
701        }
702
703        return array(
704            'ID'     => $site_id,
705            'domain' => wp_parse_url( home_url(), PHP_URL_HOST ),
706        );
707    }
708}