PluginProbe ʕ •ᴥ•ʔ
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.3
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.3
2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8 0.0.9 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.1.2 1.10.0 1.10.1 1.11.0 1.12.0 1.12.1 1.12.2 1.12.3 1.13.0 1.13.1 1.13.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.8.0 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.2 2.6.0
sureforms / inc / ai-form-builder / ai-helper.php
sureforms / inc / ai-form-builder Last commit date
ai-auth.php 5 months ago ai-form-builder.php 1 month ago ai-helper.php 3 months ago field-mapping.php 2 months ago
ai-helper.php
416 lines
1 <?php
2 /**
3 * SureForms AI Form Builder - Helper.
4 *
5 * This file contains the helper functions of SureForms AI Form Builder.
6 * Helpers are functions that are used throughout the library.
7 *
8 * @package sureforms
9 * @since 0.0.8
10 */
11
12 namespace SRFM\Inc\AI_Form_Builder;
13
14 use SRFM\Inc\Traits\Get_Instance;
15 use SRFM_Pro\Admin\Licensing;
16
17 // Exit if accessed directly.
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 /**
23 * The Helper Class.
24 */
25 class AI_Helper {
26 use Get_Instance;
27
28 /**
29 * Get the SureForms AI Response from the SureForms Credit Server.
30 *
31 * @param array<mixed> $body The data to be passed as the request body, if any.
32 * @param array<mixed> $extra_args Extra arguments to be passed to the request, if any.
33 * @since 0.0.8
34 * @return array<array<array<array<mixed>>>|string>|mixed The SureForms AI Response.
35 */
36 public static function get_chat_completions_response( $body = [], $extra_args = [] ) {
37 // Set the API URL.
38 $api_url = SRFM_AI_MIDDLEWARE . 'generate/form';
39
40 $api_args = [
41 'headers' => [
42 'X-Token' => base64_encode( self::get_user_token() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- This is not for obfuscation.
43 'Content-Type' => 'application/json',
44 'Referer' => site_url(),
45 ],
46 'timeout' => 90, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout -- 90 seconds is required sometime for open ai responses
47 ];
48
49 // If the data array was passed, add it to the args.
50 if ( ! empty( $body ) && is_array( $body ) ) {
51 $api_args['body'] = wp_json_encode( $body );
52 }
53
54 // If there are any extra arguments, then we can overwrite the required arguments.
55 if ( ! empty( $extra_args ) && is_array( $extra_args ) ) {
56 $api_args = array_merge( $api_args, $extra_args );
57 }
58
59 // Get the response from the endpoint.
60 $response = wp_remote_post( $api_url, $api_args );
61
62 // If the response was an error, or not a 200 status code, then abandon ship.
63 if ( is_wp_error( $response ) || empty( $response['response'] ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
64 return self::get_error_message( $response );
65 }
66
67 // Get the response body.
68 $response_body = wp_remote_retrieve_body( $response );
69
70 return self::decode_json_response(
71 $response_body,
72 wp_remote_retrieve_response_code( $response ),
73 'generate/form'
74 );
75 }
76
77 /**
78 * Get the SureForms Token from the SureForms AI Settings.
79 *
80 * @since 0.0.8
81 * @return array<mixed>|void The SureForms Token.
82 */
83 public static function get_current_usage_details() {
84 $current_usage_details = [];
85
86 // Get the response from the endpoint.
87 $response = self::get_usage_response();
88
89 // check if response is an array if not then send error.
90 if ( ! is_array( $response ) ) {
91 wp_send_json_error( [ 'message' => __( 'Unable to get usage response.', 'sureforms' ) ] );
92 }
93
94 // If the response is not an error, then use it - else create an error response array.
95 if ( empty( $response['error'] ) && is_array( $response ) ) {
96 $current_usage_details = $response;
97 if ( empty( $current_usage_details['status'] ) ) {
98 $current_usage_details['status'] = 'ok';
99 }
100 } else {
101 $current_usage_details['status'] = 'error';
102 if ( ! empty( $response['error'] ) ) {
103 $current_usage_details['error'] = $response['error'];
104 }
105 }
106
107 return $current_usage_details;
108 }
109
110 /**
111 * Get a response from the SureForms API server.
112 *
113 * @since 0.0.8
114 * @return array<mixed>|mixed The SureForms API Response.
115 */
116 public static function get_usage_response() {
117 // Set the API URL.
118 $api_url = SRFM_AI_MIDDLEWARE . 'usage';
119
120 // Get the response from the endpoint.
121 $response = wp_remote_post(
122 $api_url,
123 [
124 'headers' => [
125 'X-Token' => base64_encode( self::get_user_token() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- This is not for obfuscation.
126 'Content-Type' => 'application/json',
127 'Referer' => site_url(),
128 ],
129 'timeout' => 30, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout -- 30 seconds is required sometime for the SureForms API response
130 ]
131 );
132
133 // If the response was an error, or not a 200 status code, then abandon ship.
134 if ( is_wp_error( $response ) || empty( $response['response'] ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
135 return self::get_error_message( $response );
136 }
137
138 // Get the response body.
139 $response_body = wp_remote_retrieve_body( $response );
140
141 return self::decode_json_response(
142 $response_body,
143 wp_remote_retrieve_response_code( $response ),
144 'usage',
145 __( 'The SureForms API server encountered an error.', 'sureforms' )
146 );
147 }
148
149 /**
150 * Get the Error Message.
151 *
152 * @param array<string,mixed>|array<int|string,mixed>|\WP_Error $response The response from the SureForms API server.
153 * @since 0.0.10
154 * @return array<string, mixed> The Error Message.
155 */
156 public static function get_error_message( $response ) {
157 $errors = $response->errors ?? [];
158
159 if ( empty( $errors )
160 && is_array( $response ) && isset( $response['body'] ) && is_string( $response['body'] )
161 ) {
162 $errors = json_decode( $response['body'], true );
163 $error_key = is_array( $errors ) && isset( $errors['code'] ) ? $errors['code'] : '';
164 } else {
165 $error_key = array_key_first( $errors );
166 if ( empty( $errors[ $error_key ] ) ) {
167 $message = __( 'An unknown error occurred.', 'sureforms' );
168 }
169 }
170
171 // Error Codes with Messages.
172 switch ( $error_key ) {
173 case 'http_request_failed':
174 $title = __( 'HTTP Request Failed', 'sureforms' );
175 $message = __( 'Unable to connect to SureForms API. Please check your connection.', 'sureforms' );
176 break;
177 case 'license_verification_failed':
178 $title = __( 'License Verification Failed', 'sureforms' );
179 $message = __( 'Unable to verify license. Please check your license key.', 'sureforms' );
180 break;
181 case 'user_verification_failed':
182 $title = __( 'User Verification Failed', 'sureforms' );
183 $message = __( 'An error occurred while trying to verify your email. Please check your email you have used to log in or sign up on billing.sureforms.com.', 'sureforms' );
184 break;
185 case 'referer_mismatch':
186 $title = __( 'Referer Mismatch', 'sureforms' );
187 $message = __( 'Unable to verify referer. Please check your referer.', 'sureforms' );
188 break;
189 case 'invalid_token':
190 $title = __( 'Invalid Website URL', 'sureforms' );
191 $message = __( 'AI Form Builder does not work on localhost. Please try on a live website.', 'sureforms' );
192 break;
193 case 'domain_verification_failed':
194 $title = __( 'Domain Verification Failed', 'sureforms' );
195 $message = __( 'Domain Verification Failed on current site. Please try again on another website.', 'sureforms' );
196 break;
197 default:
198 $title = __( 'Unknown Error', 'sureforms' );
199 $message = __( 'An unknown error occurred.', 'sureforms' );
200 }
201
202 return [
203 'code' => $error_key,
204 'title' => $title,
205 'message' => $message,
206 ];
207 }
208
209 /**
210 * Check if the SureForms Pro license is active.
211 *
212 * @since 0.0.10
213 * @return bool|string True if the SureForms Pro license is active, false otherwise.
214 */
215 public static function is_pro_license_active() {
216 $licensing = self::get_licensing_instance();
217 if ( ! $licensing || ! method_exists( $licensing, 'is_license_active' )
218 ) {
219 return '';
220 }
221 // Check if the SureForms Pro license is active.
222 return $licensing->is_license_active();
223 }
224
225 /**
226 * Sanitize an upstream error message before returning it to the client.
227 *
228 * The OpenAI / SureForms middleware sometimes echoes infrastructure details
229 * (URLs, request IDs, model names, organization/user IDs, raw API keys)
230 * inside error messages. The endpoints surfacing these messages are
231 * capability-gated, but contributors-and-up shouldn't see infra leaks.
232 *
233 * Pass-through behaviour is preserved when the message has no sensitive
234 * tokens — only matched patterns are stripped. Returns an empty string
235 * if nothing useful remains, so callers can fall back to a canonical
236 * translated message.
237 *
238 * @param mixed $raw Raw upstream message; non-strings are coerced.
239 * @param string $endpoint Optional endpoint label; when set, the raw input
240 * is passed through {@see self::log_ai_response_failure()}
241 * so the unredacted form is preserved server-side
242 * (subject to the usual WP_DEBUG / WP_DEBUG_LOG gates).
243 * @param int|string $status_code Optional HTTP status, forwarded to the logger.
244 * @since 2.8.2
245 * @return string Sanitized message safe to return to the client.
246 */
247 public static function sanitize_ai_error_message( $raw, $endpoint = '', $status_code = '' ) {
248 if ( ! is_string( $raw ) ) {
249 return '';
250 }
251 $raw = trim( $raw );
252 if ( '' === $raw ) {
253 return '';
254 }
255
256 if ( '' !== $endpoint ) {
257 self::log_ai_response_failure( $endpoint, $status_code, 'upstream_error', $raw );
258 }
259
260 $patterns = [
261 // URLs (http / https / protocol-relative).
262 '#https?://\S+#i',
263 '#(?<=\s)//\S+#i',
264 // OpenAI-shape opaque IDs: org-/user-/key-/sess-/req-/file-/chatcmpl-/asst-/run-/thread-.
265 // Both '-' and '_' separators are observed in the wild (e.g. req-… and req_…).
266 '/\b(?:org|user|key|sess|req|file|chatcmpl|asst|run|thread)[-_][A-Za-z0-9_]{6,}/i',
267 // Generic "request id: …" / "request-id …" trailers — require a separator and a substantive id.
268 '/\brequest[_\s-]?id[:\s]+[A-Za-z0-9_-]{4,}/i',
269 // Bearer / API-key shapes.
270 '/\bsk-[A-Za-z0-9_-]{12,}/i',
271 '/\bBearer\s+[A-Za-z0-9._-]+/i',
272 // Model identifiers that would otherwise leak the underlying provider.
273 '/\bgpt-[A-Za-z0-9.-]+/i',
274 ];
275 $cleaned = (string) preg_replace( $patterns, '', $raw );
276 // Collapse the gaps left by removed tokens.
277 $cleaned = (string) preg_replace( '/\s+/', ' ', $cleaned );
278 return trim( $cleaned, " \t\n\r\0\x0B.,;:" );
279 }
280
281 /**
282 * Decode the response body from the SureForms AI Middleware, returning
283 * a structured error payload when the body is empty or invalid JSON.
284 *
285 * @param string $response_body Raw HTTP response body.
286 * @param int|string $status_code HTTP status code, used for debug logging.
287 * @param string $endpoint Short endpoint label, used for debug logging.
288 * @param string|null $error_fallback Translated fallback message on decode failure.
289 * @since 2.8.2
290 * @return array<mixed>
291 */
292 protected static function decode_json_response( $response_body, $status_code, $endpoint, $error_fallback = null ) {
293 if ( null === $error_fallback ) {
294 $error_fallback = __( 'The SureForms AI Middleware encountered an error.', 'sureforms' );
295 }
296
297 if ( '' === $response_body || null === $response_body ) {
298 self::log_ai_response_failure( $endpoint, $status_code, 'empty_body', '' );
299 return [ 'error' => $error_fallback ];
300 }
301
302 $decoded = json_decode( $response_body, true );
303
304 if ( JSON_ERROR_NONE !== json_last_error() ) {
305 self::log_ai_response_failure( $endpoint, $status_code, 'invalid_json', $response_body );
306 return [ 'error' => $error_fallback ];
307 }
308
309 if ( ! is_array( $decoded ) ) {
310 self::log_ai_response_failure( $endpoint, $status_code, 'non_array_json', $response_body );
311 return [ 'error' => $error_fallback ];
312 }
313
314 return $decoded;
315 }
316
317 /**
318 * Log an AI middleware response failure when WP_DEBUG and WP_DEBUG_LOG are both enabled.
319 *
320 * Newlines are collapsed to prevent log injection, and known sensitive JSON keys
321 * (email, token, license_key, prompt, query) are redacted before logging.
322 *
323 * @param string $endpoint Short endpoint label.
324 * @param int|string $status_code HTTP status code.
325 * @param string $reason Failure reason identifier.
326 * @param string $body Raw response body (will be truncated).
327 * @since 2.8.2
328 * @return void
329 */
330 protected static function log_ai_response_failure( $endpoint, $status_code, $reason, $body ) {
331 if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
332 return;
333 }
334
335 // Only write to the debug log file when WP_DEBUG_LOG is also enabled.
336 // Without this guard, error_log() falls back to the host's PHP error log.
337 if ( ! defined( 'WP_DEBUG_LOG' ) || ! WP_DEBUG_LOG ) {
338 return;
339 }
340
341 $snippet = is_string( $body ) ? substr( $body, 0, 500 ) : '';
342 // Collapse all whitespace (including CR/LF) to a single space to prevent log injection.
343 $snippet = (string) preg_replace( '/\s+/', ' ', $snippet );
344 // Redact known sensitive keys if echoed in the body.
345 $snippet = (string) preg_replace(
346 '/("(?:email|token|license_key|prompt|query)"\s*:\s*")[^"]*"/i',
347 '$1[redacted]"',
348 $snippet
349 );
350
351 error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only logging behind WP_DEBUG && WP_DEBUG_LOG.
352 sprintf(
353 '[SureForms AI] %s %s status=%s body=%s',
354 $endpoint,
355 $reason,
356 (string) $status_code,
357 $snippet
358 )
359 );
360 }
361
362 /**
363 * Get the User Token.
364 *
365 * @since 0.0.8
366 * @return string The User Token.
367 */
368 private static function get_user_token() {
369 // if the license is active then use the license key as the token.
370 if ( defined( 'SRFM_PRO_VER' ) ) {
371 $license_key = self::get_license_key();
372 if ( ! empty( $license_key ) ) {
373 return $license_key;
374 }
375 }
376
377 $user_email = get_option( 'srfm_ai_auth_user_email' );
378
379 // if the license is not active then use the user email/site url as the token.
380 return ! empty( $user_email ) && is_array( $user_email ) ? $user_email['user_email'] : site_url();
381 }
382
383 /**
384 * Get the Licensing Instance.
385 *
386 * @since 0.0.10
387 * @return object|null The Licensing Instance.
388 */
389 private static function get_licensing_instance() {
390 if ( ! class_exists( 'SRFM_Pro\Admin\Licensing' ) ) {
391 return null;
392 }
393 return Licensing::get_instance();
394 }
395
396 /**
397 * Get the SureForms Pro License Key.
398 *
399 * @since 0.0.10
400 * @return string The SureForms Pro License Key.
401 */
402 private static function get_license_key() {
403 $licensing = self::get_licensing_instance();
404 if ( ! $licensing ||
405 ! method_exists( $licensing, 'licensing_setup' ) || ! method_exists( $licensing->licensing_setup(), 'settings' ) ) {
406 return '';
407 }
408 // Check if the SureForms Pro license is active.
409 $is_license_active = self::is_pro_license_active();
410 // If the license is active, get the license key.
411 $license_setup = $licensing->licensing_setup();
412 return ! empty( $is_license_active ) && is_object( $license_setup ) && method_exists( $license_setup, 'settings' ) ? $license_setup->settings()->license_key : '';
413 }
414
415 }
416