'array', 'sanitize_callback' => array( self::class, 'sanitize_settings' ), 'default' => self::default_settings(), ) ); add_settings_section( 'ai_receptionist_connection', esc_html__( 'Widget connection', 'aireceptionist-so' ), array( self::class, 'render_connection_section' ), self::PAGE_SLUG ); add_settings_field( 'ai_receptionist_office_id', esc_html__( 'Office ID', 'aireceptionist-so' ), array( self::class, 'render_office_id_field' ), self::PAGE_SLUG, 'ai_receptionist_connection' ); add_settings_field( 'ai_receptionist_enabled', esc_html__( 'Widget', 'aireceptionist-so' ), array( self::class, 'render_enabled_field' ), self::PAGE_SLUG, 'ai_receptionist_connection' ); add_settings_field( 'ai_receptionist_display_scope', esc_html__( 'Display on', 'aireceptionist-so' ), array( self::class, 'render_display_scope_field' ), self::PAGE_SLUG, 'ai_receptionist_connection' ); } /** * Adds the plugin settings page under Settings. * * @return void */ public static function add_settings_page() { add_options_page( esc_html__( 'AI Receptionist', 'aireceptionist-so' ), esc_html__( 'AI Receptionist', 'aireceptionist-so' ), 'manage_options', self::PAGE_SLUG, array( self::class, 'render_settings_page' ) ); } /** * Adds a direct settings link on the Plugins page. * * @param string[] $links Existing action links. * @return string[] */ public static function add_settings_link( $links ) { $settings_link = sprintf( '%2$s', esc_url( self::settings_page_url() ), esc_html__( 'Settings', 'aireceptionist-so' ) ); array_unshift( $links, $settings_link ); return $links; } /** * Describes the settings section. * * @return void */ public static function render_connection_section() { echo '

' . esc_html__( 'Connect this site to an AI Receptionist office, then choose where the widget appears.', 'aireceptionist-so' ) . '

'; } /** * Renders the Office ID field. * * @return void */ public static function render_office_id_field() { $settings = self::get_settings(); printf( '', esc_attr( self::OPTION_NAME ), esc_attr( $settings['office_id'] ) ); printf( '

%1$s %3$s

', esc_html__( 'Copy the Office ID from your AI Receptionist settings.', 'aireceptionist-so' ), esc_url( self::DASHBOARD_URL ), esc_html__( 'Open dashboard', 'aireceptionist-so' ) ); } /** * Renders the enabled field and its consent disclosure. * * @return void */ public static function render_enabled_field() { $settings = self::get_settings(); ?>


$office_id, 'enabled' => $enabled, 'display_scope' => $display_scope, ); } /** * Checks the saved Office ID against the hosted service. * * @return void */ public static function handle_connection_check() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to check this connection.', 'aireceptionist-so' ) ); } check_admin_referer( 'ai_receptionist_check_connection' ); $settings = self::get_settings(); $office_id = $settings['office_id']; if ( '' === $office_id ) { self::save_connection_status( 'error', '', '' ); self::redirect_to_settings(); } $request_url = add_query_arg( 'officeId', $office_id, self::STATUS_URL ); $response = wp_safe_remote_get( esc_url_raw( $request_url ), array( 'limit_response_size' => 32768, 'redirection' => 0, 'timeout' => 10, ) ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { self::save_connection_status( 'error', $office_id, '' ); self::redirect_to_settings(); } $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $data ) || ! isset( $data['connected'] ) || ! is_bool( $data['connected'] ) ) { self::save_connection_status( 'error', $office_id, '' ); self::redirect_to_settings(); } if ( ! $data['connected'] ) { self::save_connection_status( 'not_connected', $office_id, '' ); self::redirect_to_settings(); } $office_name = isset( $data['officeName'] ) && is_string( $data['officeName'] ) ? self::sanitize_office_name( $data['officeName'] ) : ''; self::save_connection_status( 'connected', $office_id, $office_name ); self::redirect_to_settings(); } /** * Enqueues the hosted widget when its saved settings allow it. * * @return void */ public static function enqueue_widget() { $settings = self::get_settings(); if ( ! $settings['enabled'] || '' === $settings['office_id'] ) { return; } if ( ! self::should_display( $settings['display_scope'] ) ) { return; } wp_enqueue_script( self::SCRIPT_HANDLE, self::SCRIPT_URL, array(), AI_RECEPTIONIST_VERSION, array( 'in_footer' => true, 'strategy' => 'async', ) ); } /** * Adds the Office ID and source label to the widget script tag. * * @param string $tag Generated script tag. * @param string $handle Registered script handle. * @param string $src Generated script source. * @return string */ public static function add_widget_script_attributes( $tag, $handle, $src ) { if ( self::SCRIPT_HANDLE !== $handle ) { return $tag; } $settings = self::get_settings(); if ( ! $settings['enabled'] || '' === $settings['office_id'] ) { return $tag; } return wp_get_script_tag( array( 'id' => 'ai-receptionist-widget-js', 'src' => esc_url_raw( $src ), 'async' => true, 'data-office' => $settings['office_id'], 'data-source' => 'wordpress', ) ); } /** * Shows the most recent result for the current Office ID. * * @return void */ private static function render_connection_status() { $status = get_option( self::STATUS_OPTION_NAME, array() ); $settings = self::get_settings(); if ( ! is_array( $status ) || ( $status['office_id'] ?? '' ) !== $settings['office_id'] ) { return; } $state = isset( $status['state'] ) && is_string( $status['state'] ) ? $status['state'] : ''; if ( 'connected' === $state ) { $office_name = isset( $status['office_name'] ) && is_string( $status['office_name'] ) ? self::sanitize_office_name( $status['office_name'] ) : ''; $message = '' !== $office_name ? sprintf( /* translators: %s: office name returned by AI Receptionist. */ esc_html__( 'Connected to %s.', 'aireceptionist-so' ), $office_name ) : esc_html__( 'Connection confirmed.', 'aireceptionist-so' ); echo '

' . esc_html( $message ) . '

'; return; } if ( 'not_connected' === $state ) { echo '

' . esc_html__( 'No AI Receptionist office was found for this Office ID.', 'aireceptionist-so' ) . '

'; return; } if ( 'error' === $state ) { echo '

' . esc_html__( 'The connection could not be checked. Confirm the Office ID and try again.', 'aireceptionist-so' ) . '

'; } } /** * Stores a sanitized connection result. * * @param string $state Connection state. * @param string $office_id Saved Office ID. * @param string $office_name Office name returned by the service. * @return void */ private static function save_connection_status( $state, $office_id, $office_name ) { $allowed_states = array( 'connected', 'not_connected', 'error' ); $state = in_array( $state, $allowed_states, true ) ? $state : 'error'; update_option( self::STATUS_OPTION_NAME, array( 'state' => $state, 'office_id' => self::sanitize_office_id( $office_id ), 'office_name' => self::sanitize_office_name( $office_name ), ), false ); } /** * Redirects a connection check back to the plugin settings page. * * @return void */ private static function redirect_to_settings() { wp_safe_redirect( self::settings_page_url() ); exit; } /** * Returns the plugin settings page URL. * * @return string */ private static function settings_page_url() { return add_query_arg( 'page', self::PAGE_SLUG, admin_url( 'options-general.php' ) ); } /** * Normalizes an Office ID. * * @param mixed $value Raw Office ID. * @return string */ private static function sanitize_office_id( $value ) { if ( ! is_string( $value ) && ! is_numeric( $value ) ) { return ''; } $office_id = trim( sanitize_text_field( (string) $value ) ); if ( '' === $office_id ) { return ''; } if ( strlen( $office_id ) > 128 || ! preg_match( '/\A[A-Za-z0-9_-]+\z/D', $office_id ) ) { return ''; } return $office_id; } /** * Normalizes an office name returned by the external service. * * @param mixed $value Raw office name. * @return string */ private static function sanitize_office_name( $value ) { if ( ! is_string( $value ) ) { return ''; } return wp_html_excerpt( sanitize_text_field( $value ), 200, '' ); } /** * Returns normalized saved settings. * * @return array{office_id:string,enabled:bool,display_scope:string} */ private static function get_settings() { $saved = get_option( self::OPTION_NAME, array() ); $saved = is_array( $saved ) ? $saved : array(); $settings = wp_parse_args( $saved, self::default_settings() ); $scope = is_string( $settings['display_scope'] ) ? sanitize_key( $settings['display_scope'] ) : ''; if ( ! array_key_exists( $scope, self::display_scopes() ) ) { $scope = 'entire_site'; } return array( 'office_id' => self::sanitize_office_id( $settings['office_id'] ), 'enabled' => ! empty( $settings['enabled'] ), 'display_scope' => $scope, ); } /** * Returns default settings. * * @return array{office_id:string,enabled:bool,display_scope:string} */ private static function default_settings() { return array( 'office_id' => '', 'enabled' => false, 'display_scope' => 'entire_site', ); } /** * Returns supported display scopes. * * @return array */ private static function display_scopes() { return array( 'entire_site' => esc_html__( 'Entire site', 'aireceptionist-so' ), 'posts' => esc_html__( 'Posts only', 'aireceptionist-so' ), 'pages' => esc_html__( 'Pages only', 'aireceptionist-so' ), 'front_page' => esc_html__( 'Front page only', 'aireceptionist-so' ), ); } /** * Determines whether the widget should load for the current request. * * @param string $scope Saved display scope. * @return bool */ private static function should_display( $scope ) { switch ( $scope ) { case 'posts': return is_singular( 'post' ); case 'pages': return is_page(); case 'front_page': return is_front_page(); case 'entire_site': default: return true; } } }