class-restai-widget.php
1 <?php 2 /** 3 * Embed the RESTai chat widget on the front-end via a single <script> tag. 4 * 5 * Lazily provisions a widget on the support bot project the first time the 6 * tag is rendered, so the embedded script gets a real `wk_…` widget key and 7 * authenticates instead of falling into preview mode. 8 * 9 * @package RESTai 10 */ 11 12 namespace RESTai; 13 14 if ( ! defined( 'ABSPATH' ) ) { 15 exit; 16 } 17 18 class Widget { 19 20 const STORE_KEY = 'restai_widget_credentials'; 21 22 public function __construct() { 23 add_action( 'wp_footer', array( $this, 'maybe_embed' ) ); 24 add_shortcode( 'restai_chat', array( $this, 'shortcode' ) ); 25 } 26 27 public function maybe_embed() { 28 $settings = get_option( 'restai_settings', array() ); 29 if ( empty( $settings['enable_widget'] ) ) { 30 return; 31 } 32 echo $this->script_tag(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 33 } 34 35 public function shortcode( $atts = array() ) { 36 return $this->script_tag( $atts ); 37 } 38 39 private function script_tag( $overrides = array() ) { 40 $settings = get_option( 'restai_settings', array() ); 41 $widget_cfg = get_option( 'restai_widget_settings', array() ); 42 $plugin = Plugin::instance(); 43 $project_id = $plugin->provisioner->project_for( 'support_bot' ); 44 if ( empty( $settings['url'] ) || $project_id <= 0 ) { 45 return ''; 46 } 47 48 $widget_key = $this->ensure_widget_key( $project_id, $widget_cfg ); 49 if ( empty( $widget_key ) ) { 50 // Fall back to preview mode rather than a broken script tag. 51 return ''; 52 } 53 54 $url = untrailingslashit( $settings['url'] ); 55 $title = isset( $widget_cfg['title'] ) ? $widget_cfg['title'] : __( 'Chat with us', 'restai' ); 56 $color = isset( $widget_cfg['color'] ) ? $widget_cfg['color'] : '#6366f1'; 57 $welcome = isset( $widget_cfg['welcome'] ) ? $widget_cfg['welcome'] : __( 'Hi! How can I help?', 'restai' ); 58 59 $title = isset( $overrides['title'] ) ? $overrides['title'] : $title; 60 $color = isset( $overrides['color'] ) ? $overrides['color'] : $color; 61 $welcome = isset( $overrides['welcome'] ) ? $overrides['welcome'] : $welcome; 62 63 return sprintf( 64 '<script src="%1$s/widget/chat.js" data-widget-key="%2$s" data-stream="true" data-title="%3$s" data-primary-color="%4$s" data-welcome-message="%5$s"></script>', 65 esc_url( $url ), 66 esc_attr( $widget_key ), 67 esc_attr( $title ), 68 esc_attr( $color ), 69 esc_attr( $welcome ) 70 ); 71 } 72 73 /** 74 * Returns a `wk_…` widget key for the given support-bot project. If we've 75 * already provisioned one (and it's still bound to the same project) we 76 * reuse it; otherwise we create a new one on RESTai and stash the key. 77 * 78 * @param int $project_id 79 * @param array $widget_cfg user-tunable widget settings (title/color/...) 80 * @return string|null 81 */ 82 private function ensure_widget_key( $project_id, $widget_cfg ) { 83 $stored = get_option( self::STORE_KEY, array() ); 84 if ( ! empty( $stored['widget_key'] ) && (int) ( $stored['project_id'] ?? 0 ) === $project_id ) { 85 return $stored['widget_key']; 86 } 87 88 $client = Plugin::instance()->client; 89 $home_host = wp_parse_url( home_url(), PHP_URL_HOST ); 90 91 $payload = array( 92 'name' => 'WordPress chat widget', 93 'allowed_domains' => $home_host ? array( $home_host ) : array(), 94 'config' => array( 95 'title' => isset( $widget_cfg['title'] ) ? (string) $widget_cfg['title'] : 'Chat with us', 96 'subtitle' => '', 97 'primaryColor' => isset( $widget_cfg['color'] ) ? (string) $widget_cfg['color'] : '#6366f1', 98 'welcomeMessage' => isset( $widget_cfg['welcome'] ) ? (string) $widget_cfg['welcome'] : 'Hi! How can I help?', 99 'stream' => true, 100 ), 101 ); 102 103 $resp = $client->post( 'projects/' . $project_id . '/widgets', $payload ); 104 if ( is_wp_error( $resp ) || empty( $resp['widget_key'] ) ) { 105 if ( is_wp_error( $resp ) ) { 106 error_log( '[RESTai] widget create failed: ' . $resp->get_error_message() ); 107 } 108 return null; 109 } 110 111 $record = array( 112 'widget_key' => $resp['widget_key'], 113 'widget_id' => isset( $resp['id'] ) ? (int) $resp['id'] : 0, 114 'project_id' => $project_id, 115 'created_at' => time(), 116 ); 117 update_option( self::STORE_KEY, $record ); 118 return $record['widget_key']; 119 } 120 }