handle_create_form(); } // Handle form update. if ( isset( $_POST['update_form'] ) ) { $this->handle_update_form(); } // Handle form duplication. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in duplicate_form() if ( isset( $_GET['action'] ) && 'duplicate' === $_GET['action'] && isset( $_GET['form_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in duplicate_form() $this->duplicate_form( absint( wp_unslash( $_GET['form_id'] ) ) ); } // Handle form deletion. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in delete_form() if ( isset( $_GET['action'] ) && 'delete' === $_GET['action'] && isset( $_GET['form_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in delete_form() $this->delete_form( absint( wp_unslash( $_GET['form_id'] ) ) ); } // Handle form status toggle. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in toggle_form_status() if ( isset( $_GET['action'] ) && 'toggle_status' === $_GET['action'] && isset( $_GET['form_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in toggle_form_status() $this->toggle_form_status( absint( wp_unslash( $_GET['form_id'] ) ) ); } // phpcs:enable } /** * Duplicate form * * @param int $form_id Form ID to duplicate. */ private function duplicate_form( $form_id ) { check_admin_referer( 'duplicate-form-' . $form_id ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'airy-frontend-forms' ) ); } $form_repo = new AFFCF_Form_Repository(); $original_form = $form_repo->get_by_id( $form_id ); if ( ! $original_form ) { wp_die( esc_html__( 'Form not found.', 'airy-frontend-forms' ) ); } // Create duplicate. $duplicate = clone $original_form; $duplicate->id = null; $duplicate->form_name = $original_form->form_name . ' (Copy)'; $duplicate->form_slug = sanitize_title( $duplicate->form_name ); $duplicate->shortcode = AFFCF_Helper::generate_unique_shortcode(); $duplicate->created_by = get_current_user_id(); $new_id = $form_repo->create( $duplicate ); if ( is_wp_error( $new_id ) ) { wp_die( esc_html( $new_id->get_error_message() ) ); } wp_safe_redirect( add_query_arg( 'duplicated', '1', admin_url( 'admin.php?page=affcf-forms' ) ) ); exit; } /** * Delete form * * @param int $form_id Form ID to delete. */ private function delete_form( $form_id ) { check_admin_referer( 'delete-form-' . $form_id ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'airy-frontend-forms' ) ); } $form_repo = new AFFCF_Form_Repository(); $result = $form_repo->delete( $form_id ); if ( ! $result ) { wp_die( esc_html__( 'Failed to delete form.', 'airy-frontend-forms' ) ); } wp_safe_redirect( add_query_arg( 'deleted', '1', admin_url( 'admin.php?page=affcf-forms' ) ) ); exit; } /** * Toggle form status * * @param int $form_id Form ID to toggle. */ private function toggle_form_status( $form_id ) { check_admin_referer( 'toggle-status-' . $form_id ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'airy-frontend-forms' ) ); } global $wpdb; $table = $wpdb->prefix . 'affcf_forms'; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, plugin-controlled $current_status = $wpdb->get_var( $wpdb->prepare( "SELECT status FROM {$table} WHERE id = %d", $form_id ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter $new_status = ( 'active' === $current_status ) ? 'inactive' : 'active'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update, no WP API available $wpdb->update( $table, array( 'status' => $new_status ), array( 'id' => $form_id ), array( '%s' ), array( '%d' ) ); wp_safe_redirect( add_query_arg( 'status_changed', '1', admin_url( 'admin.php?page=affcf-forms' ) ) ); exit; } /** * Handle create form submission */ private function handle_create_form() { check_admin_referer( 'affcf_create_form', 'affcf_form_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to create forms.', 'airy-frontend-forms' ) ); } $form_repo = new AFFCF_Form_Repository(); // Sanitize basic fields. $form_name = isset( $_POST['form_name'] ) ? sanitize_text_field( wp_unslash( $_POST['form_name'] ) ) : ''; $post_type = isset( $_POST['form_post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['form_post_type'] ) ) : ''; $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : 'active'; // Validate required fields. if ( empty( $form_name ) ) { wp_safe_redirect( add_query_arg( array( 'page' => 'affcf-add-form', 'error' => rawurlencode( __( 'Form name is required.', 'airy-frontend-forms' ) ), ), admin_url( 'admin.php' ) ) ); exit; } if ( empty( $post_type ) ) { wp_safe_redirect( add_query_arg( array( 'page' => 'affcf-add-form', 'error' => rawurlencode( __( 'Post type is required.', 'airy-frontend-forms' ) ), ), admin_url( 'admin.php' ) ) ); exit; } // Parse JSON data from hidden inputs. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, sanitized via sanitize_form_config() immediately after parsing $field_groups = $this->parse_json_input( isset( $_POST['field_groups_json'] ) ? wp_unslash( $_POST['field_groups_json'] ) : '' ); $field_groups = $this->sanitize_form_config( $field_groups, 'field_groups' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, sanitized via sanitize_form_config() immediately after parsing $default_fields = $this->parse_json_input( isset( $_POST['default_fields_json'] ) ? wp_unslash( $_POST['default_fields_json'] ) : '' ); $default_fields = $this->sanitize_form_config( $default_fields, 'default_fields' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, sanitized via sanitize_form_config() immediately after parsing $taxonomies = $this->parse_json_input( isset( $_POST['taxonomies_json'] ) ? wp_unslash( $_POST['taxonomies_json'] ) : '' ); $taxonomies = $this->sanitize_form_config( $taxonomies, 'taxonomies' ); // Validate that at least one field is configured. $has_fields = ! empty( $field_groups ) || ! empty( $default_fields ); if ( ! $has_fields ) { wp_safe_redirect( add_query_arg( array( 'page' => 'affcf-add-form', 'error' => rawurlencode( __( 'Please configure at least one field for the form.', 'airy-frontend-forms' ) ), ), admin_url( 'admin.php' ) ) ); exit; } // Generate unique slug. $form_slug = sanitize_title( $form_name ); $form_slug = $this->ensure_unique_slug( $form_slug ); // Generate unique shortcode. $shortcode = $form_repo->generate_next_shortcode(); // Create form model. $form = new AFFCF_Form_Model( array( 'form_name' => $form_name, 'form_slug' => $form_slug, 'shortcode' => $shortcode, 'post_type' => $post_type, 'status' => $status, 'field_groups' => $field_groups, 'default_fields' => $default_fields, 'taxonomies' => $taxonomies, 'settings' => array(), 'created_by' => get_current_user_id(), ) ); // Save form. $form_id = $form_repo->create( $form ); if ( is_wp_error( $form_id ) ) { wp_safe_redirect( add_query_arg( array( 'page' => 'affcf-add-form', 'error' => rawurlencode( $form_id->get_error_message() ), ), admin_url( 'admin.php' ) ) ); exit; } // Update submit page with form shortcode if this is the first form. $this->maybe_update_submit_page( $form_id ); // Redirect to forms list with success message. wp_safe_redirect( add_query_arg( array( 'page' => 'affcf-forms', 'created' => 1, 'form_id' => $form_id, ), admin_url( 'admin.php' ) ) ); exit; } /** * Handle update form submission */ private function handle_update_form() { check_admin_referer( 'affcf_update_form', 'affcf_form_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to update forms.', 'airy-frontend-forms' ) ); } $form_id = isset( $_POST['form_id'] ) ? absint( wp_unslash( $_POST['form_id'] ) ) : 0; $form_repo = new AFFCF_Form_Repository(); // Get existing form. $existing_form = $form_repo->get_by_id( $form_id ); if ( ! $existing_form ) { wp_die( esc_html__( 'Form not found.', 'airy-frontend-forms' ) ); } // Sanitize basic fields. $form_name = isset( $_POST['form_name'] ) ? sanitize_text_field( wp_unslash( $_POST['form_name'] ) ) : ''; $post_type = isset( $_POST['form_post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['form_post_type'] ) ) : ''; $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : 'active'; // Validate required fields. if ( empty( $form_name ) ) { wp_safe_redirect( add_query_arg( array( 'page' => 'affcf-add-form', 'action' => 'edit', 'form_id' => $form_id, 'error' => rawurlencode( __( 'Form name is required.', 'airy-frontend-forms' ) ), ), admin_url( 'admin.php' ) ) ); exit; } if ( empty( $post_type ) ) { wp_safe_redirect( add_query_arg( array( 'page' => 'affcf-add-form', 'action' => 'edit', 'form_id' => $form_id, 'error' => rawurlencode( __( 'Post type is required.', 'airy-frontend-forms' ) ), ), admin_url( 'admin.php' ) ) ); exit; } // Parse JSON data from hidden inputs. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, sanitized via sanitize_form_config() immediately after parsing $field_groups = $this->parse_json_input( isset( $_POST['field_groups_json'] ) ? wp_unslash( $_POST['field_groups_json'] ) : '' ); $field_groups = $this->sanitize_form_config( $field_groups, 'field_groups' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, sanitized via sanitize_form_config() immediately after parsing $default_fields = $this->parse_json_input( isset( $_POST['default_fields_json'] ) ? wp_unslash( $_POST['default_fields_json'] ) : '' ); $default_fields = $this->sanitize_form_config( $default_fields, 'default_fields' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, sanitized via sanitize_form_config() immediately after parsing $taxonomies = $this->parse_json_input( isset( $_POST['taxonomies_json'] ) ? wp_unslash( $_POST['taxonomies_json'] ) : '' ); $taxonomies = $this->sanitize_form_config( $taxonomies, 'taxonomies' ); // Validate that at least one field is configured. $has_fields = ! empty( $field_groups ) || ! empty( $default_fields ); if ( ! $has_fields ) { wp_safe_redirect( add_query_arg( array( 'page' => 'affcf-add-form', 'action' => 'edit', 'form_id' => $form_id, 'error' => rawurlencode( __( 'Please configure at least one field for the form.', 'airy-frontend-forms' ) ), ), admin_url( 'admin.php' ) ) ); exit; } // Generate slug (may need to ensure uniqueness if name changed). $form_slug = sanitize_title( $form_name ); if ( $form_slug !== $existing_form->form_slug ) { $form_slug = $this->ensure_unique_slug( $form_slug, $form_id ); } // Update form model. $form = new AFFCF_Form_Model( array( 'form_name' => $form_name, 'form_slug' => $form_slug, 'shortcode' => $existing_form->shortcode, // Keep existing shortcode. 'post_type' => $post_type, 'status' => $status, 'field_groups' => $field_groups, 'default_fields' => $default_fields, 'taxonomies' => $taxonomies, 'settings' => array(), 'created_by' => $existing_form->created_by, ) ); // Update form. $result = $form_repo->update( $form_id, $form ); if ( ! $result ) { wp_safe_redirect( add_query_arg( array( 'page' => 'affcf-add-form', 'action' => 'edit', 'form_id' => $form_id, 'error' => rawurlencode( __( 'Failed to update form.', 'airy-frontend-forms' ) ), ), admin_url( 'admin.php' ) ) ); exit; } // Redirect to forms list with success message. wp_safe_redirect( add_query_arg( array( 'page' => 'affcf-forms', 'updated' => 1, 'form_id' => $form_id, ), admin_url( 'admin.php' ) ) ); exit; } /** * Sanitize field groups data * * @param array $field_groups Field groups data to sanitize. * @return array Sanitized field groups. */ private function sanitize_field_groups( $field_groups ) { if ( ! is_array( $field_groups ) ) { return array(); } $sanitized = array(); foreach ( $field_groups as $group ) { if ( ! is_array( $group ) ) { continue; } $sanitized[] = array( 'group_id' => sanitize_text_field( $group['group_id'] ?? '' ), 'enabled' => ! empty( $group['enabled'] ), 'fields' => isset( $group['fields'] ) && is_array( $group['fields'] ) ? array_map( 'sanitize_text_field', $group['fields'] ) : array(), ); } return $sanitized; } /** * Sanitize default fields data * * @param array $default_fields Default fields data to sanitize. * @return array Sanitized default fields. */ private function sanitize_default_fields( $default_fields ) { if ( ! is_array( $default_fields ) ) { return array(); } $sanitized = array(); foreach ( $default_fields as $field_name => $field_config ) { if ( ! is_array( $field_config ) ) { continue; } $sanitized[ $field_name ] = array( 'enabled' => ! empty( $field_config['enabled'] ), 'label' => sanitize_text_field( $field_config['label'] ?? '' ), 'required' => ! empty( $field_config['required'] ), 'placeholder' => sanitize_text_field( $field_config['placeholder'] ?? '' ), ); } return $sanitized; } /** * Sanitize taxonomies data * * @param array $taxonomies Taxonomies data to sanitize. * @return array Sanitized taxonomies. */ private function sanitize_taxonomies( $taxonomies ) { if ( ! is_array( $taxonomies ) ) { return array(); } $sanitized = array(); foreach ( $taxonomies as $taxonomy ) { if ( ! is_array( $taxonomy ) ) { continue; } $sanitized[] = array( 'taxonomy' => sanitize_text_field( $taxonomy['taxonomy'] ?? '' ), 'enabled' => ! empty( $taxonomy['enabled'] ), 'label' => sanitize_text_field( $taxonomy['label'] ?? '' ), 'display_type' => sanitize_text_field( $taxonomy['display_type'] ?? 'checkbox' ), 'required' => ! empty( $taxonomy['required'] ), 'allow_new' => ! empty( $taxonomy['allow_new'] ), 'help_text' => sanitize_textarea_field( $taxonomy['help_text'] ?? '' ), ); } return $sanitized; } /** * Sanitize form settings * * @param array $settings Form settings to sanitize. * @return array Sanitized settings. */ private function sanitize_form_settings( $settings ) { if ( ! is_array( $settings ) ) { return array(); } return array( 'submit_button_text' => sanitize_text_field( $settings['submit_button_text'] ?? 'Submit' ), 'success_message' => sanitize_textarea_field( $settings['success_message'] ?? '' ), 'redirect_url' => esc_url_raw( $settings['redirect_url'] ?? '' ), ); } /** * Enqueue admin assets * * @param string $hook Current admin page hook. */ public function enqueue_assets( $hook ) { if ( strpos( $hook, 'affcf' ) === false ) { return; } // Global admin CSS. wp_enqueue_style( 'affcf-admin', AFFCF_PLUGIN_URL . 'assets/css/admin/admin-global.css', array(), AFFCF_VERSION ); // Global admin JS. wp_enqueue_script( 'affcf-admin', AFFCF_PLUGIN_URL . 'assets/js/admin/admin-global.js', array( 'jquery' ), AFFCF_VERSION, true ); // Localize script. wp_localize_script( 'affcf-admin', 'affcfAdmin', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'affcf_admin_nonce' ), 'strings' => array( 'loading' => __( 'Loading...', 'airy-frontend-forms' ), 'saved' => __( 'Form saved successfully!', 'airy-frontend-forms' ), 'error' => __( 'Error saving form.', 'airy-frontend-forms' ), 'confirmDelete' => __( 'Are you sure you want to delete this form?', 'airy-frontend-forms' ), ), ) ); // Add inline CSS for forms list page. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for conditional asset loading only if ( isset( $_GET['page'] ) && 'affcf-forms' === $_GET['page'] && ! isset( $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $forms_list_css = ' .affcf-empty-state { text-align: center; padding: 80px 20px; } .affcf-empty-state-icon .dashicons { font-size: 120px; width: 120px; height: 120px; color: #ddd; } .affcf-badge { display: inline-block; padding: 4px 10px; border-radius: 3px; font-size: 12px; font-weight: 600; } .affcf-badge-success { background: #d4edda; color: #155724; } .affcf-badge-inactive { background: #f8d7da; color: #721c24; } .affcf-copy-shortcode { margin-left: 8px; vertical-align: middle; } .affcf-copy-shortcode .dashicons { font-size: 14px; width: 14px; height: 14px; margin-right: 4px; }'; wp_add_inline_style( 'affcf-admin', $forms_list_css ); } // Add inline CSS for submissions list page. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for conditional asset loading only if ( isset( $_GET['page'] ) && 'affcf-submissions' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $submissions_list_css = ' .affcf-stat-box { box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .affcf-badge { display: inline-block; font-weight: 600; }'; wp_add_inline_style( 'affcf-admin', $submissions_list_css ); } // Form Builder specific assets. // Load on: Add New Form page OR Edit Form page. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameters for conditional asset loading only $is_form_builder_page = ( 'airy-forms_page_affcf-add-form' === $hook || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for conditional asset loading only ( isset( $_GET['page'] ) && 'affcf-add-form' === $_GET['page'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameters for conditional asset loading only ( isset( $_GET['page'] ) && 'affcf-forms' === $_GET['page'] && isset( $_GET['action'] ) && 'edit' === $_GET['action'] ) ); if ( $is_form_builder_page ) { // Form Builder CSS. wp_enqueue_style( 'affcf-form-builder', AFFCF_PLUGIN_URL . 'assets/css/admin/form-builder.css', array( 'affcf-admin' ), AFFCF_VERSION ); // Form Builder JS. wp_enqueue_script( 'affcf-form-builder', AFFCF_PLUGIN_URL . 'assets/js/admin/form-builder.js', array( 'jquery', 'jquery-ui-sortable', 'affcf-admin' ), AFFCF_VERSION, true ); // Localize Form Builder. wp_localize_script( 'affcf-form-builder', 'affcfBuilder', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'affcf_admin_nonce' ), 'strings' => array( 'loading' => __( 'Loading...', 'airy-frontend-forms' ), 'loadingFields' => __( 'Loading fields...', 'airy-frontend-forms' ), 'noFields' => __( 'No field groups found', 'airy-frontend-forms' ), 'selectPostType' => __( 'Please select a post type first', 'airy-frontend-forms' ), 'copied' => __( 'Copied!', 'airy-frontend-forms' ), ), ) ); // Add inline script for form-builder page functionality. $inline_script = " jQuery(document).ready(function($) { // Copy shortcode button functionality (edit page). $('.affcf-copy-shortcode').on('click', function() { var shortcode = $(this).data('shortcode'); var \$button = $(this); navigator.clipboard.writeText(shortcode).then(function() { var originalText = \$button.text(); \$button.text(affcfBuilder.strings.copied); \$button.addClass('button-primary'); setTimeout(function() { \$button.text(originalText); \$button.removeClass('button-primary'); }, 2000); }); }); // Show/hide field groups and taxonomies sections when post type changes (add page). $('#form_post_type').on('change', function() { if ($(this).val()) { $('#field-groups-section, #taxonomies-section').slideDown(300); } else { $('#field-groups-section, #taxonomies-section').slideUp(300); } }); // Initialize edit mode - trigger change to load field groups. if ($('#form_post_type').val()) { $('#form_post_type').trigger('change'); } }); "; wp_add_inline_script( 'affcf-form-builder', $inline_script ); } } /** * AJAX: Save form */ public function ajax_save_form() { check_ajax_referer( 'affcf_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'airy-frontend-forms' ) ) ); } $form_repo = new AFFCF_Form_Repository(); // Get form ID (0 for new form). $form_id = isset( $_POST['form_id'] ) ? absint( wp_unslash( $_POST['form_id'] ) ) : 0; // Sanitize basic fields. $form_name = isset( $_POST['form_name'] ) ? sanitize_text_field( wp_unslash( $_POST['form_name'] ) ) : ''; $post_type = isset( $_POST['post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['post_type'] ) ) : ''; $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : 'active'; // Validate required fields. if ( empty( $form_name ) ) { wp_send_json_error( array( 'message' => __( 'Form name is required.', 'airy-frontend-forms' ) ) ); } if ( empty( $post_type ) ) { wp_send_json_error( array( 'message' => __( 'Post type is required.', 'airy-frontend-forms' ) ) ); } // Parse JSON data from hidden inputs and sanitize. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, sanitized via sanitize_form_config() immediately after parsing $field_groups = $this->parse_json_input( isset( $_POST['field_groups_json'] ) ? wp_unslash( $_POST['field_groups_json'] ) : '' ); $field_groups = $this->sanitize_form_config( $field_groups, 'field_groups' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, sanitized via sanitize_form_config() immediately after parsing $default_fields = $this->parse_json_input( isset( $_POST['default_fields_json'] ) ? wp_unslash( $_POST['default_fields_json'] ) : '' ); $default_fields = $this->sanitize_form_config( $default_fields, 'default_fields' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data, sanitized via sanitize_form_config() immediately after parsing $taxonomies = $this->parse_json_input( isset( $_POST['taxonomies_json'] ) ? wp_unslash( $_POST['taxonomies_json'] ) : '' ); $taxonomies = $this->sanitize_form_config( $taxonomies, 'taxonomies' ); // Update existing form. if ( $form_id > 0 ) { $form = $form_repo->get_by_id( $form_id ); if ( ! $form ) { wp_send_json_error( array( 'message' => __( 'Form not found.', 'airy-frontend-forms' ) ) ); } // Update form properties. $form->form_name = $form_name; $form->post_type = $post_type; $form->status = $status; $form->field_groups = $field_groups; $form->default_fields = $default_fields; $form->taxonomies = $taxonomies; $result = $form_repo->update( $form_id, $form ); if ( ! $result ) { wp_send_json_error( array( 'message' => __( 'Failed to update form.', 'airy-frontend-forms' ) ) ); } wp_send_json_success( array( 'message' => __( 'Form updated successfully!', 'airy-frontend-forms' ), 'form_id' => $form_id, ) ); } else { // Create new form. // Generate unique slug. $form_slug = sanitize_title( $form_name ); $form_slug = $this->ensure_unique_slug( $form_slug ); // Generate unique shortcode. $shortcode = $form_repo->generate_next_shortcode(); // Create form model - v1.8.8 style (pass data as-is). $form = new AFFCF_Form_Model( array( 'form_name' => $form_name, 'form_slug' => $form_slug, 'shortcode' => $shortcode, 'post_type' => $post_type, 'status' => $status, 'field_groups' => $field_groups, 'default_fields' => $default_fields, 'taxonomies' => $taxonomies, 'settings' => array(), 'created_by' => get_current_user_id(), ) ); // Save form. $new_form_id = $form_repo->create( $form ); if ( is_wp_error( $new_form_id ) ) { wp_send_json_error( array( 'message' => $new_form_id->get_error_message() ) ); } wp_send_json_success( array( 'message' => __( 'Form created successfully!', 'airy-frontend-forms' ), 'form_id' => $new_form_id, 'redirect_url' => add_query_arg( array( 'page' => 'affcf-forms', 'created' => 1, ), admin_url( 'admin.php' ) ), ) ); } } /** * AJAX: Delete form */ public function ajax_delete_form() { check_ajax_referer( 'affcf_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'airy-frontend-forms' ) ) ); } $form_id = isset( $_POST['form_id'] ) ? absint( wp_unslash( $_POST['form_id'] ) ) : 0; if ( ! $form_id ) { wp_send_json_error( array( 'message' => __( 'Invalid form ID.', 'airy-frontend-forms' ) ) ); } $form_repo = new AFFCF_Form_Repository(); $result = $form_repo->delete( $form_id ); if ( $result ) { wp_send_json_success( array( 'message' => __( 'Form deleted.', 'airy-frontend-forms' ) ) ); } else { wp_send_json_error( array( 'message' => __( 'Failed to delete form.', 'airy-frontend-forms' ) ) ); } } /** * AJAX: Get field groups for post type */ public function ajax_get_field_groups() { check_ajax_referer( 'affcf_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'airy-frontend-forms' ) ) ); } $post_type = isset( $_POST['post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['post_type'] ) ) : ''; if ( ! $post_type ) { wp_send_json_error( array( 'message' => __( 'Invalid post type.', 'airy-frontend-forms' ) ) ); } $fm = AFFCF_Field_Manager::get_instance(); // Check if field manager is available. if ( ! $fm->is_available() ) { wp_send_json_error( array( 'message' => __( 'No field plugin (ACF or SCF) detected. Please install and activate ACF or SCF.', 'airy-frontend-forms' ), ) ); } try { $field_groups = $fm->get_field_groups( $post_type ); $taxonomies = $fm->get_taxonomies( $post_type ); wp_send_json_success( array( 'field_groups' => $field_groups, 'taxonomies' => $taxonomies, 'debug' => array( 'plugin_type' => $fm->get_plugin_type(), 'post_type' => $post_type, 'field_groups_count' => count( $field_groups ), 'taxonomies_count' => count( $taxonomies ), ), ) ); } catch ( Exception $e ) { wp_send_json_error( array( 'message' => sprintf( /* translators: %s: The specific error message returned when field groups fail to load */ __( 'Error loading field groups: %s', 'airy-frontend-forms' ), $e->getMessage() ), ) ); } } /** * AJAX: Load field groups for post type (Form Builder) */ public function ajax_load_field_groups() { check_ajax_referer( 'affcf_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'airy-frontend-forms' ) ) ); } $post_type = isset( $_POST['post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['post_type'] ) ) : ''; if ( ! $post_type ) { wp_send_json_error( array( 'message' => __( 'Invalid post type.', 'airy-frontend-forms' ) ) ); } $fm = AFFCF_Field_Manager::get_instance(); if ( ! $fm->is_available() ) { wp_send_json_error( array( 'message' => __( 'No field plugin (ACF/SCF) detected.', 'airy-frontend-forms' ) ) ); } // Get field groups. $field_groups = $fm->get_field_groups( $post_type ); wp_send_json_success( $field_groups ); } /** * AJAX: Load taxonomies for post type (Form Builder) */ public function ajax_load_taxonomies() { check_ajax_referer( 'affcf_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'airy-frontend-forms' ) ) ); } $post_type = isset( $_POST['post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['post_type'] ) ) : ''; if ( ! $post_type ) { wp_send_json_error( array( 'message' => __( 'Invalid post type.', 'airy-frontend-forms' ) ) ); } $fm = AFFCF_Field_Manager::get_instance(); $taxonomies = $fm->get_taxonomies( $post_type ); wp_send_json_success( $taxonomies ); } /** * AJAX: Get taxonomies (called by form-builder.js) */ public function ajax_get_taxonomies() { check_ajax_referer( 'affcf_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'airy-frontend-forms' ) ) ); } $post_type = isset( $_POST['post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['post_type'] ) ) : ''; if ( ! $post_type ) { wp_send_json_error( array( 'message' => __( 'Invalid post type.', 'airy-frontend-forms' ) ) ); } $fm = AFFCF_Field_Manager::get_instance(); $taxonomies = $fm->get_taxonomies( $post_type ); wp_send_json_success( $taxonomies ); } /** * Sanitize form configuration data * * @param mixed $data Data to sanitize. * @param string $type Type of data (field_groups, default_fields, taxonomies). * @return array Sanitized data. */ private function sanitize_form_config( $data, $type ) { if ( ! is_array( $data ) ) { return array(); } // Recursively sanitize nested arrays. $sanitized = array(); foreach ( $data as $key => $value ) { $sanitized_key = sanitize_key( $key ); if ( is_array( $value ) ) { // Recursively sanitize nested arrays. $sanitized[ $sanitized_key ] = $this->sanitize_form_config( $value, $type ); } elseif ( is_bool( $value ) ) { // Preserve boolean values. $sanitized[ $sanitized_key ] = (bool) $value; } elseif ( is_numeric( $value ) ) { // Preserve numeric values. $sanitized[ $sanitized_key ] = $value; } else { // Sanitize text values. $sanitized[ $sanitized_key ] = sanitize_text_field( $value ); } } return $sanitized; } /** * Parse JSON input * * @param string $json JSON string. * @return array Parsed array or empty array on error. */ private function parse_json_input( $json ) { if ( empty( $json ) ) { return array(); } // Decode JSON (wp_unslash should be done before calling this method). $data = json_decode( $json, true ); // Check for errors. if ( JSON_ERROR_NONE !== json_last_error() ) { return array(); } return is_array( $data ) ? $data : array(); } /** * Ensure form slug is unique * * @param string $slug Base slug. * @param int $exclude_id Form ID to exclude from check (for updates). * @return string Unique slug. */ private function ensure_unique_slug( $slug, $exclude_id = 0 ) { global $wpdb; $table_name = $wpdb->prefix . 'affcf_forms'; $original_slug = $slug; $counter = 1; while ( true ) { // Check if slug exists (excluding current form if updating). if ( $exclude_id ) { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, plugin-controlled $exists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE form_slug = %s AND id != %d", $slug, $exclude_id ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter } else { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix, plugin-controlled $exists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE form_slug = %s", $slug ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter } if ( ! $exists ) { break; } // Append counter and try again. $slug = $original_slug . '-' . $counter; ++$counter; } return $slug; } /** * Update submit page with form shortcode if this is the first form * * @param int $form_id Form ID. */ private function maybe_update_submit_page( $form_id ) { // Get default pages. $pages = get_option( 'affcf_default_pages', array() ); if ( empty( $pages['submit_page_id'] ) ) { return; // No submit page configured. } // Check if page still has the placeholder text. $page = get_post( $pages['submit_page_id'] ); if ( ! $page ) { return; // Page doesn't exist. } $placeholder_text = 'Please create a form first in Airy Forms settings.'; // Only update if it still has the placeholder. if ( strpos( $page->post_content, $placeholder_text ) !== false ) { // Update page content with form shortcode. wp_update_post( array( 'ID' => $pages['submit_page_id'], 'post_content' => '[affcf_form id="' . absint( $form_id ) . '"]', ) ); } } }