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}
{$brand} App Input Data{$suffix}
|
|
|
This is the data you provided:
|
|
App Results:
{$ai_output_block}
|
|
Thanks for using {$brand}, provided by {$safe_site_name}!
|
|
This email was requested by the recipient and sent by the {$safe_site_name} website using AIappOnsite.
|
| |
|
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.';
}
}