Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
45.19% |
141 / 312 |
|
33.33% |
6 / 18 |
CRAP | |
0.00% |
0 / 1 |
| ChatEditor | |
45.19% |
141 / 312 |
|
33.33% |
6 / 18 |
806.05 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
| register_rest_routes | |
90.91% |
30 / 33 |
|
0.00% |
0 / 1 |
1.00 | |||
| get_config | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
72 | |||
| get_brand_id | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
| enqueue_site_editor_assets | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
3.14 | |||
| is_site_editor_chat_screen | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| is_post_editor_chat_screen | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
13.12 | |||
| register_assets | |
4.44% |
2 / 45 |
|
0.00% |
0 / 1 |
49.75 | |||
| get_plan_upgrade_banner_data | |
46.67% |
7 / 15 |
|
0.00% |
0 / 1 |
17.71 | |||
| load_script_translation_file | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| add_admin_body_class | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
6.05 | |||
| add_editor_canvas_styles | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
| get_site_context | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
| load_text_domain | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
| admin_bar_menu | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
| enqueue_admin_bar_assets | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| upload_temp_file | |
76.00% |
38 / 50 |
|
0.00% |
0 / 1 |
6.50 | |||
| delete_temp_file | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace NewfoldLabs\WP\Module\EditorChat; |
| 4 | |
| 5 | /** |
| 6 | * ChatEditor main class |
| 7 | * |
| 8 | * Handles the registration and loading of the AI chat editor assets |
| 9 | * in the WordPress block editor, and provides a config REST endpoint |
| 10 | * for the CF AI Gateway Worker handshake. |
| 11 | */ |
| 12 | final class ChatEditor { |
| 13 | /** |
| 14 | * Array of allowed referrers for site editor access |
| 15 | * |
| 16 | * @var array |
| 17 | */ |
| 18 | protected static $allowed_referrers = array( |
| 19 | 'nfd-editor-chat', |
| 20 | ); |
| 21 | |
| 22 | /** |
| 23 | * Constructor. |
| 24 | */ |
| 25 | public function __construct() { |
| 26 | \add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_site_editor_assets' ) ); |
| 27 | \add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) ); |
| 28 | \add_action( 'init', array( __CLASS__, 'load_text_domain' ), 100 ); |
| 29 | \add_filter( 'load_script_translation_file', array( __CLASS__, 'load_script_translation_file' ), 10, 3 ); |
| 30 | \add_action( 'admin_bar_menu', array( __CLASS__, 'admin_bar_menu' ), 99 ); |
| 31 | \add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_bar_assets' ) ); |
| 32 | \add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_admin_bar_assets' ) ); |
| 33 | // Editor settings build before admin_enqueue_scripts, so register here. |
| 34 | \add_filter( 'block_editor_settings_all', array( __CLASS__, 'add_editor_canvas_styles' ), 10, 2 ); |
| 35 | } |
| 36 | |
| 37 | /** |
| 38 | * Register REST API routes. |
| 39 | */ |
| 40 | public static function register_rest_routes() { |
| 41 | \register_rest_route( |
| 42 | 'nfd-editor-chat/v1', |
| 43 | '/config', |
| 44 | array( |
| 45 | 'methods' => \WP_REST_Server::READABLE, |
| 46 | 'callback' => array( __CLASS__, 'get_config' ), |
| 47 | 'permission_callback' => function () { |
| 48 | return Permissions::is_editor(); |
| 49 | }, |
| 50 | ) |
| 51 | ); |
| 52 | \register_rest_route( |
| 53 | 'nfd-editor-chat/v1', |
| 54 | '/upload', |
| 55 | array( |
| 56 | 'methods' => \WP_REST_Server::CREATABLE, |
| 57 | 'callback' => array( __CLASS__, 'upload_temp_file' ), |
| 58 | 'permission_callback' => function () { |
| 59 | return Permissions::is_editor(); |
| 60 | }, |
| 61 | ) |
| 62 | ); |
| 63 | |
| 64 | \register_rest_route( |
| 65 | 'nfd-editor-chat/v1', |
| 66 | '/upload/(?P<filename>[a-zA-Z0-9_\-\.]+)', |
| 67 | array( |
| 68 | 'methods' => \WP_REST_Server::DELETABLE, |
| 69 | 'callback' => array( __CLASS__, 'delete_temp_file' ), |
| 70 | 'permission_callback' => function () { |
| 71 | return Permissions::is_editor(); |
| 72 | }, |
| 73 | ) |
| 74 | ); |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Get configuration for the editor chat frontend. |
| 79 | * |
| 80 | * Performs a server-to-server handshake with the CF Worker to exchange |
| 81 | * the Hiive auth token for a short-lived session JWT. The Hiive token |
| 82 | * never reaches the browser. |
| 83 | * |
| 84 | * @return \WP_REST_Response|\WP_Error |
| 85 | */ |
| 86 | public static function get_config() { |
| 87 | $worker_url = defined( 'NFD_EDITOR_CHAT_WORKER_URL' ) |
| 88 | ? \NFD_EDITOR_CHAT_WORKER_URL |
| 89 | : 'https://cf-worker-ai-chat.bluehost.workers.dev'; |
| 90 | |
| 91 | if ( empty( $worker_url ) ) { |
| 92 | return new \WP_Error( |
| 93 | 'worker_url_not_configured', |
| 94 | __( 'Editor chat Worker URL is not configured. Set NFD_EDITOR_CHAT_WORKER_URL in wp-config.php.', 'nfd-editor-chat' ), |
| 95 | array( 'status' => 500 ) |
| 96 | ); |
| 97 | } |
| 98 | |
| 99 | $worker_url = \untrailingslashit( $worker_url ); |
| 100 | |
| 101 | // Get Hiive auth token for server-to-server handshake |
| 102 | $hiive_token = ''; |
| 103 | if ( class_exists( '\NewfoldLabs\WP\Module\Data\HiiveConnection' ) ) { |
| 104 | $hiive_token = \NewfoldLabs\WP\Module\Data\HiiveConnection::get_auth_token(); |
| 105 | } |
| 106 | |
| 107 | if ( empty( $hiive_token ) ) { |
| 108 | return new \WP_Error( |
| 109 | 'hiive_token_unavailable', |
| 110 | __( 'Unable to retrieve Hiive authentication token.', 'nfd-editor-chat' ), |
| 111 | array( 'status' => 500 ) |
| 112 | ); |
| 113 | } |
| 114 | |
| 115 | // Server-to-server handshake with Worker |
| 116 | $handshake_response = \wp_remote_post( |
| 117 | $worker_url . '/handshake', |
| 118 | array( |
| 119 | 'headers' => array( |
| 120 | 'X-Hiive-Token' => $hiive_token, |
| 121 | 'Content-Type' => 'application/json', |
| 122 | ), |
| 123 | 'body' => \wp_json_encode( |
| 124 | array( |
| 125 | 'site_url' => \get_site_url(), |
| 126 | 'brand_id' => self::get_brand_id(), |
| 127 | ) |
| 128 | ), |
| 129 | 'timeout' => 10, |
| 130 | ) |
| 131 | ); |
| 132 | |
| 133 | if ( \is_wp_error( $handshake_response ) ) { |
| 134 | return new \WP_Error( |
| 135 | 'handshake_failed', |
| 136 | $handshake_response->get_error_message(), |
| 137 | array( 'status' => 502 ) |
| 138 | ); |
| 139 | } |
| 140 | |
| 141 | $status_code = \wp_remote_retrieve_response_code( $handshake_response ); |
| 142 | if ( 200 !== $status_code ) { |
| 143 | return new \WP_Error( |
| 144 | 'handshake_failed', |
| 145 | /* translators: %d: HTTP status code from the Worker handshake. */ |
| 146 | \sprintf( __( 'Worker handshake returned HTTP %d.', 'nfd-editor-chat' ), $status_code ), |
| 147 | array( 'status' => 502 ) |
| 148 | ); |
| 149 | } |
| 150 | |
| 151 | $data = json_decode( \wp_remote_retrieve_body( $handshake_response ), true ); |
| 152 | |
| 153 | if ( empty( $data['session_token'] ) ) { |
| 154 | return new \WP_Error( |
| 155 | 'handshake_failed', |
| 156 | __( 'Worker handshake did not return a session token.', 'nfd-editor-chat' ), |
| 157 | array( 'status' => 502 ) |
| 158 | ); |
| 159 | } |
| 160 | |
| 161 | return new \WP_REST_Response( |
| 162 | array( |
| 163 | 'worker_url' => $worker_url, |
| 164 | 'session_token' => $data['session_token'], |
| 165 | 'expires_in' => $data['expires_in'] ?? 3600, |
| 166 | ) |
| 167 | ); |
| 168 | } |
| 169 | |
| 170 | /** |
| 171 | * Get the brand identifier for the current plugin. |
| 172 | * |
| 173 | * @return string |
| 174 | */ |
| 175 | private static function get_brand_id() { |
| 176 | if ( defined( 'STARTER_PLUGIN_BRAND' ) ) { |
| 177 | return \STARTER_PLUGIN_BRAND; |
| 178 | } |
| 179 | // Fallback: derive from plugin directory name |
| 180 | $plugin_dir = \basename( \dirname( __DIR__, 3 ) ); |
| 181 | $brand_map = array( |
| 182 | 'wp-plugin-bluehost' => 'bluehost', |
| 183 | 'wp-plugin-hostgator' => 'hostgator', |
| 184 | 'wp-plugin-web' => 'web', |
| 185 | 'wp-plugin-crazy-domains' => 'crazydomains', |
| 186 | ); |
| 187 | |
| 188 | return $brand_map[ $plugin_dir ] ?? 'bluehost'; |
| 189 | } |
| 190 | |
| 191 | /** |
| 192 | * Enqueue editor chat assets on Site Editor (referrer) or post block editor screens. |
| 193 | * |
| 194 | * @return void |
| 195 | */ |
| 196 | public static function enqueue_site_editor_assets() { |
| 197 | global $pagenow; |
| 198 | |
| 199 | if ( self::is_site_editor_chat_screen( $pagenow ) ) { |
| 200 | self::register_assets( 'site' ); |
| 201 | \add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_class' ) ); |
| 202 | return; |
| 203 | } |
| 204 | |
| 205 | if ( self::is_post_editor_chat_screen( $pagenow ) ) { |
| 206 | self::register_assets( 'post' ); |
| 207 | \add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_class' ) ); |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * Whether the current request is the Site Editor with an allowed referrer. |
| 213 | * |
| 214 | * @param string $pagenow Current admin page. |
| 215 | * @return bool |
| 216 | */ |
| 217 | private static function is_site_editor_chat_screen( $pagenow ) { |
| 218 | if ( 'site-editor.php' !== $pagenow ) { |
| 219 | return false; |
| 220 | } |
| 221 | |
| 222 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Referrer parameter is validated against allowed list, no data modification. |
| 223 | return isset( $_GET['referrer'] ) && \in_array( $_GET['referrer'], self::$allowed_referrers, true ); |
| 224 | } |
| 225 | |
| 226 | /** |
| 227 | * Whether the current request is a block post editor screen (post.php / post-new.php). |
| 228 | * |
| 229 | * @param string $pagenow Current admin page. |
| 230 | * @return bool |
| 231 | */ |
| 232 | private static function is_post_editor_chat_screen( $pagenow ) { |
| 233 | if ( ! \in_array( $pagenow, array( 'post.php', 'post-new.php' ), true ) ) { |
| 234 | return false; |
| 235 | } |
| 236 | |
| 237 | if ( ! Permissions::is_editor() ) { |
| 238 | return false; |
| 239 | } |
| 240 | |
| 241 | $screen = \get_current_screen(); |
| 242 | |
| 243 | if ($screen && \method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor()){ |
| 244 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Referrer parameter is validated against allowed list, no data modification. |
| 245 | return isset( $_GET['referrer'] ) && \in_array( $_GET['referrer'], self::$allowed_referrers, true ); |
| 246 | } |
| 247 | |
| 248 | return false; |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * Register and enqueue chat editor assets. |
| 253 | * |
| 254 | * @param string $editor_type `site` or `post`. |
| 255 | */ |
| 256 | private static function register_assets( $editor_type = 'site' ) { |
| 257 | |
| 258 | $asset_file = NFD_EDITOR_CHAT_BUILD_DIR . '/chat-editor.asset.php'; |
| 259 | |
| 260 | if ( \is_readable( $asset_file ) ) { |
| 261 | $asset = include_once $asset_file; |
| 262 | |
| 263 | \wp_register_script( |
| 264 | 'nfd-editor-chat', |
| 265 | NFD_EDITOR_CHAT_BUILD_URL . '/chat-editor.js', |
| 266 | array_merge( $asset['dependencies'], array() ), |
| 267 | $asset['version'], |
| 268 | true |
| 269 | ); |
| 270 | |
| 271 | // In dev, version by file mtime so CSS-only rebuilds bust the cache |
| 272 | // ($asset['version'] only changes with the JS bundle). |
| 273 | $style_version = $asset['version']; |
| 274 | |
| 275 | if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { |
| 276 | $style_path = NFD_EDITOR_CHAT_BUILD_DIR . '/chat-editor.css'; |
| 277 | |
| 278 | if ( \is_readable( $style_path ) ) { |
| 279 | $style_version = (string) \filemtime( $style_path ); |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | \wp_register_style( |
| 284 | 'nfd-editor-chat', |
| 285 | NFD_EDITOR_CHAT_BUILD_URL . '/chat-editor.css', |
| 286 | array(), |
| 287 | $style_version |
| 288 | ); |
| 289 | |
| 290 | $args = array( |
| 291 | 'nonce' => \wp_create_nonce( 'wp_rest' ), |
| 292 | 'nfdRestURL' => \get_home_url() . '/index.php?rest_route=/nfd-editor-chat/v1', |
| 293 | 'mcpUrl' => \esc_url_raw( \rest_url( 'blu/mcp' ) ), |
| 294 | 'configEndpoint' => \esc_url_raw( \rest_url( 'nfd-editor-chat/v1/config' ) ), |
| 295 | 'homeUrl' => \esc_url( \get_home_url() ), |
| 296 | 'wpVer' => \esc_html( \get_bloginfo( 'version' ) ), |
| 297 | 'nfdChatVersion' => \esc_html( NFD_EDITOR_CHAT_VERSION ), |
| 298 | 'model' => defined( 'NFD_EDITOR_CHAT_MODEL' ) ? \NFD_EDITOR_CHAT_MODEL : '', |
| 299 | 'site' => self::get_site_context(), |
| 300 | 'pagesCount' => \array_sum( (array) \wp_count_posts( 'page' ) ), |
| 301 | 'editorType' => $editor_type, |
| 302 | ); |
| 303 | |
| 304 | $upgrade_banner_data = self::get_plan_upgrade_banner_data(); |
| 305 | if ( $upgrade_banner_data ) { |
| 306 | $args['planUpgradeBanner'] = $upgrade_banner_data; |
| 307 | } |
| 308 | |
| 309 | \wp_localize_script( 'nfd-editor-chat', 'nfdEditorChat', $args ); |
| 310 | |
| 311 | \wp_set_script_translations( |
| 312 | 'nfd-editor-chat', |
| 313 | 'nfd-editor-chat', |
| 314 | NFD_EDITOR_CHAT_DIR . '/languages' |
| 315 | ); |
| 316 | |
| 317 | \wp_enqueue_script( 'nfd-editor-chat' ); |
| 318 | \wp_enqueue_style( 'nfd-editor-chat' ); |
| 319 | } |
| 320 | } |
| 321 | |
| 322 | /** |
| 323 | * Retrieve the upgrade banner data. |
| 324 | * |
| 325 | * @return array |
| 326 | */ |
| 327 | private static function get_plan_upgrade_banner_data() { |
| 328 | static $data = null; |
| 329 | |
| 330 | if ( is_null( $data ) ) { |
| 331 | $data = array(); |
| 332 | $plan_data = \get_option( 'wvc_plan_data', '{}' ); |
| 333 | $plan_data = (bool) $plan_data && \is_string( $plan_data ) ? \json_decode( $plan_data, true ) : array(); |
| 334 | |
| 335 | if ( $plan_data ) { |
| 336 | $plan_data = \is_array( $plan_data ) ? $plan_data : array(); |
| 337 | $message = \sanitize_text_field( $plan_data['infoBannerText'] ?? '' ); |
| 338 | $upgrade_url = \esc_url_raw( $plan_data['upgrade_url'] ?? '' ); |
| 339 | |
| 340 | if ( $message && $upgrade_url ) { |
| 341 | $data = array( |
| 342 | 'message' => $message, |
| 343 | 'upgradeUrl' => $upgrade_url, |
| 344 | ); |
| 345 | } |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | return $data; |
| 350 | } |
| 351 | |
| 352 | /** |
| 353 | * Filter default WP script translations file to load the correct one |
| 354 | * |
| 355 | * @param string $file The translations file. |
| 356 | * @param string $handle Script handle. |
| 357 | * @param string $domain The strings textdomain. |
| 358 | * |
| 359 | * @return string |
| 360 | */ |
| 361 | public static function load_script_translation_file( $file, $handle, $domain ) { |
| 362 | |
| 363 | if ( 'nfd-editor-chat' === $handle ) { |
| 364 | $locale = \determine_locale(); |
| 365 | $key = \md5( 'build/' . NFD_EDITOR_CHAT_VERSION . '/chat-editor.js' ); |
| 366 | $file = NFD_EDITOR_CHAT_DIR . "/languages/{$domain}-{$locale}-{$key}.json"; |
| 367 | } |
| 368 | |
| 369 | return $file; |
| 370 | } |
| 371 | |
| 372 | /** |
| 373 | * Add custom admin class on block editor pages. |
| 374 | * |
| 375 | * @param string $classes Body classes. |
| 376 | * |
| 377 | * @return string |
| 378 | */ |
| 379 | public static function add_admin_body_class( $classes ) { |
| 380 | global $pagenow; |
| 381 | $current_screen = \get_current_screen(); |
| 382 | |
| 383 | if ( $current_screen && \method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor() ) { |
| 384 | $classes .= ' nfd-editor-chat-enabled'; |
| 385 | |
| 386 | if ( \in_array( $pagenow, array( 'post.php', 'post-new.php' ), true ) ) { |
| 387 | $classes .= ' nfd-editor-chat--post-editor'; |
| 388 | } |
| 389 | |
| 390 | if ( self::get_plan_upgrade_banner_data() ) { |
| 391 | $classes .= ' nfd-editor-chat--has-plan-upgrade-banner'; |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | return $classes; |
| 396 | } |
| 397 | |
| 398 | /** |
| 399 | * Round the top block's selection outline to match the framed canvas corner |
| 400 | * (the outline is drawn inside the iframe, out of reach of our stylesheet). |
| 401 | * |
| 402 | * @param array $settings Block editor settings. |
| 403 | * @param \WP_Block_Editor_Context $context Editor context. |
| 404 | * |
| 405 | * @return array |
| 406 | */ |
| 407 | public static function add_editor_canvas_styles( $settings, $context ) { |
| 408 | if ( ! isset( $context->name ) || 'core/edit-site' !== $context->name ) { |
| 409 | return $settings; |
| 410 | } |
| 411 | |
| 412 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Referrer parameter is validated against allowed list, no data modification. |
| 413 | if ( ! isset( $_GET['referrer'] ) || ! \in_array( $_GET['referrer'], self::$allowed_referrers, true ) ) { |
| 414 | return $settings; |
| 415 | } |
| 416 | |
| 417 | // 12px matches $radius in _main.scss. |
| 418 | $css = '.is-root-container > .block-editor-block-list__block:first-child,' |
| 419 | . '.is-root-container > .block-editor-block-list__block:first-child::before,' |
| 420 | . '.is-root-container > .block-editor-block-list__block:first-child::after' |
| 421 | . '{border-start-start-radius:12px;}'; |
| 422 | |
| 423 | $settings['styles'][] = array( 'css' => $css ); |
| 424 | |
| 425 | return $settings; |
| 426 | } |
| 427 | |
| 428 | /** |
| 429 | * Get site context data for the AI assistant. |
| 430 | * |
| 431 | * @return array |
| 432 | */ |
| 433 | private static function get_site_context() { |
| 434 | $onboarding = \get_option( 'nfd_module_onboarding_state_input', array() ); |
| 435 | |
| 436 | return array( |
| 437 | 'title' => \get_bloginfo( 'name' ), |
| 438 | 'description' => ! empty( $onboarding['prompt'] ) ? $onboarding['prompt'] : \get_bloginfo( 'description' ), |
| 439 | 'siteType' => $onboarding['siteType'] ?? '', |
| 440 | 'locale' => \get_locale(), |
| 441 | 'classification' => \get_option( 'nfd-ai-site-gen-siteclassification', '' ), |
| 442 | ); |
| 443 | } |
| 444 | |
| 445 | /** |
| 446 | * Load text domain for Module |
| 447 | * |
| 448 | * @return void |
| 449 | */ |
| 450 | public static function load_text_domain() { |
| 451 | |
| 452 | \load_plugin_textdomain( |
| 453 | 'nfd-editor-chat', |
| 454 | false, |
| 455 | NFD_EDITOR_CHAT_DIR . '/languages' |
| 456 | ); |
| 457 | |
| 458 | \load_script_textdomain( |
| 459 | 'nfd-editor-chat', |
| 460 | 'nfd-editor-chat', |
| 461 | NFD_EDITOR_CHAT_DIR . '/languages' |
| 462 | ); |
| 463 | } |
| 464 | |
| 465 | /** |
| 466 | * Add menu in admin bar. |
| 467 | * |
| 468 | * @param \WP_Admin_Bar $wp_admin_bar The admin bar. |
| 469 | */ |
| 470 | public static function admin_bar_menu( $wp_admin_bar ) { |
| 471 | $icon = '<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.99937 1.85729C5.108 1.85731 5.21368 1.8902 5.3004 1.95096C5.38712 2.01173 5.45017 2.09707 5.48001 2.19408L6.02198 3.95604C6.13867 4.33549 6.35759 4.68106 6.65806 4.9601C6.95853 5.23915 7.33063 5.44246 7.73922 5.55083L9.63645 6.05416C9.74084 6.08193 9.83266 6.14051 9.89802 6.22104C9.96338 6.30157 9.99874 6.39968 9.99874 6.50053C9.99874 6.60137 9.96338 6.69948 9.89802 6.78001C9.83266 6.86054 9.74084 6.91912 9.63645 6.9469L7.73922 7.45022C7.33063 7.5586 6.95853 7.76191 6.65806 8.04095C6.35759 8.31999 6.13867 8.66556 6.02198 9.04502L5.48001 10.807C5.4501 10.9039 5.38703 10.9892 5.30031 11.0499C5.2136 11.1106 5.10796 11.1434 4.99937 11.1434C4.89078 11.1434 4.78514 11.1106 4.69843 11.0499C4.61171 10.9892 4.54863 10.9039 4.51873 10.807L3.97676 9.04502C3.86006 8.66556 3.64115 8.31999 3.34068 8.04095C3.04021 7.76191 2.66811 7.5586 2.25952 7.45022L0.362287 6.9469C0.257896 6.91912 0.166078 6.86054 0.100716 6.78001C0.0353537 6.69948 0 6.60137 0 6.50053C0 6.39968 0.0353537 6.30157 0.100716 6.22104C0.166078 6.14051 0.257896 6.08193 0.362287 6.05416L2.25952 5.55083C2.66811 5.44246 3.04021 5.23915 3.34068 4.9601C3.64115 4.68106 3.86006 4.33549 3.97676 3.95604L4.51873 2.19408C4.54857 2.09707 4.61162 2.01173 4.69834 1.95096C4.78506 1.8902 4.89073 1.85731 4.99937 1.85729ZM10.999 7.20546e-08C11.1106 -5.7614e-05 11.2189 0.0345233 11.3069 0.0982423C11.3948 0.161961 11.4573 0.251159 11.4844 0.351648L11.6563 0.993033C11.8137 1.57499 12.303 2.0294 12.9296 2.17551L13.6202 2.33524C13.7286 2.36018 13.8249 2.41811 13.8938 2.49979C13.9626 2.58148 14 2.68222 14 2.78594C14 2.88966 13.9626 2.9904 13.8938 3.07209C13.8249 3.15377 13.7286 3.21171 13.6202 3.23664L12.9296 3.39637C12.303 3.54248 11.8137 3.9969 11.6563 4.57885L11.4844 5.22023C11.4575 5.32091 11.3951 5.41035 11.3072 5.47427C11.2192 5.53819 11.1107 5.57292 10.999 5.57292C10.8874 5.57292 10.7789 5.53819 10.6909 5.47427C10.603 5.41035 10.5406 5.32091 10.5137 5.22023L10.3417 4.57885C10.2648 4.29309 10.1057 4.03212 9.88145 3.82384C9.65718 3.61556 9.37618 3.46781 9.06848 3.39637L8.37785 3.23664C8.26944 3.21171 8.17314 3.15377 8.10431 3.07209C8.03548 2.9904 7.99809 2.88966 7.99809 2.78594C7.99809 2.68222 8.03548 2.58148 8.10431 2.49979C8.17314 2.41811 8.26944 2.36018 8.37785 2.33524L9.06848 2.17551C9.37618 2.10407 9.65718 1.95632 9.88145 1.74804C10.1057 1.53976 10.2648 1.27879 10.3417 0.993033L10.5137 0.351648C10.5408 0.251159 10.6033 0.161961 10.6912 0.0982423C10.7792 0.0345233 10.8875 -5.7614e-05 10.999 7.20546e-08ZM9.9991 8.35782C10.1041 8.35776 10.2065 8.38841 10.2917 8.44542C10.3769 8.50243 10.4406 8.5829 10.4737 8.67542L10.7364 9.40781C10.8364 9.68455 11.0697 9.90247 11.3684 9.99471L12.157 10.2393C12.2563 10.2702 12.3426 10.3294 12.4038 10.4083C12.4649 10.4873 12.4978 10.5822 12.4978 10.6794C12.4978 10.7767 12.4649 10.8715 12.4038 10.9505C12.3426 11.0295 12.2563 11.0887 12.157 11.1196L11.3684 11.3642C11.0704 11.457 10.8357 11.6737 10.7364 11.9511L10.4731 12.6835C10.4397 12.7757 10.376 12.8559 10.291 12.9126C10.206 12.9694 10.1038 13 9.9991 13C9.89435 13 9.79224 12.9694 9.70719 12.9126C9.62215 12.8559 9.55846 12.7757 9.52512 12.6835L9.2618 11.9511C9.2127 11.8144 9.13004 11.6903 9.02035 11.5884C8.91067 11.4865 8.77698 11.4098 8.62984 11.3642L7.84121 11.1196C7.74191 11.0887 7.65558 11.0295 7.59443 10.9505C7.53327 10.8715 7.50037 10.7767 7.50037 10.6794C7.50037 10.5822 7.53327 10.4873 7.59443 10.4083C7.65558 10.3294 7.74191 10.2702 7.84121 10.2393L8.62984 9.99471C8.92782 9.90185 9.16248 9.68517 9.2618 9.40781L9.52512 8.67542C9.55827 8.583 9.62187 8.5026 9.70693 8.4456C9.792 8.3886 9.89421 8.35789 9.9991 8.35782Z" fill="white"/></svg>'; |
| 472 | // translators: %s is the "Bluehost" brand name and should not be translated. Example: "Bluehost AI Editor". |
| 473 | $title = \sprintf( __( '%s AI Editor', 'nfd-editor-chat' ), 'Bluehost' ); |
| 474 | |
| 475 | $editor_args = array( |
| 476 | 'canvas' => 'edit', |
| 477 | 'referrer' => 'nfd-editor-chat', |
| 478 | ); |
| 479 | $url = \add_query_arg( $editor_args, \admin_url( 'site-editor.php' ) ); |
| 480 | |
| 481 | $args = array( |
| 482 | 'id' => 'nfd-editor-chat', |
| 483 | 'parent' => 'top-secondary', |
| 484 | 'title' => $icon . $title, |
| 485 | 'href' => $url, |
| 486 | ); |
| 487 | |
| 488 | $wp_admin_bar->add_node( $args ); |
| 489 | } |
| 490 | |
| 491 | /** |
| 492 | * Enqueue styles for admin-bar. |
| 493 | */ |
| 494 | public static function enqueue_admin_bar_assets() { |
| 495 | if ( is_admin_bar_showing() ) { |
| 496 | \wp_enqueue_style( 'nfd-editor-chat-admin-bar', \NFD_EDITOR_CHAT_ASSETS_URL . 'css/admin-bar.css', array(), NFD_EDITOR_CHAT_VERSION ); |
| 497 | } |
| 498 | } |
| 499 | |
| 500 | /** |
| 501 | * Upload a file to temporary storage (not Media Library). |
| 502 | * |
| 503 | * @param \WP_REST_Request $request The REST request. |
| 504 | * @return \WP_REST_Response|\WP_Error |
| 505 | */ |
| 506 | public static function upload_temp_file( \WP_REST_Request $request ) { |
| 507 | $files = $request->get_file_params(); |
| 508 | |
| 509 | if ( empty( $files['file'] ) || ! empty( $files['file']['error'] ) ) { |
| 510 | return new \WP_Error( |
| 511 | 'no_file', |
| 512 | __( 'No file provided or upload error.', 'nfd-editor-chat' ), |
| 513 | array( 'status' => 400 ) |
| 514 | ); |
| 515 | } |
| 516 | |
| 517 | $file = $files['file']; |
| 518 | |
| 519 | $allowed_mime_types = array( |
| 520 | 'image/png', |
| 521 | 'image/jpeg', |
| 522 | 'image/webp', |
| 523 | 'image/gif', |
| 524 | 'application/pdf', |
| 525 | 'text/plain', |
| 526 | 'text/csv', |
| 527 | 'text/markdown', |
| 528 | ); |
| 529 | |
| 530 | if ( ! in_array( $file['type'], $allowed_mime_types, true ) ) { |
| 531 | return new \WP_Error( |
| 532 | 'invalid_file_type', |
| 533 | __( 'File type not allowed.', 'nfd-editor-chat' ), |
| 534 | array( 'status' => 400 ) |
| 535 | ); |
| 536 | } |
| 537 | |
| 538 | $upload_dir = \wp_upload_dir(); |
| 539 | $temp_dir = $upload_dir['basedir'] . '/nfd-chat-temp/'; |
| 540 | $temp_url = $upload_dir['baseurl'] . '/nfd-chat-temp/'; |
| 541 | |
| 542 | if ( ! \file_exists( $temp_dir ) ) { |
| 543 | \wp_mkdir_p( $temp_dir ); |
| 544 | // Prevent directory listing and PHP execution. |
| 545 | \file_put_contents( $temp_dir . 'index.php', '<?php // Silence is golden.' ); |
| 546 | } |
| 547 | |
| 548 | $filename = \wp_unique_filename( $temp_dir, \sanitize_file_name( $file['name'] ) ); |
| 549 | $filepath = $temp_dir . $filename; |
| 550 | |
| 551 | if ( ! \move_uploaded_file( $file['tmp_name'], $filepath ) ) { |
| 552 | return new \WP_Error( |
| 553 | 'upload_failed', |
| 554 | __( 'Failed to save uploaded file.', 'nfd-editor-chat' ), |
| 555 | array( 'status' => 500 ) |
| 556 | ); |
| 557 | } |
| 558 | |
| 559 | $file_url = $temp_url . $filename; |
| 560 | |
| 561 | // Track file for cleanup after 24h. |
| 562 | \set_transient( 'nfd_chat_temp_' . \md5( $filename ), $filepath, \DAY_IN_SECONDS * 3 ); |
| 563 | |
| 564 | return new \WP_REST_Response( |
| 565 | array( |
| 566 | 'url' => $file_url, |
| 567 | 'filename' => $filename, |
| 568 | 'name' => $file['name'], |
| 569 | 'type' => $file['type'], |
| 570 | 'size' => $file['size'], |
| 571 | ), |
| 572 | 201 |
| 573 | ); |
| 574 | } |
| 575 | |
| 576 | /** |
| 577 | * Delete a temporary uploaded file. |
| 578 | * |
| 579 | * @param \WP_REST_Request $request The REST request. |
| 580 | * @return \WP_REST_Response|\WP_Error |
| 581 | */ |
| 582 | public static function delete_temp_file( \WP_REST_Request $request ) { |
| 583 | $filename = $request->get_param( 'filename' ); |
| 584 | |
| 585 | $upload_dir = \wp_upload_dir(); |
| 586 | $filepath = $upload_dir['basedir'] . '/nfd-chat-temp/' . \sanitize_file_name( $filename ); |
| 587 | |
| 588 | if ( ! \file_exists( $filepath ) ) { |
| 589 | return new \WP_Error( |
| 590 | 'file_not_found', |
| 591 | __( 'File not found.', 'nfd-editor-chat' ), |
| 592 | array( 'status' => 404 ) |
| 593 | ); |
| 594 | } |
| 595 | |
| 596 | \wp_delete_file( $filepath ); |
| 597 | \delete_transient( 'nfd_chat_temp_' . \md5( $filename ) ); |
| 598 | |
| 599 | return new \WP_REST_Response( null, 204 ); |
| 600 | } |
| 601 | } |