form_is_rendering = true; $atts = shortcode_atts( array( 'id' => 0, ), $atts, 'affcf_form' ); $form_id = absint( $atts['id'] ); // Check if loading a draft via URL parameter. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameter to load draft for display, no action performed $draft_id = isset( $_GET['draft_id'] ) ? absint( wp_unslash( $_GET['draft_id'] ) ) : 0; if ( $draft_id ) { // Load draft from database. global $wpdb; $drafts_table = $wpdb->prefix . 'affcf_drafts'; // 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 $draft = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$drafts_table} WHERE id = %d AND user_id = %d", $draft_id, get_current_user_id() ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter if ( ! $draft ) { return '
' . esc_html__( 'Draft not found or you do not have permission to edit it.', 'airy-frontend-forms' ) . '
'; } // Use form_id from draft (override shortcode parameter). $form_id = absint( $draft->form_id ); } if ( ! $form_id ) { return '
' . esc_html__( 'Form ID is required.', 'airy-frontend-forms' ) . '
'; } // Get form from database. $form_repo = new AFFCF_Form_Repository(); $form = $form_repo->get_by_id( $form_id ); if ( ! $form ) { return '
' . esc_html__( 'Form not found.', 'airy-frontend-forms' ) . '
'; } // Check if form is active. if ( 'active' !== $form->status ) { return '
' . esc_html__( 'This form is currently inactive.', 'airy-frontend-forms' ) . '
'; } // Check if ACF/SCF is available. if ( ! AFFCF_Plugin_Checker::has_field_plugin() ) { return '
' . esc_html__( 'Required field plugin (ACF or SCF) is not active.', 'airy-frontend-forms' ) . '
'; } // Render the form (pass draft_id if available). $renderer = new AFFCF_Form_Renderer( $form ); return $renderer->render( $draft_id ); } /** * Render dashboard shortcode. * * @param array $atts Shortcode attributes (unused but required by shortcode API). * @return string Rendered dashboard HTML. */ public function render_dashboard_shortcode( $atts ) { // Set flag that dashboard is rendering (for page builder compatibility). $this->dashboard_is_rendering = true; // Suppress unused parameter warning - required by WordPress shortcode API. unset( $atts ); if ( ! is_user_logged_in() ) { return '
' . esc_html__( 'Please log in to view your dashboard.', 'airy-frontend-forms' ) . '
'; } // Render user dashboard. $dashboard = new AFFCF_User_Dashboard(); return $dashboard->render(); } /** * Render edit form shortcode. * * @param array $atts Shortcode attributes (unused but required by shortcode API). * @return string Rendered edit form HTML. */ public function render_edit_form_shortcode( $atts ) { // Set flag that edit form is rendering (for page builder compatibility). $this->edit_form_is_rendering = true; // Suppress unused parameter warning - required by WordPress shortcode API. unset( $atts ); if ( ! is_user_logged_in() ) { return '
' . esc_html__( 'Please log in to edit posts.', 'airy-frontend-forms' ) . '
'; } // Get post ID from URL parameter. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameter to load post for editing, nonce verified during form submission $post_id = isset( $_GET['post_id'] ) ? absint( wp_unslash( $_GET['post_id'] ) ) : 0; if ( ! $post_id ) { return '
' . esc_html__( 'No post specified for editing.', 'airy-frontend-forms' ) . '
'; } // Get the post. $post = get_post( $post_id ); if ( ! $post ) { return '
' . esc_html__( 'Post not found.', 'airy-frontend-forms' ) . '
'; } // Verify ownership. if ( get_current_user_id() !== (int) $post->post_author ) { return '
' . esc_html__( 'You do not have permission to edit this post.', 'airy-frontend-forms' ) . '
'; } // Get the form ID that was used to create this post. $form_id = get_post_meta( $post_id, '_affcf_form_id', true ); if ( ! $form_id ) { return '
' . esc_html__( 'This post was not created with Airy Frontend Forms.', 'airy-frontend-forms' ) . '
'; } // Get the form. $form_repo = new AFFCF_Form_Repository(); $form = $form_repo->get_by_id( $form_id ); if ( ! $form ) { return '
' . esc_html__( 'Form not found.', 'airy-frontend-forms' ) . '
'; } // Check if form's post type matches the post. if ( $form->post_type !== $post->post_type ) { return '
' . esc_html__( 'Form post type mismatch.', 'airy-frontend-forms' ) . '
'; } // Check if ACF or SCF is active. $field_manager = AFFCF_Field_Manager::get_instance(); if ( ! $field_manager->is_available() ) { return '
' . esc_html__( 'Required field plugin (ACF or SCF) is not active.', 'airy-frontend-forms' ) . '
'; } // Render the form in EDIT MODE. $renderer = new AFFCF_Form_Renderer( $form, $post_id ); return $renderer->render(); } /** * Enqueue frontend assets. * * @return void */ public function enqueue_assets() { global $post; // Check if any of our shortcodes are present. $has_form = is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, 'affcf_form' ); $has_dashboard = is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, 'affcf_dashboard' ); $has_edit_form = is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, 'affcf_edit_form' ); if ( ! $has_form && ! $has_dashboard && ! $has_edit_form ) { return; } // Enqueue form styles and scripts if form shortcode present. if ( $has_form || $has_edit_form ) { // Load WordPress Media Library for image/gallery fields. wp_enqueue_media(); // CSS. wp_enqueue_style( 'dashicons' ); wp_enqueue_style( 'affcf-frontend', AFFCF_PLUGIN_URL . 'assets/css/frontend/form-styles.css', array( 'dashicons' ), AFFCF_VERSION ); // JavaScript. wp_enqueue_script( 'affcf-form-handler', AFFCF_PLUGIN_URL . 'assets/js/frontend/form-handler.js', array( 'jquery' ), AFFCF_VERSION, true ); } // ALWAYS load auto-save script if ANY shortcode is present (form, dashboard, or edit). if ( $has_form || $has_edit_form || $has_dashboard ) { wp_enqueue_script( 'affcf-auto-save', AFFCF_PLUGIN_URL . 'assets/js/frontend/auto-save.js', array( 'jquery' ), AFFCF_VERSION, true ); } // Load reCAPTCHA if needed. if ( ( $has_form || $has_edit_form ) && AFFCF_Captcha::is_enabled() ) { $site_key = AFFCF_Captcha::get_site_key(); wp_enqueue_script( 'google-recaptcha', 'https://www.google.com/recaptcha/api.js?render=' . $site_key, array(), null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- External Google reCAPTCHA script, version controlled by Google true ); } // Enqueue dashboard styles and scripts if dashboard shortcode present. if ( $has_dashboard ) { wp_enqueue_style( 'affcf-dashboard', AFFCF_PLUGIN_URL . 'assets/css/frontend/user-dashboard.css', array(), AFFCF_VERSION ); wp_enqueue_script( 'affcf-dashboard', AFFCF_PLUGIN_URL . 'assets/js/frontend/user-dashboard.js', array( 'jquery' ), AFFCF_VERSION, true ); } // Get settings. $settings = get_option( 'affcf_settings', array() ); // Localize script. $localize_data = array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'affcf_frontend_nonce' ), 'autoSaveEnabled' => true, // Force true for debugging. 'autoSaveInterval' => 10, 'strings' => array( // Form submission strings. 'submitting' => __( 'Submitting...', 'airy-frontend-forms' ), 'success' => __( 'Form submitted successfully!', 'airy-frontend-forms' ), 'error' => __( 'An error occurred. Please try again.', 'airy-frontend-forms' ), // Auto-save strings. 'saving' => __( 'Saving draft...', 'airy-frontend-forms' ), 'saved' => __( 'Draft saved', 'airy-frontend-forms' ), 'saveError' => __( 'Failed to save draft', 'airy-frontend-forms' ), 'autosave' => __( 'Auto-save', 'airy-frontend-forms' ), 'unsavedChanges' => __( 'You have unsaved changes. Are you sure you want to leave?', 'airy-frontend-forms' ), 'draftLoaded' => __( 'Draft loaded. Continue editing or submit when ready.', 'airy-frontend-forms' ), // Dashboard strings. 'confirmDeleteDraft' => __( 'Are you sure you want to delete this draft?', 'airy-frontend-forms' ), 'deleting' => __( 'Deleting...', 'airy-frontend-forms' ), 'delete' => __( 'Delete', 'airy-frontend-forms' ), ), ); // Suppress unused variable warning - $settings used for future expansion. unset( $settings ); // Add reCAPTCHA config if enabled. if ( AFFCF_Captcha::is_enabled() ) { $localize_data['recaptcha'] = array( 'enabled' => true, 'siteKey' => AFFCF_Captcha::get_site_key(), 'action' => 'affcf_submit', ); } // ALWAYS localize auto-save if it's enqueued. if ( wp_script_is( 'affcf-auto-save', 'enqueued' ) ) { wp_localize_script( 'affcf-auto-save', 'affcfData', $localize_data ); } // Localize for the appropriate script. if ( $has_form || $has_edit_form ) { wp_localize_script( 'affcf-form-handler', 'affcfData', $localize_data ); } elseif ( $has_dashboard ) { wp_localize_script( 'affcf-dashboard', 'affcfData', $localize_data ); } } /** * Process form submission. * * @return void */ public function process_form_submission() { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in AFFCF_Form_Submission::handle_submission() // Check if form is being submitted. if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== $_SERVER['REQUEST_METHOD'] ) { return; } // Check if this is a form submission. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified below in AFFCF_Form_Submission::handle_submission() if ( ! isset( $_POST['affcf_action'] ) || 'submit_form' !== sanitize_text_field( wp_unslash( $_POST['affcf_action'] ) ) ) { return; } // Get form ID. $form_id = isset( $_POST['affcf_form_id'] ) ? absint( wp_unslash( $_POST['affcf_form_id'] ) ) : 0; if ( ! $form_id ) { return; } // Handle the submission. $submission = new AFFCF_Form_Submission(); $result = $submission->handle_submission(); // Handle result. if ( is_wp_error( $result ) ) { // Store error in transient for display. $error_message = $result->get_error_message(); set_transient( 'affcf_submission_error_' . get_current_user_id(), $error_message, 300 ); // Redirect back to form. wp_safe_redirect( wp_get_referer() ); exit; } // Success - store success message. $post_id = $result['post_id']; $success_message = $result['message']; set_transient( 'affcf_submission_success_' . get_current_user_id(), $success_message, 300 ); set_transient( 'affcf_submission_post_id_' . get_current_user_id(), $post_id, 300 ); // Redirect to success page or back to form. $pages = get_option( 'affcf_default_pages', array() ); $dashboard_page_id = ! empty( $pages['custom_dashboard_page_id'] ) ? $pages['custom_dashboard_page_id'] : $pages['dashboard_page_id']; if ( $dashboard_page_id ) { $redirect_url = get_permalink( $dashboard_page_id ); } else { $redirect_url = add_query_arg( 'submission', 'success', wp_get_referer() ); } wp_safe_redirect( $redirect_url ); exit; // phpcs:enable } /** * Handle AJAX form submission. * * @return void */ public function ajax_submit_form() { // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler -- Nonce verified in AFFCF_Form_Submission::handle_submission(), error handler needed for AJAX stability // Emergency error handler - catches errors even during class loading. set_error_handler( function ( $errno, $errstr, $errfile, $errline ) { // Suppress unused parameters - required by set_error_handler signature. unset( $errno, $errstr, $errfile, $errline ); } ); // Wrap ENTIRE method to ensure we ALWAYS return JSON. try { // Verify nonce exists. if ( ! isset( $_POST['affcf_nonce'] ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed - nonce missing.', 'airy-frontend-forms' ), 'debug_step' => 'nonce_missing', ) ); return; } // Verify form_id exists. if ( ! isset( $_POST['affcf_form_id'] ) ) { wp_send_json_error( array( 'message' => __( 'Form ID is missing.', 'airy-frontend-forms' ), 'debug_step' => 'form_id_missing', ) ); return; } // Check if class exists. if ( ! class_exists( 'AFFCF_Form_Submission' ) ) { wp_send_json_error( array( 'message' => __( 'Form submission class not loaded. Please check plugin installation.', 'airy-frontend-forms' ), 'debug_step' => 'class_missing', ) ); return; } // Handle the submission. $submission = new AFFCF_Form_Submission(); $result = $submission->handle_submission(); // Handle result. if ( is_wp_error( $result ) ) { $error_message = $result->get_error_message(); $error_code = $result->get_error_code(); // Build error response. $response = array( 'message' => $error_message, ); wp_send_json_error( $response ); return; } // Success! wp_send_json_success( array( 'message' => $result['message'], 'post_id' => $result['post_id'], 'redirect_url' => $this->get_redirect_url(), ) ); } catch ( Throwable $e ) { // This catches EVERYTHING in PHP 8+: Error, Exception, TypeError, etc. // Always return proper JSON error response. $response = array( 'message' => __( 'An error occurred while processing your submission. Please try again.', 'airy-frontend-forms' ), ); wp_send_json_error( $response ); } finally { restore_error_handler(); } } /** * Get redirect URL after successful submission. * * @return string Redirect URL. */ private function get_redirect_url() { $pages = get_option( 'affcf_default_pages', array() ); $dashboard_page_id = ! empty( $pages['custom_dashboard_page_id'] ) ? $pages['custom_dashboard_page_id'] : ( $pages['dashboard_page_id'] ?? 0 ); if ( $dashboard_page_id ) { return get_permalink( $dashboard_page_id ); } return home_url(); // phpcs:enable } /** * Handle delete post action from dashboard. * * @return void */ public function handle_delete_post() { // Check if user is logged in. if ( ! is_user_logged_in() ) { wp_die( esc_html__( 'You must be logged in to delete posts.', 'airy-frontend-forms' ) ); } // Get and validate post ID first (needed for nonce verification). $post_id = isset( $_GET['post_id'] ) ? absint( wp_unslash( $_GET['post_id'] ) ) : 0; if ( ! $post_id ) { wp_die( esc_html__( 'No post specified.', 'airy-frontend-forms' ) ); } // Verify nonce immediately after getting the post_id it depends on. if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'affcf_delete_post_' . $post_id ) ) { wp_die( esc_html__( 'Security check failed.', 'airy-frontend-forms' ) ); } // Verify user capability. if ( ! current_user_can( 'delete_post', $post_id ) ) { wp_die( esc_html__( 'You do not have permission to delete this post.', 'airy-frontend-forms' ) ); } // Get post. $post = get_post( $post_id ); if ( ! $post ) { wp_die( esc_html__( 'Post not found.', 'airy-frontend-forms' ) ); } // Verify ownership. if ( get_current_user_id() !== (int) $post->post_author ) { wp_die( esc_html__( 'You do not have permission to delete this post.', 'airy-frontend-forms' ) ); } // Only allow deleting pending/draft posts (not published ones). if ( ! in_array( $post->post_status, array( 'pending', 'draft' ), true ) ) { wp_die( esc_html__( 'You can only delete pending or draft posts.', 'airy-frontend-forms' ) ); } // Delete the post (true = force delete, bypass trash). $result = wp_delete_post( $post_id, true ); if ( ! $result ) { wp_die( esc_html__( 'Failed to delete post.', 'airy-frontend-forms' ) ); } // Redirect back to dashboard with success message. $redirect_url = add_query_arg( 'deleted', '1', $this->get_redirect_url() ); wp_safe_redirect( $redirect_url ); exit; } /** * Handle AJAX image upload to media library. * * @return void */ public function ajax_upload_image() { // Verify nonce. check_ajax_referer( 'affcf_frontend_nonce', 'nonce' ); // Verify user is logged in and has upload capability. if ( ! is_user_logged_in() || ! current_user_can( 'upload_files' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to upload files.', 'airy-frontend-forms' ), ) ); return; } // Check if file was uploaded. if ( empty( $_FILES['file'] ) ) { wp_send_json_error( array( 'message' => __( 'No file uploaded.', 'airy-frontend-forms' ), ) ); return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File upload, sanitized via wp_handle_upload $file = isset( $_FILES['file'] ) ? $_FILES['file'] : array(); // Check for upload errors. if ( UPLOAD_ERR_OK !== $file['error'] ) { wp_send_json_error( array( 'message' => __( 'File upload error.', 'airy-frontend-forms' ), ) ); return; } // Validate file type using server-side check (don't trust client-provided MIME type). $file_ext = strtolower( pathinfo( sanitize_file_name( $file['name'] ), PATHINFO_EXTENSION ) ); $allowed_extensions = array( 'jpg', 'jpeg', 'png', 'gif', 'webp' ); if ( ! in_array( $file_ext, $allowed_extensions, true ) ) { wp_send_json_error( array( 'message' => __( 'Invalid file type. Only images allowed.', 'airy-frontend-forms' ), ) ); return; } // Double-check with WordPress MIME type verification. $file_check = wp_check_filetype( sanitize_file_name( $file['name'] ) ); if ( ! $file_check['type'] || strpos( $file_check['type'], 'image/' ) !== 0 ) { wp_send_json_error( array( 'message' => __( 'Invalid file type. Only images allowed.', 'airy-frontend-forms' ), ) ); return; } // Validate file size (2MB max). $max_size = 2 * 1024 * 1024; if ( $file['size'] > $max_size ) { wp_send_json_error( array( 'message' => __( 'File size exceeds 2MB limit.', 'airy-frontend-forms' ), ) ); return; } // Include WordPress file handling functions. require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; // Upload file to media library using media_handle_upload with custom settings. $attachment_id = media_handle_upload( 'file', 0, array(), array( 'test_form' => false, 'action' => 'affcf_upload_image', ) ); if ( is_wp_error( $attachment_id ) ) { wp_send_json_error( array( 'message' => $attachment_id->get_error_message(), ) ); } // Get attachment URL. $image_url = wp_get_attachment_image_url( $attachment_id, 'medium' ); if ( ! $image_url ) { $image_url = wp_get_attachment_url( $attachment_id ); } // Return success with attachment ID and URL. wp_send_json_success( array( 'attachment_id' => $attachment_id, 'url' => $image_url, 'message' => __( 'Image uploaded successfully.', 'airy-frontend-forms' ), ) ); } /** * Enqueue assets in footer if shortcode rendered but wasn't detected in post_content. * This handles page builders like Breakdance, Elementor, etc. * * @return void */ public function enqueue_assets_footer() { // If no shortcode rendered, do nothing. if ( ! $this->form_is_rendering && ! $this->dashboard_is_rendering && ! $this->edit_form_is_rendering ) { return; } // If form shortcode rendered, ensure scripts are enqueued. if ( $this->form_is_rendering || $this->edit_form_is_rendering ) { // Check if form handler already enqueued. if ( ! wp_script_is( 'affcf-form-handler', 'enqueued' ) ) { wp_enqueue_style( 'affcf-frontend', AFFCF_PLUGIN_URL . 'assets/css/frontend/form-styles.css', array(), AFFCF_VERSION ); wp_enqueue_script( 'affcf-form-handler', AFFCF_PLUGIN_URL . 'assets/js/frontend/form-handler.js', array( 'jquery' ), AFFCF_VERSION, true ); } // ALWAYS enqueue auto-save if not already enqueued. if ( ! wp_script_is( 'affcf-auto-save', 'enqueued' ) ) { $auto_save_url = AFFCF_PLUGIN_URL . 'assets/js/frontend/auto-save.js'; $auto_save_version = AFFCF_VERSION . '-footer-' . time(); wp_enqueue_script( 'affcf-auto-save', $auto_save_url, array( 'jquery' ), $auto_save_version, true ); // Localize script. $localize_data = array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'affcf_frontend_nonce' ), 'autoSaveEnabled' => true, 'autoSaveInterval' => 10, 'strings' => array( 'submitting' => __( 'Submitting...', 'airy-frontend-forms' ), 'success' => __( 'Form submitted successfully!', 'airy-frontend-forms' ), 'error' => __( 'An error occurred. Please try again.', 'airy-frontend-forms' ), 'saving' => __( 'Saving draft...', 'airy-frontend-forms' ), 'saved' => __( 'Draft saved', 'airy-frontend-forms' ), 'saveError' => __( 'Failed to save draft', 'airy-frontend-forms' ), 'autosave' => __( 'Auto-save', 'airy-frontend-forms' ), 'unsavedChanges' => __( 'You have unsaved changes. Are you sure you want to leave?', 'airy-frontend-forms' ), 'draftLoaded' => __( 'Draft loaded. Continue editing or submit when ready.', 'airy-frontend-forms' ), ), ); wp_localize_script( 'affcf-auto-save', 'affcfData', $localize_data ); wp_localize_script( 'affcf-form-handler', 'affcfData', $localize_data ); } } // If dashboard shortcode rendered, ensure dashboard scripts are enqueued. if ( $this->dashboard_is_rendering ) { if ( ! wp_script_is( 'affcf-dashboard', 'enqueued' ) ) { wp_enqueue_style( 'affcf-dashboard', AFFCF_PLUGIN_URL . 'assets/css/frontend/user-dashboard.css', array(), AFFCF_VERSION ); wp_enqueue_script( 'affcf-dashboard', AFFCF_PLUGIN_URL . 'assets/js/frontend/user-dashboard.js', array( 'jquery' ), AFFCF_VERSION, true ); $localize_data = array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'affcf_frontend_nonce' ), 'autoSaveEnabled' => true, 'autoSaveInterval' => 10, 'strings' => array( 'confirmDeleteDraft' => __( 'Are you sure you want to delete this draft?', 'airy-frontend-forms' ), 'deleting' => __( 'Deleting...', 'airy-frontend-forms' ), 'delete' => __( 'Delete', 'airy-frontend-forms' ), ), ); wp_localize_script( 'affcf-dashboard', 'affcfData', $localize_data ); } // Also enqueue auto-save for dashboard. if ( ! wp_script_is( 'affcf-auto-save', 'enqueued' ) ) { wp_enqueue_script( 'affcf-auto-save', AFFCF_PLUGIN_URL . 'assets/js/frontend/auto-save.js', array( 'jquery' ), AFFCF_VERSION . '-footer-' . time(), true ); } } } }