admin_url( 'admin-ajax.php' ), 'previewNonce' => wp_create_nonce( 'accua_forms_field_preview' ), // For the inline slug validation while adding a field. 'existingSlugs' => array_values( array_map( 'strval', array_keys( (array) get_option( 'accua_forms_avail_fields', array() ) ) ) ), 'l10n' => array( 'allowedExtensionsLabel' => __( 'Allowed extensions', 'contact-forms' ) . ':', 'allowedExtensionsHelp' => __( 'Enter one extension per line without dot (e.g. pdf, jpg, docx). Leave blank to use defaults.', 'contact-forms' ), 'allowedValuesLabel' => __( 'Allowed values', 'contact-forms' ) . ':', 'allowedValuesHelp' => __( 'Enter one value per line, in the format key|label. The key is the value stored in the database. The label is optional.', 'contact-forms' ), 'queryParamsLabel' => __( 'Additional query parameters', 'contact-forms' ) . ':', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- This is example help text, not actual code. 'queryParamsHelp' => __( 'Optional: Filter posts using query parameters (e.g., authors=admin or meta_key=featured&meta_value=1). Add post_status=publish,private to also include private posts (their titles become visible to all visitors of this form). Leave empty for all published posts of the selected type.', 'contact-forms' ), 'defaultValueLabel' => __( 'Default value(s)', 'contact-forms' ) . ':', 'defaultValueHelp' => __( 'For multiple default values in multiple select and multiple checkboxes, use | as separator.', 'contact-forms' ), 'customHtmlLabel' => __( 'Custom HTML content', 'contact-forms' ) . ':', 'customHtmlHelp' => __( 'The HTML code that will be rendered at this position in the form.', 'contact-forms' ), 'errorLabelRequired' => __( 'Field label is required.', 'contact-forms' ), 'errorSlugRequired' => __( 'Field slug is required.', 'contact-forms' ), 'errorAllowedValuesRequired' => __( 'Allowed values are required for this field type.', 'contact-forms' ), /* translators: %s is the field slug */ 'confirmDelete' => __( 'Delete the field "%s" permanently? Forms using it will no longer show it.', 'contact-forms' ), // Same wording as the server-side save validation. 'slugInvalidChars' => __( 'Only letters, numbers, hyphens, and underscores allowed in field slug', 'contact-forms' ), 'slugDoubleUnderscore' => __( 'The field slug cannot start with two underscores (__)', 'contact-forms' ), 'slugTooLong' => __( 'The field slug cannot be longer than 70 characters', 'contact-forms' ), 'slugExists' => __( 'This slug is already used by another field.', 'contact-forms' ), ), ) ); } /** * Get the available field types. * * @return array Associative array of type_id => translated label. */ function accua_forms_fields_get_types() { $types = array( 'textfield' => __( 'Text Field', 'contact-forms' ), 'textarea' => __( 'Text Area', 'contact-forms' ), 'email' => __( 'Email', 'contact-forms' ), 'autoreply_email' => __( 'Autoreply Email', 'contact-forms' ), 'telephone' => __( 'Telephone', 'contact-forms' ), 'checkbox' => __( 'Checkbox', 'contact-forms' ), 'select' => __( 'Select', 'contact-forms' ), 'radio' => __( 'Radio buttons', 'contact-forms' ), 'multiselect' => __( 'Multiple selections area', 'contact-forms' ), 'multicheckbox' => __( 'Multiple checkboxes', 'contact-forms' ), 'post-select' => __( 'Post select', 'contact-forms' ), 'post-multicheckbox' => __( 'Multiple post checkboxes', 'contact-forms' ), 'colorpicker' => __( 'Color picker', 'contact-forms' ), 'hidden' => __( 'Hidden value', 'contact-forms' ), 'file' => __( 'File upload', 'contact-forms' ), 'submit' => __( 'Submit button', 'contact-forms' ), 'html' => __( 'Custom HTML', 'contact-forms' ), 'captcha' => __( 'Captcha', 'contact-forms' ), 'captcha_v3' => __( 'Captcha (reCAPTCHA v3)', 'contact-forms' ), 'cap' => __( 'Captcha (Cap)', 'contact-forms' ), 'turnstile' => __( 'Captcha (Turnstile)', 'contact-forms' ), 'password' => __( 'Password', 'contact-forms' ), 'password-and-confirm' => __( 'Password and password confirmation', 'contact-forms' ), 'date' => __( 'Date', 'contact-forms' ), ); /** * Filter the available field types. * * @param array $types Associative array of type_id => label. */ return apply_filters( 'accua_forms_field_types', $types ); } /** * Validate and filter a date string (YYYY-MM-DD). * * @param string $value Date string. * @return string Valid date or empty string. */ function accua_forms_filter_date( $value ) { if ( ( $value !== '' ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) { try { $date = new DateTime( $value ); if ( $date ) { return $value; } } catch ( Exception $e ) { } } return ''; } /** * Validate and filter field data from form submission. * * @param array $post Form POST data. * @param array $old_data Previous field data (for edits). * @return array { 'data' => array, 'valid' => bool, 'message' => string } */ function accua_forms_fields_filter_values( $post, $old_data = array() ) { $data = array( 'version' => 2, 'id' => $post['form-field-id'], 'name' => $post['form-field-name'], 'type' => $post['form-field-type'], 'description' => $post['form-field-description'], 'default_value' => $post['form-field-default-value'], 'default_date_value' => $post['form-field-default-date-value'], 'allowed_values' => $post['form-field-allowed-values'], 'allowed_extensions' => '', 'min_date' => $post['form-field-min-of-date'], 'max_date' => $post['form-field-max-of-date'], 'custom_required_message' => sanitize_text_field( $post['form-field-custom-required-message'] ), 'custom_format_message' => sanitize_text_field( $post['form-field-custom-format-message'] ), // Unchecked checkboxes are absent from the POST, so the key is always // written explicitly: a missing key means "never saved on this page" // (relevant for the upgrade migration in accua_forms_install()). 'essential_column' => empty( $post['form-field-essential-column'] ) ? 0 : 1, ); $valid = true; $message = ''; if ( trim( $data['name'] ) === '' ) { $message .= '
' . __( 'Field label is required', 'contact-forms' ) . '
'; $valid = false; } $types = accua_forms_fields_get_types(); if ( ! isset( $types[ $data['type'] ] ) ) { // An unchanged type that is not currently registered is kept as-is: // the extension plugin providing it may just be deactivated (or it is // a legacy type). The edit form offers it as a "(currently not // registered)" option, so editing the field must not overwrite it. $keep_unregistered = ! empty( $old_data['type'] ) && $data['type'] === $old_data['type']; if ( ! $keep_unregistered ) { $message .= '' . __( 'Invalid type', 'contact-forms' ) . '
'; $valid = false; $data['type'] = 'textfield'; } } if ( ! current_user_can( 'unfiltered_html' ) ) { $filter_fields = array( 'name', 'description', 'default_value', 'allowed_values' ); foreach ( $filter_fields as $k ) { $data[ $k ] = wp_kses( $data[ $k ], 'post' ); } } if ( $data['type'] == 'file' ) { $data['allowed_extensions'] = accua_forms_filter_extensions( $data['allowed_values'] ); } // Require allowed values for types that need options $types_needing_options = array( 'select', 'radio', 'multiselect', 'multicheckbox' ); if ( in_array( $data['type'], $types_needing_options ) && trim( $data['allowed_values'] ) === '' ) { $message .= '' . __( 'Allowed values are required for this field type', 'contact-forms' ) . '
'; $valid = false; } $dates = array( 'default_date_value' => __( 'Invalid default date', 'contact-forms' ), 'min_date' => __( 'Invalid min date', 'contact-forms' ), 'max_date' => __( 'Invalid max date', 'contact-forms' ), ); foreach ( $dates as $k => $errormsg ) { if ( $data[ $k ] !== '' ) { $data[ $k ] = accua_forms_filter_date( $data[ $k ] ); if ( $data[ $k ] === '' ) { $message .= '' . $errormsg . '
'; $valid = false; } } } return array( 'data' => $data, 'valid' => $valid, 'message' => $message, ); } /** * Render the Fields admin page (request handling + output). */ function accua_forms_fields_page() { $message = ''; $avail_fields = get_option( 'accua_forms_avail_fields', array() ); $default_form_values = array( 'version' => 1, 'id' => '', 'name' => '', 'type' => 'textfield', 'description' => '', 'default_value' => '', 'default_date_value' => '', 'allowed_values' => '', 'allowed_extensions' => '', 'min_date' => '', 'max_date' => '', 'custom_required_message' => '', 'custom_format_message' => '', 'essential_column' => 0, ); $editing = false; $adding = true; $message_type = ''; if ( ! empty( $_POST['action'] ) ) { check_admin_referer( 'edit_form_field', '_wpnonce_edit_form_field' ); $post = stripslashes_deep( $_POST ) + $default_form_values; switch ( $post['action'] ) { case 'edit-form-field': if ( empty( $avail_fields[ $post['form-field-id'] ] ) ) { $message .= 'Field "' . esc_html( sanitize_text_field( $post['form-field-id'] ) ) . '" doesn\'t exists
'; $message_type = 'error'; } else { if ( empty( $post['delete-field'] ) ) { $filtered_data = accua_forms_fields_filter_values( $post, $avail_fields[ $post['form-field-id'] ] ); $message .= $filtered_data['message']; if ( $filtered_data['valid'] ) { $avail_fields[ $post['form-field-id'] ] = $filtered_data['data']; /* translators: %s is the field slug */ $message .= '' . sprintf( __( 'Field "%s" updated', 'contact-forms' ), esc_html( $post['form-field-id'] ) ) . '
'; $message_type = 'success'; update_option( 'accua_forms_avail_fields', $avail_fields ); do_action( 'accua_forms_field_updated', $avail_fields[ $post['form-field-id'] ] ); } else { $message_type = 'error'; $editing = true; $adding = false; $default_form_values = $filtered_data['data']; } } else { $deleting_field = $avail_fields[ $post['form-field-id'] ]; unset( $avail_fields[ $post['form-field-id'] ] ); /* translators: %s is the field slug */ $message .= '' . sprintf( __( 'Field "%s" deleted', 'contact-forms' ), esc_html( $post['form-field-id'] ) ) . '
'; $message_type = 'success'; do_action( 'accua_forms_field_deleted', $deleting_field ); update_option( 'accua_forms_avail_fields', $avail_fields ); } } break; case 'add-form-field': $fill_form_fields = true; $valid = true; // New slugs are lowercased. Slugs are case-sensitive everywhere // (PHP array keys, and the submission values column since 2.3.0), // so "role" and "Role" are two different fields - legal, but a // confusing pair to end up with by accident. Existing mixed-case // slugs are never touched: this runs only when adding a field. if ( isset( $post['form-field-id'] ) && is_string( $post['form-field-id'] ) ) { $post['form-field-id'] = strtolower( $post['form-field-id'] ); } if ( empty( $post['form-field-id'] ) || ! preg_match( '/^[a-z0-9_-]+$/i', $post['form-field-id'] ) ) { $message .= '' . __( 'Only letters, numbers, hyphens, and underscores allowed in field slug', 'contact-forms' ) . '
'; $valid = false; } if ( substr( $post['form-field-id'], 0, 2 ) == '__' ) { $message .= '' . __( 'The field slug cannot start with two underscores (__)', 'contact-forms' ) . '
'; $valid = false; } if ( ! empty( $avail_fields[ $post['form-field-id'] ] ) ) { /* translators: %s is the field slug */ $message .= sprintf( __( 'A field with slug "%s" already exists.
Field was not added.
', 'contact-forms' ), esc_html( $post['form-field-id'] ) ); $valid = false; } if ( strlen( $post['form-field-id'] ) > 70 ) { $message .= '' . __( 'The field slug cannot be longer than 70 characters', 'contact-forms' ) . '
'; $valid = false; } $filtered_data = accua_forms_fields_filter_values( $post ); $message .= $filtered_data['message']; $valid = $valid && $filtered_data['valid']; if ( $valid ) { $fill_form_fields = false; $avail_fields[ $post['form-field-id'] ] = $filtered_data['data']; update_option( 'accua_forms_avail_fields', $avail_fields ); /* translators: %s is the field slug */ $message .= '' . sprintf( __( 'Field "%s" created', 'contact-forms' ), esc_html( $post['form-field-id'] ) ) . '
'; $message_type = 'success'; do_action( 'accua_forms_field_added', $avail_fields[ $post['form-field-id'] ] ); } else { $message_type = 'error'; } if ( $fill_form_fields ) { $editing = true; $default_form_values = $filtered_data['data']; } break; } } elseif ( ! empty( $_GET['edit-fid'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only field lookup $fid = sanitize_text_field( wp_unslash( $_GET['edit-fid'] ) ); if ( empty( $avail_fields[ $fid ] ) ) { /* translators: %s is the field slug */ $message .= sprintf( __( 'Field "%s" doesn\'t exists', 'contact-forms' ), esc_html( $fid ) ); } else { $adding = false; $editing = true; $default_form_values = $avail_fields[ $fid ] + $default_form_values; } } elseif ( ! empty( $_GET['delete-fid'] ) ) { $fid = sanitize_text_field( wp_unslash( $_GET['delete-fid'] ) ); check_admin_referer( 'delete_form_field_' . $fid ); if ( empty( $avail_fields[ $fid ] ) ) { /* translators: %s is the field slug */ $message .= '' . sprintf( __( 'Field "%s" doesn\'t exists', 'contact-forms' ), esc_html( $fid ) ) . '
'; $message_type = 'error'; } else { $deleting_field = $avail_fields[ $fid ]; unset( $avail_fields[ $fid ] ); update_option( 'accua_forms_avail_fields', $avail_fields ); /* translators: %s is the field slug */ $message .= '' . sprintf( __( 'Field "%s" deleted', 'contact-forms' ), esc_html( $fid ) ) . '
'; $message_type = 'success'; do_action( 'accua_forms_field_deleted', $deleting_field ); } } elseif ( ! empty( $_GET['fields'] ) && is_array( $_GET['fields'] ) ) { $bulk_action = ''; if ( isset( $_GET['action'] ) && $_GET['action'] !== '-1' ) { $bulk_action = sanitize_text_field( wp_unslash( $_GET['action'] ) ); } elseif ( isset( $_GET['action2'] ) && $_GET['action2'] !== '-1' ) { $bulk_action = sanitize_text_field( wp_unslash( $_GET['action2'] ) ); } if ( 'delete' === $bulk_action ) { check_admin_referer( 'bulk-fields' ); $fields_to_delete = array_map( 'sanitize_text_field', wp_unslash( $_GET['fields'] ) ); $deleted_count = 0; foreach ( $fields_to_delete as $fid ) { if ( ! empty( $avail_fields[ $fid ] ) ) { do_action( 'accua_forms_field_deleted', $avail_fields[ $fid ] ); unset( $avail_fields[ $fid ] ); $deleted_count++; } } if ( $deleted_count > 0 ) { update_option( 'accua_forms_avail_fields', $avail_fields ); /* translators: %d is the number of deleted fields */ $message .= '' . sprintf( _n( '%d field deleted.', '%d fields deleted.', $deleted_count, 'contact-forms' ), $deleted_count ) . '
'; $message_type = 'success'; } } } if ( $default_form_values['version'] >= 2 && $default_form_values['type'] == 'file' ) { // Show allowed_extensions value in allowed_values field $default_form_values['allowed_values'] = $default_form_values['allowed_extensions']; } ?>' . esc_html__( 'This field has no visible output on the page with the current settings (hidden fields, or captcha fields whose keys are not configured).', 'contact-forms' ) . '
'; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Form render manages its own escaping echo $rendered; wp_print_footer_scripts(); // The preview validates like a real form (the field is marked required // for demonstration) but must never actually submit: wrap the generated // submit handler so a passing validation shows a note instead. $preview_note = wp_json_encode( '' . esc_html__( 'This is a preview: the form is not actually submitted.', 'contact-forms' ) . '
' ); echo ''; echo ''; die( '' ); } add_action( 'wp_ajax_accua_forms_field_preview', 'accua_forms_field_preview' );