prefix . 'ai_app_onsite_prompt_editor'; $table_name_model_settings = $wpdb->prefix . 'ai_app_onsite_model_settings'; $table_name_plugin_settings = $wpdb->prefix . 'ai_app_onsite_plugin_settings'; $table_name_field_selector = $wpdb->prefix . 'ai_app_onsite_field_selector_settings'; $table_name_app_properties = $wpdb->prefix . 'ai_app_onsite_app_properties'; $table_name_models = $wpdb->prefix . 'ai_app_onsite_models'; $response_message = ''; $app_name = ''; $properties = $wpdb->get_results("SELECT response_message, app_name_show, app_name FROM $table_name_app_properties WHERE app_id = 1 LIMIT 1", ARRAY_A); if (!empty($properties[0])) { $response_message = $properties[0]['response_message'] ?? ''; $app_name = !empty($properties[0]['app_name_show']) ? $properties[0]['app_name_show'] : ($properties[0]['app_name'] ?? ''); } $model_settings = $wpdb->get_results("SELECT * FROM $table_name_model_settings WHERE app_id = 1", ARRAY_A); $plugin_settings = $wpdb->get_results("SELECT * FROM $table_name_plugin_settings WHERE app_id = 1", ARRAY_A); $formData = $_POST; $field_names = array_keys($formData); $field_values = array_values($formData); $pairs = array_combine($field_names, $field_values); $field_selector_settings = $wpdb->get_results("SELECT `field_label`, `field_tag`, `field_id` FROM $table_name_field_selector WHERE app_id = 1", ARRAY_A); $map = []; foreach ($field_selector_settings as $r) { $cleanTag = preg_replace('/[{}\s]/', '', $r['field_tag']); $normalized = strtolower(preg_replace('/\s+/', '_', trim($r['field_label']))); $map[$r['field_label']] = $cleanTag; $map[$normalized] = $cleanTag; // Also map by field_id (form name attribute now uses field_id) $map[$r['field_id']] = $cleanTag; } // Replace label keys with their field_tag (keep unmapped as-is) $tag_pairs = []; foreach ($formData as $label => $value) { $normalized = strtolower(preg_replace('/\s+/', '_', trim($label))); if (isset($map[$label]) || isset($map[$normalized])) { $key = $map[$label] ?? $map[$normalized]; $tag_pairs[$key] = sanitize_text_field($value); } } $field_tag_names = array_keys($tag_pairs); $field_tag_values = array_values($tag_pairs); $field_tag_parirs = array_combine($field_tag_names, $field_tag_values); $banned_words = (!empty($plugin_settings) && isset($plugin_settings[0]['banned_words']) && !is_null($plugin_settings[0]['banned_words'])) ? unserialize($plugin_settings[0]['banned_words']) : []; $app_email = isset($plugin_settings[0]['app_email']) ? $plugin_settings[0]['app_email'] : null; function containsBannedWordPart($value, $banned_word) { // Split banned word into parts by space $banned_parts = explode(' ', $banned_word); foreach ($banned_parts as $part) { // Use preg_match to find exact words if (preg_match('/\b' . preg_quote($part, '/') . '\b/i', $value)) { return true; } } return false; } // Iterate through field values and check against banned words if (!empty($banned_words)) { foreach ($field_tag_values as $value) { foreach ($banned_words as $banned_word) { if (containsBannedWordPart($value, $banned_word)) { wp_send_json_error("Banned word included in: " . $value); break 2; } } } } $fieldLabels = array_column($field_selector_settings, 'field_label'); // Process each element of the array $fieldLabels = array_map(function ($item) { // Replace spaces with underscores in each item return preg_replace('/\s+/', '_', $item); }, $fieldLabels); $fieldTagLabels = array_column($field_selector_settings, 'field_tag'); // Process each element of the array $fieldTagLabels = array_map(function ($item) { // Remove {{ and }} from each item return trim(str_replace(['{{', '}}'], '', $item)); }, $fieldTagLabels); if (empty($model_settings)) { wp_send_json_error('Plugin Model settings are missing'); } $model_setting = $model_settings[0]; // Determine the active provider $active_provider = isset($model_setting['active_provider']) ? $model_setting['active_provider'] : 'openai'; // Get the provider handler if ( class_exists( 'AI_App_Onsite_Provider_Factory' ) ) { try { $provider = AI_App_Onsite_Provider_Factory::get_provider( $active_provider ); } catch ( \InvalidArgumentException $e ) { wp_send_json_error('Unsupported AI provider configured.'); return; } } else { // Fallback: assume OpenAI if provider factory not loaded $active_provider = 'openai'; $provider = null; } // Get the correct API key based on provider $api_key_column = $provider ? $provider->get_key_column() : 'openai_key'; $key_status_column = $provider ? $provider->get_key_status_column() : 'key_status'; $api_key = $model_setting[$api_key_column] ?? ''; $key_status = $model_setting[$key_status_column] ?? 'true'; $model = $model_setting['model']; $temperature = $model_setting['temperature'] ?: ''; $f_penalty = $model_setting['f_penalty'] ?: ''; $p_penalty = $model_setting['p_penalty'] ?: ''; $max_tokens = $model_setting['max_tokens'] ?: ''; $top_p = $model_setting['top_p'] ?: ''; // Check for missing keys if (empty($api_key)) { wp_send_json_error( ucfirst($active_provider) . ' API key is missing. Please add your API key in Model Settings.'); } if (empty($model)) { wp_send_json_error('No AI model is selected. Please go to Model Settings, select a model, and click "Save Settings".'); } // Block API calls for shut-down models (deprecated models with future shutdown dates remain usable) $model_shutdown_date = $wpdb->get_var( $wpdb->prepare( "SELECT shutdown_date FROM $table_name_models WHERE model = %s", $model ) ); if ( $model_shutdown_date !== null && $model_shutdown_date < current_time( 'Y-m-d' ) ) { wp_send_json_error('The selected AI model "' . $model . '" has been shut down by OpenAI and is no longer available. Please contact the site administrator to update the model in settings.'); } if ($key_status === 'false') { wp_send_json_error( ucfirst($active_provider) . ' API key is disabled'); } $formatted_field_names = array_map(function ($key) { return '{{' . strtolower($key) . '}}'; }, $field_tag_names); $replace_array = array_combine($formatted_field_names, $field_tag_values); $prompt_results = $wpdb->get_results("SELECT * FROM $table_name_prompt_editor WHERE app_id = 1", ARRAY_A); if (empty($prompt_results)) { wp_send_json_error('No prompt results found'); } $prompt_result = $prompt_results[0]; if (!isset($prompt_result['prompt']) || empty($prompt_result['prompt'])) { wp_send_json_error('Prompt string is empty'); } $prompt = str_replace(array_keys($replace_array), array_values($replace_array), $prompt_result['prompt']); // Check for empty prompt if (empty($prompt)) { wp_send_json_error('Prompt is missing'); } $model_parameters = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $table_name_models WHERE model = %s LIMIT 1", $model), ARRAY_A ); if (empty($model_parameters)) { wp_send_json_error('Model parameters not found for the selected model'); } // Validate provider/model match — prevent sending to wrong API endpoint $model_provider = $model_parameters['provider'] ?? 'openai'; if ( $provider && $model_provider !== $active_provider ) { $provider_label = ucfirst( $active_provider ); $model_provider_label = ucfirst( $model_provider ); error_log('[AI App Onsite] Provider/model mismatch: active_provider=' . $active_provider . ' but model "' . $model . '" belongs to provider "' . $model_provider . '"'); wp_send_json_error( 'Provider mismatch: Your AI provider is set to ' . $provider_label . ', but the currently saved model "' . $model . '" is a ' . $model_provider_label . ' model. Please go to Model Settings, select a ' . $provider_label . ' model from the dropdown, and click "Save Settings".' ); return; } $api_url = $model_parameters['endpoint'] ?? 'https://api.openai.com/v1/chat/completions'; $model = $model_parameters['model'] ?? $model; $model_parameters['supported_parameters'] = json_decode($model_parameters['supported_parameters'], true); $supported_parameters = $model_parameters['supported_parameters'] ?? []; // Build request using provider abstraction if ( $provider ) { $headers = $provider->build_headers( $api_key ); $data = $provider->build_payload([ 'model' => $model, 'prompt' => $prompt, 'endpoint' => $api_url, 'max_tokens' => $max_tokens, 'temperature' => $temperature, 'top_p' => $top_p, 'f_penalty' => $f_penalty, 'p_penalty' => $p_penalty, 'supported_parameters' => $supported_parameters, 'default_tools' => $model_parameters['default_tools'] ?? '', ]); } else { // Legacy fallback for OpenAI $headers = [ "Content-Type" => "application/json", "Authorization" => "Bearer $api_key" ]; $is_reasoning = (strpos($api_url, '/v1/responses') !== false); if ($is_reasoning) { // Responses API: only model, input, max_output_tokens // Do NOT send temperature, top_p, frequency_penalty, presence_penalty $data = [ "model" => $model, "input" => $prompt, "max_output_tokens" => !empty($max_tokens) ? (int)$max_tokens : 3000, ]; } else { $data = [ "model" => $model, "messages" => [["role" => "user", "content" => $prompt]], "max_tokens" => !empty($max_tokens) ? (int)$max_tokens : 3000, ]; // Only add these parameters for Chat Completions API if (in_array('temperature', $supported_parameters) && !empty($temperature)) { $data["temperature"] = (float)$temperature; } if (in_array('top_p', $supported_parameters) && !empty($top_p)) { $data["top_p"] = (float)$top_p; } if (in_array('frequency_penalty', $supported_parameters) && !empty($f_penalty)) { $data["frequency_penalty"] = (float)$f_penalty; } if (in_array('presence_penalty', $supported_parameters) && !empty($p_penalty)) { $data["presence_penalty"] = (float)$p_penalty; } } if (!empty($model_parameters['default_tools'])) { $data['tools'] = [['type' => trim($model_parameters['default_tools'])]]; } } // Send request (with automatic retry for transient network errors) $response = ai_app_onsite_remote_post_with_retry($api_url, [ 'headers' => $headers, 'body' => wp_json_encode($data), 'timeout' => 120, 'data_format' => 'body', ]); // Handle WP_Error (network failure, timeout, DNS, connection reset, etc.) if ( is_wp_error( $response ) ) { error_log('[AI App Onsite] wp_remote_post WP_Error: ' . $response->get_error_message()); wp_send_json_error( 'API request failed: ' . $response->get_error_message() ); return; } $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); $result = json_decode($response_body, true); // Parse response using provider abstraction if ( $provider ) { // Some models now reject `temperature` as deprecated. When the API says // so, retry once without it — this is model-agnostic and self-heals as // providers deprecate the parameter on more models over time. if ( ( $response_code >= 400 || ( is_array($result) && isset($result['error']) ) ) && isset($data['temperature']) && stripos((string) $response_body, 'temperature') !== false && stripos((string) $response_body, 'deprecat') !== false ) { unset($data['temperature']); $response = ai_app_onsite_remote_post_with_retry($api_url, [ 'headers' => $headers, 'body' => wp_json_encode($data), 'timeout' => 120, 'data_format' => 'body', ]); if ( is_wp_error( $response ) ) { error_log('[AI App Onsite] wp_remote_post WP_Error (temperature retry): ' . $response->get_error_message()); wp_send_json_error( 'API request failed: ' . $response->get_error_message() ); return; } $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); $result = json_decode($response_body, true); } if ( $response_code >= 400 || ( is_array($result) && isset($result['error']) ) ) { $error_msg = $provider->parse_error( is_array($result) ? $result : [], $response_code ); error_log('[AI App Onsite] Provider error (' . ($provider->get_id()) . '): ' . $error_msg . ' | HTTP ' . $response_code . ' | Raw: ' . substr($response_body, 0, 500)); wp_send_json_error( $error_msg ); } $response_content = $provider->parse_response( is_array($result) ? $result : [], $api_url ); } else { // Legacy OpenAI response parsing $is_reasoning = (strpos($api_url, '/v1/responses') !== false); if ($is_reasoning) { $output = $result['output'] ?? []; $response_content = null; foreach ($output as $item) { if (isset($item['type'], $item['content']) && $item['type'] === 'message' && is_array($item['content'])) { foreach ($item['content'] as $content) { if (!empty($content['text'])) { $response_content = $content['text']; break 2; } } } } } else { $response_content = $result['choices'][0]['message']['content'] ?? null; } } if (!$response_content) { error_log('[AI App Onsite] Null response_content. HTTP code: ' . $response_code . ' | model: ' . $model . ' | Response body: ' . substr((string) $response_body, 0, 1000)); wp_send_json_error( ai_app_onsite_explain_empty_ai_response( $result, $model ) ); } // Format raw provider text into safe HTML via shared formatter. $response_content = ai_app_onsite_format_ai_response_html( (string) $response_content ); if (!empty($app_email)) { $resultArray = array_filter( $pairs, function ($key) use ($fieldLabels) { return in_array($key, $fieldLabels); }, ARRAY_FILTER_USE_KEY ); self::ai_app_onsite_send_email($app_email, $resultArray, $response_content, $app_name); } $user_stats_handler = new AI_APP_ONSITE_User_Stats_Handler(); $response_Id = $user_stats_handler->ai_app_onsite_save_user_stats($result, $prompt, $formData,$response_content); // Optionally handle response from external URL $response = [ 'success' => true, 'message' => 'Form data received successfully', 'data' => $response_content, 'response_message' => $response_message, 'response_id' => $response_Id, ]; wp_send_json($response); } // Sending email with dynamic data public static function ai_app_onsite_send_email($to, $data, $ai_output, $app_name): void { // Normalize $data -> safe HTML list items $dynamic_list = ''; foreach ($data as $key => $value) { if (is_array($value) || is_object($value)) { $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } // Convert simple newlines to
for readability $safeValue = nl2br(htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')); $safeKey = htmlspecialchars(ucfirst(str_replace('_', ' ', (string)$key)), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $dynamic_list .= "{$safeKey}: {$safeValue}"; } // If AI output is plain text, keep formatting; if HTML, it will pass through. $ai_output_block = $ai_output; // Best-effort: if it looks plain (no tags), escape then nl2br if ($ai_output === strip_tags($ai_output)) { $ai_output_block = nl2br(htmlspecialchars($ai_output, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')); } $brand = 'AIappOnsite'; $suffix = !empty($app_name) ? " – " . htmlspecialchars($app_name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') : ''; $site_name = get_bloginfo('name'); $safe_site_name = htmlspecialchars($site_name ?: 'this site', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $subject = "{$brand} App Usage{$suffix}"; // Hidden preheader (appears in inbox preview) $preheader = "Your {$brand} submission and AI response{$suffix}."; // NOTE: Keep styles inline & table-based for broad client support $email_template = << {$brand}{$suffix} {$preheader}
HTML; // Build headers (prefer wp_mail when available) $headers = []; $headers[] = 'MIME-Version: 1.0'; $headers[] = 'Content-Type: text/html; charset=UTF-8'; $fromEmail = defined('AIAPP_FROM_EMAIL') ? AIAPP_FROM_EMAIL : null; if ($fromEmail) { $headers[] = 'From: AIappOnsite <' . $fromEmail . '>'; $headers[] = 'Reply-To: ' . $fromEmail; } // Use wp_mail if available; fallback to mail() if (function_exists('wp_mail')) { wp_mail($to, $subject, $email_template, $headers); } else { // mail() expects headers as a string $headers_str = implode("\r\n", $headers); @mail($to, $subject, $email_template, $headers_str); } } } new AI_APP_ONSITE_OpenAi_Api_Handler(); // Instantiate the class } function AI_APP_ONSITE_Break_Paragraph($text, $words_per_chunk = 200) { // Split the text into an array of words $words = preg_split('/\s+/', $text); $chunks = array_chunk($words, $words_per_chunk); $result = ''; foreach ($chunks as $chunk) { $result .= implode(' ', $chunk) . "\n\n"; } return $result; } if (! function_exists('ai_app_onsite_remote_post_with_retry')) { /** * wp_remote_post wrapper that automatically retries transient network errors * (e.g. cURL 56 "Connection reset by peer", timeouts, connection/SSL failures, * empty replies). These are momentary transport-level drops that usually * succeed on a second attempt. * * Any successful HTTP response (including 4xx/5xx from the provider) is * returned immediately without retrying, and non-transient WP_Errors are not * retried either — so only genuine connection blips trigger a retry. * * @param string $url Endpoint URL. * @param array $args Arguments passed to wp_remote_post(). * @param int $max_retries Number of extra attempts after the first (default 1). * @return array|WP_Error The wp_remote_post() result (or last WP_Error). */ function ai_app_onsite_remote_post_with_retry($url, $args, $max_retries = 1) { $transient_needles = array( 'cURL error 7', // couldn't connect to host 'cURL error 28', // operation timed out 'cURL error 35', // SSL connect error 'cURL error 52', // empty reply from server 'cURL error 56', // recv failure: connection reset by peer 'Connection reset', 'Recv failure', 'Empty reply', 'timed out', ); $response = null; for ($attempt = 0; $attempt <= $max_retries; $attempt++) { $response = wp_remote_post($url, $args); // Got an actual HTTP response (any status) — done. if (! is_wp_error($response)) { return $response; } $message = $response->get_error_message(); $is_transient = false; foreach ($transient_needles as $needle) { if (stripos($message, $needle) !== false) { $is_transient = true; break; } } // Stop on non-transient errors or after the final attempt. if (! $is_transient || $attempt === $max_retries) { break; } error_log('[AI App Onsite] Transient network error (attempt ' . ($attempt + 1) . '): ' . $message . ' — retrying...'); // Linear backoff: 0.5s, then 1.0s. usleep(500000 * ($attempt + 1)); } return $response; } } if (! function_exists('ai_app_onsite_explain_empty_ai_response')) { /** * Build a detailed, user-friendly message when the AI returned HTTP 200 but * no usable text (e.g. a reasoning model that spent all its output tokens on * internal reasoning, an incomplete response, a content-filter block, or a * refusal). Replaces the old generic "Invalid response from AI provider". * * @param mixed $result Decoded response body (associative array) or null. * @param string $model Model name, for context in the message. * @return string Human-readable explanation. */ function ai_app_onsite_explain_empty_ai_response($result, $model = '') { $model_label = ($model !== '' && $model !== null) ? '"' . $model . '"' : 'The selected model'; if (is_array($result)) { $status = isset($result['status']) ? (string) $result['status'] : ''; $reason = isset($result['incomplete_details']['reason']) ? (string) $result['incomplete_details']['reason'] : ''; // OpenAI Responses API: output-token budget exhausted (common with reasoning models). if ($reason === 'max_output_tokens') { $limit = isset($result['max_output_tokens']) ? (int) $result['max_output_tokens'] : 0; $limit_text = $limit > 0 ? ' (limit: ' . $limit . ' tokens)' : ''; return $model_label . ' ran out of output tokens' . $limit_text . ' before it could finish its answer. ' . 'Reasoning models such as GPT-5 spend part of the output budget on internal reasoning, so a low "Max Tokens" value can leave no room for the reply. ' . 'Please increase "Max Tokens" in Model Settings and try again.'; } // Blocked by the provider's safety/content filter. if ($reason === 'content_filter' || $status === 'content_filter') { return 'The response from ' . $model_label . ' was blocked by the provider\'s content filter. Please rephrase your input and try again.'; } // Responses API refusal item. if (isset($result['output']) && is_array($result['output'])) { foreach ($result['output'] as $item) { if (isset($item['content']) && is_array($item['content'])) { foreach ($item['content'] as $content) { if (isset($content['type']) && $content['type'] === 'refusal' && ! empty($content['refusal'])) { return $model_label . ' declined to answer: ' . (string) $content['refusal']; } } } } } // Chat Completions API: truncated / filtered finish reasons. if (isset($result['choices'][0]['finish_reason'])) { $finish = (string) $result['choices'][0]['finish_reason']; if ($finish === 'length') { return $model_label . ' ran out of output tokens before finishing its answer. Please increase "Max Tokens" in Model Settings and try again.'; } if ($finish === 'content_filter') { return 'The response from ' . $model_label . ' was blocked by the provider\'s content filter. Please rephrase your input and try again.'; } } // Generic incomplete status. if ($status === 'incomplete') { $reason_text = $reason !== '' ? ' (reason: ' . $reason . ')' : ''; return $model_label . ' returned an incomplete response' . $reason_text . '. Please try again, and consider increasing "Max Tokens" in Model Settings.'; } } return 'The AI provider returned an empty response for ' . $model_label . '. This can happen when the model uses all available tokens on reasoning. Please try again or increase "Max Tokens" in Model Settings.'; } }