' . esc_html__('All Forms', 'formfork-connect') . ''; $builder_link = '' . esc_html__('Form Builder', 'formfork-connect') . ''; $settings_link = '' . esc_html__('Settings', 'formfork-connect') . ''; array_unshift($links, $forms_link, $builder_link, $settings_link); return $links; } /** * Track and flush 60-second WordPress transients without direct SQL queries */ private function remember_transient_key($key) { $clean_key = sanitize_key($key); $keys = get_option('formfork_transient_keys', array()); if (!is_array($keys)) { $keys = array(); } if (!in_array($clean_key, $keys, true)) { $keys[] = $clean_key; if (count($keys) > 100) { $keys = array_slice($keys, -100); } update_option('formfork_transient_keys', $keys, false); } } public function flush_cached_transients() { $keys = get_option('formfork_transient_keys', array()); if (is_array($keys)) { foreach ($keys as $k) { delete_transient(sanitize_key($k)); } } update_option('formfork_transient_keys', array(), false); } public function register_elementor_widget($widgets_manager) { $widget_file = plugin_dir_path(__FILE__) . 'includes/class-formfork-elementor-widget.php'; if (file_exists($widget_file)) { require_once $widget_file; if (class_exists('FormFork_Elementor_Widget') && is_object($widgets_manager) && method_exists($widgets_manager, 'register')) { $widgets_manager->register(new FormFork_Elementor_Widget()); } } } public function register_elementor_widget_legacy() { if (!did_action('elementor/widgets/register') && class_exists('\Elementor\Plugin')) { $widget_file = plugin_dir_path(__FILE__) . 'includes/class-formfork-elementor-widget.php'; if (file_exists($widget_file)) { require_once $widget_file; if (class_exists('FormFork_Elementor_Widget') && isset(\Elementor\Plugin::$instance->widgets_manager)) { \Elementor\Plugin::$instance->widgets_manager->register_widget_type(new FormFork_Elementor_Widget()); } } } } public static function get_synced_forms_list() { if (!self::$instance) { return array(); } $state = self::$instance->fetch_workspace_forms(); return isset($state['forms']) && is_array($state['forms']) ? $state['forms'] : array(); } public static function get_default_platform_url() { if (!self::$instance) { return 'https://formfork.io'; } return self::$instance->get_platform_url(); } public static function get_default_embed_token() { if (!self::$instance) { return ''; } return self::$instance->get_embed_token(); } private function get_platform_url() { return $this->default_platform_url; } private function get_api_key() { return trim((string) get_option('formfork_api_key', '')); } private function get_embed_token() { return trim((string) get_option('formfork_embed_token', '')); } private function get_site_domain() { $host = wp_parse_url(home_url(), PHP_URL_HOST); if (empty($host) && isset($_SERVER['HTTP_HOST'])) { $host = sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])); } return $host ? strtolower(preg_replace('/^www\./i', '', trim((string) $host))) : ''; } public function register_shortcode_and_block() { add_shortcode('formfork', array($this, 'render_form_shortcode')); if (function_exists('register_block_type')) { register_block_type('formfork/form-embed', array( 'attributes' => array( 'formId' => array('type' => 'string', 'default' => ''), 'mode' => array('type' => 'string', 'default' => 'native'), 'theme' => array('type' => 'string', 'default' => ''), 'showTitle' => array('type' => 'boolean', 'default' => true), 'showDescription' => array('type' => 'boolean', 'default' => true), 'buttonText' => array('type' => 'string', 'default' => ''), 'height' => array('type' => 'string', 'default' => '600'), ), 'render_callback' => array($this, 'render_gutenberg_block'), )); } } public function render_gutenberg_block($attributes) { $atts = array( 'id' => isset($attributes['formId']) ? $attributes['formId'] : '', 'mode' => isset($attributes['mode']) ? $attributes['mode'] : 'native', 'theme' => isset($attributes['theme']) ? $attributes['theme'] : '', 'title' => (!isset($attributes['showTitle']) || $attributes['showTitle']) ? 'true' : 'false', 'description' => (!isset($attributes['showDescription']) || $attributes['showDescription']) ? 'true' : 'false', 'button_text' => isset($attributes['buttonText']) ? $attributes['buttonText'] : '', 'height' => isset($attributes['height']) ? $attributes['height'] : '600', ); return $this->render_form_shortcode($atts); } public function enqueue_block_editor_assets() { wp_enqueue_script( 'formfork-block-editor', plugin_dir_url(__FILE__) . 'js/formfork-block.js', array('wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components'), $this->version, true ); $sync_state = $this->fetch_workspace_forms(); $simple_forms = array(); if (!empty($sync_state['forms']) && is_array($sync_state['forms'])) { foreach ($sync_state['forms'] as $f) { if (!empty($f['_id'])) { $simple_forms[] = array( 'id' => $f['_id'], 'name' => isset($f['name']) ? $f['name'] : esc_html__('Untitled Form', 'formfork-connect'), ); } } } wp_localize_script('formfork-block-editor', 'formforkBlockData', array( 'forms' => $simple_forms, 'platformUrl' => $this->get_platform_url(), 'builderUrl' => admin_url('admin.php?page=formfork-builder'), )); } /** * Enqueue Admin Studio, Onboarding & Classic Editor assets via standard WordPress hooks */ public function enqueue_admin_assets($hook_suffix) { $is_formfork_page = (strpos((string) $hook_suffix, 'formfork') !== false); $is_post_editor = in_array($hook_suffix, array('post.php', 'post-new.php'), true); $show_welcome_card = in_array($hook_suffix, array('plugins.php', 'index.php'), true) && empty($this->get_api_key()) && !get_option('formfork_onboarding_dismissed'); if (!$is_formfork_page && !$is_post_editor && !$show_welcome_card) { return; } wp_enqueue_style( 'formfork-admin-style', plugin_dir_url(__FILE__) . 'css/formfork.css', array(), $this->version ); wp_enqueue_script( 'formfork-admin-script', plugin_dir_url(__FILE__) . 'js/formfork-block.js', array(), $this->version, true ); $editing_form = null; $can_write = true; $is_connected = false; if ($is_formfork_page && current_user_can('manage_options')) { $sync_state = $this->fetch_workspace_forms(); $is_connected = !empty($sync_state['connected']); $can_write = !isset($sync_state['canWriteForms']) || !empty($sync_state['canWriteForms']); $nonce_verified = isset($_GET['_wpnonce']) && wp_verify_nonce( sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'formfork_edit_form' ); $edit_id = ($nonce_verified && isset($_GET['edit_id'])) ? sanitize_text_field(wp_unslash($_GET['edit_id'])) : ''; if (!empty($edit_id) && !empty($sync_state['forms']) && is_array($sync_state['forms'])) { foreach ($sync_state['forms'] as $f) { if (isset($f['_id']) && $f['_id'] === $edit_id) { $editing_form = $f; break; } } } } wp_localize_script('formfork-admin-script', 'formforkAdminData', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'adminNonce' => wp_create_nonce('formfork_admin_nonce'), 'editNonce' => wp_create_nonce('formfork_edit_form'), 'platformUrl' => $this->get_platform_url(), 'siteDomain' => $this->get_site_domain(), 'builderUrl' => admin_url('admin.php?page=formfork-builder'), 'formsUrl' => admin_url('admin.php?page=formfork-forms'), 'settingsUrl' => admin_url('admin.php?page=formfork-settings'), 'connected' => $is_connected, 'canWriteForms' => $can_write, 'editingForm' => $editing_form, )); } /** * Dismissible Welcome Banner on Plugins & Dashboard screens when not yet connected */ public function render_welcome_admin_notice() { if (!current_user_can('manage_options')) { return; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || !in_array($screen->base, array('plugins', 'dashboard'), true)) { return; } if (!empty($this->get_api_key()) || get_option('formfork_onboarding_dismissed')) { return; } $wizard_url = wp_nonce_url( admin_url('admin.php?page=formfork-forms&onboarding=1'), 'formfork_onboarding_wizard' ); ?>

→
version ); wp_register_script( 'formfork-embed-script', plugin_dir_url(__FILE__) . 'js/formfork.js', array(), $this->version, true ); wp_localize_script('formfork-embed-script', 'formforkConfig', array( 'platformUrl' => $this->get_platform_url(), 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('formfork_public_nonce'), 'defaultToken' => $this->get_embed_token(), 'useProxy' => true, )); } /** * Render [formfork] shortcode */ public function render_form_shortcode($atts) { $atts = shortcode_atts(array( 'id' => '', 'name' => '', 'mode' => 'native', 'token' => '', 'theme' => '', 'title' => 'true', 'description' => 'true', 'button_text' => '', 'redirect_url' => '', 'height' => '600', ), $atts, 'formfork'); $form_id = sanitize_text_field($atts['id']); $form_name = sanitize_text_field($atts['name']); if (empty($form_id) && empty($form_name)) { return '
' . esc_html__('FormFork:', 'formfork-connect') . ' ' . esc_html__('Please provide a Form ID in the shortcode, e.g.', 'formfork-connect') . ' [formfork id="your-form-id"].
'; } wp_enqueue_style('formfork-embed-style'); if (strtolower($atts['mode']) === 'iframe' && !empty($form_id)) { $share_url = esc_url($this->get_platform_url() . '/share/' . rawurlencode($form_id)); $height = intval($atts['height']) > 200 ? intval($atts['height']) : 600; return sprintf( '
', $share_url, $height ); } wp_enqueue_script('formfork-embed-script'); $container_id = 'formfork-container-' . wp_generate_uuid4(); $token = !empty($atts['token']) ? sanitize_text_field($atts['token']) : $this->get_embed_token(); return sprintf( '
%s
', esc_attr($container_id), esc_attr($form_id), esc_attr($form_name), esc_attr($token), esc_attr($atts['theme']), esc_attr($atts['title']), esc_attr($atts['description']), esc_attr($atts['button_text']), esc_url($atts['redirect_url']), esc_html__('Loading form...', 'formfork-connect') ); } /** * Classic Editor: Add "Add FormFork Form" button next to Add Media */ public function add_classic_editor_button() { if (!current_user_can('edit_posts')) { return; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || !in_array($screen->base, array('post', 'page'), true)) { return; } echo ''; } public function render_classic_editor_modal() { if (!current_user_can('edit_posts')) { return; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || !in_array($screen->base, array('post', 'page'), true)) { return; } $sync_state = $this->fetch_workspace_forms(); ?>




esc_html__('Missing form_id parameter.', 'formfork-connect')), 400); } $platform_url = $this->get_platform_url(); $api_key = $this->get_api_key(); $cache_key = 'ff_schema_' . md5($platform_url . '|' . $api_key . '|' . $form_id); $cached_data = get_transient($cache_key); if (is_array($cached_data) && !empty($cached_data['_id'])) { wp_send_json_success($cached_data); } $endpoint = $platform_url . '/api/v1/forms/' . rawurlencode($form_id); $headers = array( 'Accept' => 'application/json', 'Origin' => home_url(), 'X-Site-Domain' => $this->get_site_domain(), ); if (!empty($api_key)) { $headers['X-API-Key'] = $api_key; } $response = wp_remote_get($endpoint, array( 'timeout' => 15, 'headers' => $headers, )); if (is_wp_error($response)) { wp_send_json_error(array( 'message' => esc_html__('Failed to reach FormFork platform: ', 'formfork-connect') . $response->get_error_message(), ), 502); } $status_code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($status_code >= 200 && $status_code < 300 && !empty($body['success']) && is_array($body['data'])) { $this->remember_transient_key($cache_key); set_transient($cache_key, $body['data'], $this->cache_ttl); wp_send_json_success($body['data']); } wp_send_json_error(array( 'message' => isset($body['error']) ? sanitize_text_field($body['error']) : (isset($body['message']) ? sanitize_text_field($body['message']) : esc_html__('Form not found.', 'formfork-connect')), ), $status_code ? $status_code : 404); } /** * Recursively sanitize decoded JSON arrays while preserving boolean and numeric types */ private function sanitize_deep_payload($value) { if (is_array($value)) { $cleaned = array(); foreach ($value as $k => $v) { $clean_key = is_string($k) ? sanitize_text_field($k) : $k; $cleaned[$clean_key] = $this->sanitize_deep_payload($v); } return $cleaned; } if (is_bool($value) || is_int($value) || is_float($value)) { return $value; } if (is_string($value)) { return sanitize_textarea_field($value); } return ''; } /** * AJAX: Proxy Form Submission (Supports both JSON and Multipart File Uploads) */ public function ajax_submit_entry() { check_ajax_referer('formfork_public_nonce', 'nonce'); $payload = null; if (isset($_POST['_ff_json_payload'])) { // Decode raw JSON envelope first before running sanitize_deep_payload so angle brackets (< or >) inside field values do not corrupt JSON syntax. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Every decoded key and scalar value is recursively sanitized immediately below via sanitize_deep_payload(). $raw_envelope = (string) wp_unslash($_POST['_ff_json_payload']); $decoded = json_decode($raw_envelope, true); if (is_array($decoded)) { $payload = $this->sanitize_deep_payload($decoded); } } if (!is_array($payload)) { $payload = map_deep(wp_unslash($_POST), 'sanitize_text_field'); unset($payload['action'], $payload['nonce'], $payload['_wpnonce']); } $form_id = isset($payload['_formId']) ? sanitize_text_field($payload['_formId']) : ''; $form_name = isset($payload['_formName']) ? sanitize_text_field($payload['_formName']) : ''; $token = isset($payload['_embed_token']) ? sanitize_text_field($payload['_embed_token']) : $this->get_embed_token(); $api_key = $this->get_api_key(); $headers = array( 'Accept' => 'application/json', 'X-Forwarded-For' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '127.0.0.1', 'User-Agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : 'FormFork-WP-Plugin/1.0.0', 'Origin' => home_url(), 'Referer' => isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : home_url(), 'X-Site-Domain' => $this->get_site_domain(), ); if (!empty($api_key)) { $headers['X-API-Key'] = $api_key; } if (!empty($token)) { $headers['X-FormFork-Token'] = $token; } if (!empty($form_id)) { $endpoint = $this->get_platform_url() . '/f/' . rawurlencode($form_id); } else { $endpoint = $this->get_platform_url() . '/api/v1/submit'; if (empty($payload['formName']) && !empty($form_name)) { $payload['formName'] = $form_name; } } $has_uploaded_files = !empty($_FILES); if ($has_uploaded_files) { $boundary = '----FormForkWPBoundary' . wp_generate_password(16, false, false); $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary; $body_stream = '--' . $boundary . "\r\n"; $body_stream .= 'Content-Disposition: form-data; name="_ff_json_payload"' . "\r\n\r\n"; $body_stream .= wp_json_encode($payload) . "\r\n"; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each file property is individually sanitized and validated below. foreach ($_FILES as $raw_field_key => $file_info) { if (!is_array($file_info)) { continue; } $field_key = sanitize_key($raw_field_key); // Do not call wp_unslash on tmp_name as it strips directory separators on Windows servers. $tmp_name = isset($file_info['tmp_name']) ? (string) $file_info['tmp_name'] : ''; $orig_name = isset($file_info['name']) ? sanitize_file_name(wp_unslash($file_info['name'])) : 'upload.bin'; $mime_type = isset($file_info['type']) ? sanitize_mime_type(wp_unslash($file_info['type'])) : 'application/octet-stream'; $err_code = isset($file_info['error']) ? absint($file_info['error']) : UPLOAD_ERR_NO_FILE; $checked_type = wp_check_filetype($orig_name); $ext_lower = strtolower((string) pathinfo($orig_name, PATHINFO_EXTENSION)); $blocked_exts = array('php', 'phtml', 'phar', 'htaccess', 'exe', 'sh', 'bat', 'cmd', 'dll', 'jsp', 'asp'); if (in_array($ext_lower, $blocked_exts, true)) { wp_send_json_error(array( 'message' => esc_html__('This file type is not permitted for security reasons.', 'formfork-connect'), ), 400); } if ($err_code === UPLOAD_ERR_OK && !empty($tmp_name) && is_uploaded_file($tmp_name)) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading verified local PHP upload temp file into multipart buffer. $file_bytes = file_get_contents($tmp_name); if ($file_bytes !== false) { $resolved_mime = !empty($checked_type['type']) ? $checked_type['type'] : (!empty($mime_type) ? $mime_type : 'application/octet-stream'); $body_stream .= '--' . $boundary . "\r\n"; $body_stream .= 'Content-Disposition: form-data; name="' . $field_key . '"; filename="' . $orig_name . '"' . "\r\n"; $body_stream .= 'Content-Type: ' . $resolved_mime . "\r\n\r\n"; $body_stream .= $file_bytes . "\r\n"; } } } $body_stream .= '--' . $boundary . "--\r\n"; $response = wp_remote_post($endpoint, array( 'timeout' => 30, 'headers' => $headers, 'body' => $body_stream, )); } else { $headers['Content-Type'] = 'application/json'; $response = wp_remote_post($endpoint, array( 'timeout' => 20, 'headers' => $headers, 'body' => wp_json_encode($payload), )); } if (is_wp_error($response)) { wp_send_json_error(array( 'message' => esc_html__('Submission connection error: ', 'formfork-connect') . $response->get_error_message(), ), 502); } $status_code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($status_code >= 200 && $status_code < 300 && !empty($body['success'])) { wp_send_json_success($body); } $err_msg = esc_html__('Submission failed.', 'formfork-connect'); if (is_array($body)) { if (!empty($body['message'])) { $err_msg = sanitize_text_field($body['message']); } elseif (!empty($body['error'])) { $err_msg = sanitize_text_field($body['error']); } } wp_send_json_error(array( 'message' => $err_msg, 'details' => $body, ), $status_code >= 400 ? $status_code : 400); } /** * WP Admin AJAX: Flush Transient Cache & Re-Sync Workspace Forms */ public function ajax_admin_sync_now() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => esc_html__('Unauthorized', 'formfork-connect')), 403); } check_ajax_referer('formfork_admin_nonce', 'nonce'); $this->flush_cached_transients(); $state = $this->fetch_workspace_forms(true); wp_send_json_success(array( 'connected' => $state['connected'], 'formsCount' => count($state['forms']), )); } /** * WP Admin AJAX: Live API Connection & Key Scope Diagnostic Test */ public function ajax_admin_test_connection() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => esc_html__('Unauthorized', 'formfork-connect')), 403); } check_ajax_referer('formfork_admin_nonce', 'nonce'); $raw_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : ''; $platform_url = $this->get_platform_url(); $api_key = !empty($raw_key) ? $raw_key : $this->get_api_key(); if (empty($api_key)) { wp_send_json_error(array( 'message' => esc_html__('Please enter a Server or Domain-Scoped API Key (ff_live_...) to test.', 'formfork-connect'), ), 400); } $site_domain = $this->get_site_domain(); $start_time = microtime(true); $endpoint = $platform_url . '/api/v1/forms'; $response = wp_remote_get($endpoint, array( 'timeout' => 12, 'headers' => array( 'Accept' => 'application/json', 'X-API-Key' => $api_key, 'Origin' => home_url(), 'X-Site-Domain' => $site_domain, ), )); $latency_ms = round((microtime(true) - $start_time) * 1000); if (is_wp_error($response)) { wp_send_json_error(array( 'message' => $response->get_error_message(), ), 502); } $code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($code === 200 && !empty($body['success']) && isset($body['data']) && is_array($body['data'])) { $this->flush_cached_transients(); $scoped = !empty($body['scoped']); $can_write_forms = $this->extract_can_write_forms($body); $scopes_list = $this->extract_scopes_list($body); $scope_label = ''; if ($scoped && !empty($body['scopeSummary']) && is_array($body['scopeSummary'])) { $summary = $body['scopeSummary']; if (!empty($summary['allowedDomains']) && is_array($summary['allowedDomains'])) { $scope_label = implode(', ', array_map('sanitize_text_field', $summary['allowedDomains'])); } elseif (!empty($summary['projectGroup'])) { $scope_label = sanitize_text_field($summary['projectGroup']); } elseif (!empty($summary['preset'])) { $scope_label = sanitize_text_field($summary['preset']); } } wp_send_json_success(array( 'latencyMs' => $latency_ms, 'formsCount' => count($body['data']), 'scoped' => $scoped, 'scopeLabel' => $scope_label, 'siteDomain' => $site_domain, 'canWriteForms' => $can_write_forms, 'scopes' => $scopes_list, )); } $err_text = isset($body['error']) ? sanitize_text_field($body['error']) : esc_html__('Authentication failed (HTTP ', 'formfork-connect') . intval($code) . ').'; wp_send_json_error(array('message' => $err_text), $code ? $code : 400); } /** * Extract whether the active API key has forms:write permission from /api/v1/forms response */ private function extract_can_write_forms($body) { if (!is_array($body)) { return true; } if (isset($body['permissions']) && is_array($body['permissions']) && isset($body['permissions']['canWriteForms'])) { return (bool) $body['permissions']['canWriteForms']; } if (isset($body['scopeSummary']) && is_array($body['scopeSummary']) && isset($body['scopeSummary']['canWriteForms'])) { return (bool) $body['scopeSummary']['canWriteForms']; } if (isset($body['permissions']) && is_array($body['permissions']) && isset($body['permissions']['scopes']) && is_array($body['permissions']['scopes'])) { $scopes = $body['permissions']['scopes']; return in_array('*', $scopes, true) || in_array('forms:write', $scopes, true); } return true; } /** * Extract sanitized scopes array from /api/v1/forms response */ private function extract_scopes_list($body) { if (is_array($body) && isset($body['permissions']['scopes']) && is_array($body['permissions']['scopes'])) { return array_values(array_map('sanitize_text_field', $body['permissions']['scopes'])); } if (is_array($body) && isset($body['scopeSummary']['scopes']) && is_array($body['scopeSummary']['scopes'])) { return array_values(array_map('sanitize_text_field', $body['scopeSummary']['scopes'])); } return array('*'); } /** * Check if a synced form is locked to the FormFork Web Dashboard (cannot be edited in WP Admin) */ private function get_form_edit_lock($form) { if (!is_array($form) || empty($form['settings']) || !is_array($form['settings'])) { return 'unlocked'; } $lock = isset($form['settings']['editLock']) ? sanitize_key($form['settings']['editLock']) : 'unlocked'; if (!in_array($lock, array('unlocked', 'wordpress_only', 'dashboard_only'), true)) { return 'unlocked'; } return $lock; } /** * Sanitize decoded form schema array after json_decode() so angle brackets inside labels, placeholders, or options never corrupt JSON syntax */ private function sanitize_form_schema_payload($decoded) { if (!is_array($decoded)) { return array(); } $allowed_types = array( 'text', 'email', 'tel', 'url', 'textarea', 'number', 'date', 'select', 'radio', 'checkbox', 'rating', 'signature', 'matrix', 'file', 'heading', ); $sanitized = array( 'name' => isset($decoded['name']) ? sanitize_text_field((string) $decoded['name']) : '', 'description' => isset($decoded['description']) ? sanitize_textarea_field((string) $decoded['description']) : '', 'isActive' => !isset($decoded['isActive']) || (bool) $decoded['isActive'], 'fields' => array(), 'settings' => array(), ); if (isset($decoded['fields']) && is_array($decoded['fields'])) { foreach ($decoded['fields'] as $idx => $raw_field) { if (!is_array($raw_field)) { continue; } $ftype = isset($raw_field['type']) ? sanitize_key((string) $raw_field['type']) : 'text'; if (!in_array($ftype, $allowed_types, true)) { $ftype = 'text'; } $field_item = array( 'id' => !empty($raw_field['id']) ? sanitize_text_field((string) $raw_field['id']) : ('field-' . ($idx + 1)), 'name' => !empty($raw_field['name']) ? sanitize_text_field((string) $raw_field['name']) : ('field_' . ($idx + 1)), 'label' => isset($raw_field['label']) ? sanitize_text_field((string) $raw_field['label']) : ('Field ' . ($idx + 1)), 'type' => $ftype, 'width' => (isset($raw_field['width']) && $raw_field['width'] === 'half') ? 'half' : 'full', 'step' => isset($raw_field['step']) ? max(1, intval($raw_field['step'])) : 1, 'required' => !empty($raw_field['required']), 'placeholder' => isset($raw_field['placeholder']) ? sanitize_text_field((string) $raw_field['placeholder']) : '', 'helpText' => isset($raw_field['helpText']) ? sanitize_text_field((string) $raw_field['helpText']) : '', ); if (isset($raw_field['options']) && is_array($raw_field['options'])) { $field_item['options'] = array_values(array_filter(array_map('sanitize_text_field', $raw_field['options']), 'strlen')); } if ($ftype === 'matrix') { if (isset($raw_field['matrixRows']) && is_array($raw_field['matrixRows'])) { $field_item['matrixRows'] = array_values(array_filter(array_map('sanitize_text_field', $raw_field['matrixRows']), 'strlen')); } if (isset($raw_field['matrixColumns']) && is_array($raw_field['matrixColumns'])) { $field_item['matrixColumns'] = array_values(array_filter(array_map('sanitize_text_field', $raw_field['matrixColumns']), 'strlen')); } } if ($ftype === 'file') { $field_item['fileAccept'] = isset($raw_field['fileAccept']) ? sanitize_text_field((string) $raw_field['fileAccept']) : '.pdf,.png,.jpg,.doc,.docx'; $field_item['maxFileSizeMB'] = isset($raw_field['maxFileSizeMB']) ? max(1, min(50, intval($raw_field['maxFileSizeMB']))) : 10; } if (isset($raw_field['logic']) && is_array($raw_field['logic']) && !empty($raw_field['logic']['field'])) { $op = isset($raw_field['logic']['operator']) ? sanitize_key((string) $raw_field['logic']['operator']) : 'equals'; if (!in_array($op, array('equals', 'not_equals', 'contains'), true)) { $op = 'equals'; } $field_item['logic'] = array( 'action' => (isset($raw_field['logic']['action']) && $raw_field['logic']['action'] === 'hide') ? 'hide' : 'show', 'field' => sanitize_text_field((string) $raw_field['logic']['field']), 'operator' => $op, 'value' => isset($raw_field['logic']['value']) ? sanitize_text_field((string) $raw_field['logic']['value']) : '', ); } $sanitized['fields'][] = $field_item; } } if (isset($decoded['settings']) && is_array($decoded['settings'])) { $raw_settings = $decoded['settings']; if (isset($raw_settings['theme'])) { $sanitized['settings']['theme'] = sanitize_key((string) $raw_settings['theme']); } if (isset($raw_settings['editLock']) && $raw_settings['editLock'] !== '') { $sanitized['settings']['editLock'] = sanitize_key((string) $raw_settings['editLock']); } if (isset($raw_settings['buttonText'])) { $sanitized['settings']['buttonText'] = sanitize_text_field((string) $raw_settings['buttonText']); } if (isset($raw_settings['successMessage'])) { $sanitized['settings']['successMessage'] = sanitize_text_field((string) $raw_settings['successMessage']); } if (isset($raw_settings['redirectUrl'])) { $sanitized['settings']['redirectUrl'] = esc_url_raw((string) $raw_settings['redirectUrl']); } if (isset($raw_settings['spamProtection'])) { $sanitized['settings']['spamProtection'] = (bool) $raw_settings['spamProtection']; } if (isset($raw_settings['showProgressBar'])) { $sanitized['settings']['showProgressBar'] = (bool) $raw_settings['showProgressBar']; } if (isset($raw_settings['steps']) && is_array($raw_settings['steps'])) { $clean_steps = array(); foreach ($raw_settings['steps'] as $s_idx => $raw_step) { if (!is_array($raw_step)) { continue; } $clean_steps[] = array( 'id' => !empty($raw_step['id']) ? sanitize_text_field((string) $raw_step['id']) : ('step-' . ($s_idx + 1)), 'title' => isset($raw_step['title']) ? sanitize_text_field((string) $raw_step['title']) : ('Step ' . ($s_idx + 1)), 'description' => isset($raw_step['description']) ? sanitize_textarea_field((string) $raw_step['description']) : '', ); } if (!empty($clean_steps)) { $sanitized['settings']['steps'] = $clean_steps; } } } return $sanitized; } /** * WP Admin AJAX: Create or Update a Form on FormFork Platform (Enforces Edit Authority Lock & forms:write Scope) */ public function ajax_admin_save_form() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => esc_html__('Unauthorized', 'formfork-connect')), 403); } check_ajax_referer('formfork_admin_nonce', 'nonce'); $api_key = $this->get_api_key(); if (empty($api_key)) { wp_send_json_error(array( 'message' => esc_html__('Please save your FormFork Server API Key (ff_live_...) in FormFork -> Settings first.', 'formfork-connect'), ), 400); } $edit_id = isset($_POST['form_id']) ? sanitize_text_field(wp_unslash($_POST['form_id'])) : ''; // Decode raw JSON payload first before running sanitize_form_schema_payload so angle brackets inside labels or options do not break json_decode(). // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Every decoded schema property and field is explicitly sanitized immediately below via sanitize_form_schema_payload(). $raw_form = isset($_POST['form_payload']) ? (string) wp_unslash($_POST['form_payload']) : ''; $raw_decoded = json_decode($raw_form, true); if (!is_array($raw_decoded)) { wp_send_json_error(array('message' => esc_html__('Invalid form payload JSON.', 'formfork-connect')), 400); } $has_explicit_lock = isset($raw_decoded['settings']) && is_array($raw_decoded['settings']) && isset($raw_decoded['settings']['editLock']) && $raw_decoded['settings']['editLock'] !== ''; $decoded = $this->sanitize_form_schema_payload($raw_decoded); if (empty($decoded['name'])) { wp_send_json_error(array('message' => esc_html__('Form name and valid field configuration are required.', 'formfork-connect')), 400); } $sync_state = $this->fetch_workspace_forms(); if (!empty($sync_state['connected']) && isset($sync_state['canWriteForms']) && !$sync_state['canWriteForms']) { wp_send_json_error(array( 'message' => esc_html__('This API key is read-only for form schemas (missing forms:write permission). Connect an API key with forms:write scope in Settings or edit forms on the FormFork Web Dashboard.', 'formfork-connect'), ), 403); } $is_update = !empty($edit_id); $existing_form = null; if ($is_update) { if (!empty($sync_state['forms']) && is_array($sync_state['forms'])) { foreach ($sync_state['forms'] as $f) { if (isset($f['_id']) && $f['_id'] === $edit_id) { $existing_form = $f; break; } } } if ($existing_form && $this->get_form_edit_lock($existing_form) === 'dashboard_only') { wp_send_json_error(array( 'message' => esc_html__('This form is locked to the FormFork Web Dashboard (dashboard_only) and cannot be edited from WordPress.', 'formfork-connect'), ), 403); } } if (!isset($decoded['settings']) || !is_array($decoded['settings'])) { $decoded['settings'] = array(); } if ($is_update && !$has_explicit_lock && $existing_form) { $requested_lock = $this->get_form_edit_lock($existing_form); } else { $requested_lock = isset($decoded['settings']['editLock']) ? sanitize_key($decoded['settings']['editLock']) : 'unlocked'; if (!in_array($requested_lock, array('unlocked', 'wordpress_only', 'dashboard_only'), true)) { $requested_lock = 'unlocked'; } } $decoded['settings']['editLock'] = $requested_lock; $decoded['settings']['originDomain'] = $this->get_site_domain(); if (!$is_update || empty($existing_form['settings']['createdVia'])) { $decoded['settings']['createdVia'] = 'wordpress'; } else { $decoded['settings']['createdVia'] = sanitize_key($existing_form['settings']['createdVia']); } $endpoint = $is_update ? $this->get_platform_url() . '/api/v1/forms/' . rawurlencode($edit_id) : $this->get_platform_url() . '/api/v1/forms'; $response = wp_remote_request($endpoint, array( 'method' => $is_update ? 'PUT' : 'POST', 'timeout' => 20, 'headers' => array( 'Content-Type' => 'application/json', 'Accept' => 'application/json', 'X-API-Key' => $api_key, 'Origin' => home_url(), 'X-Site-Domain' => $this->get_site_domain(), ), 'body' => wp_json_encode($decoded), )); if (is_wp_error($response)) { wp_send_json_error(array('message' => $response->get_error_message()), 502); } $status_code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($status_code >= 200 && $status_code < 300 && !empty($body['success'])) { $this->flush_cached_transients(); wp_send_json_success($body['data']); } wp_send_json_error(array( 'message' => isset($body['error']) ? sanitize_text_field($body['error']) : esc_html__('Failed to save form on FormFork platform.', 'formfork-connect'), ), $status_code ? $status_code : 400); } /** * WP Admin AJAX: Onboarding Step 1 — Verify & Save API Key in 1 Click */ public function ajax_admin_onboarding_connect() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => esc_html__('Unauthorized', 'formfork-connect')), 403); } check_ajax_referer('formfork_admin_nonce', 'nonce'); $raw_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : ''; $platform_url = $this->get_platform_url(); $api_key = trim($raw_key); if (empty($api_key)) { wp_send_json_error(array( 'message' => esc_html__('Please enter your FormFork API Key (ff_live_...) to connect.', 'formfork-connect'), ), 400); } $site_domain = $this->get_site_domain(); $endpoint = $platform_url . '/api/v1/forms'; $response = wp_remote_get($endpoint, array( 'timeout' => 15, 'headers' => array( 'Accept' => 'application/json', 'X-API-Key' => $api_key, 'Origin' => home_url(), 'X-Site-Domain' => $site_domain, ), )); if (is_wp_error($response)) { wp_send_json_error(array( 'message' => $response->get_error_message(), ), 502); } $code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($code === 200 && !empty($body['success']) && isset($body['data']) && is_array($body['data'])) { update_option('formfork_api_key', $api_key); update_option('formfork_onboarding_dismissed', '1'); $this->flush_cached_transients(); $simple_forms = array(); foreach ($body['data'] as $f) { if (!empty($f['_id'])) { $simple_forms[] = array( 'id' => sanitize_text_field($f['_id']), 'name' => isset($f['name']) ? sanitize_text_field($f['name']) : esc_html__('Untitled Form', 'formfork-connect'), 'fields' => isset($f['fields']) && is_array($f['fields']) ? count($f['fields']) : 0, ); } } wp_send_json_success(array( 'formsCount' => count($simple_forms), 'forms' => $simple_forms, 'scoped' => !empty($body['scoped']), 'canWriteForms' => $this->extract_can_write_forms($body), 'scopes' => $this->extract_scopes_list($body), )); } $err_text = isset($body['error']) ? sanitize_text_field($body['error']) : esc_html__('Could not verify API key (HTTP ', 'formfork-connect') . intval($code) . ').'; wp_send_json_error(array('message' => $err_text), $code ? $code : 400); } /** * WP Admin AJAX: Onboarding Step 3 — Create a Draft WordPress Page with the Form Embedded */ public function ajax_admin_create_demo_page() { if (!current_user_can('publish_pages') && !current_user_can('manage_options')) { wp_send_json_error(array('message' => esc_html__('Unauthorized to create pages.', 'formfork-connect')), 403); } check_ajax_referer('formfork_admin_nonce', 'nonce'); $form_id = isset($_POST['form_id']) ? sanitize_text_field(wp_unslash($_POST['form_id'])) : ''; $form_name = isset($_POST['form_name']) ? sanitize_text_field(wp_unslash($_POST['form_name'])) : ''; if (empty($form_id)) { wp_send_json_error(array('message' => esc_html__('Missing Form ID.', 'formfork-connect')), 400); } $page_title = !empty($form_name) ? $form_name : esc_html__('FormFork Contact Page', 'formfork-connect'); // Insert a single shortcode block (works in both Gutenberg and Classic Editor without duplicate rendering) $page_content = sprintf( "\n[formfork id=\"%s\"]\n", esc_attr($form_id) ); $post_id = wp_insert_post(array( 'post_title' => $page_title, 'post_content' => $page_content, 'post_status' => 'draft', 'post_type' => 'page', ), true); if (is_wp_error($post_id)) { wp_send_json_error(array('message' => $post_id->get_error_message()), 500); } update_option('formfork_onboarding_completed', '1'); update_option('formfork_onboarding_dismissed', '1'); $edit_url = get_edit_post_link($post_id, 'raw'); $preview_url = get_preview_post_link($post_id); wp_send_json_success(array( 'pageId' => $post_id, 'pageTitle' => $page_title, 'editUrl' => $edit_url ? $edit_url : admin_url('post.php?post=' . intval($post_id) . '&action=edit'), 'previewUrl' => $preview_url ? $preview_url : home_url('?page_id=' . intval($post_id) . '&preview=true'), )); } /** * WP Admin AJAX: Dismiss Onboarding Notice / Wizard */ public function ajax_admin_dismiss_onboarding() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => esc_html__('Unauthorized', 'formfork-connect')), 403); } check_ajax_referer('formfork_admin_nonce', 'nonce'); update_option('formfork_onboarding_dismissed', '1'); update_option('formfork_onboarding_completed', '1'); wp_send_json_success(array('dismissed' => true)); } /** * Top-Level WordPress Admin Menu + 3 Submenus (All Forms, Form Builder, Settings) */ public function add_admin_menu() { add_menu_page( esc_html__('FormFork Forms', 'formfork-connect'), esc_html__('FormFork', 'formfork-connect'), 'manage_options', 'formfork-forms', array($this, 'render_forms_page'), 'dashicons-feedback', 26 ); add_submenu_page( 'formfork-forms', esc_html__('All Forms — FormFork', 'formfork-connect'), esc_html__('All Forms', 'formfork-connect'), 'manage_options', 'formfork-forms', array($this, 'render_forms_page') ); add_submenu_page( 'formfork-forms', esc_html__('Form Builder — FormFork', 'formfork-connect'), esc_html__('Form Builder', 'formfork-connect'), 'manage_options', 'formfork-builder', array($this, 'render_builder_page') ); add_submenu_page( 'formfork-forms', esc_html__('Settings — FormFork', 'formfork-connect'), esc_html__('Settings', 'formfork-connect'), 'manage_options', 'formfork-settings', array($this, 'render_settings_page') ); } public function register_settings() { register_setting('formfork_settings_group', 'formfork_api_key', array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '', )); } /** * Fetch Workspace Forms with 60-Second WordPress Transient Caching */ private function fetch_workspace_forms($force_refresh = false) { $api_key = $this->get_api_key(); if (empty($api_key)) { return array( 'connected' => false, 'forms' => array(), 'scoped' => false, 'scopeSummary' => null, 'canWriteForms' => false, 'scopes' => array(), 'cached' => false, 'error' => esc_html__('No Server API Key configured yet.', 'formfork-connect'), ); } $platform_url = $this->get_platform_url(); $cache_key = 'ff_forms_' . md5($platform_url . '|' . $api_key); if (!$force_refresh) { $cached = get_transient($cache_key); if (is_array($cached) && isset($cached['forms'])) { if (!isset($cached['canWriteForms'])) { $cached['canWriteForms'] = true; } if (!isset($cached['scopes']) || !is_array($cached['scopes'])) { $cached['scopes'] = array('*'); } $cached['cached'] = true; return $cached; } } $endpoint = $platform_url . '/api/v1/forms'; $response = wp_remote_get($endpoint, array( 'timeout' => 12, 'headers' => array( 'Accept' => 'application/json', 'X-API-Key' => $api_key, 'Origin' => home_url(), 'X-Site-Domain' => $this->get_site_domain(), ), )); if (is_wp_error($response)) { return array( 'connected' => false, 'forms' => array(), 'scoped' => false, 'scopeSummary' => null, 'canWriteForms' => false, 'scopes' => array(), 'cached' => false, 'error' => $response->get_error_message(), ); } $code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($code === 200 && !empty($body['success']) && isset($body['data']) && is_array($body['data'])) { $result = array( 'connected' => true, 'forms' => $body['data'], 'scoped' => !empty($body['scoped']), 'scopeSummary' => isset($body['scopeSummary']) && is_array($body['scopeSummary']) ? $body['scopeSummary'] : null, 'canWriteForms' => $this->extract_can_write_forms($body), 'scopes' => $this->extract_scopes_list($body), 'cached' => false, 'error' => '', ); $this->remember_transient_key($cache_key); set_transient($cache_key, $result, $this->cache_ttl); return $result; } return array( 'connected' => false, 'forms' => array(), 'scoped' => false, 'scopeSummary' => null, 'canWriteForms' => false, 'scopes' => array(), 'cached' => false, 'error' => isset($body['error']) ? sanitize_text_field($body['error']) : esc_html__('Could not authenticate with FormFork API (HTTP ', 'formfork-connect') . intval($code) . ').', ); } private function render_admin_header($active_tab, $sync_state) { $platform_url = $this->get_platform_url(); $forms_url = admin_url('admin.php?page=formfork-forms'); $builder_url = admin_url('admin.php?page=formfork-builder'); $settings_url = admin_url('admin.php?page=formfork-settings'); $connected = !empty($sync_state['connected']); $form_count = isset($sync_state['forms']) && is_array($sync_state['forms']) ? count($sync_state['forms']) : 0; $is_scoped = !empty($sync_state['scoped']); $can_write_forms = !isset($sync_state['canWriteForms']) || !empty($sync_state['canWriteForms']); $wizard_url = wp_nonce_url( admin_url('admin.php?page=formfork-forms&onboarding=1'), 'formfork_onboarding_wizard' ); ?>
version); ?> · get_site_domain()); ?> 🔒

✓ ( ) → +
⚡ ↗
Pick/Create Starter Form -> Embed on WP Page) */ private function render_onboarding_wizard($sync_state, $force_open = false) { $connected = !empty($sync_state['connected']); $can_write_forms = !isset($sync_state['canWriteForms']) || !empty($sync_state['canWriteForms']); $forms = isset($sync_state['forms']) && is_array($sync_state['forms']) ? $sync_state['forms'] : array(); $completed = get_option('formfork_onboarding_completed') === '1'; $should_open = $force_open || !$connected || (empty($forms) && !$completed); $initial_step = !$connected ? 1 : 2; $platform_url = $this->get_platform_url(); ?>

1
2
3

·

·

·

·

[formfork id="..."]
fetch_workspace_forms(); $builder_url = admin_url('admin.php?page=formfork-builder'); $settings_url = admin_url('admin.php?page=formfork-settings'); $can_write_forms = !isset($sync_state['canWriteForms']) || !empty($sync_state['canWriteForms']); $wizard_nonce_ok = isset($_GET['_wpnonce']) && wp_verify_nonce( sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'formfork_onboarding_wizard' ); $force_wizard = ($wizard_nonce_ok && isset($_GET['onboarding']) && sanitize_text_field(wp_unslash($_GET['onboarding'])) === '1'); ?>
render_admin_header('forms', $sync_state); ?>

render_onboarding_wizard($sync_state, $force_wizard); ?>

+
get_form_edit_lock($f); $created_via = isset($f['settings']['createdVia']) ? sanitize_key($f['settings']['createdVia']) : 'dashboard'; $native_sc = '[formfork id="' . $fid . '"]'; $iframe_sc = '[formfork id="' . $fid . '" mode="iframe"]'; $dashboard_url = $this->get_platform_url() . '/forms/' . rawurlencode($fid); $edit_link = wp_nonce_url( add_query_arg(array('page' => 'formfork-builder', 'edit_id' => $fid), admin_url('admin.php')), 'formfork_edit_form' ); ?>
🔒 🔒
1 ? esc_html__('steps', 'formfork-connect') : esc_html__('step', 'formfork-connect'); ?>
🔒 ↗ ↗
fetch_workspace_forms(); $can_write_forms = !isset($sync_state['canWriteForms']) || !empty($sync_state['canWriteForms']); $is_read_only = !empty($sync_state['connected']) && !$can_write_forms; $settings_url = admin_url('admin.php?page=formfork-settings'); $nonce_verified = isset($_GET['_wpnonce']) && wp_verify_nonce( sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'formfork_edit_form' ); $edit_id = ($nonce_verified && isset($_GET['edit_id'])) ? sanitize_text_field(wp_unslash($_GET['edit_id'])) : ''; $editing_form = null; if (!empty($edit_id) && !empty($sync_state['forms'])) { foreach ($sync_state['forms'] as $f) { if (isset($f['_id']) && $f['_id'] === $edit_id) { $editing_form = $f; break; } } } $is_locked_to_dashboard = $editing_form && $this->get_form_edit_lock($editing_form) === 'dashboard_only'; ?>
render_admin_header('builder', $sync_state); ?>

🔒

—

[formfork id="..."]



fetch_workspace_forms(); $platform_url = $this->get_platform_url(); $site_domain = $this->get_site_domain(); ?>
render_admin_header('settings', $sync_state); ?>

✓