' . 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(
'',
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 '';
echo ' ';
echo esc_html__('Add FormFork Form', 'formfork-connect');
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'
);
?>
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();
?>
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');
?>
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); ?>
fetch_workspace_forms();
$platform_url = $this->get_platform_url();
$site_domain = $this->get_site_domain();
?>
render_admin_header('settings', $sync_state); ?>