__( 'Insufficient permissions.', 'advanced-form-integration' ) ), 403 ); } if( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'advanced-form-integration' ) ) { return; } $form_provider = isset( $_POST['formProviderId'] ) ? sanitize_text_field( wp_unslash( $_POST['formProviderId'] ) ) : ''; // Whitelist the provider slug and confirm the callback exists before // call_user_func — prevents the "adfoin_{$slug}_get_forms" string from // resolving to an arbitrary function in the namespace. if( $form_provider && array_key_exists( $form_provider, adfoin_get_form_providers() ) ) { $callback = "adfoin_{$form_provider}_get_forms"; if( function_exists( $callback ) ) { $forms = call_user_func( $callback, $form_provider ); if( !is_wp_error( $forms ) ) { wp_send_json_success( $forms ); } } } wp_die(); } /* * Get all fields for a specific form */ public function get_form_fields() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'advanced-form-integration' ) ), 403 ); } if( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'advanced-form-integration' ) ) { return; } $form_provider = isset( $_POST['formProviderId'] ) ? sanitize_text_field( wp_unslash( $_POST['formProviderId'] ) ) : ''; $form_id = isset( $_POST['formId'] ) ? sanitize_text_field( wp_unslash( $_POST['formId'] ) ) : ''; if( $form_provider && $form_id && array_key_exists( $form_provider, adfoin_get_form_providers() ) ) { $callback = "adfoin_{$form_provider}_get_form_fields"; if( function_exists( $callback ) ) { $fields = call_user_func( $callback, $form_provider, $form_id ); if( !is_wp_error( $fields ) ) { wp_send_json_success( $fields ); } } } wp_die(); } /* * Get Tasks for a action provider */ public function get_tasks() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'advanced-form-integration' ) ), 403 ); } if( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'advanced-form-integration' ) ) { return; } $action_provider = isset( $_POST['actionProviderId'] ) ? sanitize_text_field( wp_unslash( $_POST['actionProviderId'] ) ) : ''; if( $action_provider && array_key_exists( $action_provider, adfoin_get_action_providers() ) ) { $tasks = adfoin_get_action_tasks( $action_provider ); if( !is_wp_error( $tasks ) ) { wp_send_json_success( $tasks ); } } wp_die(); } /* * Save Integration */ public function save_integration() { // Defense-in-depth: gate behind manage_options just like the AJAX twin // (save_integration_ajax). The nonce alone wouldn't stop a logged-in // user who lifted a token off an admin page. if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'advanced-form-integration' ) ); } if( !wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), 'adfoin-integration' ) ) { return; } $action_provider_id = isset( $_POST['action_provider'] ) ? sanitize_text_field( wp_unslash( $_POST['action_provider'] ) ) : ''; $trigger_data = isset( $_POST['triggerData'] ) ? adfoin_sanitize_text_or_array_field( $_POST['triggerData'] ) : array(); if( $trigger_data && is_string( $trigger_data ) ) { $trigger_data = json_decode( $trigger_data, true ); } $action_data = isset( $_POST['actionData'] ) ? adfoin_sanitize_text_or_array_field( $_POST['actionData'] ) : array(); if( $action_data && is_string( $action_data ) ) { $action_data = json_decode( $action_data, true ); } $field_data = isset( $_POST['fieldData'] ) ? adfoin_sanitize_text_or_array_field( $_POST['fieldData'] ) : array(); $integration_title = isset( $trigger_data['integrationTitle'] ) ? sanitize_text_field( $trigger_data['integrationTitle'] ) : ''; $form_provider_id = isset( $trigger_data['formProviderId'] ) ? $trigger_data['formProviderId'] : ''; $form_id = isset( $trigger_data['formId'] ) ? $trigger_data['formId'] : ''; $form_name = isset( $trigger_data['formName'] ) ? sanitize_text_field( $trigger_data['formName'] ) : ''; $action_provider = isset( $action_data['actionProviderId'] ) ? $action_data['actionProviderId'] : ''; $task = isset( $action_data['task'] ) ? $action_data['task'] : ''; $type = isset( $_POST['type'] ) ? adfoin_sanitize_text_or_array_field( $_POST['type'] ) : ''; $all_data = array( 'trigger_data' => $trigger_data, 'action_data' => $action_data, 'field_data' => $field_data ); global $wpdb; $integration_table = $wpdb->prefix . 'adfoin_integration'; if ( $type == 'new_integration' ) { $result = $wpdb->insert( $integration_table, array( 'title' => $integration_title, 'form_provider' => $form_provider_id, 'form_id' => $form_id, 'form_name' => $form_name, 'action_provider' => $action_provider, 'task' => $task, 'data' => wp_json_encode( $all_data ), 'status' => 1 ) ); $id = $wpdb->insert_id; } if ( $type == 'update_integration' ) { // Mirror the AJAX twin (save_integration_ajax): the row id is an // integer primary key — cast it rather than esc_sql() a trimmed // raw string, which only quotes and leaves the value type-unsafe. $id = isset( $_POST['edit_id'] ) ? (int) $_POST['edit_id'] : 0; if ( $id <= 0 ) { wp_die( esc_html__( 'Invalid integration id.', 'advanced-form-integration' ) ); } $result = $wpdb->update( $integration_table, array( 'title' => $integration_title, 'form_provider' => $form_provider_id, 'form_id' => $form_id, 'form_name' => $form_name, 'action_provider' => $action_provider, 'task' => $task, 'data' => wp_json_encode( $all_data ), ), array( 'id' => $id ) ); } if ( function_exists( 'adfoin_clear_action_platform_settings_cache' ) ) { adfoin_clear_action_platform_settings_cache(); } /** * Fires after an integration is saved (admin-post path). * * @param int $id Integration row id. * @param array $trigger_data Trigger payload (incl. formFields). * @param array $action_data Action payload. * @param mixed $field_data Field mapping payload. */ do_action( 'adfoin_integration_saved', $id, $trigger_data, $action_data, $field_data ); advanced_form_integration_redirect( 'admin.php?page=advanced-form-integration&action=edit&id='. $id ); } /** * AJAX variant of save_integration. * * Mirrors the data shape and DB writes of save_integration() but * returns JSON instead of redirecting, so the integration form can * submit in-place via fetch/jQuery without losing scroll position * or Vue state. The non-AJAX handler remains as a graceful fallback * for browsers with JS disabled. * * Expected POST payload: same field names as the form (action, * type, _wpnonce, edit_id, integration_title, form_provider_id, * form_id, form_name, action_provider, task, triggerData, * actionData, fieldData). * * Response: JSON via wp_send_json_success / wp_send_json_error. * * @since 1.128.1 * @return void */ public function save_integration_ajax() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'advanced-form-integration' ) ), 403 ); } if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), 'adfoin-integration' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed. Please reload and try again.', 'advanced-form-integration' ) ), 403 ); } $type = isset( $_POST['type'] ) ? adfoin_sanitize_text_or_array_field( $_POST['type'] ) : ''; if ( ! in_array( $type, array( 'new_integration', 'update_integration' ), true ) ) { wp_send_json_error( array( 'message' => __( 'Invalid integration type.', 'advanced-form-integration' ) ), 400 ); } $trigger_data = isset( $_POST['triggerData'] ) ? adfoin_sanitize_text_or_array_field( $_POST['triggerData'] ) : array(); if ( $trigger_data && is_string( $trigger_data ) ) { $trigger_data = json_decode( $trigger_data, true ); } $action_data = isset( $_POST['actionData'] ) ? adfoin_sanitize_text_or_array_field( $_POST['actionData'] ) : array(); if ( $action_data && is_string( $action_data ) ) { $action_data = json_decode( $action_data, true ); } $field_data = isset( $_POST['fieldData'] ) ? adfoin_sanitize_text_or_array_field( $_POST['fieldData'] ) : array(); // Defensive: if a caller posts fieldData as a JSON string (rather // than the bracketed array form admin-post.php uses), decode it // so what we persist always matches the array shape edit-reload // expects. if ( is_string( $field_data ) ) { $decoded = json_decode( wp_unslash( $field_data ), true ); $field_data = is_array( $decoded ) ? $decoded : array(); } elseif ( ! is_array( $field_data ) ) { $field_data = array(); } $integration_title = isset( $trigger_data['integrationTitle'] ) ? sanitize_text_field( $trigger_data['integrationTitle'] ) : ''; $form_provider_id = isset( $trigger_data['formProviderId'] ) ? $trigger_data['formProviderId'] : ''; $form_id = isset( $trigger_data['formId'] ) ? $trigger_data['formId'] : ''; $form_name = isset( $trigger_data['formName'] ) ? sanitize_text_field( $trigger_data['formName'] ) : ''; $action_provider = isset( $action_data['actionProviderId'] ) ? $action_data['actionProviderId'] : ''; $task = isset( $action_data['task'] ) ? $action_data['task'] : ''; // Server-side guard mirroring the client-side canSave check. $missing = array(); if ( '' === $integration_title ) $missing[] = 'integration_title'; if ( '' === $form_provider_id ) $missing[] = 'form_provider_id'; if ( '' === $form_id ) $missing[] = 'form_id'; if ( '' === $action_provider ) $missing[] = 'action_provider'; if ( '' === $task ) $missing[] = 'task'; if ( ! empty( $missing ) ) { wp_send_json_error( array( 'message' => __( 'Please complete all required fields.', 'advanced-form-integration' ), 'missing' => $missing, ), 422 ); } $all_data = array( 'trigger_data' => $trigger_data, 'action_data' => $action_data, 'field_data' => $field_data, ); global $wpdb; $integration_table = $wpdb->prefix . 'adfoin_integration'; $integration_id = 0; $is_new = false; // Pre-flight: if the integration table is missing entirely (e.g. the // plugin was copied into wp-content/plugins without activation, or // dbDelta failed during a previous upgrade), every insert/update will // fail with the same useless "Saving failed" message. Surface this // specific condition with an actionable hint so the admin knows to // re-activate the plugin instead of guessing. $suppress = $wpdb->suppress_errors( true ); $table_present = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $integration_table ) ); $wpdb->suppress_errors( $suppress ); if ( ! $table_present ) { wp_send_json_error( array( 'message' => sprintf( /* translators: %s is the database table name */ __( 'Database table %s is missing. Deactivate and reactivate Advanced Form Integration to (re)create it, then try saving again.', 'advanced-form-integration' ), $integration_table ), 'diagnostic' => array( 'missing_table' => $integration_table ), ), 500 ); } // Encode the payload once. JSON_INVALID_UTF8_SUBSTITUTE replaces any // lone surrogate / malformed byte with U+FFFD so an emoji or a stray // \xFF in the user's input doesn't make wp_json_encode return false // and trigger an "empty data" insert error. $encoded_data = wp_json_encode( $all_data, JSON_INVALID_UTF8_SUBSTITUTE ); if ( false === $encoded_data ) { wp_send_json_error( array( 'message' => __( 'Could not serialize the integration data — it contains characters that cannot be encoded as JSON. Please remove any unusual characters and try again.', 'advanced-form-integration' ), 'diagnostic' => array( 'json_last_error' => json_last_error_msg() ), ), 500 ); } if ( 'new_integration' === $type ) { $is_new = true; $result = $wpdb->insert( $integration_table, array( 'title' => $integration_title, 'form_provider' => $form_provider_id, 'form_id' => $form_id, 'form_name' => $form_name, 'action_provider' => $action_provider, 'task' => $task, 'data' => $encoded_data, 'status' => 1, ) ); if ( false === $result ) { self::report_db_save_failure( $wpdb, 'insert', $integration_table, array( 'title_len' => strlen( $integration_title ), 'data_len' => strlen( $encoded_data ), 'form_provider' => $form_provider_id, 'action_provider' => $action_provider, ) ); } $integration_id = (int) $wpdb->insert_id; } else { $integration_id = isset( $_POST['edit_id'] ) ? (int) $_POST['edit_id'] : 0; if ( $integration_id <= 0 ) { wp_send_json_error( array( 'message' => __( 'Invalid integration id.', 'advanced-form-integration' ) ), 400 ); } $result = $wpdb->update( $integration_table, array( 'title' => $integration_title, 'form_provider' => $form_provider_id, 'form_id' => $form_id, 'form_name' => $form_name, 'action_provider' => $action_provider, 'task' => $task, 'data' => $encoded_data, ), array( 'id' => $integration_id ) ); if ( false === $result ) { self::report_db_save_failure( $wpdb, 'update', $integration_table, array( 'integration_id' => $integration_id, 'title_len' => strlen( $integration_title ), 'data_len' => strlen( $encoded_data ), ) ); } } if ( function_exists( 'adfoin_clear_action_platform_settings_cache' ) ) { adfoin_clear_action_platform_settings_cache(); } /** * Fires after an integration is saved (AJAX path). Mirrors the * admin-post handler so listeners only need to hook once. * * @param int $integration_id Integration row id. * @param array $trigger_data Trigger payload (incl. formFields). * @param array $action_data Action payload. * @param array $field_data Field mapping payload. */ do_action( 'adfoin_integration_saved', $integration_id, $trigger_data, $action_data, $field_data ); wp_send_json_success( array( 'integration_id' => $integration_id, 'is_new' => $is_new, 'edit_url' => admin_url( 'admin.php?page=advanced-form-integration&action=edit&id=' . $integration_id ), 'message' => $is_new ? __( 'Integration created.', 'advanced-form-integration' ) : __( 'Integration updated.', 'advanced-form-integration' ), ) ); } /** * Build and emit an actionable JSON error when $wpdb->insert/update returns * false. The previous "Could not save the integration" message was a dead * end for support — it hid the real MySQL error (collation mismatch on * 4-byte UTF-8, oversize column, lock timeout, missing table, etc.). * * Safe to expose the raw $wpdb->last_error here because save_integration_ajax * already gates the endpoint on `current_user_can('manage_options')` — this * function is only reachable by site admins. * * Always exits via wp_send_json_error. * * @param wpdb $wpdb The global $wpdb instance. * @param string $op Either 'insert' or 'update' — for the log line. * @param string $table Fully-qualified table name (for the diagnostic). * @param array $context Free-form data that helps narrow down the cause * (lengths, ids, slugs). Never includes secrets. */ protected static function report_db_save_failure( $wpdb, $op, $table, array $context = array() ) { $db_error = is_object( $wpdb ) && isset( $wpdb->last_error ) ? (string) $wpdb->last_error : ''; // Map the most common MySQL error fragments to actionable hints. $hint = ''; if ( $db_error ) { $lc = strtolower( $db_error ); if ( false !== strpos( $lc, 'incorrect string value' ) ) { $hint = __( 'The database table is using an older charset that cannot store 4-byte characters (emojis, some CJK glyphs). Convert the table to utf8mb4 — see "ALTER TABLE … CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci".', 'advanced-form-integration' ); } elseif ( false !== strpos( $lc, "doesn't exist" ) || false !== strpos( $lc, 'no such table' ) ) { $hint = __( 'A required database table is missing. Deactivate and reactivate the plugin to (re)create it.', 'advanced-form-integration' ); } elseif ( false !== strpos( $lc, 'data too long' ) ) { $hint = __( 'One of the values exceeds its database column size. Shorten the integration title or remove unused field mappings.', 'advanced-form-integration' ); } elseif ( false !== strpos( $lc, 'lock wait timeout' ) || false !== strpos( $lc, 'deadlock' ) ) { $hint = __( 'The database is temporarily busy. Wait a moment and retry the save.', 'advanced-form-integration' ); } elseif ( false !== strpos( $lc, 'has gone away' ) ) { $hint = __( 'The database connection dropped mid-write — most often because the payload exceeds max_allowed_packet. Ask your host to raise that setting.', 'advanced-form-integration' ); } } // Always log to error_log so the admin can grep their debug.log even // if they didn't capture the in-browser response. error_log( sprintf( '[ADFOIN] save_integration_ajax %s on %s failed. db_error=%s context=%s', $op, $table, $db_error !== '' ? $db_error : '(empty — check WP_DEBUG)', wp_json_encode( $context ) ) ); $base_message = ( 'insert' === $op ) ? __( 'Could not save the integration.', 'advanced-form-integration' ) : __( 'Could not update the integration.', 'advanced-form-integration' ); $message = $base_message; if ( $db_error !== '' ) { $message .= ' ' . sprintf( /* translators: %s is the raw MySQL error message */ __( 'Database said: %s', 'advanced-form-integration' ), $db_error ); } if ( $hint !== '' ) { $message .= ' — ' . $hint; } wp_send_json_error( array( 'message' => $message, 'diagnostic' => array( 'operation' => $op, 'table' => $table, 'db_error' => $db_error, 'context' => $context, ), ), 500 ); } /* * Resend Log Data * * This method handles resending failed API requests from the log. * For OAuth2-based platforms, it automatically updates the Authorization * header with the current bearer token to ensure the request succeeds. */ public function resend_log_data() { // Capability gate. This handler relays an arbitrary URL + HTTP args // from the server (admin-authenticated SSRF by design). The nonce is // currently only emitted on the manage_options-gated log view, but // gating explicitly here means a future reuse of the same nonce can // never open this surface to a lower-privilege user. if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'advanced-form-integration' ) ); } // Verify nonce for security if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), 'adfoin-resend-log' ) ) { wp_die( esc_html__( 'Security check failed.', 'advanced-form-integration' ) ); } // Sanitize and validate input data $log_id = isset( $_POST['log_id'] ) ? sanitize_text_field( wp_unslash( $_POST['log_id'] ) ) : ''; $integration_id = isset( $_POST['integration_id'] ) ? sanitize_text_field( wp_unslash( $_POST['integration_id'] ) ) : ''; $raw_data = isset( $_POST['request-data'] ) ? wp_unslash( $_POST['request-data'] ) : ''; if ( empty( $log_id ) || empty( $integration_id ) || empty( $raw_data ) ) { wp_die( __( 'Required data is missing.', 'advanced-form-integration' ) ); } $data = json_decode( $raw_data, true ); if ( json_last_error() !== JSON_ERROR_NONE ) { wp_die( __( 'Invalid request data format.', 'advanced-form-integration' ) ); } $url = isset( $data['url'] ) ? esc_url_raw( $data['url'] ) : ''; $args = isset( $data['args'] ) ? $data['args'] : array(); if ( empty( $url ) || ! is_array( $args ) ) { wp_die( __( 'Invalid URL or request arguments.', 'advanced-form-integration' ) ); } // Get integration record to identify the platform global $wpdb; $integration_table = $wpdb->prefix . 'adfoin_integration'; $integration = $wpdb->get_row( $wpdb->prepare( "SELECT action_provider, data FROM {$integration_table} WHERE id = %d", $integration_id ), ARRAY_A ); // Update bearer token for OAuth2-based platforms if ( $integration && $this->is_oauth2_platform( $integration['action_provider'] ) ) { $platform_info = $this->get_oauth2_platform_info( $integration['action_provider'], $url ); if ( $platform_info ) { // Get the credential ID from the integration data $integration_data = json_decode( $integration['data'], true ); $cred_id = ''; if ( isset( $integration_data['field_data']['credId'] ) ) { $cred_id = $integration_data['field_data']['credId']; } else { // Fallback: get the first available credential $credentials = adfoin_read_credentials( $integration['action_provider'] ); if ( !empty( $credentials ) && is_array( $credentials ) ) { $first_credential = reset( $credentials ); $cred_id = isset( $first_credential['id'] ) ? $first_credential['id'] : ''; } } if ( $cred_id && class_exists( $platform_info['class'] ) ) { // Get fresh OAuth2 instance and set credentials $oauth_instance = null; // Try to get instance using get_instance method if ( method_exists( $platform_info['class'], 'get_instance' ) ) { $oauth_instance = call_user_func( array( $platform_info['class'], 'get_instance' ) ); } else { // Fallback: create new instance $oauth_instance = new $platform_info['class'](); } if ( $oauth_instance && method_exists( $oauth_instance, 'set_credentials' ) ) { $oauth_instance->set_credentials( $cred_id ); // Update the Authorization header with current bearer token if ( ! isset( $args['headers'] ) ) { $args['headers'] = array(); } if ( method_exists( $oauth_instance, 'get_bearer_token' ) ) { $args['headers']['Authorization'] = $oauth_instance->get_bearer_token(); } } } } } // Determine whether the request method routes the body into the URL as a query string // (WP sets data_format='query' for these). For these methods the cURL transport requires // an array body — re-encoding to a JSON/form string would crash http_build_query(). $method = isset( $args['method'] ) ? strtoupper( (string) $args['method'] ) : 'GET'; $body_in_url = in_array( $method, array( 'GET', 'HEAD', 'DELETE', 'TRACE' ), true ); // Ensure the body is properly formatted for resending // Step 1: If body is a JSON string, decode it to array for processing if ( isset( $args['body'] ) && is_string( $args['body'] ) ) { $decoded_body = json_decode( $args['body'], true ); if ( json_last_error() === JSON_ERROR_NONE && ( is_array( $decoded_body ) || is_object( $decoded_body ) ) ) { $args['body'] = $decoded_body; } } // Step 1.5: If body is still a string and either Content-Type is form-urlencoded // (or unset, which WP defaults to form-urlencoded) or the method routes the body into // the URL, parse it back to an array. Without this, GET/HEAD/DELETE/TRACE requests // fatal in WP Requests' format_get() because http_build_query() requires an array. if ( isset( $args['body'] ) && is_string( $args['body'] ) && '' !== $args['body'] ) { $content_type = ''; if ( isset( $args['headers'] ) && is_array( $args['headers'] ) ) { foreach ( $args['headers'] as $header_name => $header_value ) { if ( is_string( $header_name ) && 'content-type' === strtolower( $header_name ) ) { $content_type = (string) $header_value; break; } } } $is_form_urlencoded = ( '' === $content_type ) || ( false !== stripos( $content_type, 'application/x-www-form-urlencoded' ) ); if ( $is_form_urlencoded || $body_in_url ) { $parsed = array(); wp_parse_str( $args['body'], $parsed ); if ( ! empty( $parsed ) ) { $args['body'] = $parsed; } } } // Step 2: If Content-Type indicates JSON, encode array body back to JSON string. // This handles cases like "application/json; charset=utf-8". // Skip for GET/HEAD/DELETE/TRACE: those methods send the body as URL query params, // and WP Requests' format_get() needs an array — a JSON-encoded string would crash. if ( ! $body_in_url && isset( $args['headers']['Content-Type'] ) && strpos( $args['headers']['Content-Type'], 'application/json' ) !== false ) { if ( isset( $args['body'] ) && is_array( $args['body'] ) ) { $args['body'] = wp_json_encode( $args['body'] ); } } // Handle merge-patch JSON format as well (same caveat for GET-style methods) if ( ! $body_in_url && isset( $args['headers']['Content-Type'] ) && strpos( $args['headers']['Content-Type'], 'application/merge-patch+json' ) !== false ) { if ( isset( $args['body'] ) && is_array( $args['body'] ) ) { $args['body'] = wp_json_encode( $args['body'] ); } } // Perform the remote request $response = adfoin_remote_request( $url, $args ); // Log the response adfoin_add_to_log( $response, $url, $args, array( 'id' => $integration_id ), $log_id ); // Redirect to the log view page wp_safe_redirect( admin_url( 'admin.php?page=advanced-form-integration-log&action=view&id=' . $log_id ) ); exit; } /** * Check if a platform uses OAuth2 authentication */ private function is_oauth2_platform( $platform ) { $oauth2_platforms = array( 'zohocrm', 'aweber', 'zohobooks', 'zoomwebinar', 'googledrive', 'zohoma', 'googlecalendar', 'zohopeople', 'bigin', 'salesforce', 'gotowebinar', 'zohocampaigns', 'verticalresponse', 'googlesheets', 'zohorecruit', 'constantcontact', 'liondesk', 'zohosheet', 'zohodesk', 'mailup', 'googletasks', 'cleverreach', 'bombbomb', 'zohoinventory', 'zohoinvoice', 'zohosubscriptions', 'zohofsm', 'zohocreator', 'drchrono', 'clio', 'practicepanther' ); return in_array( $platform, $oauth2_platforms ); } /** * Get OAuth2 platform information including class name and API domains */ private function get_oauth2_platform_info( $platform, $url ) { $platform_map = array( 'zohocrm' => array( 'class' => 'ADFOIN_ZohoCRM', 'domains' => array( 'zohoapis.com', 'zohoapis.eu', 'zohoapis.in', 'zohoapis.com.cn', 'zohoapis.com.au', 'zohoapis.jp', 'zohoapis.sa', 'zohoapis.ca' ) ), 'aweber' => array( 'class' => 'ADFOIN_Aweber', 'domains' => array( 'api.aweber.com' ) ), 'zohobooks' => array( 'class' => 'ADFOIN_ZohoBooks', 'domains' => array( 'books.zoho.com', 'books.zoho.eu', 'books.zoho.in', 'books.zoho.com.cn', 'books.zoho.com.au', 'books.zoho.jp' ) ), 'googledrive' => array( 'class' => 'ADFOIN_GoogleDrive', 'domains' => array( 'www.googleapis.com' ) ), 'googlecalendar' => array( 'class' => 'ADFOIN_GoogleCalendar', 'domains' => array( 'www.googleapis.com' ) ), 'googlesheets' => array( 'class' => 'ADFOIN_GoogleSheets', 'domains' => array( 'sheets.googleapis.com' ) ), 'googletasks' => array( 'class' => 'ADFOIN_GoogleTasks', 'domains' => array( 'tasks.googleapis.com' ) ), 'salesforce' => array( 'class' => 'ADFOIN_Salesforce', 'domains' => array( '.salesforce.com', '.force.com' ) ), 'constantcontact' => array( 'class' => 'ADFOIN_ConstantContact', 'domains' => array( 'api.cc.email' ) ), 'bigin' => array( 'class' => 'ADFOIN_Bigin', 'domains' => array( 'bigin.zoho.com', 'bigin.zoho.eu', 'bigin.zoho.in' ) ), 'zohoma' => array( 'class' => 'ADFOIN_ZohoMA', 'domains' => array( 'campaigns.zoho.com', 'campaigns.zoho.eu', 'campaigns.zoho.in' ) ), 'zohocampaigns' => array( 'class' => 'ADFOIN_ZohoCampaigns', 'domains' => array( 'campaigns.zoho.com', 'campaigns.zoho.eu', 'campaigns.zoho.in' ) ), 'zohopeople' => array( 'class' => 'ADFOIN_ZohoPeople', 'domains' => array( 'people.zoho.com', 'people.zoho.eu', 'people.zoho.in' ) ), 'zohorecruit' => array( 'class' => 'ADFOIN_ZohoRecruit', 'domains' => array( 'recruit.zoho.com', 'recruit.zoho.eu', 'recruit.zoho.in' ) ), 'zohosheet' => array( 'class' => 'ADFOIN_Zohosheet', 'domains' => array( 'sheet.zoho.com', 'sheet.zoho.eu', 'sheet.zoho.in' ) ), 'zohodesk' => array( 'class' => 'ADFOIN_ZohoDesk', 'domains' => array( 'desk.zoho.com', 'desk.zoho.eu', 'desk.zoho.in' ) ), 'zohoinventory' => array( 'class' => 'ADFOIN_ZohoInventory', 'domains' => array( 'zohoapis.com', 'zohoapis.eu', 'zohoapis.in', 'zohoapis.com.cn', 'zohoapis.com.au', 'zohoapis.jp', 'zohoapis.sa' ) ), 'zohoinvoice' => array( 'class' => 'ADFOIN_ZohoInvoice', 'domains' => array( 'zohoapis.com', 'zohoapis.eu', 'zohoapis.in', 'zohoapis.com.cn', 'zohoapis.com.au', 'zohoapis.jp', 'zohoapis.sa' ) ), 'zohosubscriptions' => array( 'class' => 'ADFOIN_ZohoSubscriptions', 'domains' => array( 'zohoapis.com', 'zohoapis.eu', 'zohoapis.in', 'zohoapis.com.cn', 'zohoapis.com.au', 'zohoapis.jp', 'zohoapis.sa' ) ), 'zohofsm' => array( 'class' => 'ADFOIN_ZohoFSM', 'domains' => array( 'zohoapis.com', 'zohoapis.eu', 'zohoapis.in', 'zohoapis.com.cn', 'zohoapis.com.au', 'zohoapis.jp', 'zohoapis.sa' ) ), 'zohocreator' => array( 'class' => 'ADFOIN_ZohoCreator', 'domains' => array( 'zohoapis.com', 'zohoapis.eu', 'zohoapis.in', 'zohoapis.com.cn', 'zohoapis.com.au', 'zohoapis.jp', 'zohoapis.sa' ) ), 'drchrono' => array( 'class' => 'ADFOIN_DrChrono', 'domains' => array( 'app.drchrono.com' ) ), 'clio' => array( 'class' => 'ADFOIN_Clio', 'domains' => array( 'app.clio.com', 'eu.app.clio.com', 'ca.app.clio.com', 'au.app.clio.com' ) ), 'practicepanther' => array( 'class' => 'ADFOIN_PracticePanther', 'domains' => array( 'app.practicepanther.com' ) ) ); if ( ! isset( $platform_map[ $platform ] ) ) { return false; } $info = $platform_map[ $platform ]; // Check if the URL matches any of the platform's API domains foreach ( $info['domains'] as $domain ) { if ( strpos( $url, $domain ) !== false ) { return $info; } } return false; } /** * Enables or disables an integration. */ public function adfoin_enable_integration() { // Capability check first — defense-in-depth even though the // endpoint is wp-admin only. Reach this branch and you're an // unauthenticated/under-privileged user, fail closed before // touching $wpdb. if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'advanced-form-integration' ) ), 403 ); } // Nonce check. Returns JSON (not wp_die-rendered HTML) so the // jQuery toggle handler's .fail() path can read .data.message // consistently across every error case below. if ( ! isset( $_POST['_nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['_nonce'] ), 'advanced-form-integration' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed.', 'advanced-form-integration' ) ), 403 ); } $id = isset( $_POST['id'] ) ? absint( wp_unslash( $_POST['id'] ) ) : 0; $enabled = isset( $_POST['enabled'] ) ? sanitize_text_field( wp_unslash( $_POST['enabled'] ) ) : ''; if ( $id <= 0 || ! in_array( $enabled, array( '0', '1' ), true ) ) { wp_send_json_error( array( 'message' => __( 'Invalid input data.', 'advanced-form-integration' ) ), 400 ); } global $wpdb; $table = $wpdb->prefix . 'adfoin_integration'; $status = ( $enabled === '1' ) ? 1 : 0; $action_status = $wpdb->update( $table, array( 'status' => $status ), array( 'id' => $id ), array( '%d' ), array( '%d' ) ); if ( false === $action_status ) { wp_send_json_error( array( 'message' => __( 'Failed to update integration status.', 'advanced-form-integration' ) ), 500 ); } if ( function_exists( 'adfoin_clear_action_platform_settings_cache' ) ) { adfoin_clear_action_platform_settings_cache(); } wp_send_json_success( array( 'message' => __( 'Integration status updated successfully.', 'advanced-form-integration' ) ) ); } }