'array', 'sanitize_callback' => array( $this, 'sanitize_settings' ), 'default' => $this->get_default_settings(), ) ); add_settings_section( 'apsw_provider_section', __( 'Product generation controls', 'ai-product-studio-for-woocommerce' ), array( $this, 'render_provider_section_description' ), 'apsw_settings_page' ); add_settings_field( 'ai_provider', __( 'AI Service', 'ai-product-studio-for-woocommerce' ), array( $this, 'render_provider_field' ), 'apsw_settings_page', 'apsw_provider_section' ); } /** * Retrieve current settings with defaults merged. * * @return array */ public function get_settings() { $raw_settings = get_option( self::OPTION_KEY, null ); $saved_settings = is_array( $raw_settings ) ? $raw_settings : array(); $defaults = $this->get_default_settings(); $settings = wp_parse_args( $saved_settings, $defaults ); $settings[ self::FIELD_PROVIDER ] = $this->normalize_provider_key( $settings[ self::FIELD_PROVIDER ] ); $settings[ self::FIELD_CLUGINS_ACCESS_KEY ] = isset( $settings[ self::FIELD_CLUGINS_ACCESS_KEY ] ) ? $this->sanitize_secret_value( $settings[ self::FIELD_CLUGINS_ACCESS_KEY ] ) : ''; $settings[ self::FIELD_CLUGINS_CONNECTED_HOST ] = isset( $settings[ self::FIELD_CLUGINS_CONNECTED_HOST ] ) ? sanitize_text_field( $settings[ self::FIELD_CLUGINS_CONNECTED_HOST ] ) : ''; $settings[ self::FIELD_CLUGINS_CONNECTED_AT ] = isset( $settings[ self::FIELD_CLUGINS_CONNECTED_AT ] ) ? absint( $settings[ self::FIELD_CLUGINS_CONNECTED_AT ] ) : 0; $settings[ self::FIELD_DESCRIPTION_LENGTH ] = $this->normalize_description_length( $settings[ self::FIELD_DESCRIPTION_LENGTH ] ); foreach ( array_keys( $this->get_generation_option_fields() ) as $field_key ) { $settings[ $field_key ] = $this->normalize_boolean_setting( isset( $settings[ $field_key ] ) ? $settings[ $field_key ] : $defaults[ $field_key ] ); } return array_intersect_key( $settings, $defaults ); } /** * Get default settings. * * @return array */ public function get_default_settings() { return array( self::FIELD_PROVIDER => 'clugins_managed', self::FIELD_CLUGINS_ACCESS_KEY => '', self::FIELD_CLUGINS_CONNECTED_HOST => '', self::FIELD_CLUGINS_CONNECTED_AT => 0, self::FIELD_DESCRIPTION_LENGTH => 'standard', self::FIELD_APPLY_TAGS => 1, self::FIELD_APPLY_ATTRIBUTES => 1, self::FIELD_APPLY_IMAGE_ALT_TEXT => 1, self::FIELD_APPLY_SHIPPING_SPECS => 1, self::FIELD_STORE_SEO_META => 1, ); } /** * Return description length options for product generation. * * @return array */ public function get_description_length_options() { return array( 'short' => __( 'Short', 'ai-product-studio-for-woocommerce' ), 'standard' => __( 'Standard', 'ai-product-studio-for-woocommerce' ), 'detailed' => __( 'Detailed', 'ai-product-studio-for-woocommerce' ), ); } /** * Return boolean generation preference fields. * * @return array */ public function get_generation_option_fields() { return array( self::FIELD_APPLY_TAGS => array( 'label' => __( 'Apply product tags', 'ai-product-studio-for-woocommerce' ), 'description' => __( 'Include generated product tags when product details are prepared.', 'ai-product-studio-for-woocommerce' ), ), self::FIELD_APPLY_ATTRIBUTES => array( 'label' => __( 'Apply product attributes', 'ai-product-studio-for-woocommerce' ), 'description' => __( 'Include generated product attributes when product details are prepared.', 'ai-product-studio-for-woocommerce' ), ), self::FIELD_APPLY_IMAGE_ALT_TEXT => array( 'label' => __( 'Apply image alt text', 'ai-product-studio-for-woocommerce' ), 'description' => __( 'Include generated image alt text in the product details.', 'ai-product-studio-for-woocommerce' ), ), self::FIELD_APPLY_SHIPPING_SPECS => array( 'label' => __( 'Apply shipping specs', 'ai-product-studio-for-woocommerce' ), 'description' => __( 'Include generated shipping details when available.', 'ai-product-studio-for-woocommerce' ), ), self::FIELD_STORE_SEO_META => array( 'label' => __( 'Store SEO metadata', 'ai-product-studio-for-woocommerce' ), 'description' => __( 'Keep generated SEO details with the prepared product data.', 'ai-product-studio-for-woocommerce' ), ), ); } /** * Check whether a generation preference is enabled. * * @param string $key Setting key. * @return bool */ public function is_generation_option_enabled( $key ) { $key = sanitize_key( (string) $key ); $fields = $this->get_generation_option_fields(); if ( ! array_key_exists( $key, $fields ) ) { return false; } $settings = $this->get_settings(); return ! empty( $settings[ $key ] ); } /** * Normalize description length to a supported value. * * @param string $description_length Raw description length. * @return string */ public function normalize_description_length( $description_length ) { if ( is_array( $description_length ) ) { $description_length = ''; } $description_length = sanitize_key( (string) $description_length ); return array_key_exists( $description_length, $this->get_description_length_options() ) ? $description_length : 'standard'; } /** * Return allowed provider keys mapped to labels. * * @return array */ public function get_provider_options() { return array( 'clugins_managed' => __( 'Clugins Managed AI', 'ai-product-studio-for-woocommerce' ), ); } /** * Normalize provider key to internal stable key. * * @param string $provider Raw provider key. * @return string */ public function normalize_provider_key( $provider ) { if ( is_array( $provider ) ) { $provider = ''; } $provider = strtolower( trim( (string) $provider ) ); if ( ! array_key_exists( $provider, $this->get_provider_options() ) ) { $provider = 'clugins_managed'; } return $provider; } /** * Get provider label from key. * * @param string $provider Provider key. * @return string */ public function get_provider_label( $provider ) { $options = $this->get_provider_options(); $key = $this->normalize_provider_key( $provider ); return isset( $options[ $key ] ) ? $options[ $key ] : $options['clugins_managed']; } /** * Sanitize settings payload before save. * * @param array $input Raw settings input. * @return array */ public function sanitize_settings( $input ) { $input = is_array( $input ) ? $input : array(); if ( $this->internal_settings_update ) { return $this->sanitize_internal_settings( $input ); } $current = $this->get_settings(); if ( ! current_user_can( 'manage_options' ) ) { add_settings_error( self::OPTION_KEY, 'apsw_settings_permission_denied', __( 'You are not allowed to save these settings.', 'ai-product-studio-for-woocommerce' ), 'error' ); return $current; } $provider = isset( $input[ self::FIELD_PROVIDER ] ) ? $this->normalize_provider_key( wp_unslash( $input[ self::FIELD_PROVIDER ] ) ) : $current[ self::FIELD_PROVIDER ]; $clugins_access_key_input = isset( $input[ self::FIELD_CLUGINS_ACCESS_KEY ] ) ? $this->sanitize_secret_value( wp_unslash( $input[ self::FIELD_CLUGINS_ACCESS_KEY ] ) ) : ''; $description_length = isset( $input[ self::FIELD_DESCRIPTION_LENGTH ] ) ? $this->normalize_description_length( wp_unslash( $input[ self::FIELD_DESCRIPTION_LENGTH ] ) ) : $current[ self::FIELD_DESCRIPTION_LENGTH ]; $sanitized = array( self::FIELD_PROVIDER => $provider, self::FIELD_CLUGINS_ACCESS_KEY => '' !== $clugins_access_key_input ? $clugins_access_key_input : $current[ self::FIELD_CLUGINS_ACCESS_KEY ], self::FIELD_CLUGINS_CONNECTED_HOST => $current[ self::FIELD_CLUGINS_CONNECTED_HOST ], self::FIELD_CLUGINS_CONNECTED_AT => $current[ self::FIELD_CLUGINS_CONNECTED_AT ], self::FIELD_DESCRIPTION_LENGTH => $description_length, ); foreach ( array_keys( $this->get_generation_option_fields() ) as $field_key ) { $sanitized[ $field_key ] = $this->normalize_boolean_setting( isset( $input[ $field_key ] ) ? wp_unslash( $input[ $field_key ] ) : 0 ); } add_settings_error( self::OPTION_KEY, 'apsw_settings_saved', '' !== $clugins_access_key_input ? __( 'Clugins Access Key saved.', 'ai-product-studio-for-woocommerce' ) : __( 'Settings saved successfully.', 'ai-product-studio-for-woocommerce' ), 'success' ); return $sanitized; } /** * Remove the saved Clugins Access Key. * * @return void */ public function handle_remove_clugins_access_key() { if ( ! $this->current_user_can_manage_settings() ) { wp_die( esc_html__( 'You are not allowed to perform this action.', 'ai-product-studio-for-woocommerce' ) ); } check_admin_referer( 'apsw_remove_clugins_access_key' ); $settings = get_option( self::OPTION_KEY, array() ); if ( ! is_array( $settings ) ) { $settings = array(); } $settings = wp_parse_args( $settings, $this->get_default_settings() ); $settings[ self::FIELD_CLUGINS_ACCESS_KEY ] = ''; $settings[ self::FIELD_PROVIDER ] = $this->normalize_provider_key( $settings[ self::FIELD_PROVIDER ] ); $settings[ self::FIELD_CLUGINS_CONNECTED_HOST ] = ''; $settings[ self::FIELD_CLUGINS_CONNECTED_AT ] = 0; $this->update_internal_settings( $settings ); wp_safe_redirect( add_query_arg( array( 'page' => 'apsw-settings', 'apsw_status' => 'clugins_access_key_removed', '_wpnonce' => wp_create_nonce( 'apsw_settings_notice' ), ), admin_url( 'admin.php' ) ) ); exit; } /** * Start the Clugins one-click connection flow after an explicit admin click. * * @return void */ public function handle_start_clugins_connect() { if ( ! $this->current_user_can_manage_settings() ) { wp_die( esc_html__( 'You are not allowed to perform this action.', 'ai-product-studio-for-woocommerce' ) ); } check_admin_referer( 'apsw_start_clugins_connect' ); $state = $this->generate_base64url_random( 32 ); $code_verifier = $this->generate_base64url_random( 64 ); $code_challenge = $this->base64url_encode( hash( 'sha256', $code_verifier, true ) ); $site_url = home_url(); $callback_url = add_query_arg( array( 'action' => 'apsw_clugins_connect_callback', ), admin_url( 'admin-post.php' ) ); set_transient( $this->get_clugins_connect_transient_key( $state ), array( 'state' => $state, 'code_verifier' => $code_verifier, 'site_url' => $site_url, 'user_id' => get_current_user_id(), 'expires' => time() + self::CLUGINS_CONNECT_TRANSIENT_TTL, ), self::CLUGINS_CONNECT_TRANSIENT_TTL ); // Clugins.com is contacted only after the admin explicitly clicks Connect with Clugins. $authorize_url = add_query_arg( array( 'clg_connect' => '1', 'site_url' => $site_url, 'callback_url' => $callback_url, 'state' => $state, 'code_challenge' => $code_challenge, 'plugin_slug' => 'ai-product-studio-for-woocommerce', 'plugin_version' => defined( 'APSW_VERSION' ) ? APSW_VERSION : '', ), self::CLUGINS_CONNECT_AUTHORIZE_URL ); add_filter( 'allowed_redirect_hosts', array( $this, 'allow_clugins_redirect_host' ) ); wp_safe_redirect( esc_url_raw( $authorize_url ) ); exit; } /** * Complete the Clugins one-click callback and save the returned Access Key locally. * * @return void */ public function handle_clugins_connect_callback() { if ( ! $this->current_user_can_manage_settings() ) { wp_die( esc_html__( 'You are not allowed to perform this action.', 'ai-product-studio-for-woocommerce' ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External Clugins callback is protected by stored state validation. $connect_token = isset( $_GET['connect_token'] ) ? sanitize_text_field( wp_unslash( $_GET['connect_token'] ) ) : ''; if ( '' === $connect_token ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External Clugins callback is protected by stored state validation. $connect_token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : ''; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External Clugins callback is protected by stored state validation. $callback_site_url = isset( $_GET['site_url'] ) ? esc_url_raw( wp_unslash( $_GET['site_url'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Legacy Clugins callback is protected by stored state and PKCE-style verifier validation. $code = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- External Clugins callback is protected by stored state validation. $state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : ''; if ( '' === $state || ! $this->is_valid_clugins_connect_token( $state ) ) { $this->redirect_with_notice( 'error', __( 'Clugins connection state is missing or invalid. Please start the connection again.', 'ai-product-studio-for-woocommerce' ) ); } $transient_key = $this->get_clugins_connect_transient_key( $state ); $connection = get_transient( $transient_key ); if ( ! is_array( $connection ) || empty( $connection['state'] ) || ! hash_equals( (string) $connection['state'], $state ) ) { delete_transient( $transient_key ); $this->redirect_with_notice( 'error', __( 'Clugins connection expired. Please start the connection again.', 'ai-product-studio-for-woocommerce' ) ); } if ( empty( $connection['user_id'] ) || get_current_user_id() !== absint( $connection['user_id'] ) ) { delete_transient( $transient_key ); $this->redirect_with_notice( 'error', __( 'Clugins connection could not be verified. Please start the connection again.', 'ai-product-studio-for-woocommerce' ) ); } if ( empty( $connection['site_url'] ) || empty( $connection['expires'] ) || time() > absint( $connection['expires'] ) ) { delete_transient( $transient_key ); $this->redirect_with_notice( 'error', __( 'Clugins connection expired. Please start the connection again.', 'ai-product-studio-for-woocommerce' ) ); } $exchange_site_url = (string) $connection['site_url']; $is_token_flow = '' !== $connect_token; if ( $is_token_flow ) { if ( '' === $callback_site_url || ! $this->is_valid_clugins_site_url( $callback_site_url ) ) { delete_transient( $transient_key ); $this->redirect_with_notice( 'error', __( 'Clugins returned an invalid site URL. Please start the connection again.', 'ai-product-studio-for-woocommerce' ) ); } if ( ! $this->clugins_site_urls_match( $exchange_site_url, $callback_site_url ) ) { delete_transient( $transient_key ); $this->redirect_with_notice( 'error', __( 'Clugins connection site could not be verified. Please start the connection again.', 'ai-product-studio-for-woocommerce' ) ); } $exchange_site_url = $callback_site_url; $response = wp_remote_post( self::CLUGINS_CONNECT_EXCHANGE_URL, array( 'headers' => array( 'Accept' => 'application/json', 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( array( 'token' => $connect_token, 'site_url' => $exchange_site_url, 'state' => $state, ) ), 'timeout' => 20, ) ); } else { if ( '' === $code ) { delete_transient( $transient_key ); $this->redirect_with_notice( 'error', __( 'Clugins did not return a connection token. Please try again or use manual setup.', 'ai-product-studio-for-woocommerce' ) ); } if ( empty( $connection['code_verifier'] ) || ! $this->is_valid_clugins_connect_token( (string) $connection['code_verifier'] ) ) { delete_transient( $transient_key ); $this->redirect_with_notice( 'error', __( 'Clugins connection expired. Please start the connection again.', 'ai-product-studio-for-woocommerce' ) ); } $response = wp_remote_post( self::CLUGINS_CONNECT_LEGACY_EXCHANGE_URL, array( 'headers' => array( 'Accept' => 'application/json', ), 'body' => array( 'code' => $code, 'state' => $state, 'code_verifier' => (string) $connection['code_verifier'], 'site_url' => $exchange_site_url, ), 'timeout' => 20, ) ); } delete_transient( $transient_key ); if ( is_wp_error( $response ) ) { $this->redirect_with_notice( 'error', __( 'Clugins could not complete the key exchange. Please try again or use manual setup.', 'ai-product-studio-for-woocommerce' ) ); } $status_code = (int) wp_remote_retrieve_response_code( $response ); if ( $status_code < 200 || $status_code >= 300 ) { $this->redirect_with_notice( 'error', __( 'Clugins could not complete the key exchange. Please try again or use manual setup.', 'ai-product-studio-for-woocommerce' ) ); } $decoded = json_decode( (string) wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $decoded ) ) { $this->redirect_with_notice( 'error', __( 'Clugins returned an invalid connection response. Please try again or use manual setup.', 'ai-product-studio-for-woocommerce' ) ); } $access_key = $this->extract_clugins_access_key_from_response( $decoded ); if ( '' === $access_key ) { $this->redirect_with_notice( 'error', __( 'Clugins did not return an Access Key. Please try again or use manual setup.', 'ai-product-studio-for-woocommerce' ) ); } $settings = $this->get_settings(); $settings[ self::FIELD_PROVIDER ] = 'clugins_managed'; $settings[ self::FIELD_CLUGINS_ACCESS_KEY ] = $access_key; $settings[ self::FIELD_CLUGINS_CONNECTED_HOST ] = $this->get_clugins_connected_host_from_response( $decoded, $exchange_site_url ); $settings[ self::FIELD_CLUGINS_CONNECTED_AT ] = time(); $this->update_internal_settings( $settings ); $this->redirect_with_notice( 'success', __( 'Connected to Clugins. Your Access Key was saved securely on this site.', 'ai-product-studio-for-woocommerce' ) ); } /** * Disconnect the local Clugins connection without calling Clugins.com. * * @return void */ public function handle_disconnect_clugins_connection() { if ( ! $this->current_user_can_manage_settings() ) { wp_die( esc_html__( 'You are not allowed to perform this action.', 'ai-product-studio-for-woocommerce' ) ); } check_admin_referer( 'apsw_disconnect_clugins' ); $settings = $this->get_settings(); $settings[ self::FIELD_CLUGINS_ACCESS_KEY ] = ''; $settings[ self::FIELD_CLUGINS_CONNECTED_HOST ] = ''; $settings[ self::FIELD_CLUGINS_CONNECTED_AT ] = 0; $this->update_internal_settings( $settings ); $this->redirect_with_notice( 'success', __( 'Disconnected from Clugins on this site. You can reconnect or use manual setup anytime.', 'ai-product-studio-for-woocommerce' ) ); } /** * Sanitize secret values without removing valid key slug characters. * * @param string $secret Raw secret value. * @return string */ private function sanitize_secret_value( $secret ) { $secret = sanitize_text_field( (string) $secret ); $secret = preg_replace( '/\\s+/', '', $secret ); return is_string( $secret ) ? $secret : ''; } /** * Normalize a boolean-like setting to an integer flag. * * @param mixed $value Raw setting value. * @return int */ private function normalize_boolean_setting( $value ) { if ( is_array( $value ) ) { return 0; } $value = is_string( $value ) ? strtolower( trim( $value ) ) : $value; return in_array( $value, array( 1, '1', true, 'true', 'yes', 'on', 'enabled' ), true ) ? 1 : 0; } /** * Check whether the current admin can manage plugin settings. * * @return bool */ private function current_user_can_manage_settings() { return current_user_can( 'manage_options' ); } /** * Update settings for internal connect/disconnect flows. * * @param array $settings Settings array. * @return void */ private function update_internal_settings( array $settings ) { $settings = $this->sanitize_internal_settings( $settings ); try { $this->internal_settings_update = true; update_option( self::OPTION_KEY, $settings ); } finally { $this->internal_settings_update = false; } wp_cache_delete( self::OPTION_KEY, 'options' ); wp_cache_delete( 'alloptions', 'options' ); wp_cache_delete( 'notoptions', 'options' ); } /** * Sanitize a complete settings array for trusted internal updates. * * @param array $settings Settings array. * @return array */ private function sanitize_internal_settings( array $settings ) { $defaults = $this->get_default_settings(); $settings = array_intersect_key( wp_parse_args( $settings, $defaults ), $defaults ); $sanitized = array( self::FIELD_PROVIDER => $this->normalize_provider_key( $settings[ self::FIELD_PROVIDER ] ), self::FIELD_CLUGINS_ACCESS_KEY => $this->sanitize_secret_value( $settings[ self::FIELD_CLUGINS_ACCESS_KEY ] ), self::FIELD_CLUGINS_CONNECTED_HOST => sanitize_text_field( $settings[ self::FIELD_CLUGINS_CONNECTED_HOST ] ), self::FIELD_CLUGINS_CONNECTED_AT => absint( $settings[ self::FIELD_CLUGINS_CONNECTED_AT ] ), self::FIELD_DESCRIPTION_LENGTH => $this->normalize_description_length( $settings[ self::FIELD_DESCRIPTION_LENGTH ] ), ); foreach ( array_keys( $this->get_generation_option_fields() ) as $field_key ) { $sanitized[ $field_key ] = $this->normalize_boolean_setting( $settings[ $field_key ] ); } return $sanitized; } /** * Generate URL-safe random data. * * @param int $bytes Number of random bytes. * @return string */ private function generate_base64url_random( $bytes ) { $bytes = max( 16, absint( $bytes ) ); try { return $this->base64url_encode( random_bytes( $bytes ) ); } catch ( Exception $exception ) { return $this->base64url_encode( wp_generate_password( $bytes * 2, true, true ) . wp_rand() ); } } /** * Encode data using unpadded base64url. * * @param string $data Raw data. * @return string */ private function base64url_encode( $data ) { return rtrim( strtr( base64_encode( (string) $data ), '+/', '-_' ), '=' ); } /** * Build a transient key for a state value without storing the state in the key itself. * * @param string $state Connect state. * @return string */ private function get_clugins_connect_transient_key( $state ) { return self::CLUGINS_CONNECT_TRANSIENT_PREFIX . hash_hmac( 'sha256', (string) $state, wp_salt( 'auth' ) ); } /** * Validate URL-safe high-entropy connect tokens. * * @param string $token Connect state or verifier token. * @return bool */ private function is_valid_clugins_connect_token( $token ) { return is_string( $token ) && 1 === preg_match( '/\A[A-Za-z0-9_-]{32,256}\z/', $token ); } /** * Allow the explicit one-click connect redirect to Clugins.com. * * @param array $hosts Allowed redirect hosts. * @return array */ public function allow_clugins_redirect_host( array $hosts ) { $hosts[] = 'clugins.com'; return array_unique( $hosts ); } /** * Extract a raw Access Key from the exchange response without logging it. * * @param array $decoded Decoded response body. * @return string */ private function extract_clugins_access_key_from_response( array $decoded ) { $candidates = array( isset( $decoded['access_key'] ) ? $decoded['access_key'] : '', isset( $decoded['key'] ) ? $decoded['key'] : '', isset( $decoded['data']['access_key'] ) ? $decoded['data']['access_key'] : '', ); foreach ( $candidates as $candidate ) { $access_key = $this->sanitize_secret_value( $candidate ); if ( '' !== $access_key ) { return $access_key; } } return ''; } /** * Validate a site URL returned by the Clugins connection callback. * * @param string $site_url Site URL. * @return bool */ private function is_valid_clugins_site_url( $site_url ) { $scheme = wp_parse_url( $site_url, PHP_URL_SCHEME ); $host = wp_parse_url( $site_url, PHP_URL_HOST ); return is_string( $scheme ) && in_array( strtolower( $scheme ), array( 'http', 'https' ), true ) && is_string( $host ) && '' !== $host; } /** * Compare two site URLs by host for connection callback validation. * * @param string $expected_url Expected site URL. * @param string $actual_url Callback site URL. * @return bool */ private function clugins_site_urls_match( $expected_url, $actual_url ) { $expected_host = wp_parse_url( $expected_url, PHP_URL_HOST ); $actual_host = wp_parse_url( $actual_url, PHP_URL_HOST ); return is_string( $expected_host ) && is_string( $actual_host ) && hash_equals( strtolower( $expected_host ), strtolower( $actual_host ) ); } /** * Resolve the connected host from an exchange response or fallback site URL. * * @param array $decoded Decoded exchange response. * @param string $fallback_site_url Fallback site URL. * @return string */ private function get_clugins_connected_host_from_response( array $decoded, $fallback_site_url ) { $site_url_candidates = array( isset( $decoded['site_url'] ) ? $decoded['site_url'] : '', isset( $decoded['data']['site_url'] ) ? $decoded['data']['site_url'] : '', $fallback_site_url, ); foreach ( $site_url_candidates as $site_url ) { $site_url = esc_url_raw( (string) $site_url ); $host = wp_parse_url( $site_url, PHP_URL_HOST ); if ( is_string( $host ) && '' !== $host ) { return sanitize_text_field( $host ); } } return $this->get_current_site_host(); } /** * Return the current site host for connection status display. * * @return string */ private function get_current_site_host() { $host = wp_parse_url( home_url(), PHP_URL_HOST ); return is_string( $host ) ? sanitize_text_field( $host ) : ''; } /** * Redirect to Settings with a short admin notice. * * @param string $status Notice status. * @param string $message Notice message. * @return void */ private function redirect_with_notice( $status, $message ) { $url = add_query_arg( array( 'page' => 'apsw-settings', 'apsw_test_status' => sanitize_key( $status ), 'apsw_test_message' => sanitize_text_field( $message ), '_wpnonce' => wp_create_nonce( 'apsw_settings_notice' ), ), admin_url( 'admin.php' ) ); wp_safe_redirect( $url ); exit; } /** * Section description callback. * * @return void */ public function render_provider_section_description() { echo '
' . esc_html__( 'Choose how AI Product Studio should connect for product creation.', 'ai-product-studio-for-woocommerce' ) . '
'; } /** * Provider field callback. * * @return void */ public function render_provider_field() { $settings = $this->get_settings(); $provider = $this->normalize_provider_key( $settings[ self::FIELD_PROVIDER ] ); $label = $this->get_provider_label( $provider ); ?>