Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
66.96% |
231 / 345 |
|
37.50% |
9 / 24 |
CRAP | |
0.00% |
0 / 1 |
| Agents_Manager | |
67.15% |
231 / 344 |
|
37.50% |
9 / 24 |
659.25 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| should_display_menu_panel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_icon | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
| add_menu_panel | |
0.00% |
0 / 61 |
|
0.00% |
0 / 1 |
2 | |||
| enqueue_scripts | |
68.97% |
40 / 58 |
|
0.00% |
0 / 1 |
12.99 | |||
| get_variant | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
12.04 | |||
| is_enabled | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
| passes_admin_checks | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
9 | |||
| enqueue_script | |
95.65% |
22 / 23 |
|
0.00% |
0 / 1 |
7 | |||
| get_assets_json | |
59.09% |
13 / 22 |
|
0.00% |
0 / 1 |
14.55 | |||
| calypso_preferences_update | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
6 | |||
| init | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| is_proxied | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| is_dev_mode | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
13 | |||
| register_rest_api | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| should_use_unified_experience | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
8.03 | |||
| has_unified_chat_opt_in_enabled | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
| fetch_unified_experience_preference | |
88.46% |
23 / 26 |
|
0.00% |
0 / 1 |
8.10 | |||
| is_loading_on_frontend | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
10.37 | |||
| is_block_editor | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
4.25 | |||
| is_ciab_environment | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| is_jetpack_disconnected | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
| get_current_user_data | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
4.03 | |||
| get_current_site | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Agents manager |
| 4 | * |
| 5 | * @package automattic/jetpack-mu-wpcom |
| 6 | */ |
| 7 | |
| 8 | namespace A8C\FSE; |
| 9 | |
| 10 | use Automattic\Jetpack\Connection\Manager as Connection_Manager; |
| 11 | |
| 12 | /** |
| 13 | * Class Agents_Manager |
| 14 | */ |
| 15 | class Agents_Manager { |
| 16 | /** |
| 17 | * Help Center URL for disconnected variants. |
| 18 | * |
| 19 | * @var string |
| 20 | */ |
| 21 | private const HELP_CENTER_URL = 'https://wordpress.com/help?help-center=home'; |
| 22 | |
| 23 | /** |
| 24 | * Class instance. |
| 25 | * |
| 26 | * @var Agents_Manager |
| 27 | */ |
| 28 | private static $instance = null; |
| 29 | |
| 30 | /** |
| 31 | * Agents_Manager constructor. |
| 32 | */ |
| 33 | public function __construct() { |
| 34 | add_action( 'rest_api_init', array( $this, 'register_rest_api' ) ); |
| 35 | add_filter( 'calypso_preferences_update', array( $this, 'calypso_preferences_update' ) ); |
| 36 | |
| 37 | add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ), 101 ); |
| 38 | add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ), 101 ); |
| 39 | add_action( 'next_admin_init', array( $this, 'enqueue_scripts' ), 1001 ); |
| 40 | add_filter( 'agents_manager_use_unified_experience', array( $this, 'should_use_unified_experience' ) ); |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * Check if the agents manager menu panel should be displayed. |
| 45 | * |
| 46 | * @return bool True if the menu panel should be displayed. |
| 47 | */ |
| 48 | public function should_display_menu_panel() { |
| 49 | return apply_filters( 'agents_manager_use_unified_experience', false ); |
| 50 | } |
| 51 | |
| 52 | /** |
| 53 | * Get the SVG icon markup for a given icon name. |
| 54 | * |
| 55 | * @param string $icon_name The name of the icon to retrieve. |
| 56 | * @return string The SVG markup. |
| 57 | */ |
| 58 | private function get_icon( $icon_name ) { |
| 59 | $icons = array( |
| 60 | 'comment' => '<svg class="help-center-menu-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18 4H6c-1.1 0-2 .9-2 2v12.9c0 .6.5 1.1 1.1 1.1.3 0 .5-.1.8-.3L8.5 17H18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm.5 11c0 .3-.2.5-.5.5H7.9l-2.4 2.4V6c0-.3.2-.5.5-.5h12c.3 0 .5.2.5.5v9z" /></svg>', |
| 61 | 'backup' => '<svg class="help-center-menu-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.5 12h1.75l-2.5 3-2.5-3H4a8 8 0 113.134 6.35l.907-1.194A6.5 6.5 0 105.5 12zm9.53 1.97l-2.28-2.28V8.5a.75.75 0 00-1.5 0V12a.747.747 0 00.218.529l1.282-.84-1.28.842 2.5 2.5a.75.75 0 101.06-1.061z" /></svg>', |
| 62 | 'page' => '<svg class="help-center-menu-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.5 7.5h-7V9h7V7.5Zm-7 3.5h7v1.5h-7V11Zm7 3.5h-7V16h7v-1.5Z" /><path d="M17 4H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2ZM7 5.5h10a.5.5 0 0 1 .5.5v12a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V6a.5.5 0 0 1 .5-.5Z" /></svg>', |
| 63 | 'video' => '<svg class="help-center-menu-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18.7 3H5.3C4 3 3 4 3 5.3v13.4C3 20 4 21 5.3 21h13.4c1.3 0 2.3-1 2.3-2.3V5.3C21 4 20 3 18.7 3zm.8 15.7c0 .4-.4.8-.8.8H5.3c-.4 0-.8-.4-.8-.8V5.3c0-.4.4-.8.8-.8h13.4c.4 0 .8.4.8.8v13.4zM10 15l5-3-5-3v6z" /></svg>', |
| 64 | 'rss' => '<svg class="help-center-menu-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 10.2h-.8v1.5H5c1.9 0 3.8.8 5.1 2.1 1.4 1.4 2.1 3.2 2.1 5.1v.8h1.5V19c0-2.3-.9-4.5-2.6-6.2-1.6-1.6-3.8-2.6-6.1-2.6zm10.4-1.6C12.6 5.8 8.9 4.2 5 4.2h-.8v1.5H5c3.5 0 6.9 1.4 9.4 3.9s3.9 5.8 3.9 9.4v.8h1.5V19c0-3.9-1.6-7.6-4.4-10.4zM4 20h3v-3H4v3z" /></svg>', |
| 65 | ); |
| 66 | |
| 67 | return $icons[ $icon_name ] ?? ''; |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Add the agents manager menu panel to the admin bar. |
| 72 | * |
| 73 | * @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance. |
| 74 | */ |
| 75 | public function add_menu_panel( $wp_admin_bar ) { |
| 76 | // Add chat support group |
| 77 | $wp_admin_bar->add_group( |
| 78 | array( |
| 79 | 'parent' => 'agents-manager', |
| 80 | 'id' => 'agents-manager-menu-panel-chat', |
| 81 | 'meta' => array( |
| 82 | 'class' => 'ab-sub-secondary', |
| 83 | ), |
| 84 | ) |
| 85 | ); |
| 86 | |
| 87 | // Add chat support menu item |
| 88 | $wp_admin_bar->add_node( |
| 89 | array( |
| 90 | 'parent' => 'agents-manager-menu-panel-chat', |
| 91 | 'id' => 'agents-manager-chat-support', |
| 92 | 'title' => $this->get_icon( 'comment' ) . '<span>' . __( 'Chat support', 'jetpack-mu-wpcom' ) . '</span>', |
| 93 | ) |
| 94 | ); |
| 95 | |
| 96 | // Add chat history menu item |
| 97 | $wp_admin_bar->add_node( |
| 98 | array( |
| 99 | 'parent' => 'agents-manager-menu-panel-chat', |
| 100 | 'id' => 'agents-manager-chat-history', |
| 101 | 'title' => $this->get_icon( 'backup' ) . '<span>' . __( 'Chat history', 'jetpack-mu-wpcom' ) . '</span>', |
| 102 | ) |
| 103 | ); |
| 104 | |
| 105 | // Add links group |
| 106 | $wp_admin_bar->add_group( |
| 107 | array( |
| 108 | 'parent' => 'agents-manager', |
| 109 | 'id' => 'agents-manager-menu-panel-links', |
| 110 | 'meta' => array( |
| 111 | 'class' => 'ab-sub-secondary', |
| 112 | ), |
| 113 | ) |
| 114 | ); |
| 115 | |
| 116 | // Add support guides menu item |
| 117 | $wp_admin_bar->add_node( |
| 118 | array( |
| 119 | 'parent' => 'agents-manager-menu-panel-links', |
| 120 | 'id' => 'agents-manager-support-guides', |
| 121 | 'title' => $this->get_icon( 'page' ) . '<span>' . __( 'Support guides', 'jetpack-mu-wpcom' ) . '</span>', |
| 122 | ) |
| 123 | ); |
| 124 | |
| 125 | // Add courses menu item |
| 126 | $wp_admin_bar->add_node( |
| 127 | array( |
| 128 | 'parent' => 'agents-manager-menu-panel-links', |
| 129 | 'id' => 'agents-manager-courses', |
| 130 | 'title' => $this->get_icon( 'video' ) . '<span>' . __( 'Courses', 'jetpack-mu-wpcom' ) . '</span>', |
| 131 | 'href' => 'https://wordpress.com/support/courses/', |
| 132 | 'meta' => array( |
| 133 | 'target' => '_blank', |
| 134 | ), |
| 135 | ) |
| 136 | ); |
| 137 | |
| 138 | // Add product updates menu item |
| 139 | $wp_admin_bar->add_node( |
| 140 | array( |
| 141 | 'parent' => 'agents-manager-menu-panel-links', |
| 142 | 'id' => 'agents-manager-product-updates', |
| 143 | 'title' => $this->get_icon( 'rss' ) . '<span>' . __( 'Product updates', 'jetpack-mu-wpcom' ) . '</span>', |
| 144 | 'href' => 'https://wordpress.com/blog/category/product-features/', |
| 145 | 'meta' => array( |
| 146 | 'target' => '_blank', |
| 147 | ), |
| 148 | ) |
| 149 | ); |
| 150 | } |
| 151 | |
| 152 | /** |
| 153 | * Enqueue Agents Manager scripts and add inline script data. |
| 154 | */ |
| 155 | public function enqueue_scripts() { |
| 156 | // Early return for P2 frontend - don't add admin bar or enqueue scripts. |
| 157 | $stylesheet = get_stylesheet(); |
| 158 | $is_p2 = str_contains( $stylesheet, 'pub/p2' ) || function_exists( '\WPForTeams\is_wpforteams_site' ) && \WPForTeams\is_wpforteams_site( get_current_blog_id() ); |
| 159 | |
| 160 | if ( ! is_admin() && $is_p2 ) { |
| 161 | return; |
| 162 | } |
| 163 | |
| 164 | // Determine which variant to load (null = don't load). |
| 165 | $variant = $this->get_variant(); |
| 166 | if ( null === $variant ) { |
| 167 | return; |
| 168 | } |
| 169 | $use_disconnected = str_contains( $variant, 'disconnected' ); |
| 170 | $is_gutenberg = $this->is_block_editor(); |
| 171 | |
| 172 | // In Gutenberg, dequeue Help Center so we don't end up with two buttons. |
| 173 | // Agents Manager fires at priority 101, after Help Center at 100, so HC is already enqueued. |
| 174 | if ( $is_gutenberg ) { |
| 175 | wp_dequeue_script( 'help-center' ); |
| 176 | wp_dequeue_style( 'help-center-style' ); |
| 177 | } |
| 178 | |
| 179 | // For non-Gutenberg environments, add to admin bar |
| 180 | // Gutenberg doesn't have an admin bar, so JS will handle UI insertion |
| 181 | if ( ! $is_gutenberg ) { |
| 182 | add_action( |
| 183 | 'admin_bar_menu', |
| 184 | function ( $wp_admin_bar ) use ( $use_disconnected ) { |
| 185 | // Remove the help-center menu item |
| 186 | $wp_admin_bar->remove_node( 'help-center' ); |
| 187 | |
| 188 | $menu_args = array( |
| 189 | 'id' => 'agents-manager', |
| 190 | 'title' => '<span title="' . __( 'Help Center', 'jetpack-mu-wpcom' ) . '"><svg id="agents-manager-icon" class="ab-icon" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
| 191 | <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm-1 16v-2h2v2h-2zm2-3v-1.141A3.991 3.991 0 0016 10a4 4 0 00-8 0h2c0-1.103.897-2 2-2s2 .897 2 2-.897 2-2 2a1 1 0 00-1 1v2h2z" /> |
| 192 | </svg></span>', |
| 193 | 'parent' => 'top-secondary', |
| 194 | ); |
| 195 | |
| 196 | // For disconnected variants, link directly to help center instead of showing dropdown |
| 197 | if ( $use_disconnected ) { |
| 198 | $menu_args['href'] = self::HELP_CENTER_URL; |
| 199 | $menu_args['meta'] = array( |
| 200 | 'target' => '_blank', |
| 201 | ); |
| 202 | } else { |
| 203 | // For full variants, show the dropdown menu panel |
| 204 | $menu_args['meta'] = array( |
| 205 | 'html' => '<div id="agents-manager-masterbar" />', |
| 206 | 'class' => 'menupop', |
| 207 | 'target' => '_blank', |
| 208 | ); |
| 209 | } |
| 210 | |
| 211 | // Add the main agents manager menu node |
| 212 | $wp_admin_bar->add_menu( $menu_args ); |
| 213 | }, |
| 214 | // Add the agents manager icon to the admin bar after the help center is added, so we can remove it. |
| 215 | 100 |
| 216 | ); |
| 217 | |
| 218 | // Initialize the agents manager menu panel (only for full variants, not disconnected) |
| 219 | if ( ! $use_disconnected ) { |
| 220 | add_action( 'admin_bar_menu', array( $this, 'add_menu_panel' ), 100 ); |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * Filter to register agent provider modules for the Agents Manager. |
| 226 | * |
| 227 | * Plugins can hook into this filter to register script module IDs that export |
| 228 | * toolProvider and/or contextProvider. The Agents Manager JS will dynamically |
| 229 | * import these modules and merge their providers. |
| 230 | * |
| 231 | * @param array $providers Array of provider script module IDs. |
| 232 | */ |
| 233 | $agent_providers = apply_filters( 'agents_manager_agent_providers', array() ); |
| 234 | |
| 235 | /** |
| 236 | * Filter to determine if user should see the unified chat experience. |
| 237 | * |
| 238 | * When true, Help Center will render UnifiedAIAgent instead of traditional UI. |
| 239 | * The filter is hooked by should_use_unified_experience() in this class. |
| 240 | * |
| 241 | * @param bool $use_unified_experience Whether to use unified experience. Default false. |
| 242 | */ |
| 243 | $use_unified_experience = apply_filters( 'agents_manager_use_unified_experience', false ); |
| 244 | |
| 245 | $this->enqueue_script( $variant ); |
| 246 | |
| 247 | wp_add_inline_script( |
| 248 | 'agents-manager', |
| 249 | 'const agentsManagerData = ' . wp_json_encode( |
| 250 | array( |
| 251 | 'agentProviders' => $agent_providers, |
| 252 | 'useUnifiedExperience' => $use_unified_experience, |
| 253 | 'isDevMode' => self::is_dev_mode(), |
| 254 | 'sectionName' => $variant, |
| 255 | 'currentUser' => $this->get_current_user_data(), |
| 256 | 'site' => $this->get_current_site(), |
| 257 | 'helpCenterUrl' => self::HELP_CENTER_URL, |
| 258 | ), |
| 259 | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP |
| 260 | ) . ';', |
| 261 | 'before' |
| 262 | ); |
| 263 | } |
| 264 | |
| 265 | /** |
| 266 | * Determine which script variant to load, or null if none should be loaded. |
| 267 | * |
| 268 | * Combines the gating logic (should we load at all?) with variant selection |
| 269 | * (which build to use?) into a single method so the two cannot get out of sync. |
| 270 | * |
| 271 | * @return string|null The variant name, or null if scripts should not be loaded. |
| 272 | */ |
| 273 | private function get_variant() { |
| 274 | // CIAB/Next Admin: only load when disconnected (connected CIAB is handled by Help Center). |
| 275 | if ( $this->is_ciab_environment() ) { |
| 276 | if ( $this->is_enabled() && $this->is_jetpack_disconnected() ) { |
| 277 | return 'ciab-disconnected'; |
| 278 | } |
| 279 | return null; |
| 280 | } |
| 281 | |
| 282 | // Frontend: load disconnected variant for eligible logged-in editors. |
| 283 | if ( ! is_admin() ) { |
| 284 | if ( $this->is_loading_on_frontend() && $this->is_enabled() ) { |
| 285 | return 'wp-admin-disconnected'; |
| 286 | } |
| 287 | return null; |
| 288 | } |
| 289 | |
| 290 | // Apply wp-admin exclusions (WooCommerce, customizer, preview contexts). |
| 291 | if ( ! $this->passes_admin_checks() ) { |
| 292 | return null; |
| 293 | } |
| 294 | |
| 295 | if ( ! $this->is_enabled() ) { |
| 296 | return null; |
| 297 | } |
| 298 | |
| 299 | $disconnected = $this->is_jetpack_disconnected(); |
| 300 | |
| 301 | if ( $this->is_block_editor() ) { |
| 302 | return $disconnected ? 'gutenberg-disconnected' : 'gutenberg'; |
| 303 | } |
| 304 | |
| 305 | return $disconnected ? 'wp-admin-disconnected' : 'wp-admin'; |
| 306 | } |
| 307 | |
| 308 | /** |
| 309 | * Returns true if the Agents Manager should be loaded in the current context. |
| 310 | * |
| 311 | * @return bool |
| 312 | */ |
| 313 | private function is_enabled() { |
| 314 | // Full unified experience: Agents Manager with support guides, Help Center takeover, etc. |
| 315 | if ( apply_filters( 'agents_manager_use_unified_experience', false ) ) { |
| 316 | return true; |
| 317 | } |
| 318 | |
| 319 | // Block editor only: Agents Manager replaces Big Sky's native UI. Hooked by Big Sky. |
| 320 | if ( $this->is_block_editor() && apply_filters( 'agents_manager_enabled_in_block_editor', false ) ) { |
| 321 | return true; |
| 322 | } |
| 323 | |
| 324 | return false; |
| 325 | } |
| 326 | |
| 327 | /** |
| 328 | * Returns true if the current wp-admin context passes all exclusion checks. |
| 329 | * |
| 330 | * Excludes WooCommerce Admin home, customizer preview, Gutenberg asset requests, |
| 331 | * and preview query param contexts. |
| 332 | * |
| 333 | * @return bool |
| 334 | */ |
| 335 | private function passes_admin_checks() { |
| 336 | // Don't load on WooCommerce Admin home page to avoid UI conflicts. |
| 337 | global $current_screen; |
| 338 | if ( $current_screen && $current_screen->id === 'woocommerce_page_wc-admin' ) { |
| 339 | return false; |
| 340 | } |
| 341 | |
| 342 | // Don't load in customizer preview iframe. |
| 343 | if ( is_customize_preview() ) { |
| 344 | return false; |
| 345 | } |
| 346 | |
| 347 | // Don't load during Gutenberg asset requests or preview contexts. |
| 348 | $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; |
| 349 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission. |
| 350 | $is_preview = isset( $_GET['preview'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['preview'] ) ); |
| 351 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission. |
| 352 | $is_preview_overlay = isset( $_GET['preview_overlay'] ); |
| 353 | if ( str_contains( $request_uri, 'wp-content/plugins/gutenberg-core' ) || $is_preview || $is_preview_overlay ) { |
| 354 | return false; |
| 355 | } |
| 356 | |
| 357 | return true; |
| 358 | } |
| 359 | |
| 360 | /** |
| 361 | * Enqueue Agents Manager script based on context. |
| 362 | * |
| 363 | * @param string $variant The variant of the asset file to get. |
| 364 | */ |
| 365 | private function enqueue_script( $variant ) { |
| 366 | $cache_key = 'agents-manager-asset-' . $variant . '.asset.json'; |
| 367 | $asset_file = get_transient( $cache_key ); |
| 368 | |
| 369 | if ( ! $asset_file ) { |
| 370 | $asset_file = self::get_assets_json( 'widgets.wp.com/agents-manager/agents-manager-' . $variant . '.asset.json' ); |
| 371 | if ( ! $asset_file ) { |
| 372 | return; |
| 373 | } |
| 374 | set_transient( $cache_key, $asset_file, HOUR_IN_SECONDS ); |
| 375 | } |
| 376 | |
| 377 | // When the request is dev mode, use a random cache buster as the version for easier debugging. |
| 378 | $version = self::is_dev_mode() ? wp_rand() : $asset_file['version']; |
| 379 | |
| 380 | $script_dependencies = $asset_file['dependencies'] ?? array(); |
| 381 | |
| 382 | wp_enqueue_script( |
| 383 | 'agents-manager', |
| 384 | 'https://widgets.wp.com/agents-manager/agents-manager-' . $variant . '.min.js', |
| 385 | $script_dependencies, |
| 386 | $version, |
| 387 | true |
| 388 | ); |
| 389 | |
| 390 | if ( 'gutenberg-disconnected' !== $variant && 'ciab-disconnected' !== $variant ) { |
| 391 | wp_enqueue_style( |
| 392 | 'agents-manager-style', |
| 393 | 'https://widgets.wp.com/agents-manager/agents-manager-' . $variant . ( is_rtl() ? '.rtl.css' : '.css' ), |
| 394 | array(), |
| 395 | $version |
| 396 | ); |
| 397 | } |
| 398 | } |
| 399 | |
| 400 | /** |
| 401 | * Get the asset via file-system on wpcom and via network on Atomic sites. |
| 402 | * |
| 403 | * @param string $filepath The URL to download the asset file from. |
| 404 | * @return array|null The asset file data or null on failure. |
| 405 | */ |
| 406 | private static function get_assets_json( $filepath ) { |
| 407 | $accessible_directly = file_exists( ABSPATH . $filepath ); |
| 408 | |
| 409 | if ( $accessible_directly ) { |
| 410 | $file_contents = file_get_contents( ABSPATH . $filepath ); |
| 411 | |
| 412 | if ( false === $file_contents ) { |
| 413 | return null; |
| 414 | } |
| 415 | |
| 416 | return json_decode( $file_contents, true ); |
| 417 | } |
| 418 | |
| 419 | $request = wp_remote_get( 'https://' . $filepath ); |
| 420 | |
| 421 | if ( is_wp_error( $request ) ) { |
| 422 | return null; |
| 423 | } |
| 424 | |
| 425 | $response_code = wp_remote_retrieve_response_code( $request ); |
| 426 | if ( 200 !== $response_code ) { |
| 427 | return null; |
| 428 | } |
| 429 | |
| 430 | $content_type = wp_remote_retrieve_header( $request, 'content-type' ); |
| 431 | if ( is_string( $content_type ) && false === strpos( $content_type, 'json' ) ) { |
| 432 | return null; |
| 433 | } |
| 434 | |
| 435 | $body = wp_remote_retrieve_body( $request ); |
| 436 | if ( '' === $body ) { |
| 437 | return null; |
| 438 | } |
| 439 | |
| 440 | $decoded = json_decode( $body, true ); |
| 441 | if ( json_last_error() !== JSON_ERROR_NONE ) { |
| 442 | return null; |
| 443 | } |
| 444 | |
| 445 | return $decoded; |
| 446 | } |
| 447 | |
| 448 | /** |
| 449 | * Update the calypso preferences. |
| 450 | * |
| 451 | * @param \stdClass $preferences The preferences. |
| 452 | * |
| 453 | * @return \stdClass The preferences. |
| 454 | */ |
| 455 | public function calypso_preferences_update( $preferences ) { |
| 456 | // Check if agents_manager_router_history exists and is a valid array structure |
| 457 | if ( ! isset( $preferences->agents_manager_router_history ) || |
| 458 | ! is_array( $preferences->agents_manager_router_history ) ) { |
| 459 | return $preferences; |
| 460 | } |
| 461 | |
| 462 | $router_history = $preferences->agents_manager_router_history; |
| 463 | |
| 464 | // Check if entries exist and is an array |
| 465 | if ( ! isset( $router_history['entries'] ) || |
| 466 | ! is_array( $router_history['entries'] ) ) { |
| 467 | return $preferences; |
| 468 | } |
| 469 | |
| 470 | $entries = $router_history['entries']; |
| 471 | |
| 472 | // Limit entries to 50 to prevent spamming entries in the router history. |
| 473 | if ( count( $entries ) > 50 ) { |
| 474 | // Keep only the last 49 entries and add the root entry at the beginning. |
| 475 | $entries = array_slice( $entries, -49 ); |
| 476 | // Keep the start at root so the back button always works. |
| 477 | array_unshift( |
| 478 | $entries, |
| 479 | array( |
| 480 | 'pathname' => '/', |
| 481 | 'search' => '', |
| 482 | 'hash' => '', |
| 483 | 'key' => 'default', |
| 484 | 'state' => null, |
| 485 | ) |
| 486 | ); |
| 487 | |
| 488 | // Update the preferences object directly |
| 489 | $preferences->agents_manager_router_history['entries'] = $entries; |
| 490 | $preferences->agents_manager_router_history['index'] = 49; |
| 491 | } |
| 492 | |
| 493 | return $preferences; |
| 494 | } |
| 495 | |
| 496 | /** |
| 497 | * Creates instance. |
| 498 | * |
| 499 | * @return void |
| 500 | */ |
| 501 | public static function init() { |
| 502 | if ( self::$instance === null ) { |
| 503 | self::$instance = new self(); |
| 504 | } |
| 505 | } |
| 506 | |
| 507 | /** |
| 508 | * Returns whether the current request is coming from the A8C proxy. |
| 509 | * |
| 510 | * @return bool |
| 511 | */ |
| 512 | private static function is_proxied() { |
| 513 | // On Simple sites, use the wpcom function if available. |
| 514 | if ( function_exists( 'wpcom_is_proxied_request' ) ) { |
| 515 | return wpcom_is_proxied_request(); |
| 516 | } |
| 517 | |
| 518 | // On WoA/Garden sites, check server variable or constant. |
| 519 | return isset( $_SERVER['A8C_PROXIED_REQUEST'] ) |
| 520 | ? (bool) sanitize_text_field( wp_unslash( $_SERVER['A8C_PROXIED_REQUEST'] ) ) |
| 521 | : defined( 'A8C_PROXIED_REQUEST' ) && A8C_PROXIED_REQUEST; |
| 522 | } |
| 523 | |
| 524 | /** |
| 525 | * Enables "Development" features that should be accessible only for admins. |
| 526 | */ |
| 527 | private static function is_dev_mode() { |
| 528 | // Known local environments. |
| 529 | $domain = wp_parse_url( get_site_url(), PHP_URL_HOST ); |
| 530 | if ( |
| 531 | $domain === 'localhost' || |
| 532 | '.jurassic.tube' === stristr( $domain, '.jurassic.tube' ) || |
| 533 | '.jurassic.ninja' === stristr( $domain, '.jurassic.ninja' ) |
| 534 | ) { |
| 535 | return true; |
| 536 | } |
| 537 | |
| 538 | // A8C development. |
| 539 | if ( self::is_proxied() ) { |
| 540 | return true; |
| 541 | } |
| 542 | |
| 543 | if ( defined( 'AT_PROXIED_REQUEST' ) && AT_PROXIED_REQUEST && defined( 'ATOMIC_CLIENT_ID' ) ) { |
| 544 | switch ( ATOMIC_CLIENT_ID ) { |
| 545 | case 1: |
| 546 | case 2: |
| 547 | case 3: // Pressable |
| 548 | case 32: |
| 549 | case 118: // Commerce garden client (ciab) |
| 550 | return true; |
| 551 | } |
| 552 | } |
| 553 | |
| 554 | return false; |
| 555 | } |
| 556 | |
| 557 | /** |
| 558 | * Register the Agents Manager endpoints. |
| 559 | */ |
| 560 | public function register_rest_api() { |
| 561 | require_once __DIR__ . '/class-wp-rest-agents-manager-persisted-open-state.php'; |
| 562 | $controller = new WP_REST_Agents_Manager_Persisted_Open_State(); |
| 563 | $controller->register_rest_route(); |
| 564 | } |
| 565 | |
| 566 | /** |
| 567 | * Determine if user should see unified experience. |
| 568 | * |
| 569 | * @return bool |
| 570 | */ |
| 571 | public function should_use_unified_experience() { |
| 572 | // Early return for non-proxied/dev mode requests. |
| 573 | // This feature is currently only available to Automattic employees testing via proxy. |
| 574 | if ( ! self::is_dev_mode() ) { |
| 575 | return false; |
| 576 | } |
| 577 | |
| 578 | $user_id = get_current_user_id(); |
| 579 | |
| 580 | if ( ! $user_id ) { |
| 581 | return false; |
| 582 | } |
| 583 | |
| 584 | $is_simple_site = ( new \Automattic\Jetpack\Status\Host() )->is_wpcom_simple(); |
| 585 | if ( $is_simple_site ) { |
| 586 | // On Simple sites, evaluate locally. |
| 587 | // Check Automattician and opt-in setting. |
| 588 | $is_automattician = function_exists( '\is_automattician' ) && \is_automattician( $user_id ); |
| 589 | if ( $is_automattician && $this->has_unified_chat_opt_in_enabled( $user_id ) ) { |
| 590 | return true; |
| 591 | } |
| 592 | } |
| 593 | |
| 594 | // On WoA and Garden sites, delegate to wpcom via the /agents-manager/state endpoint. |
| 595 | // This avoids duplicating rollout logic and handles cases where |
| 596 | // wpcom-specific functions (like get_user_attribute) aren't available. |
| 597 | if ( $this->fetch_unified_experience_preference() ) { |
| 598 | return true; |
| 599 | } |
| 600 | |
| 601 | // False, for now. |
| 602 | // In the future: users with a big sky site (similar to https://github.a8c.com/Automattic/wpcom/pull/196449/files), a big-sky free trial or a paid plan. |
| 603 | return false; |
| 604 | } |
| 605 | |
| 606 | /** |
| 607 | * Check if user has enabled unified chat opt-in in their Automattician options. |
| 608 | * |
| 609 | * This checks the unified_ai_chat calypso preference set via the wpcom profile settings. |
| 610 | * Only used on Simple sites where get_user_attribute is available. |
| 611 | * |
| 612 | * @param int $user_id User ID. |
| 613 | * |
| 614 | * @return bool |
| 615 | */ |
| 616 | private function has_unified_chat_opt_in_enabled( $user_id ) { |
| 617 | if ( ! function_exists( '\get_user_attribute' ) ) { |
| 618 | return false; |
| 619 | } |
| 620 | |
| 621 | $calypso_prefs = \get_user_attribute( $user_id, 'calypso_preferences' ); |
| 622 | return ! empty( $calypso_prefs['unified_ai_chat'] ); |
| 623 | } |
| 624 | |
| 625 | /** |
| 626 | * Fetch unified experience preference from wpcom via Jetpack Connection. |
| 627 | * |
| 628 | * Used on Atomic sites to delegate the decision to wpcom, which has |
| 629 | * access to user attributes and can evaluate the rollout logic. |
| 630 | * |
| 631 | * Calls /agents-manager/state endpoint which is accessible via Jetpack user tokens. |
| 632 | * |
| 633 | * @return bool Whether user should see unified experience. |
| 634 | */ |
| 635 | private function fetch_unified_experience_preference() { |
| 636 | $user_id = get_current_user_id(); |
| 637 | if ( ! $user_id ) { |
| 638 | return false; |
| 639 | } |
| 640 | |
| 641 | // Check transient cache first (per-user cache). |
| 642 | $cache_key = 'unified-experience-' . $user_id; |
| 643 | $cached_result = get_transient( $cache_key ); |
| 644 | if ( false !== $cached_result ) { |
| 645 | return (bool) $cached_result; |
| 646 | } |
| 647 | |
| 648 | // Check if user is connected before making API call. |
| 649 | if ( ! ( new Connection_Manager() )->is_user_connected( $user_id ) ) { |
| 650 | return false; |
| 651 | } |
| 652 | |
| 653 | // Call dedicated agents-manager/state endpoint. |
| 654 | $wpcom_request = \Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_user( |
| 655 | '/agents-manager/state?key=unified_ai_chat', |
| 656 | '2', |
| 657 | array( 'method' => 'GET' ) |
| 658 | ); |
| 659 | |
| 660 | if ( is_wp_error( $wpcom_request ) ) { |
| 661 | // Cache failures too to avoid hammering the API. |
| 662 | set_transient( $cache_key, 0, MINUTE_IN_SECONDS ); |
| 663 | return false; |
| 664 | } |
| 665 | |
| 666 | $response_code = wp_remote_retrieve_response_code( $wpcom_request ); |
| 667 | if ( 200 !== $response_code ) { |
| 668 | set_transient( $cache_key, 0, MINUTE_IN_SECONDS ); |
| 669 | return false; |
| 670 | } |
| 671 | |
| 672 | $body = wp_remote_retrieve_body( $wpcom_request ); |
| 673 | $decoded_body = json_decode( $body, true ); |
| 674 | |
| 675 | // The response is { "unified_ai_chat": true/false } when using key param. |
| 676 | $result = is_array( $decoded_body ) && ! empty( $decoded_body['unified_ai_chat'] ); |
| 677 | |
| 678 | // Cache for 1 minute. |
| 679 | set_transient( $cache_key, $result ? 1 : 0, MINUTE_IN_SECONDS ); |
| 680 | |
| 681 | return $result; |
| 682 | } |
| 683 | |
| 684 | /** |
| 685 | * Returns true if the current request is on the frontend and the user can edit posts. |
| 686 | * |
| 687 | * Mirrors Help_Center::is_loading_on_frontend(). |
| 688 | * |
| 689 | * @return bool True if loading on the frontend for an eligible user. |
| 690 | */ |
| 691 | private function is_loading_on_frontend() { |
| 692 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission. |
| 693 | if ( isset( $_GET['na_site_preview'] ) || isset( $_GET['preview_overlay'] ) ) { |
| 694 | return false; |
| 695 | } |
| 696 | |
| 697 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission. |
| 698 | if ( isset( $_GET['preview'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['preview'] ) ) ) { |
| 699 | return false; |
| 700 | } |
| 701 | |
| 702 | $can_edit_posts = current_user_can( 'edit_posts' ) && is_user_member_of_blog(); |
| 703 | return ! is_admin() && ! $this->is_block_editor() && $can_edit_posts; |
| 704 | } |
| 705 | |
| 706 | /** |
| 707 | * Returns true if the current screen is the block editor. |
| 708 | * |
| 709 | * @return bool True if the current screen is the block editor. |
| 710 | */ |
| 711 | private function is_block_editor() { |
| 712 | if ( ! function_exists( 'get_current_screen' ) ) { |
| 713 | return false; |
| 714 | } |
| 715 | |
| 716 | $current_screen = get_current_screen(); |
| 717 | // The widgets screen has the block editor but no Gutenberg top bar. |
| 718 | return $current_screen && $current_screen->is_block_editor() && $current_screen->id !== 'widgets'; |
| 719 | } |
| 720 | |
| 721 | /** |
| 722 | * Check if current environment is CIAB (Commerce in a Box) / Next Admin. |
| 723 | * |
| 724 | * Uses the same detection method as Help Center: checks if next_admin_init has fired. |
| 725 | * |
| 726 | * @return bool True if CIAB/Next Admin environment. |
| 727 | */ |
| 728 | private function is_ciab_environment() { |
| 729 | return (bool) did_action( 'next_admin_init' ); |
| 730 | } |
| 731 | |
| 732 | /** |
| 733 | * Returns true if the current user is NOT connected through Jetpack. |
| 734 | * |
| 735 | * Mirrors the logic from Help_Center::is_jetpack_disconnected(). |
| 736 | * |
| 737 | * @return bool True if the site uses Jetpack but the current user is not connected. |
| 738 | */ |
| 739 | private function is_jetpack_disconnected() { |
| 740 | $user_id = get_current_user_id(); |
| 741 | $blog_id = get_current_blog_id(); |
| 742 | |
| 743 | if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) { |
| 744 | return ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $user_id ); |
| 745 | } |
| 746 | |
| 747 | if ( true === apply_filters( 'is_jetpack_site', false, $blog_id ) ) { |
| 748 | return ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $user_id ); |
| 749 | } |
| 750 | |
| 751 | return false; |
| 752 | } |
| 753 | |
| 754 | /** |
| 755 | * Get current user data for the agents manager. |
| 756 | * |
| 757 | * Mirrors the user data structure from Help Center's helpCenterData. |
| 758 | * |
| 759 | * @return array|null User data array or null if not logged in. |
| 760 | */ |
| 761 | private function get_current_user_data() { |
| 762 | $user_id = get_current_user_id(); |
| 763 | if ( ! $user_id ) { |
| 764 | return null; |
| 765 | } |
| 766 | |
| 767 | $user_data = get_userdata( $user_id ); |
| 768 | if ( ! $user_data ) { |
| 769 | return null; |
| 770 | } |
| 771 | |
| 772 | $user_email = $user_data->user_email; |
| 773 | |
| 774 | // Use wpcom_get_avatar_url on Simple sites, fall back to get_avatar_url elsewhere. |
| 775 | if ( function_exists( 'wpcom_get_avatar_url' ) ) { |
| 776 | $avatar_url = wpcom_get_avatar_url( $user_email, 64, '', true )[0]; |
| 777 | } else { |
| 778 | $avatar_url = get_avatar_url( $user_id ); |
| 779 | } |
| 780 | |
| 781 | return array( |
| 782 | 'ID' => $user_id, |
| 783 | 'username' => $user_data->user_login, |
| 784 | 'display_name' => $user_data->display_name, |
| 785 | 'avatar_URL' => $avatar_url, |
| 786 | 'email' => $user_email, |
| 787 | ); |
| 788 | } |
| 789 | |
| 790 | /** |
| 791 | * Get current site data for the agents manager. |
| 792 | * |
| 793 | * Returns minimal site data needed by AgentsManager (ID and domain only). |
| 794 | * Uses jetpack_options['id'] on Atomic sites for the wpcom blog ID. |
| 795 | * |
| 796 | * @return array Site data with ID and domain. |
| 797 | */ |
| 798 | private function get_current_site() { |
| 799 | /* |
| 800 | * Atomic sites have the WP.com blog ID stored as a Jetpack option. |
| 801 | * This code deliberately doesn't use `Jetpack_Options::get_option` |
| 802 | * so it works even when Jetpack has not been loaded. |
| 803 | */ |
| 804 | $jetpack_options = get_option( 'jetpack_options' ); |
| 805 | if ( is_array( $jetpack_options ) && isset( $jetpack_options['id'] ) ) { |
| 806 | $site_id = (int) $jetpack_options['id']; |
| 807 | } else { |
| 808 | $site_id = get_current_blog_id(); |
| 809 | } |
| 810 | |
| 811 | return array( |
| 812 | 'ID' => $site_id, |
| 813 | 'domain' => wp_parse_url( home_url(), PHP_URL_HOST ), |
| 814 | ); |
| 815 | } |
| 816 | } |
| 817 | |
| 818 | add_action( 'init', array( __NAMESPACE__ . '\Agents_Manager', 'init' ) ); |