| @@ -66,17 +66,13 @@ | ||
| 66 | 66 | |
| 67 | 67 | // Get the response body. |
| 68 | 68 | $response_body = wp_remote_retrieve_body( $response ); |
| 69 | 69 | |
| 70 | - // If the response body is not a JSON, then abandon ship. | |
| 71 | - if ( empty( $response_body ) || ! json_decode( $response_body ) ) { | |
| 72 | - return [ | |
| 73 | - 'error' => __( 'The SureForms AI Middleware encountered an error.', 'sureforms' ), | |
| 74 | - ]; | |
| 75 | - } | |
| 76 | - | |
| 77 | - // Return the response body. | |
| 78 | - return json_decode( $response_body, true ); | |
| 70 | + return self::decode_json_response( | |
| 71 | + $response_body, | |
| 72 | + wp_remote_retrieve_response_code( $response ), | |
| 73 | + 'generate/form' | |
| 74 | + ); | |
| 79 | 75 | } |
| 80 | 76 | |
| 81 | 77 | /** |
| 82 | 78 | * Get the SureForms Token from the SureForms AI Settings. |
| @@ -108,9 +104,8 @@ | ||
| 108 | 104 | } |
| 109 | 105 | } |
| 110 | 106 | |
| 111 | 107 | return $current_usage_details; |
| 112 | - | |
| 113 | 108 | } |
| 114 | 109 | |
| 115 | 110 | /** |
| 116 | 111 | * Get a response from the SureForms API server. |
| @@ -142,41 +137,17 @@ | ||
| 142 | 137 | |
| 143 | 138 | // Get the response body. |
| 144 | 139 | $response_body = wp_remote_retrieve_body( $response ); |
| 145 | 140 | |
| 146 | - // If the response body is not a JSON, then abandon ship. | |
| 147 | - if ( empty( $response_body ) || ! json_decode( $response_body ) ) { | |
| 148 | - return [ | |
| 149 | - 'error' => __( 'The SureForms API server encountered an error.', 'sureforms' ), | |
| 150 | - ]; | |
| 151 | - } | |
| 152 | - | |
| 153 | - // Return the response body. | |
| 154 | - return json_decode( $response_body, true ); | |
| 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 | + ); | |
| 155 | 147 | } |
| 156 | 148 | |
| 157 | 149 | /** |
| 158 | - * Get the User Token. | |
| 159 | - * | |
| 160 | - * @since 0.0.8 | |
| 161 | - * @return string The User Token. | |
| 162 | - */ | |
| 163 | - private static function get_user_token() { | |
| 164 | - // if the license is active then use the license key as the token. | |
| 165 | - if ( defined( 'SRFM_PRO_VER' ) ) { | |
| 166 | - $license_key = self::get_license_key(); | |
| 167 | - if ( ! empty( $license_key ) ) { | |
| 168 | - return $license_key; | |
| 169 | - } | |
| 170 | - } | |
| 171 | - | |
| 172 | - $user_email = get_option( 'srfm_ai_auth_user_email' ); | |
| 173 | - | |
| 174 | - // if the license is not active then use the user email/site url as the token. | |
| 175 | - return ! empty( $user_email ) && is_array( $user_email ) ? $user_email['user_email'] : site_url(); | |
| 176 | - } | |
| 177 | - | |
| 178 | - /** | |
| 179 | 150 | * Get the Error Message. |
| 180 | 151 | * |
| 181 | 152 | * @param array<string,mixed>|array<int|string,mixed>|\WP_Error $response The response from the SureForms API server. |
| 182 | 153 | * @since 0.0.10 |
| @@ -182,9 +153,9 @@ | ||
| 182 | 153 | * @since 0.0.10 |
| 183 | 154 | * @return array<string, mixed> The Error Message. |
| 184 | 155 | */ |
| 185 | 156 | public static function get_error_message( $response ) { |
| 186 | - $errors = isset( $response->errors ) ? $response->errors : []; | |
| 157 | + $errors = $response->errors ?? []; | |
| 187 | 158 | |
| 188 | 159 | if ( empty( $errors ) |
| 189 | 160 | && is_array( $response ) && isset( $response['body'] ) && is_string( $response['body'] ) |
| 190 | 161 | ) { |
| @@ -200,21 +171,21 @@ | ||
| 200 | 171 | // Error Codes with Messages. |
| 201 | 172 | switch ( $error_key ) { |
| 202 | 173 | case 'http_request_failed': |
| 203 | 174 | $title = __( 'HTTP Request Failed', 'sureforms' ); |
| 204 | - $message = __( 'An error occurred while trying to connect to the SureForms API server. Please check your connection', 'sureforms' ); | |
| 175 | + $message = __( 'Unable to connect to SureForms API. Please check your connection.', 'sureforms' ); | |
| 205 | 176 | break; |
| 206 | 177 | case 'license_verification_failed': |
| 207 | 178 | $title = __( 'License Verification Failed', 'sureforms' ); |
| 208 | - $message = __( 'An error occurred while trying to verify your license. Please check your license key', 'sureforms' ); | |
| 179 | + $message = __( 'Unable to verify license. Please check your license key.', 'sureforms' ); | |
| 209 | 180 | break; |
| 210 | 181 | case 'user_verification_failed': |
| 211 | 182 | $title = __( 'User Verification Failed', 'sureforms' ); |
| 212 | - $message = __( 'An error occurred while trying to verify your email. Please check your email you have used to log in/ sign up on billing.sureforms.com.', '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' ); | |
| 213 | 184 | break; |
| 214 | 185 | case 'referer_mismatch': |
| 215 | 186 | $title = __( 'Referer Mismatch', 'sureforms' ); |
| 216 | - $message = __( 'An error occurred while trying to verify your referer. Please check your referer.', 'sureforms' ); | |
| 187 | + $message = __( 'Unable to verify referer. Please check your referer.', 'sureforms' ); | |
| 217 | 188 | break; |
| 218 | 189 | case 'invalid_token': |
| 219 | 190 | $title = __( 'Invalid Website URL', 'sureforms' ); |
| 220 | 191 | $message = __( 'AI Form Builder does not work on localhost. Please try on a live website.', 'sureforms' ); |
| @@ -220,9 +191,9 @@ | ||
| 220 | 191 | $message = __( 'AI Form Builder does not work on localhost. Please try on a live website.', 'sureforms' ); |
| 221 | 192 | break; |
| 222 | 193 | case 'domain_verification_failed': |
| 223 | 194 | $title = __( 'Domain Verification Failed', 'sureforms' ); |
| 224 | - $message = __( 'Domain Verification Failed on current site. Please try again on any another website.', 'sureforms' ); | |
| 195 | + $message = __( 'Domain Verification Failed on current site. Please try again on another website.', 'sureforms' ); | |
| 225 | 196 | break; |
| 226 | 197 | default: |
| 227 | 198 | $title = __( 'Unknown Error', 'sureforms' ); |
| 228 | 199 | $message = __( 'An unknown error occurred.', 'sureforms' ); |
| @@ -232,12 +203,185 @@ | ||
| 232 | 203 | 'code' => $error_key, |
| 233 | 204 | 'title' => $title, |
| 234 | 205 | 'message' => $message, |
| 235 | 206 | ]; |
| 207 | + } | |
| 236 | 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(); | |
| 237 | 223 | } |
| 238 | 224 | |
| 239 | 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 | + /** | |
| 240 | 384 | * Get the Licensing Instance. |
| 241 | 385 | * |
| 242 | 386 | * @since 0.0.10 |
| 243 | 387 | * @return object|null The Licensing Instance. |
| @@ -264,25 +408,8 @@ | ||
| 264 | 408 | // Check if the SureForms Pro license is active. |
| 265 | 409 | $is_license_active = self::is_pro_license_active(); |
| 266 | 410 | // If the license is active, get the license key. |
| 267 | 411 | $license_setup = $licensing->licensing_setup(); |
| 268 | - $license_key = ( ! empty( $is_license_active ) && is_object( $license_setup ) && method_exists( $license_setup, 'settings' ) ) ? $license_setup->settings()->license_key : ''; | |
| 269 | - return $license_key; | |
| 270 | - } | |
| 271 | - | |
| 272 | - /** | |
| 273 | - * Check if the SureForms Pro license is active. | |
| 274 | - * | |
| 275 | - * @since 0.0.10 | |
| 276 | - * @return bool|string True if the SureForms Pro license is active, false otherwise. | |
| 277 | - */ | |
| 278 | - public static function is_pro_license_active() { | |
| 279 | - $licensing = self::get_licensing_instance(); | |
| 280 | - if ( ! $licensing || ! method_exists( $licensing, 'is_license_active' ) | |
| 281 | - ) { | |
| 282 | - return ''; | |
| 283 | - } | |
| 284 | - // Check if the SureForms Pro license is active. | |
| 285 | - return $licensing->is_license_active(); | |
| 412 | + return ! empty( $is_license_active ) && is_object( $license_setup ) && method_exists( $license_setup, 'settings' ) ? $license_setup->settings()->license_key : ''; | |
| 286 | 413 | } |
| 287 | 414 | |
| 288 | 415 | } |