WP_REST_Server::READABLE, 'callback' => array( $this, 'get_forms' ), 'permission_callback' => array( $this, 'permissions_check' ), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'save_form' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ) ); // Collection actions are registered before /forms/{id} and /entries/{id} // so "import" / "bulk-delete" are never captured as an id. register_rest_route( self::REST_NAMESPACE, '/forms/import', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'import_forms' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ) ); register_rest_route( self::REST_NAMESPACE, '/forms/bulk-delete', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'bulk_delete_forms' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ) ); register_rest_route( self::REST_NAMESPACE, '/forms/(?P[a-zA-Z0-9-]+)/duplicate', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'duplicate_form' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ) ); register_rest_route( self::REST_NAMESPACE, '/forms/(?P[a-zA-Z0-9-]+)/active', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'set_form_active' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ) ); register_rest_route( self::REST_NAMESPACE, '/entries/bulk-delete', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'bulk_delete_entries' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ) ); register_rest_route( self::REST_NAMESPACE, '/forms/(?P[a-zA-Z0-9-]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_form' ), 'permission_callback' => array( $this, 'permissions_check' ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_form' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ) ); register_rest_route( self::REST_NAMESPACE, '/forms/(?P[a-zA-Z0-9-]+)/entries', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_form_entries' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ) ); register_rest_route( self::REST_NAMESPACE, '/submit', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'submit_form' ), 'permission_callback' => '__return_true', ) ) ); // Fresh submission token. Fetched right before submit so pages served // from a full-page cache never post an expired nonce. register_rest_route( self::REST_NAMESPACE, '/test-email', array( 'methods' => 'POST', 'callback' => array( $this, 'send_test_email' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ); register_rest_route( self::REST_NAMESPACE, '/submit-token', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_submit_token' ), 'permission_callback' => '__return_true', ) ) ); register_rest_route( self::REST_NAMESPACE, '/entries/(?P[a-zA-Z0-9-]+)', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_entry' ), 'permission_callback' => array( $this, 'permissions_check' ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_entry' ), 'permission_callback' => array( $this, 'permissions_check' ), ) ) ); } public function permissions_check() { return current_user_can( 'manage_options' ); } public function get_forms( $request ) { global $wpdb; $table_forms = $wpdb->prefix . 'wpvfb_forms'; $table_entries = $wpdb->prefix . 'wpvfb_entries'; $results = $wpdb->get_results( $wpdb->prepare( 'SELECT f.id, f.name, f.fields, f.settings, f.is_active, f.created_at, (SELECT COUNT(*) FROM %i e WHERE e.form_id = f.id) as entries_count FROM %i f ORDER BY f.created_at DESC', $table_entries, $table_forms ) ); // Settings stay on the server; add-ons may expose list badges through the filter. foreach ( (array) $results as $i => $row ) { $settings = json_decode( (string) $row->settings, true ); unset( $row->settings ); $results[ $i ] = apply_filters( 'wpvfb_form_list_item', $row, is_array( $settings ) ? $settings : array() ); } return rest_ensure_response( $results ); } public function get_form( $request ) { global $wpdb; $id = $request['id']; $table_forms = $wpdb->prefix . 'wpvfb_forms'; $form = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %s', $table_forms, $id ) ); if ( ! $form ) { return new WP_Error( 'form_not_found', __( 'Form not found', 'efe-visual-forms' ), array( 'status' => 404 ) ); } $form->fields = json_decode( $form->fields ); $form->settings = json_decode( $form->settings ); $form->confirmations = isset( $form->confirmations ) ? json_decode( (string) $form->confirmations ) : null; $form->notifications = isset( $form->notifications ) ? json_decode( (string) $form->notifications ) : null; return rest_ensure_response( $form ); } public function get_form_entries( $request ) { global $wpdb; $form_id = $request['id']; $table_entries = $wpdb->prefix . 'wpvfb_entries'; $results = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE form_id = %s ORDER BY created_at DESC', $table_entries, $form_id ) ); foreach ( $results as $row ) { $row->data = json_decode( $row->data, true ); } return rest_ensure_response( $results ); } public function save_form( $request ) { global $wpdb; $params = $request->get_json_params(); if ( empty( $params['id'] ) || empty( $params['name'] ) ) { return new WP_Error( 'missing_fields', __( 'Form ID and name are required', 'efe-visual-forms' ), array( 'status' => 400 ) ); } $id = sanitize_text_field( $params['id'] ); $name = sanitize_text_field( $params['name'] ); $fields = ! empty( $params['fields'] ) ? wp_json_encode( $params['fields'] ) : '[]'; $settings = ! empty( $params['settings'] ) ? wp_json_encode( $params['settings'] ) : '{}'; $row = array( 'name' => $name, 'fields' => $fields, 'settings' => $settings, 'updated_at' => current_time( 'mysql' ), ); // Only touch these columns when the client sent them, so older callers // (e.g. template import) never wipe saved confirmations/notifications. if ( isset( $params['confirmations'] ) && is_array( $params['confirmations'] ) ) { $row['confirmations'] = json_encode( array_values( $params['confirmations'] ) ); } if ( isset( $params['notifications'] ) && is_array( $params['notifications'] ) ) { $row['notifications'] = json_encode( array_values( $params['notifications'] ) ); } $table_forms = $wpdb->prefix . 'wpvfb_forms'; $exists = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE id = %s', $table_forms, $id ) ); if ( $exists ) { $result = $wpdb->update( $table_forms, $row, array( 'id' => $id ) ); } else { $row['id'] = $id; $row['created_at'] = current_time( 'mysql' ); $result = $wpdb->insert( $table_forms, $row ); } if ( false === $result ) { return new WP_Error( 'save_failed', __( 'Could not save form', 'efe-visual-forms' ), array( 'status' => 500 ) ); } do_action( 'wpvfb_forms_changed' ); return rest_ensure_response( array( 'success' => true, 'id' => $id ) ); } public function delete_form( $request ) { global $wpdb; $id = $request['id']; $table_forms = $wpdb->prefix . 'wpvfb_forms'; $table_entries = $wpdb->prefix . 'wpvfb_entries'; $wpdb->delete( $table_entries, array( 'form_id' => $id ) ); $deleted = $wpdb->delete( $table_forms, array( 'id' => $id ) ); if ( ! $deleted ) { return new WP_Error( 'delete_failed', __( 'Could not delete form', 'efe-visual-forms' ), array( 'status' => 500 ) ); } do_action( 'wpvfb_forms_changed' ); return rest_ensure_response( array( 'success' => true ) ); } /** Keeps only well-formed ids (uuid-like) from a client-supplied list. */ private function sanitize_id_list( $ids ) { $clean = array(); foreach ( (array) $ids as $id ) { if ( is_scalar( $id ) && preg_match( '/^[a-zA-Z0-9-]{1,36}$/', (string) $id ) ) { $clean[] = (string) $id; } } return array_values( array_unique( $clean ) ); } public function duplicate_form( $request ) { global $wpdb; $table_forms = $wpdb->prefix . 'wpvfb_forms'; $form = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %s', $table_forms, $request['id'] ), ARRAY_A ); if ( ! $form ) { return new WP_Error( 'form_not_found', __( 'Form not found', 'efe-visual-forms' ), array( 'status' => 404 ) ); } $now = current_time( 'mysql' ); $new_id = wpvfb_uuid(); $row = array( 'id' => $new_id, /* translators: %s: original form name */ 'name' => sprintf( /* translators: %s: form name */ __( '%s (Copy)', 'efe-visual-forms' ), $form['name'] ), 'fields' => $form['fields'], 'settings' => $form['settings'], 'confirmations' => $form['confirmations'], 'notifications' => $form['notifications'], 'is_active' => 1, 'created_at' => $now, 'updated_at' => $now, ); if ( false === $wpdb->insert( $table_forms, $row ) ) { return new WP_Error( 'duplicate_failed', __( 'Could not duplicate form', 'efe-visual-forms' ), array( 'status' => 500 ) ); } do_action( 'wpvfb_forms_changed' ); return rest_ensure_response( array( 'success' => true, 'id' => $new_id, 'name' => $row['name'] ) ); } public function set_form_active( $request ) { global $wpdb; $params = $request->get_json_params(); if ( ! is_array( $params ) || ! array_key_exists( 'active', $params ) ) { return new WP_Error( 'missing_active', __( 'The active flag is required', 'efe-visual-forms' ), array( 'status' => 400 ) ); } $active = rest_sanitize_boolean( $params['active'] ) ? 1 : 0; $table_forms = $wpdb->prefix . 'wpvfb_forms'; $exists = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE id = %s', $table_forms, $request['id'] ) ); if ( ! $exists ) { return new WP_Error( 'form_not_found', __( 'Form not found', 'efe-visual-forms' ), array( 'status' => 404 ) ); } $wpdb->update( $table_forms, array( 'is_active' => $active ), array( 'id' => $request['id'] ), array( '%d' ), array( '%s' ) ); do_action( 'wpvfb_forms_changed' ); return rest_ensure_response( array( 'success' => true, 'id' => $request['id'], 'is_active' => $active ) ); } /** * Imports one form ({form:{...}} or a flat {fields,...}) or a bundle ({forms:[...]}). * Every imported form gets a fresh id; nothing in the file can overwrite an existing form. */ public function import_forms( $request ) { global $wpdb; $params = $request->get_json_params(); if ( ! is_array( $params ) ) { return new WP_Error( 'invalid_import', __( 'Invalid import file', 'efe-visual-forms' ), array( 'status' => 400 ) ); } if ( isset( $params['forms'] ) && is_array( $params['forms'] ) ) { $items = array_values( $params['forms'] ); } elseif ( isset( $params['form'] ) && is_array( $params['form'] ) ) { $items = array( $params['form'] ); } else { $items = array( $params ); } if ( empty( $items ) || count( $items ) > 100 ) { return new WP_Error( 'invalid_import', __( 'Import file must contain between 1 and 100 forms', 'efe-visual-forms' ), array( 'status' => 400 ) ); } foreach ( $items as $item ) { if ( ! is_array( $item ) || ! isset( $item['fields'] ) || ! is_array( $item['fields'] ) || ( isset( $item['settings'] ) && ! is_array( $item['settings'] ) ) ) { return new WP_Error( 'invalid_import', __( 'Invalid import file: each form needs a fields array', 'efe-visual-forms' ), array( 'status' => 400 ) ); } } // A custom name only applies to single-form imports. $custom_name = ( 1 === count( $items ) && isset( $params['name'] ) && is_string( $params['name'] ) ) ? sanitize_text_field( $params['name'] ) : ''; $table_forms = $wpdb->prefix . 'wpvfb_forms'; $now = current_time( 'mysql' ); $created = array(); foreach ( $items as $item ) { $name = '' !== $custom_name ? $custom_name : sanitize_text_field( isset( $item['name'] ) && is_string( $item['name'] ) ? $item['name'] : '' ); if ( '' === $name ) { $name = __( 'Imported Form', 'efe-visual-forms' ); } $new_id = wpvfb_uuid(); // the reference builder exports (schemaVersion, smtpSettings{} ); add-ons may adapt their fields. $is_external_export = isset( $item['schemaVersion'] ) || ( isset( $item['settings'] ) && is_array( $item['settings'] ) && ( isset( $item['settings']['schemaVersion'] ) || isset( $item['settings']['smtpSettings'] ) ) ); $row = array( 'id' => $new_id, 'name' => $name, 'fields' => wp_json_encode( array_values( apply_filters( 'wpvfb_import_fields', $item['fields'], $is_external_export ) ) ), 'settings' => ! empty( $item['settings'] ) ? wp_json_encode( self::map_smtp_settings( $item['settings'] ) ) : '{}', 'is_active' => 1, 'created_at' => $now, 'updated_at' => $now, ); if ( isset( $item['confirmations'] ) && is_array( $item['confirmations'] ) ) { $row['confirmations'] = wp_json_encode( array_map( array( $this, 'normalize_confirmation' ), array_values( $item['confirmations'] ) ) ); } if ( isset( $item['notifications'] ) && is_array( $item['notifications'] ) ) { $row['notifications'] = wp_json_encode( array_values( $item['notifications'] ) ); } if ( false === $wpdb->insert( $table_forms, $row ) ) { return new WP_Error( 'import_failed', __( 'Could not import form', 'efe-visual-forms' ), array( 'status' => 500 ) ); } $created[] = array( 'id' => $new_id, 'name' => $name ); } do_action( 'wpvfb_forms_changed' ); return rest_ensure_response( array( 'success' => true, 'forms' => $created ) ); } public function bulk_delete_forms( $request ) { global $wpdb; $params = $request->get_json_params(); $ids = $this->sanitize_id_list( isset( $params['ids'] ) ? $params['ids'] : array() ); if ( empty( $ids ) ) { return new WP_Error( 'missing_ids', __( 'No forms selected', 'efe-visual-forms' ), array( 'status' => 400 ) ); } $table_forms = $wpdb->prefix . 'wpvfb_forms'; $table_entries = $wpdb->prefix . 'wpvfb_entries'; $deleted = 0; foreach ( $ids as $form_id ) { $wpdb->delete( $table_entries, array( 'form_id' => $form_id ), array( '%s' ) ); $deleted += (int) $wpdb->delete( $table_forms, array( 'id' => $form_id ), array( '%s' ) ); } do_action( 'wpvfb_forms_changed' ); return rest_ensure_response( array( 'success' => true, 'deleted' => (int) $deleted ) ); } public function bulk_delete_entries( $request ) { global $wpdb; $params = $request->get_json_params(); $ids = $this->sanitize_id_list( isset( $params['ids'] ) ? $params['ids'] : array() ); if ( empty( $ids ) ) { return new WP_Error( 'missing_ids', __( 'No entries selected', 'efe-visual-forms' ), array( 'status' => 400 ) ); } $table_entries = $wpdb->prefix . 'wpvfb_entries'; $deleted = 0; foreach ( $ids as $entry_id ) { $deleted += (int) $wpdb->delete( $table_entries, array( 'id' => $entry_id ), array( '%s' ) ); } return rest_ensure_response( array( 'success' => true, 'deleted' => (int) $deleted ) ); } public function get_submit_token( $request ) { $response = rest_ensure_response( array( 'nonce' => wp_create_nonce( self::NONCE_ACTION ) ) ); $response->header( 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0' ); $response->header( 'Pragma', 'no-cache' ); return $response; } /** * Returns a flat list of input fields. Grid columns are expanded; other containers * stay one entry (their rows are validated per subfield). Each entry keeps * the chain of container fields so container-level logic hides children too. */ private function collect_fields( $fields_list, $ancestors = array() ) { $flat = array(); if ( ! is_array( $fields_list ) ) { return $flat; } foreach ( $fields_list as $f ) { if ( ! is_array( $f ) || empty( $f['id'] ) || empty( $f['type'] ) ) { continue; } if ( 'grid' === $f['type'] ) { if ( ! empty( $f['columns'] ) && is_array( $f['columns'] ) ) { foreach ( $f['columns'] as $col ) { $flat = array_merge( $flat, $this->collect_fields( $col, array_merge( $ancestors, array( $f ) ) ) ); } } continue; } $flat[] = array( 'field' => $f, 'ancestors' => $ancestors ); } return $flat; } private function field_label( $field ) { $label = isset( $field['label'] ) && '' !== $field['label'] ? $field['label'] : ( isset( $field['name'] ) ? $field['name'] : $field['id'] ); return wp_strip_all_tags( (string) $label ); } /** "0" is a valid answer; empty strings and arrays of empty strings are not. */ private function is_blank( $val ) { if ( null === $val ) { return true; } if ( is_array( $val ) ) { foreach ( $val as $v ) { if ( ! $this->is_blank( $v ) ) { return false; } } return true; } return '' === trim( (string) $val ); } private function sanitize_deep( $val ) { if ( is_array( $val ) ) { $out = array(); foreach ( $val as $k => $v ) { $out[ sanitize_text_field( (string) $k ) ] = $this->sanitize_deep( $v ); } return $out; } return sanitize_text_field( (string) $val ); } /** On/off values of a switch: the reference builder keys, then the older WP builder keys (same order as WPVFB_Schema). */ private function switch_values( $field ) { $pick = function ( $keys, $default ) use ( $field ) { foreach ( $keys as $k ) { if ( isset( $field[ $k ] ) && is_scalar( $field[ $k ] ) && '' !== (string) $field[ $k ] ) { return (string) $field[ $k ]; } } return $default; }; return array( 'on' => sanitize_text_field( $pick( array( 'switchOnValue', 'onValue' ), 'on' ) ), 'off' => sanitize_text_field( $pick( array( 'switchOffValue', 'offValue' ), 'off' ) ), ); } private function sanitize_value( $field, $val ) { if ( null === $val ) { return ''; } if ( is_array( $val ) ) { return $this->sanitize_deep( $val ); } $val = (string) $val; switch ( $field['type'] ) { case 'email': return sanitize_email( $val ); case 'textarea': return sanitize_textarea_field( $val ); case 'url': return esc_url_raw( $val ); case 'number': return is_numeric( $val ) ? $val : ''; case 'password': return $val; // Never stored (redacted before saving). default: return sanitize_text_field( $val ); } } private function rate_limited( $form_id ) { $limit = (int) apply_filters( 'wpvfb_submission_rate_limit', 10, $form_id ); if ( $limit <= 0 ) { return false; } $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $key = 'wpvfb_rl_' . md5( $ip . '|' . $form_id ); $count = (int) get_transient( $key ); if ( $count >= $limit ) { return true; } set_transient( $key, $count + 1, MINUTE_IN_SECONDS ); return false; } /** * reCAPTCHA: with a secret key the token is verified with Google (v3 also needs a minimum score). * Without keys the field is the legacy "I'm not a robot" checkbox and only its required state applies. * * @return WP_Error|null */ private function verify_captcha( $field, $request, $submitted_raw ) { $message = __( 'Please complete the CAPTCHA verification.', 'efe-visual-forms' ); $secret = isset( $field['secretKey'] ) && is_string( $field['secretKey'] ) ? trim( $field['secretKey'] ) : ''; if ( '' === $secret ) { $val = isset( $submitted_raw[ $field['id'] ] ) ? $submitted_raw[ $field['id'] ] : null; if ( ! empty( $field['required'] ) && $this->is_blank( $val ) ) { return new WP_Error( 'captcha_required', $message, array( 'status' => 400 ) ); } return null; } $token = (string) $request->get_param( 'g-recaptcha-response' ); if ( '' === trim( $token ) ) { return new WP_Error( 'captcha_required', $message, array( 'status' => 400 ) ); } $res = wp_safe_remote_post( 'https://www.google.com/recaptcha/api/siteverify', array( 'timeout' => 10, 'body' => array( 'secret' => $secret, 'response' => $token, 'remoteip' => isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '', ), ) ); if ( is_wp_error( $res ) ) { return new WP_Error( 'captcha_unavailable', __( 'CAPTCHA verification is temporarily unavailable. Please try again.', 'efe-visual-forms' ), array( 'status' => 503 ) ); } $body = json_decode( wp_remote_retrieve_body( $res ), true ); if ( empty( $body['success'] ) ) { return new WP_Error( 'captcha_failed', $message, array( 'status' => 400 ) ); } if ( isset( $field['captchaType'] ) && 'v3' === $field['captchaType'] ) { $min = (float) apply_filters( 'wpvfb_recaptcha_min_score', 0.5, $field ); if ( ! isset( $body['score'] ) || (float) $body['score'] < $min ) { return new WP_Error( 'captcha_failed', $message, array( 'status' => 400 ) ); } } return null; } public function submit_form( $request ) { global $wpdb; $nonce = (string) $request->get_param( 'wpvfb_nonce' ); if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) { return new WP_Error( 'invalid_nonce', __( 'Security verification failed. Please refresh the page and try again.', 'efe-visual-forms' ), array( 'status' => 403 ) ); } if ( '' !== (string) $request->get_param( 'wpvfb_honeypot' ) ) { return rest_ensure_response( array( 'success' => true, 'message' => __( 'Thank you for your submission!', 'efe-visual-forms' ) ) ); } $form_id = sanitize_text_field( (string) $request->get_param( 'form_id' ) ); if ( empty( $form_id ) ) { return new WP_Error( 'missing_form_id', __( 'Form ID is missing.', 'efe-visual-forms' ), array( 'status' => 400 ) ); } if ( $this->rate_limited( $form_id ) ) { return new WP_Error( 'rate_limited', __( 'Too many submissions. Please wait a minute and try again.', 'efe-visual-forms' ), array( 'status' => 429 ) ); } $table_forms = $wpdb->prefix . 'wpvfb_forms'; $form = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %s', $table_forms, $form_id ) ); if ( ! $form ) { return new WP_Error( 'form_not_found', __( 'Form not found.', 'efe-visual-forms' ), array( 'status' => 404 ) ); } if ( isset( $form->is_active ) && ! (int) $form->is_active ) { return new WP_Error( 'form_inactive', __( 'This form is not accepting submissions.', 'efe-visual-forms' ), array( 'status' => 403 ) ); } $fields = json_decode( $form->fields, true ); $settings = json_decode( $form->settings, true ); if ( ! is_array( $settings ) ) { $settings = array(); } if ( ! empty( $settings['requireLogin'] ) && ! is_user_logged_in() ) { return new WP_Error( 'login_required', __( 'Please log in to submit this form.', 'efe-visual-forms' ), array( 'status' => 401 ) ); } $collected = $this->collect_fields( $fields ); // REST already unslashes body params (unlike raw $_POST). $submitted_raw = $request->get_param( 'fields' ); if ( ! is_array( $submitted_raw ) ) { $submitted_raw = array(); } // Shared with add-ons that store their own field types (filters below). $context = array( 'form_id' => $form_id, 'form' => $form, 'fields' => is_array( $fields ) ? $fields : array(), 'settings' => $settings, 'raw' => $submitted_raw, 'request' => $request, ); $input_types = (array) apply_filters( 'wpvfb_input_types', self::INPUT_TYPES ); // Pass 1: sanitized values for every field, used to evaluate conditional logic. $values = array(); foreach ( $collected as $entry ) { $field = $entry['field']; $fid = $field['id']; if ( in_array( $field['type'], self::LAYOUT_TYPES, true ) || 'captcha' === $field['type'] ) { continue; } $raw = isset( $submitted_raw[ $fid ] ) ? $submitted_raw[ $fid ] : null; $custom = apply_filters( 'wpvfb_field_value', null, $field, $raw, $context ); if ( null !== $custom ) { $values[ $fid ] = $custom; } elseif ( ! in_array( $field['type'], $input_types, true ) ) { continue; // Unknown field type: nothing is stored. } elseif ( 'switch' === $field['type'] && $this->is_blank( $raw ) ) { // An unchecked switch stores its off value (the storefront sends it too), so "is Off" rules match. $values[ $fid ] = $this->sanitize_value( $field, $this->switch_values( $field )['off'] ); } else { $values[ $fid ] = $this->sanitize_value( $field, $raw ); } } $context['values'] = $values; // Pass 2: visibility, required checks. $submitted_data = array(); foreach ( $collected as $entry ) { $field = $entry['field']; $fid = $field['id']; if ( in_array( $field['type'], self::LAYOUT_TYPES, true ) ) { continue; } $visible = WPVFB_Logic::passes( $field, $values ); foreach ( $entry['ancestors'] as $ancestor ) { $visible = $visible && WPVFB_Logic::passes( $ancestor, $values ); } if ( ! $visible ) { continue; // Hidden fields are neither required nor stored. } $label = $this->field_label( $field ); $required = ! empty( $field['required'] ); if ( 'captcha' === $field['type'] ) { $captcha_error = $this->verify_captcha( $field, $request, $submitted_raw ); if ( $captcha_error ) { return $captcha_error; } continue; // never stored } // Add-on field types: WP_Error, or an array of keys to store (empty array = nothing to store). $handled = apply_filters( 'wpvfb_process_field', null, $field, isset( $values[ $fid ] ) ? $values[ $fid ] : null, $context ); if ( is_wp_error( $handled ) ) { return $handled; } if ( is_array( $handled ) ) { foreach ( $handled as $key => $value ) { $submitted_data[ $key ] = $value; } continue; } if ( ! array_key_exists( $fid, $values ) ) { continue; } $val = $values[ $fid ]; if ( $required && 'switch' === $field['type'] ) { $sw = $this->switch_values( $field ); if ( $val !== $sw['on'] && $val === $sw['off'] ) { $val = ''; // A required switch must be switched on. } } if ( $required && $this->is_blank( $val ) ) { return new WP_Error( 'missing_required', sprintf( /* translators: %s: field label */ __( '%s is required.', 'efe-visual-forms' ), $label ), array( 'status' => 400 ) ); } if ( 'password' === $field['type'] ) { $submitted_data[ $fid ] = $this->is_blank( $val ) ? '' : self::REDACTED; continue; } $submitted_data[ $fid ] = $val; } // Add-ons may add derived values (or refuse the submission) before the entry is saved. $submitted_data = apply_filters( 'wpvfb_submission_data', $submitted_data, $context ); if ( is_wp_error( $submitted_data ) ) { return $submitted_data; } // Quiz score, recomputed from the form definition (never trusted from the browser). $this->extra_tags = array(); $this->extra_rows = array(); $this->compute_quiz( $fields, $settings, $submitted_data ); $this->extra_tags = (array) apply_filters( 'wpvfb_submission_merge_tags', $this->extra_tags, $submitted_data, $context ); $this->extra_rows = (array) apply_filters( 'wpvfb_submission_summary_rows', $this->extra_rows, $submitted_data, $context ); $entry_id = wpvfb_uuid(); $table_entries = $wpdb->prefix . 'wpvfb_entries'; $inserted = $wpdb->insert( $table_entries, array( 'id' => $entry_id, 'form_id' => $form_id, 'data' => wp_json_encode( $submitted_data ), 'user_ip' => isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '', 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', 'created_at' => current_time( 'mysql' ), ) ); if ( ! $inserted ) { return new WP_Error( 'save_entry_failed', __( 'Could not save entry submission.', 'efe-visual-forms' ), array( 'status' => 500 ) ); } do_action( 'wpvfb_after_submission', $entry_id, $form_id, $submitted_data, $context ); $field_list = array(); foreach ( $collected as $entry ) { $field_list[] = $entry['field']; } $confirmations = isset( $form->confirmations ) ? json_decode( (string) $form->confirmations, true ) : null; $notifications = isset( $form->notifications ) ? json_decode( (string) $form->notifications, true ) : null; $this->dispatch_notifications( $form->name, $field_list, $settings, $notifications, $submitted_data, $entry_id ); return rest_ensure_response( $this->resolve_confirmation( $form->name, $field_list, $settings, $confirmations, $submitted_data, $entry_id ) ); } /** Merge tags / {all_fields} rows added for this submission (quiz score, add-on totals). */ private $extra_tags = array(); private $extra_rows = array(); /** Stores `quiz_score` when choice options carry points and fills {quiz_score}. */ private function compute_quiz( $fields, $settings, &$submitted_data ) { if ( ! class_exists( 'WPVFB_Schema' ) ) { require_once WPVFB_PLUGIN_DIR . 'includes/render/class-js.php'; require_once WPVFB_PLUGIN_DIR . 'includes/render/class-schema.php'; } $normalized = WPVFB_Schema::to_render_form( is_array( $fields ) ? $fields : array(), $settings ); $flat = WPVFB_Quiz::flatten( $normalized['fields'] ); if ( WPVFB_Quiz::has_points( $flat ) ) { $score = WPVFB_Quiz::score( $flat, $submitted_data ); $submitted_data['quiz_score'] = $score; $this->extra_tags['{quiz_score}'] = $this->format_score( $score ); } } private function format_score( $score ) { return floor( $score ) == $score ? (string) (int) $score : rtrim( rtrim( number_format( $score, 2, '.', '' ), '0' ), '.' ); } /** Human-readable string for any stored value (arrays, repeater rows, composite fields). */ private function value_to_string( $val ) { if ( ! is_array( $val ) ) { return (string) $val; } $is_list = array_keys( $val ) === range( 0, count( $val ) - 1 ); $parts = array(); foreach ( $val as $k => $v ) { $str = $this->value_to_string( $v ); if ( '' === $str ) { continue; } $parts[] = $is_list ? $str : $k . ': ' . $str; } $has_nested = count( array_filter( $val, 'is_array' ) ) > 0; return implode( $has_nested ? ' | ' : ', ', $parts ); } /** * Merge tag map: {form_title}, {entry_id}, {entry_date}, {field_}, {quiz_score}, add-on tags, * and {Field Label}/{field name} aliases. Values are raw text. */ private function merge_tags( $form_name, $field_list, $submitted_data, $entry_id ) { $tags = array( '{form_title}' => $form_name, '{entry_id}' => $entry_id, '{entry_date}' => current_time( 'mysql' ), ); foreach ( $field_list as $field ) { $fid = $field['id']; $tags[ '{field_' . $fid . '}' ] = isset( $submitted_data[ $fid ] ) ? $this->value_to_string( $submitted_data[ $fid ] ) : ''; } $tags = array_merge( $tags, $this->extra_tags ); // storefront-style tags by field label or name ({Your Email}); never overrides the tags above. foreach ( $field_list as $field ) { if ( in_array( $field['type'], self::LAYOUT_TYPES, true ) || 'password' === $field['type'] ) { continue; } $value = isset( $tags[ '{field_' . $field['id'] . '}' ] ) ? $tags[ '{field_' . $field['id'] . '}' ] : ''; foreach ( array( 'label', 'name' ) as $key ) { $text = isset( $field[ $key ] ) && is_scalar( $field[ $key ] ) ? trim( wp_strip_all_tags( (string) $field[ $key ] ) ) : ''; if ( '' !== $text && false === strpos( $text, '}' ) && ! isset( $tags[ '{' . $text . '}' ] ) ) { $tags[ '{' . $text . '}' ] = $value; } } } return $tags; } private function replace_tags( $template, $tags, $escape = null ) { $replace = array(); foreach ( $tags as $tag => $value ) { $replace[ $tag ] = $escape ? call_user_func( $escape, $value ) : $value; } return strtr( (string) $template, $replace ); } private function all_fields_table( $field_list, $submitted_data ) { $html = ''; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; foreach ( $field_list as $field ) { // Skip layout and secret fields from table presentation if ( in_array( $field['type'], self::LAYOUT_TYPES, true ) || 'password' === $field['type'] ) { continue; } $fid = $field['id']; if ( ! array_key_exists( $fid, $submitted_data ) ) { continue; } $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; } foreach ( $this->extra_rows as $row ) { $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; } $html .= ' '; $html .= '
' . __( 'Field', 'efe-visual-forms' ) . '' . __( 'Value', 'efe-visual-forms' ) . '
' . esc_html( $this->field_label( $field ) ) . '' . esc_html( $this->value_to_string( $submitted_data[ $fid ] ) ) . '
' . esc_html( $row[0] ) . '' . esc_html( $row[1] ) . '
'; return $html; } /** phpmailer_init callback for the form's SMTP settings, or null when custom SMTP is off. */ public static function smtp_callback( $settings ) { if ( empty( $settings['smtpEnabled'] ) || empty( $settings['smtpHost'] ) || empty( $settings['smtpUsername'] ) ) { return null; } return function( $phpmailer ) use ( $settings ) { $phpmailer->isSMTP(); $phpmailer->Host = sanitize_text_field( $settings['smtpHost'] ); $phpmailer->SMTPAuth = true; $phpmailer->Port = intval( isset( $settings['smtpPort'] ) ? $settings['smtpPort'] : 587 ); $phpmailer->Username = sanitize_text_field( $settings['smtpUsername'] ); $phpmailer->Password = isset( $settings['smtpPassword'] ) ? $settings['smtpPassword'] : ''; $encryption = isset( $settings['smtpEncryption'] ) ? strtolower( (string) $settings['smtpEncryption'] ) : 'tls'; if ( 'ssl' === $encryption ) { $phpmailer->SMTPSecure = 'ssl'; } elseif ( 'tls' === $encryption ) { $phpmailer->SMTPSecure = 'tls'; } else { $phpmailer->SMTPSecure = ''; } }; } /** the reference builder `smtpSettings{}` → the flat smtp* keys WP reads (import). Flat keys already set win. */ public static function map_smtp_settings( $settings ) { if ( ! is_array( $settings ) || ! isset( $settings['smtpSettings'] ) || ! is_array( $settings['smtpSettings'] ) ) { return $settings; } $map = array( 'enabled' => 'smtpEnabled', 'provider' => 'smtpProvider', 'host' => 'smtpHost', 'port' => 'smtpPort', 'encryption' => 'smtpEncryption', 'username' => 'smtpUsername', 'password' => 'smtpPassword', 'fromEmail' => 'smtpFromEmail', 'fromName' => 'smtpFromName' ); foreach ( $map as $from => $to ) { if ( array_key_exists( $from, $settings['smtpSettings'] ) && ( ! isset( $settings[ $to ] ) || '' === $settings[ $to ] ) ) { $v = $settings['smtpSettings'][ $from ]; $settings[ $to ] = 'encryption' === $from && is_string( $v ) ? strtolower( $v ) : ( 'port' === $from && is_numeric( $v ) ? (int) $v : $v ); } } unset( $settings['smtpSettings'] ); return $settings; } /** POST /test-email (admins only): sends a test message with the posted SMTP settings. */ public function send_test_email( $request ) { $params = $request->get_json_params(); $to = isset( $params['to'] ) ? sanitize_email( (string) $params['to'] ) : ''; if ( ! $to || ! is_email( $to ) ) { return new WP_Error( 'invalid_email', __( 'Enter a valid email address to send the test to.', 'efe-visual-forms' ), array( 'status' => 400 ) ); } $settings = isset( $params['settings'] ) && is_array( $params['settings'] ) ? self::map_smtp_settings( $params['settings'] ) : array(); $error = null; $on_fail = function ( $wp_error ) use ( &$error ) { $error = $wp_error->get_error_message(); }; add_action( 'wp_mail_failed', $on_fail ); $smtp = self::smtp_callback( $settings ); if ( $smtp ) { add_action( 'phpmailer_init', $smtp ); } $headers = array( 'Content-Type: text/html; charset=UTF-8' ); $from = ! empty( $settings['smtpFromEmail'] ) ? sanitize_email( $settings['smtpFromEmail'] ) : ''; if ( $from && is_email( $from ) ) { $name = ! empty( $settings['smtpFromName'] ) ? str_replace( array( "\r", "\n", '<', '>', '"' ), '', wp_strip_all_tags( $settings['smtpFromName'] ) ) : ''; $headers[] = 'From: ' . ( $name ? $name . ' <' . $from . '>' : $from ); } $sent = wp_mail( $to, __( 'Test email from EFE Visual Forms', 'efe-visual-forms' ), '

' . esc_html__( 'Your form notification email settings work.', 'efe-visual-forms' ) . '

', $headers ); if ( $smtp ) { remove_action( 'phpmailer_init', $smtp ); } remove_action( 'wp_mail_failed', $on_fail ); if ( ! $sent ) { return new WP_Error( 'mail_failed', $error ? $error : __( 'The email could not be sent.', 'efe-visual-forms' ), array( 'status' => 500 ) ); } return rest_ensure_response( array( 'success' => true, 'smtp' => (bool) $smtp ) ); } /** True when a message template is rich text (block-level HTML from the editor); plain text gets nl2br(). */ public static function is_rich_text( $template ) { return is_string( $template ) && (bool) preg_match( '/<\/?(p|br|div|ul|ol|li|h[1-6]|blockquote|table)\b/i', $template ); } private function parse_emails( $list, $tags ) { $out = array(); foreach ( preg_split( '/[,;]+/', $this->replace_tags( $list, $tags ) ) as $addr ) { $addr = sanitize_email( trim( $addr ) ); if ( $addr && is_email( $addr ) ) { $out[] = $addr; } } return $out; } /** * Builds the list of notifications to send. Saved notifications win; forms * saved before 2.9.0 fall back to the legacy emailRecipient/Subject/Body settings. */ private function notification_list( $form_name, $settings, $notifications ) { if ( is_array( $notifications ) && ! empty( $notifications ) ) { return $notifications; } return array( array( 'id' => 'legacy', 'active' => true, 'sendTo' => 'email', 'sendToEmail' => ! empty( $settings['emailRecipient'] ) ? $settings['emailRecipient'] : get_option( 'admin_email' ), 'subject' => ! empty( $settings['emailSubject'] ) ? $settings['emailSubject'] : sprintf( /* translators: %s: form name */ __( 'New Submission: %s', 'efe-visual-forms' ), $form_name ), 'message' => ! empty( $settings['emailBody'] ) ? $settings['emailBody'] : "Here are the details:\n\n{all_fields}", ) ); } private function dispatch_notifications( $form_name, $field_list, $settings, $notifications, $submitted_data, $entry_id ) { $tags = $this->merge_tags( $form_name, $field_list, $submitted_data, $entry_id ); $all_fields_html = $this->all_fields_table( $field_list, $submitted_data ); // Dynamically configure SMTP routing filter if enabled for this form $smtp_callback = self::smtp_callback( $settings ); if ( $smtp_callback ) { add_action( 'phpmailer_init', $smtp_callback ); } foreach ( $this->notification_list( $form_name, $settings, $notifications ) as $notif ) { if ( ! is_array( $notif ) || ( isset( $notif['active'] ) && ! $notif['active'] ) ) { continue; } if ( ! WPVFB_Logic::passes( $notif, $submitted_data ) ) { continue; } if ( isset( $notif['sendTo'] ) && 'field' === $notif['sendTo'] ) { $fid = isset( $notif['sendToField'] ) ? $notif['sendToField'] : ''; $to = $fid && isset( $submitted_data[ $fid ] ) && is_string( $submitted_data[ $fid ] ) ? $this->parse_emails( $submitted_data[ $fid ], array() ) : array(); } else { $to = $this->parse_emails( isset( $notif['sendToEmail'] ) ? $notif['sendToEmail'] : '', $tags ); if ( empty( $to ) ) { $to = array( get_option( 'admin_email' ) ); } } if ( empty( $to ) ) { continue; } $subject_tpl = ! empty( $notif['subject'] ) ? $notif['subject'] : sprintf( /* translators: %s: form name */ __( 'New Submission: %s', 'efe-visual-forms' ), $form_name ); // Strip line breaks so a submitted value can never inject mail headers. $subject = str_replace( array( "\r", "\n" ), ' ', wp_strip_all_tags( $this->replace_tags( $subject_tpl, $tags ) ) ); $body_tpl = ! empty( $notif['message'] ) ? $notif['message'] : '{all_fields}'; $body = $this->replace_tags( $body_tpl, $tags, 'esc_html' ); if ( ! self::is_rich_text( $body_tpl ) ) { $body = nl2br( $body ); // Plain text: keep the line breaks. Rich text (editor HTML) is used as is. } $body = str_replace( '{all_fields}', $all_fields_html, $body ); $headers = array( 'Content-Type: text/html; charset=UTF-8' ); // Per-notification sender first, then the form's Sender Identity (smtpFromEmail / smtpFromName). $from_email = ! empty( $notif['fromEmail'] ) ? sanitize_email( $notif['fromEmail'] ) : ( ! empty( $settings['smtpFromEmail'] ) ? sanitize_email( $settings['smtpFromEmail'] ) : '' ); if ( $from_email && is_email( $from_email ) ) { $from_name_tpl = ! empty( $notif['fromName'] ) ? $notif['fromName'] : ( empty( $notif['fromEmail'] ) && ! empty( $settings['smtpFromName'] ) ? $settings['smtpFromName'] : '' ); $from_name = '' !== $from_name_tpl ? str_replace( array( "\r", "\n", '<', '>', '"' ), '', wp_strip_all_tags( $this->replace_tags( $from_name_tpl, $tags ) ) ) : ''; $headers[] = 'From: ' . ( $from_name ? $from_name . ' <' . $from_email . '>' : $from_email ); } $reply = ! empty( $notif['replyTo'] ) ? $this->parse_emails( $notif['replyTo'], $tags ) : array(); if ( empty( $notif['replyTo'] ) ) { // Blank Reply-To: answer the submitter (first email field with a value). foreach ( $field_list as $field ) { if ( isset( $field['type'] ) && 'email' === $field['type'] && ! empty( $submitted_data[ $field['id'] ] ) && is_string( $submitted_data[ $field['id'] ] ) ) { $reply = $this->parse_emails( $submitted_data[ $field['id'] ], array() ); if ( $reply ) { break; } } } } if ( $reply ) { $headers[] = 'Reply-To: ' . $reply[0]; } if ( ! empty( $notif['bcc'] ) ) { foreach ( $this->parse_emails( $notif['bcc'], $tags ) as $bcc ) { $headers[] = 'Bcc: ' . $bcc; } } wp_mail( $to, $subject, $body, $headers ); } // Remove SMTP filter immediately after sending mail to prevent side effects if ( $smtp_callback ) { remove_action( 'phpmailer_init', $smtp_callback ); } } /** * storefront-shaped confirmations (type "text", redirectURL) → the WP shape (type "message", redirectUrl). */ public function normalize_confirmation( $conf ) { if ( ! is_array( $conf ) ) { return $conf; } if ( isset( $conf['type'] ) && 'text' === $conf['type'] ) { $conf['type'] = 'message'; } if ( isset( $conf['type'] ) && 'quiz' === $conf['type'] ) { $conf['type'] = 'quiz_recommendation'; } if ( empty( $conf['redirectUrl'] ) && ! empty( $conf['redirectURL'] ) && is_string( $conf['redirectURL'] ) ) { $conf['redirectUrl'] = $conf['redirectURL']; } unset( $conf['redirectURL'] ); return $conf; } /** * Picks the confirmation to show: the first active one whose conditional * logic matches, otherwise the first active one without logic. */ private function resolve_confirmation( $form_name, $field_list, $settings, $confirmations, $submitted_data, $entry_id ) { $default_message = __( 'Thank you! Your submission has been saved.', 'efe-visual-forms' ); if ( ! is_array( $confirmations ) || empty( $confirmations ) ) { // Forms saved before 2.9.0 kept these in settings. $confirmations = array( array( 'active' => true, 'type' => isset( $settings['submitActionType'] ) && 'redirect' === $settings['submitActionType'] ? 'redirect' : 'message', 'message' => ! empty( $settings['submitSuccessMessage'] ) ? $settings['submitSuccessMessage'] : $default_message, 'redirectUrl' => ! empty( $settings['redirectUrl'] ) ? $settings['redirectUrl'] : '', ) ); } $chosen = null; $fallback = null; foreach ( $confirmations as $conf ) { if ( ! is_array( $conf ) || ( isset( $conf['active'] ) && ! $conf['active'] ) ) { continue; } $conf = $this->normalize_confirmation( $conf ); if ( WPVFB_Logic::normalize( $conf ) ) { if ( WPVFB_Logic::passes( $conf, $submitted_data ) ) { $chosen = $conf; break; } } elseif ( null === $fallback ) { $fallback = $conf; } } if ( ! $chosen ) { $chosen = $fallback; } $tags = $this->merge_tags( $form_name, $field_list, $submitted_data, $entry_id ); $response = array( 'success' => true, 'message' => $default_message ); if ( $chosen && isset( $chosen['type'] ) && 'redirect' === $chosen['type'] && ! empty( $chosen['redirectUrl'] ) ) { $url = esc_url_raw( $this->replace_tags( $chosen['redirectUrl'], $tags, 'rawurlencode' ) ); if ( $url ) { $response['redirect_url'] = $url; } } if ( $chosen && isset( $chosen['type'] ) && 'quiz_recommendation' === $chosen['type'] ) { $response = array_merge( $response, $this->quiz_outcome( $chosen, $tags, $field_list, $submitted_data ) ); } elseif ( $chosen && ! empty( $chosen['message'] ) ) { $response['message'] = $this->render_message( $chosen['message'], $tags, $field_list, $submitted_data ); } $response['hide_form'] = ! empty( $settings['hideFormOnSubmit'] ); // "Hide form after submission" switched off: the storefront keeps the form under the message. if ( isset( $settings['hideFormOnSubmit'] ) && false === $settings['hideFormOnSubmit'] ) { $response['show_form'] = true; } return $response; } /** Confirmation text: merge tags (escaped), {all_fields}, line breaks for plain text, kses. */ private function render_message( $template, $tags, $field_list, $submitted_data ) { $message = $this->replace_tags( $template, $tags, 'esc_html' ); if ( ! self::is_rich_text( $template ) ) { $message = nl2br( $message ); } if ( false !== strpos( $message, '{all_fields}' ) ) { $message = str_replace( '{all_fields}', $this->all_fields_table( $field_list, $submitted_data ), $message ); } return wp_kses_post( $message ); } /** * Quiz / recommendation outcome (the reference builder `quiz_recommendation`): the first rule whose minScore..maxScore * range holds the score (sum of the selected options' points, computed in compute_quiz()). */ private function quiz_outcome( $conf, $tags, $field_list, $submitted_data ) { $score = isset( $submitted_data['quiz_score'] ) ? (float) $submitted_data['quiz_score'] : 0.0; $rule = null; foreach ( isset( $conf['quizOutcomeRules'] ) && is_array( $conf['quizOutcomeRules'] ) ? $conf['quizOutcomeRules'] : array() as $r ) { if ( ! is_array( $r ) ) { continue; } $min = isset( $r['minScore'] ) && is_numeric( $r['minScore'] ) ? (float) $r['minScore'] : -INF; $max = isset( $r['maxScore'] ) && is_numeric( $r['maxScore'] ) ? (float) $r['maxScore'] : INF; if ( $score >= $min && $score <= $max ) { $rule = $r; break; } } $html = '
'; if ( ! empty( $conf['quizHeading'] ) ) { $html .= '

' . esc_html( $this->replace_tags( $conf['quizHeading'], $tags ) ) . '

'; } if ( ! isset( $conf['quizShowScore'] ) || false !== $conf['quizShowScore'] ) { /* translators: %s: quiz score */ $html .= '

' . esc_html( sprintf( /* translators: %s: score */ __( 'Your score: %s', 'efe-visual-forms' ), $this->format_score( $score ) ) ) . '

'; } if ( $rule ) { if ( ! empty( $rule['title'] ) ) { $html .= '

' . esc_html( $this->replace_tags( $rule['title'], $tags ) ) . '

'; } $text = ! empty( $rule['description'] ) ? $rule['description'] : ( ! empty( $rule['message'] ) ? $rule['message'] : '' ); if ( '' !== $text ) { $html .= '
' . $this->render_message( $text, $tags, $field_list, $submitted_data ) . '
'; } if ( ! empty( $rule['productTitle'] ) ) { $html .= '
'; if ( ! empty( $rule['productImage'] ) ) { $html .= '' . esc_attr( $rule['productTitle'] ) . ''; } $html .= '' . esc_html( $rule['productTitle'] ) . ''; if ( ! empty( $rule['productPrice'] ) ) { $html .= ' ' . esc_html( $rule['productPrice'] ) . ''; } $url = ! empty( $rule['productUrl'] ) ? $rule['productUrl'] : ''; if ( $url ) { $html .= ' ' . esc_html( ! empty( $rule['buttonText'] ) ? $rule['buttonText'] : __( 'View product', 'efe-visual-forms' ) ) . ''; } $html .= '
'; } } elseif ( ! empty( $conf['message'] ) ) { $html .= '
' . $this->render_message( $conf['message'], $tags, $field_list, $submitted_data ) . '
'; } $html .= '
'; $out = array( 'message' => wp_kses_post( $html ), 'quiz_score' => $score ); if ( ! isset( $conf['quizAllowRetake'] ) || false !== $conf['quizAllowRetake'] ) { $out['allow_retake'] = true; $out['retake_text'] = ! empty( $conf['quizRetakeText'] ) ? sanitize_text_field( $conf['quizRetakeText'] ) : __( 'Retake quiz', 'efe-visual-forms' ); } return $out; } public function update_entry( $request ) { global $wpdb; $id = $request['id']; $params = $request->get_json_params(); $table_entries = $wpdb->prefix . 'wpvfb_entries'; if ( ! isset( $params['data'] ) ) { return new WP_Error( 'missing_data', __( 'Entry data is required', 'efe-visual-forms' ), array( 'status' => 400 ) ); } $entry = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %s', $table_entries, $id ) ); if ( ! $entry ) { return new WP_Error( 'entry_not_found', __( 'Entry not found', 'efe-visual-forms' ), array( 'status' => 404 ) ); } $wpdb->update( $table_entries, array( 'data' => wp_json_encode( $params['data'] ), ), array( 'id' => $id ) ); return rest_ensure_response( array( 'success' => true ) ); } public function delete_entry( $request ) { global $wpdb; $id = $request['id']; $table_entries = $wpdb->prefix . 'wpvfb_entries'; $deleted = $wpdb->delete( $table_entries, array( 'id' => $id ) ); if ( ! $deleted ) { return new WP_Error( 'delete_failed', __( 'Could not delete entry', 'efe-visual-forms' ), array( 'status' => 500 ) ); } return rest_ensure_response( array( 'success' => true ) ); } } function wpvfb_uuid() { if ( function_exists( 'wp_generate_uuid4' ) ) { return wp_generate_uuid4(); } return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', wp_rand( 0, 0xffff ), wp_rand( 0, 0xffff ), wp_rand( 0, 0xffff ), wp_rand( 0, 0x0fff ) | 0x4000, wp_rand( 0, 0x3fff ) | 0x8000, wp_rand( 0, 0xffff ), wp_rand( 0, 0xffff ), wp_rand( 0, 0xffff ) ); }