| 1 |
<?php |
| 2 |
/** |
| 3 |
* Fields admin page - rendering, request handlers, and helpers. |
| 4 |
* |
| 5 |
* @package ContactForms |
| 6 |
* @since 2.2.5 |
| 7 |
*/ |
| 8 |
|
| 9 |
defined( 'ABSPATH' ) || exit; |
| 10 |
|
| 11 |
/** |
| 12 |
* Enqueue scripts for the Fields admin page. |
| 13 |
*/ |
| 14 |
function accua_forms_fields_page_enqueue_scripts() { |
| 15 |
wp_enqueue_script( |
| 16 |
'accua-forms-fields-page', |
| 17 |
plugins_url( 'assets/js/admin/fields-page.js', ACCUA_FORMS_FILE ), |
| 18 |
array( 'jquery', 'wp-a11y' ), |
| 19 |
ACCUA_FORMS_JS_VERSION, |
| 20 |
true |
| 21 |
); |
| 22 |
|
| 23 |
wp_localize_script( 'accua-forms-fields-page', 'accuaFieldsPage', array( |
| 24 |
'ajaxUrl' => admin_url( 'admin-ajax.php' ), |
| 25 |
'previewNonce' => wp_create_nonce( 'accua_forms_field_preview' ), |
| 26 |
// For the inline slug validation while adding a field. |
| 27 |
'existingSlugs' => array_values( array_map( 'strval', array_keys( (array) get_option( 'accua_forms_avail_fields', array() ) ) ) ), |
| 28 |
'l10n' => array( |
| 29 |
'allowedExtensionsLabel' => __( 'Allowed extensions', 'contact-forms' ) . ':', |
| 30 |
'allowedExtensionsHelp' => __( 'Enter one extension per line without dot (e.g. pdf, jpg, docx). Leave blank to use defaults.', 'contact-forms' ), |
| 31 |
'allowedValuesLabel' => __( 'Allowed values', 'contact-forms' ) . ':', |
| 32 |
'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' ), |
| 33 |
'queryParamsLabel' => __( 'Additional query parameters', 'contact-forms' ) . ':', |
| 34 |
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- This is example help text, not actual code. |
| 35 |
'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' ), |
| 36 |
'defaultValueLabel' => __( 'Default value(s)', 'contact-forms' ) . ':', |
| 37 |
'defaultValueHelp' => __( 'For multiple default values in multiple select and multiple checkboxes, use | as separator.', 'contact-forms' ), |
| 38 |
'customHtmlLabel' => __( 'Custom HTML content', 'contact-forms' ) . ':', |
| 39 |
'customHtmlHelp' => __( 'The HTML code that will be rendered at this position in the form.', 'contact-forms' ), |
| 40 |
'errorLabelRequired' => __( 'Field label is required.', 'contact-forms' ), |
| 41 |
'errorSlugRequired' => __( 'Field slug is required.', 'contact-forms' ), |
| 42 |
'errorAllowedValuesRequired' => __( 'Allowed values are required for this field type.', 'contact-forms' ), |
| 43 |
/* translators: %s is the field slug */ |
| 44 |
'confirmDelete' => __( 'Delete the field "%s" permanently? Forms using it will no longer show it.', 'contact-forms' ), |
| 45 |
// Same wording as the server-side save validation. |
| 46 |
'slugInvalidChars' => __( 'Only letters, numbers, hyphens, and underscores allowed in field slug', 'contact-forms' ), |
| 47 |
'slugDoubleUnderscore' => __( 'The field slug cannot start with two underscores (__)', 'contact-forms' ), |
| 48 |
'slugTooLong' => __( 'The field slug cannot be longer than 70 characters', 'contact-forms' ), |
| 49 |
'slugExists' => __( 'This slug is already used by another field.', 'contact-forms' ), |
| 50 |
), |
| 51 |
) ); |
| 52 |
} |
| 53 |
|
| 54 |
/** |
| 55 |
* Get the available field types. |
| 56 |
* |
| 57 |
* @return array Associative array of type_id => translated label. |
| 58 |
*/ |
| 59 |
function accua_forms_fields_get_types() { |
| 60 |
$types = array( |
| 61 |
'textfield' => __( 'Text Field', 'contact-forms' ), |
| 62 |
'textarea' => __( 'Text Area', 'contact-forms' ), |
| 63 |
'email' => __( 'Email', 'contact-forms' ), |
| 64 |
'autoreply_email' => __( 'Autoreply Email', 'contact-forms' ), |
| 65 |
'telephone' => __( 'Telephone', 'contact-forms' ), |
| 66 |
'checkbox' => __( 'Checkbox', 'contact-forms' ), |
| 67 |
'select' => __( 'Select', 'contact-forms' ), |
| 68 |
'radio' => __( 'Radio buttons', 'contact-forms' ), |
| 69 |
'multiselect' => __( 'Multiple selections area', 'contact-forms' ), |
| 70 |
'multicheckbox' => __( 'Multiple checkboxes', 'contact-forms' ), |
| 71 |
'post-select' => __( 'Post select', 'contact-forms' ), |
| 72 |
'post-multicheckbox' => __( 'Multiple post checkboxes', 'contact-forms' ), |
| 73 |
'colorpicker' => __( 'Color picker', 'contact-forms' ), |
| 74 |
'hidden' => __( 'Hidden value', 'contact-forms' ), |
| 75 |
'file' => __( 'File upload', 'contact-forms' ), |
| 76 |
'submit' => __( 'Submit button', 'contact-forms' ), |
| 77 |
'html' => __( 'Custom HTML', 'contact-forms' ), |
| 78 |
'captcha' => __( 'Captcha', 'contact-forms' ), |
| 79 |
'captcha_v3' => __( 'Captcha (reCAPTCHA v3)', 'contact-forms' ), |
| 80 |
'cap' => __( 'Captcha (Cap)', 'contact-forms' ), |
| 81 |
'turnstile' => __( 'Captcha (Turnstile)', 'contact-forms' ), |
| 82 |
'password' => __( 'Password', 'contact-forms' ), |
| 83 |
'password-and-confirm' => __( 'Password and password confirmation', 'contact-forms' ), |
| 84 |
'date' => __( 'Date', 'contact-forms' ), |
| 85 |
); |
| 86 |
|
| 87 |
/** |
| 88 |
* Filter the available field types. |
| 89 |
* |
| 90 |
* @param array $types Associative array of type_id => label. |
| 91 |
*/ |
| 92 |
return apply_filters( 'accua_forms_field_types', $types ); |
| 93 |
} |
| 94 |
|
| 95 |
/** |
| 96 |
* Validate and filter a date string (YYYY-MM-DD). |
| 97 |
* |
| 98 |
* @param string $value Date string. |
| 99 |
* @return string Valid date or empty string. |
| 100 |
*/ |
| 101 |
function accua_forms_filter_date( $value ) { |
| 102 |
if ( ( $value !== '' ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) { |
| 103 |
try { |
| 104 |
$date = new DateTime( $value ); |
| 105 |
if ( $date ) { |
| 106 |
return $value; |
| 107 |
} |
| 108 |
} catch ( Exception $e ) { |
| 109 |
} |
| 110 |
} |
| 111 |
return ''; |
| 112 |
} |
| 113 |
|
| 114 |
/** |
| 115 |
* Validate and filter field data from form submission. |
| 116 |
* |
| 117 |
* @param array $post Form POST data. |
| 118 |
* @param array $old_data Previous field data (for edits). |
| 119 |
* @return array { 'data' => array, 'valid' => bool, 'message' => string } |
| 120 |
*/ |
| 121 |
function accua_forms_fields_filter_values( $post, $old_data = array() ) { |
| 122 |
$data = array( |
| 123 |
'version' => 2, |
| 124 |
'id' => $post['form-field-id'], |
| 125 |
'name' => $post['form-field-name'], |
| 126 |
'type' => $post['form-field-type'], |
| 127 |
'description' => $post['form-field-description'], |
| 128 |
'default_value' => $post['form-field-default-value'], |
| 129 |
'default_date_value' => $post['form-field-default-date-value'], |
| 130 |
'allowed_values' => $post['form-field-allowed-values'], |
| 131 |
'allowed_extensions' => '', |
| 132 |
'min_date' => $post['form-field-min-of-date'], |
| 133 |
'max_date' => $post['form-field-max-of-date'], |
| 134 |
'custom_required_message' => sanitize_text_field( $post['form-field-custom-required-message'] ), |
| 135 |
'custom_format_message' => sanitize_text_field( $post['form-field-custom-format-message'] ), |
| 136 |
// Unchecked checkboxes are absent from the POST, so the key is always |
| 137 |
// written explicitly: a missing key means "never saved on this page" |
| 138 |
// (relevant for the upgrade migration in accua_forms_install()). |
| 139 |
'essential_column' => empty( $post['form-field-essential-column'] ) ? 0 : 1, |
| 140 |
); |
| 141 |
$valid = true; |
| 142 |
$message = ''; |
| 143 |
|
| 144 |
if ( trim( $data['name'] ) === '' ) { |
| 145 |
$message .= '<p>' . __( 'Field label is required', 'contact-forms' ) . '</p>'; |
| 146 |
$valid = false; |
| 147 |
} |
| 148 |
|
| 149 |
$types = accua_forms_fields_get_types(); |
| 150 |
if ( ! isset( $types[ $data['type'] ] ) ) { |
| 151 |
// An unchanged type that is not currently registered is kept as-is: |
| 152 |
// the extension plugin providing it may just be deactivated (or it is |
| 153 |
// a legacy type). The edit form offers it as a "(currently not |
| 154 |
// registered)" option, so editing the field must not overwrite it. |
| 155 |
$keep_unregistered = ! empty( $old_data['type'] ) && $data['type'] === $old_data['type']; |
| 156 |
if ( ! $keep_unregistered ) { |
| 157 |
$message .= '<p>' . __( 'Invalid type', 'contact-forms' ) . '</p>'; |
| 158 |
$valid = false; |
| 159 |
$data['type'] = 'textfield'; |
| 160 |
} |
| 161 |
} |
| 162 |
|
| 163 |
if ( ! current_user_can( 'unfiltered_html' ) ) { |
| 164 |
$filter_fields = array( 'name', 'description', 'default_value', 'allowed_values' ); |
| 165 |
foreach ( $filter_fields as $k ) { |
| 166 |
$data[ $k ] = wp_kses( $data[ $k ], 'post' ); |
| 167 |
} |
| 168 |
} |
| 169 |
|
| 170 |
if ( $data['type'] == 'file' ) { |
| 171 |
$data['allowed_extensions'] = accua_forms_filter_extensions( $data['allowed_values'] ); |
| 172 |
} |
| 173 |
|
| 174 |
// Require allowed values for types that need options |
| 175 |
$types_needing_options = array( 'select', 'radio', 'multiselect', 'multicheckbox' ); |
| 176 |
if ( in_array( $data['type'], $types_needing_options ) && trim( $data['allowed_values'] ) === '' ) { |
| 177 |
$message .= '<p>' . __( 'Allowed values are required for this field type', 'contact-forms' ) . '</p>'; |
| 178 |
$valid = false; |
| 179 |
} |
| 180 |
|
| 181 |
$dates = array( |
| 182 |
'default_date_value' => __( 'Invalid default date', 'contact-forms' ), |
| 183 |
'min_date' => __( 'Invalid min date', 'contact-forms' ), |
| 184 |
'max_date' => __( 'Invalid max date', 'contact-forms' ), |
| 185 |
); |
| 186 |
foreach ( $dates as $k => $errormsg ) { |
| 187 |
if ( $data[ $k ] !== '' ) { |
| 188 |
$data[ $k ] = accua_forms_filter_date( $data[ $k ] ); |
| 189 |
if ( $data[ $k ] === '' ) { |
| 190 |
$message .= '<p>' . $errormsg . '</p>'; |
| 191 |
$valid = false; |
| 192 |
} |
| 193 |
} |
| 194 |
} |
| 195 |
|
| 196 |
return array( |
| 197 |
'data' => $data, |
| 198 |
'valid' => $valid, |
| 199 |
'message' => $message, |
| 200 |
); |
| 201 |
} |
| 202 |
|
| 203 |
/** |
| 204 |
* Render the Fields admin page (request handling + output). |
| 205 |
*/ |
| 206 |
function accua_forms_fields_page() { |
| 207 |
$message = ''; |
| 208 |
|
| 209 |
$avail_fields = get_option( 'accua_forms_avail_fields', array() ); |
| 210 |
|
| 211 |
$default_form_values = array( |
| 212 |
'version' => 1, |
| 213 |
'id' => '', |
| 214 |
'name' => '', |
| 215 |
'type' => 'textfield', |
| 216 |
'description' => '', |
| 217 |
'default_value' => '', |
| 218 |
'default_date_value' => '', |
| 219 |
'allowed_values' => '', |
| 220 |
'allowed_extensions' => '', |
| 221 |
'min_date' => '', |
| 222 |
'max_date' => '', |
| 223 |
'custom_required_message' => '', |
| 224 |
'custom_format_message' => '', |
| 225 |
'essential_column' => 0, |
| 226 |
); |
| 227 |
|
| 228 |
$editing = false; |
| 229 |
$adding = true; |
| 230 |
$message_type = ''; |
| 231 |
|
| 232 |
if ( ! empty( $_POST['action'] ) ) { |
| 233 |
check_admin_referer( 'edit_form_field', '_wpnonce_edit_form_field' ); |
| 234 |
$post = stripslashes_deep( $_POST ) + $default_form_values; |
| 235 |
switch ( $post['action'] ) { |
| 236 |
case 'edit-form-field': |
| 237 |
if ( empty( $avail_fields[ $post['form-field-id'] ] ) ) { |
| 238 |
$message .= '<p>Field "' . esc_html( sanitize_text_field( $post['form-field-id'] ) ) . '" doesn\'t exists</p>'; |
| 239 |
$message_type = 'error'; |
| 240 |
} else { |
| 241 |
if ( empty( $post['delete-field'] ) ) { |
| 242 |
$filtered_data = accua_forms_fields_filter_values( $post, $avail_fields[ $post['form-field-id'] ] ); |
| 243 |
$message .= $filtered_data['message']; |
| 244 |
if ( $filtered_data['valid'] ) { |
| 245 |
$avail_fields[ $post['form-field-id'] ] = $filtered_data['data']; |
| 246 |
/* translators: %s is the field slug */ |
| 247 |
$message .= '<p>' . sprintf( __( 'Field "%s" updated', 'contact-forms' ), esc_html( $post['form-field-id'] ) ) . '</p>'; |
| 248 |
$message_type = 'success'; |
| 249 |
update_option( 'accua_forms_avail_fields', $avail_fields ); |
| 250 |
do_action( 'accua_forms_field_updated', $avail_fields[ $post['form-field-id'] ] ); |
| 251 |
} else { |
| 252 |
$message_type = 'error'; |
| 253 |
$editing = true; |
| 254 |
$adding = false; |
| 255 |
$default_form_values = $filtered_data['data']; |
| 256 |
} |
| 257 |
} else { |
| 258 |
$deleting_field = $avail_fields[ $post['form-field-id'] ]; |
| 259 |
unset( $avail_fields[ $post['form-field-id'] ] ); |
| 260 |
/* translators: %s is the field slug */ |
| 261 |
$message .= '<p>' . sprintf( __( 'Field "%s" deleted', 'contact-forms' ), esc_html( $post['form-field-id'] ) ) . '</p>'; |
| 262 |
$message_type = 'success'; |
| 263 |
do_action( 'accua_forms_field_deleted', $deleting_field ); |
| 264 |
update_option( 'accua_forms_avail_fields', $avail_fields ); |
| 265 |
} |
| 266 |
} |
| 267 |
break; |
| 268 |
case 'add-form-field': |
| 269 |
$fill_form_fields = true; |
| 270 |
$valid = true; |
| 271 |
// New slugs are lowercased. Slugs are case-sensitive everywhere |
| 272 |
// (PHP array keys, and the submission values column since 2.3.0), |
| 273 |
// so "role" and "Role" are two different fields - legal, but a |
| 274 |
// confusing pair to end up with by accident. Existing mixed-case |
| 275 |
// slugs are never touched: this runs only when adding a field. |
| 276 |
if ( isset( $post['form-field-id'] ) && is_string( $post['form-field-id'] ) ) { |
| 277 |
$post['form-field-id'] = strtolower( $post['form-field-id'] ); |
| 278 |
} |
| 279 |
if ( empty( $post['form-field-id'] ) || ! preg_match( '/^[a-z0-9_-]+$/i', $post['form-field-id'] ) ) { |
| 280 |
$message .= '<p>' . __( 'Only letters, numbers, hyphens, and underscores allowed in field slug', 'contact-forms' ) . '</p>'; |
| 281 |
$valid = false; |
| 282 |
} |
| 283 |
if ( substr( $post['form-field-id'], 0, 2 ) == '__' ) { |
| 284 |
$message .= '<p>' . __( 'The field slug cannot start with two underscores (__)', 'contact-forms' ) . '</p>'; |
| 285 |
$valid = false; |
| 286 |
} |
| 287 |
if ( ! empty( $avail_fields[ $post['form-field-id'] ] ) ) { |
| 288 |
/* translators: %s is the field slug */ |
| 289 |
$message .= sprintf( __( '<p>A field with slug "%s" already exists.</p><p>Field was not added.</p>', 'contact-forms' ), esc_html( $post['form-field-id'] ) ); |
| 290 |
$valid = false; |
| 291 |
} |
| 292 |
if ( strlen( $post['form-field-id'] ) > 70 ) { |
| 293 |
$message .= '<p>' . __( 'The field slug cannot be longer than 70 characters', 'contact-forms' ) . '</p>'; |
| 294 |
$valid = false; |
| 295 |
} |
| 296 |
$filtered_data = accua_forms_fields_filter_values( $post ); |
| 297 |
$message .= $filtered_data['message']; |
| 298 |
$valid = $valid && $filtered_data['valid']; |
| 299 |
if ( $valid ) { |
| 300 |
$fill_form_fields = false; |
| 301 |
$avail_fields[ $post['form-field-id'] ] = $filtered_data['data']; |
| 302 |
update_option( 'accua_forms_avail_fields', $avail_fields ); |
| 303 |
/* translators: %s is the field slug */ |
| 304 |
$message .= '<p>' . sprintf( __( 'Field "%s" created', 'contact-forms' ), esc_html( $post['form-field-id'] ) ) . '</p>'; |
| 305 |
$message_type = 'success'; |
| 306 |
do_action( 'accua_forms_field_added', $avail_fields[ $post['form-field-id'] ] ); |
| 307 |
} else { |
| 308 |
$message_type = 'error'; |
| 309 |
} |
| 310 |
if ( $fill_form_fields ) { |
| 311 |
$editing = true; |
| 312 |
$default_form_values = $filtered_data['data']; |
| 313 |
} |
| 314 |
break; |
| 315 |
} |
| 316 |
} elseif ( ! empty( $_GET['edit-fid'] ) ) { |
| 317 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only field lookup |
| 318 |
$fid = sanitize_text_field( wp_unslash( $_GET['edit-fid'] ) ); |
| 319 |
if ( empty( $avail_fields[ $fid ] ) ) { |
| 320 |
/* translators: %s is the field slug */ |
| 321 |
$message .= sprintf( __( 'Field "%s" doesn\'t exists', 'contact-forms' ), esc_html( $fid ) ); |
| 322 |
} else { |
| 323 |
$adding = false; |
| 324 |
$editing = true; |
| 325 |
$default_form_values = $avail_fields[ $fid ] + $default_form_values; |
| 326 |
} |
| 327 |
} elseif ( ! empty( $_GET['delete-fid'] ) ) { |
| 328 |
$fid = sanitize_text_field( wp_unslash( $_GET['delete-fid'] ) ); |
| 329 |
check_admin_referer( 'delete_form_field_' . $fid ); |
| 330 |
if ( empty( $avail_fields[ $fid ] ) ) { |
| 331 |
/* translators: %s is the field slug */ |
| 332 |
$message .= '<p>' . sprintf( __( 'Field "%s" doesn\'t exists', 'contact-forms' ), esc_html( $fid ) ) . '</p>'; |
| 333 |
$message_type = 'error'; |
| 334 |
} else { |
| 335 |
$deleting_field = $avail_fields[ $fid ]; |
| 336 |
unset( $avail_fields[ $fid ] ); |
| 337 |
update_option( 'accua_forms_avail_fields', $avail_fields ); |
| 338 |
/* translators: %s is the field slug */ |
| 339 |
$message .= '<p>' . sprintf( __( 'Field "%s" deleted', 'contact-forms' ), esc_html( $fid ) ) . '</p>'; |
| 340 |
$message_type = 'success'; |
| 341 |
do_action( 'accua_forms_field_deleted', $deleting_field ); |
| 342 |
} |
| 343 |
} elseif ( ! empty( $_GET['fields'] ) && is_array( $_GET['fields'] ) ) { |
| 344 |
$bulk_action = ''; |
| 345 |
if ( isset( $_GET['action'] ) && $_GET['action'] !== '-1' ) { |
| 346 |
$bulk_action = sanitize_text_field( wp_unslash( $_GET['action'] ) ); |
| 347 |
} elseif ( isset( $_GET['action2'] ) && $_GET['action2'] !== '-1' ) { |
| 348 |
$bulk_action = sanitize_text_field( wp_unslash( $_GET['action2'] ) ); |
| 349 |
} |
| 350 |
if ( 'delete' === $bulk_action ) { |
| 351 |
check_admin_referer( 'bulk-fields' ); |
| 352 |
$fields_to_delete = array_map( 'sanitize_text_field', wp_unslash( $_GET['fields'] ) ); |
| 353 |
$deleted_count = 0; |
| 354 |
foreach ( $fields_to_delete as $fid ) { |
| 355 |
if ( ! empty( $avail_fields[ $fid ] ) ) { |
| 356 |
do_action( 'accua_forms_field_deleted', $avail_fields[ $fid ] ); |
| 357 |
unset( $avail_fields[ $fid ] ); |
| 358 |
$deleted_count++; |
| 359 |
} |
| 360 |
} |
| 361 |
if ( $deleted_count > 0 ) { |
| 362 |
update_option( 'accua_forms_avail_fields', $avail_fields ); |
| 363 |
/* translators: %d is the number of deleted fields */ |
| 364 |
$message .= '<p>' . sprintf( _n( '%d field deleted.', '%d fields deleted.', $deleted_count, 'contact-forms' ), $deleted_count ) . '</p>'; |
| 365 |
$message_type = 'success'; |
| 366 |
} |
| 367 |
} |
| 368 |
} |
| 369 |
|
| 370 |
if ( $default_form_values['version'] >= 2 && $default_form_values['type'] == 'file' ) { |
| 371 |
// Show allowed_extensions value in allowed_values field |
| 372 |
$default_form_values['allowed_values'] = $default_form_values['allowed_extensions']; |
| 373 |
} |
| 374 |
|
| 375 |
?> |
| 376 |
<div id="accua_forms_fields_page" class="accua_forms_admin_page wrap nosubsub"> |
| 377 |
<h1><?php esc_html_e( 'Contact Forms - Fields', 'contact-forms' ); ?> |
| 378 |
<?php |
| 379 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only search parameter |
| 380 |
$accua_fields_search = isset( $_REQUEST['s'] ) ? trim( sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) ) : ''; |
| 381 |
if ( ! $editing && '' !== $accua_fields_search ) { |
| 382 |
echo '<span class="subtitle">' . sprintf( |
| 383 |
/* translators: %s is the search term the user typed */ |
| 384 |
esc_html__( 'Search results for: %s', 'contact-forms' ), |
| 385 |
'<strong>' . esc_html( $accua_fields_search ) . '</strong>' |
| 386 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Format string and argument are both escaped above |
| 387 |
) . '</span>'; |
| 388 |
} |
| 389 |
?> |
| 390 |
</h1> |
| 391 |
<?php if ( ! $adding ) : ?> |
| 392 |
<p class="accua-fields-back-link"><a href="admin.php?page=accua_forms_fields">← <?php esc_html_e( 'Back to the fields list', 'contact-forms' ); ?></a></p> |
| 393 |
<?php endif; ?> |
| 394 |
<?php if ( $message !== '' ) : ?> |
| 395 |
<div id="ajax-response" class="notice notice-<?php echo $message_type === 'success' ? 'success' : 'error'; ?> is-dismissible"><?php echo wp_kses_post( $message ); ?></div> |
| 396 |
<?php endif; ?> |
| 397 |
|
| 398 |
<div id="col-container"> |
| 399 |
|
| 400 |
<div id="col-right"> |
| 401 |
<div class="col-wrap"> |
| 402 |
|
| 403 |
<div id="accua-field-preview-panel"<?php if ( trim( $default_form_values['name'] ) === '' ) { echo ' style="display:none"'; } ?>> |
| 404 |
<h3><?php esc_html_e( 'Live preview', 'contact-forms' ); ?></h3> |
| 405 |
<p class="description"><?php esc_html_e( 'How this field will look when added to a new form with the default settings. The preview updates as you edit.', 'contact-forms' ); ?> |
| 406 |
<?php esc_html_e( 'The field is treated as mandatory here only to demonstrate the validation messages (leave it empty and click elsewhere to see them); in your forms it is mandatory only if you enable "Required" on the field.', 'contact-forms' ); ?></p> |
| 407 |
<div id="accua-field-preview-wrapper" class="accua-form-preview-loading"> |
| 408 |
<iframe id="accua-field-preview" name="accua-field-preview" title="<?php esc_attr_e( 'Field preview', 'contact-forms' ); ?>" src="about:blank"></iframe> |
| 409 |
</div> |
| 410 |
</div> |
| 411 |
|
| 412 |
<?php |
| 413 |
if ( ! $editing ) { |
| 414 |
require_once __DIR__ . '/class-fields-list-table.php'; |
| 415 |
$fields_table = new Accua_Forms_Fields_List_Table(); |
| 416 |
$fields_table->prepare_items(); |
| 417 |
?> |
| 418 |
<?php |
| 419 |
// The search gets its own GET form, the way the categories and tags screen |
| 420 |
// does it. Sharing the table's form would serialize the bulk-action selects, |
| 421 |
// the bulk nonce and the referer into the URL on every search. |
| 422 |
// search_box() carries the current orderby and order across by itself. |
| 423 |
?> |
| 424 |
<form class="search-form wp-clearfix" method="get"> |
| 425 |
<input type="hidden" name="page" value="accua_forms_fields" /> |
| 426 |
<?php $fields_table->search_box( __( 'Search fields', 'contact-forms' ), 'accua-field' ); ?> |
| 427 |
</form> |
| 428 |
|
| 429 |
<form method="get"> |
| 430 |
<input type="hidden" name="page" value="accua_forms_fields" /> |
| 431 |
<?php if ( '' !== $accua_fields_search ) : ?> |
| 432 |
<?php // Keeps the search when a bulk action or a page link is used. ?> |
| 433 |
<input type="hidden" name="s" value="<?php echo esc_attr( $accua_fields_search ); ?>" /> |
| 434 |
<?php endif; ?> |
| 435 |
<?php $fields_table->display(); ?> |
| 436 |
</form> |
| 437 |
<?php } ?> |
| 438 |
|
| 439 |
</div> |
| 440 |
</div><!-- /col-right --> |
| 441 |
|
| 442 |
<div id="col-left"> |
| 443 |
<div class="col-wrap"> |
| 444 |
|
| 445 |
<?php |
| 446 |
$types = accua_forms_fields_get_types(); |
| 447 |
if ( ! isset( $types[ $default_form_values['type'] ] ) ) { |
| 448 |
// The field's type is not currently registered (deactivated extension |
| 449 |
// plugin or legacy type). Keep it selectable so saving the field does not |
| 450 |
// silently fall back to the first option and overwrite the type. |
| 451 |
/* translators: %s is the raw field type key */ |
| 452 |
$types[ $default_form_values['type'] ] = esc_html( sprintf( __( '%s (currently not registered)', 'contact-forms' ), $default_form_values['type'] ) ); |
| 453 |
} |
| 454 |
?> |
| 455 |
<div class="form-wrap"> |
| 456 |
<h3><?php |
| 457 |
if ( $adding ) { |
| 458 |
esc_html_e( 'Add new field', 'contact-forms' ); |
| 459 |
} else { |
| 460 |
/* translators: %s is the field slug */ |
| 461 |
echo esc_html( sprintf( __( 'Edit field: %s', 'contact-forms' ), $default_form_values['id'] ) ); |
| 462 |
} |
| 463 |
?></h3> |
| 464 |
<form id="addtag" method="post" action="admin.php?page=accua_forms_fields" class="validate"> |
| 465 |
<input type="hidden" name="action" value="<?php echo $adding ? 'add' : 'edit'; ?>-form-field" /> |
| 466 |
<?php wp_nonce_field( 'edit_form_field', '_wpnonce_edit_form_field' ); ?> |
| 467 |
|
| 468 |
<div class="form-field form-required"> |
| 469 |
<label for="tag-name"><?php esc_html_e( 'Field label', 'contact-forms' ); ?></label> |
| 470 |
<input name="form-field-name" id="tag-name" type="text" value="<?php echo esc_attr( $default_form_values['name'] ); ?>" size="40" aria-required="true" aria-describedby="field-name-help" /> |
| 471 |
<p id="field-name-help"><?php esc_html_e( 'The name is how it appears on your site.', 'contact-forms' ); ?></p> |
| 472 |
</div> |
| 473 |
<div class="form-field<?php echo $adding ? ' form-required' : ''; ?>"> |
| 474 |
<label for="tag-slug"><?php esc_html_e( 'Field slug (identifier)', 'contact-forms' ); ?></label> |
| 475 |
<input name="form-field-id" id="tag-slug" type="text" value="<?php echo esc_attr( $default_form_values['id'] ); ?>" <?php if ( ! $adding ) { echo 'disabled="disabled"'; } ?> size="40" <?php if ( $adding ) { echo 'aria-required="true"'; } ?> aria-describedby="slug-feedback field-slug-help" /> |
| 476 |
<?php if ( ! $adding ) { echo '<input type="hidden" name="form-field-id" value="' . esc_attr( $default_form_values['id'] ) . '" />'; } ?> |
| 477 |
<p id="slug-feedback" class="accua-slug-feedback" aria-live="polite" hidden></p> |
| 478 |
<p id="field-slug-help"><?php esc_html_e( 'The “slug” is the URL-friendly version of the name, used as the unique identifier of the field. It is lowercase, must contain only letters, numbers, hyphens and underscores, and cannot be changed once the field exists: it is the key every form and every stored submission refers to.', 'contact-forms' ); ?></p> |
| 479 |
</div> |
| 480 |
<div class="form-field"> |
| 481 |
<label for="parent"><?php esc_html_e( 'Field type', 'contact-forms' ); ?></label> |
| 482 |
<select class="postform" id="parent" name="form-field-type"> |
| 483 |
<?php |
| 484 |
foreach ( $types as $typeid => $typename ) { |
| 485 |
$selected = ( $default_form_values['type'] == $typeid ) ? 'selected="selected"' : ''; |
| 486 |
// phpcs:disable PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped |
| 487 |
echo <<<EOT |
| 488 |
<option value="{$typeid}" class="level-0" {$selected} >{$typename}</option> |
| 489 |
EOT; |
| 490 |
// phpcs:enable PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped |
| 491 |
} |
| 492 |
?> |
| 493 |
</select> |
| 494 |
</div> |
| 495 |
<div class="form-field"> |
| 496 |
<label for="tag-description"><?php esc_html_e( 'Field description', 'contact-forms' ); ?></label> |
| 497 |
<textarea name="form-field-description" id="tag-description" rows="5" cols="40" aria-describedby="field-description-help"><?php echo esc_textarea( $default_form_values['description'] ); ?></textarea> |
| 498 |
<p id="field-description-help"><?php esc_html_e( 'The description is not prominent by default; however, some themes may show it.', 'contact-forms' ); ?></p> |
| 499 |
</div> |
| 500 |
|
| 501 |
<div class="form-field" id="field-section-default-value"> |
| 502 |
<label for="form-field-default-value" id="default-value-label"><?php esc_html_e( 'Default value(s)', 'contact-forms' ); ?>:</label> |
| 503 |
<textarea name="form-field-default-value" id="form-field-default-value" rows="5" cols="40" aria-describedby="default-value-help"><?php echo esc_textarea( $default_form_values['default_value'] ); ?></textarea> |
| 504 |
<p id="default-value-help"><?php esc_html_e( 'For multiple default values in multiple select and multiple checkboxes, use | as separator.', 'contact-forms' ); ?></p> |
| 505 |
</div> |
| 506 |
|
| 507 |
<div class="form-field" id="field-section-allowed-values"> |
| 508 |
<label for="form-field-allowed-values" id="allowed-values-label"><?php esc_html_e( 'Allowed values', 'contact-forms' ); ?>:</label> |
| 509 |
<textarea rows="5" cols="40" name="form-field-allowed-values" id="form-field-allowed-values" aria-describedby="allowed-values-help"><?php echo esc_textarea( $default_form_values['allowed_values'] ); ?></textarea> |
| 510 |
<p id="allowed-values-help"><?php esc_html_e( 'Options used in select, radio and multiple checkboxes. Enter one value per line, in the format key|label. The key is the value that will be stored in the database. The label is optional, and the key will be used as the label if no label is specified. For file fields, this indicates allowed extensions (one per line without dot)', 'contact-forms' ); ?></p> |
| 511 |
</div> |
| 512 |
|
| 513 |
<div class="form-field" id="field-section-date"> |
| 514 |
<strong><?php esc_html_e( 'Settings for date fields', 'contact-forms' ); ?></strong> |
| 515 |
<div class="form-field"> |
| 516 |
<label for="form-field-default-date-value"><?php esc_html_e( 'Default value', 'contact-forms' ); ?>:</label> |
| 517 |
<input type="date" name="form-field-default-date-value" id="form-field-default-date-value" value="<?php echo esc_attr( $default_form_values['default_date_value'] ); ?>"> |
| 518 |
</div> |
| 519 |
|
| 520 |
<label for="form-field-min-of-date"><?php esc_html_e( 'Min date', 'contact-forms' ); ?>:</label> |
| 521 |
<input type="date" id="form-field-min-of-date" name="form-field-min-of-date" value="<?php echo esc_attr( $default_form_values['min_date'] ); ?>"> |
| 522 |
|
| 523 |
<label for="form-field-max-of-date"><?php esc_html_e( 'Max date', 'contact-forms' ); ?>:</label> |
| 524 |
<input type="date" id="form-field-max-of-date" name="form-field-max-of-date" value="<?php echo esc_attr( $default_form_values['max_date'] ); ?>"> |
| 525 |
|
| 526 |
</div> |
| 527 |
|
| 528 |
<div class="form-field" id="field-section-custom-required"> |
| 529 |
<label for="form-field-custom-required-message"><?php esc_html_e( 'Custom required message', 'contact-forms' ); ?></label> |
| 530 |
<input name="form-field-custom-required-message" id="form-field-custom-required-message" type="text" value="<?php echo esc_attr( $default_form_values['custom_required_message'] ); ?>" aria-describedby="custom-required-help" /> |
| 531 |
<?php // translators: %s is the field name/label placeholder ?> |
| 532 |
<p id="custom-required-help"><?php esc_html_e( 'Overrides the default "required" error message for this field. Use %s for the field name. Leave blank to use the default translated message.', 'contact-forms' ); ?></p> |
| 533 |
</div> |
| 534 |
|
| 535 |
<div class="form-field" id="field-section-custom-format"> |
| 536 |
<label for="form-field-custom-format-message"><?php esc_html_e( 'Custom format message', 'contact-forms' ); ?></label> |
| 537 |
<input name="form-field-custom-format-message" id="form-field-custom-format-message" type="text" value="<?php echo esc_attr( $default_form_values['custom_format_message'] ); ?>" aria-describedby="custom-format-help" /> |
| 538 |
<?php // translators: %s is the field name/label placeholder ?> |
| 539 |
<p id="custom-format-help"><?php esc_html_e( 'Overrides the default format error message for email and telephone fields. Use %s for the field name. Leave blank to use the default translated message.', 'contact-forms' ); ?></p> |
| 540 |
</div> |
| 541 |
|
| 542 |
<?php // Last on purpose, behind an hr: an admin-list-only setting with no effect on how the field renders in forms - kept apart from the settings above, which all shape the field itself. ?> |
| 543 |
<div class="form-field" id="field-section-essential-column"> |
| 544 |
<hr /> |
| 545 |
<label for="form-field-essential-column"> |
| 546 |
<input name="form-field-essential-column" id="form-field-essential-column" type="checkbox" value="1"<?php checked( ! empty( $default_form_values['essential_column'] ) ); ?> aria-describedby="essential-column-help" /> |
| 547 |
<?php esc_html_e( 'Show in essential columns', 'contact-forms' ); ?> |
| 548 |
</label> |
| 549 |
<p id="essential-column-help"><?php esc_html_e( 'The "Essential Columns" button on the submissions list hides every column except the essential ones: ID, Actions, Form, Submitted, and the fields with this option enabled. Enable it for the fields you want to keep visible in that compact view.', 'contact-forms' ); ?></p> |
| 550 |
</div> |
| 551 |
|
| 552 |
<?php |
| 553 |
if ( $adding ) { |
| 554 |
// Primary like the core taxonomy screens (edit-tags.php uses |
| 555 |
// 'primary' for Add New Category / Update). |
| 556 |
submit_button( __( 'Add new field', 'contact-forms' ), 'primary' ); |
| 557 |
} else { |
| 558 |
submit_button( __( 'Save changes', 'contact-forms' ), 'primary' ); |
| 559 |
submit_button( __( 'Delete field', 'contact-forms' ), 'button-link-delete', 'delete-field' ); |
| 560 |
} |
| 561 |
?> |
| 562 |
</form> |
| 563 |
</div> |
| 564 |
|
| 565 |
</div> |
| 566 |
</div><!-- /col-left --> |
| 567 |
|
| 568 |
</div><!-- /col-container --> |
| 569 |
</div><!-- /wrap --> |
| 570 |
<?php |
| 571 |
} |
| 572 |
|
| 573 |
/** |
| 574 |
* AJAX: render the live preview for the field being added/edited on the |
| 575 |
* Fields page. |
| 576 |
* |
| 577 |
* The submitted (possibly unsaved) settings run through the same |
| 578 |
* accua_forms_fields_filter_values() used on save, and the field is rendered |
| 579 |
* through the real frontend pipeline (AccuaForm + accua_forms_form_generate) |
| 580 |
* as the only field of a synthetic form using the site's default form |
| 581 |
* settings - exactly how the field would look freshly added to a new form. |
| 582 |
* Nothing is written to the database: the definition is injected with an |
| 583 |
* option filter and the one-field form with a transient pre-filter. |
| 584 |
* |
| 585 |
* @since 2.2.45 |
| 586 |
*/ |
| 587 |
function accua_forms_field_preview() { |
| 588 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 589 |
die( '' ); |
| 590 |
} |
| 591 |
|
| 592 |
$nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : ''; |
| 593 |
if ( ! wp_verify_nonce( $nonce, 'accua_forms_field_preview' ) ) { |
| 594 |
wp_die( esc_html__( 'Security check failed.', 'contact-forms' ), 403 ); |
| 595 |
} |
| 596 |
|
| 597 |
$post = stripslashes_deep( $_POST ); |
| 598 |
$post += array( |
| 599 |
'form-field-id' => '', |
| 600 |
'form-field-name' => '', |
| 601 |
'form-field-type' => 'textfield', |
| 602 |
'form-field-description' => '', |
| 603 |
'form-field-default-value' => '', |
| 604 |
'form-field-default-date-value' => '', |
| 605 |
'form-field-allowed-values' => '', |
| 606 |
'form-field-min-of-date' => '', |
| 607 |
'form-field-max-of-date' => '', |
| 608 |
'form-field-custom-required-message' => '', |
| 609 |
'form-field-custom-format-message' => '', |
| 610 |
'form-field-essential-column' => '', |
| 611 |
); |
| 612 |
|
| 613 |
// Passing the submitted type as $old_data keeps unregistered types |
| 614 |
// (deactivated extension plugins, legacy types) renderable in the preview |
| 615 |
// instead of falling back to textfield, mirroring the edit-form behavior. |
| 616 |
$filtered = accua_forms_fields_filter_values( $post, array( 'type' => $post['form-field-type'] ) ); |
| 617 |
$field_data = $filtered['data']; |
| 618 |
|
| 619 |
// Reserved slug: real slugs cannot start with two underscores. |
| 620 |
$preview_slug = '__accua-field-preview'; |
| 621 |
$field_data['id'] = $preview_slug; |
| 622 |
|
| 623 |
$inject_definition = function ( $fields ) use ( $preview_slug, $field_data ) { |
| 624 |
if ( ! is_array( $fields ) ) { |
| 625 |
$fields = array(); |
| 626 |
} |
| 627 |
$fields[ $preview_slug ] = $field_data; |
| 628 |
return $fields; |
| 629 |
}; |
| 630 |
add_filter( 'option_accua_forms_avail_fields', $inject_definition ); |
| 631 |
add_filter( 'default_option_accua_forms_avail_fields', $inject_definition ); |
| 632 |
|
| 633 |
// A synthetic one-field draft. The instance is marked required and the |
| 634 |
// form uses AJAX on purpose: that emits the client-side validation, so |
| 635 |
// the preview also demonstrates the validation messages (leave the field |
| 636 |
// empty and click elsewhere) including the custom required/format |
| 637 |
// messages being edited. A script printed below prevents the form from |
| 638 |
// ever actually submitting. |
| 639 |
add_filter( 'pre_transient_' . _accua_forms_get_draft_key( $preview_slug ), function () use ( $preview_slug ) { |
| 640 |
return array( |
| 641 |
'fields' => array( |
| 642 |
array( |
| 643 |
'ref' => $preview_slug, |
| 644 |
'required' => true, |
| 645 |
), |
| 646 |
), |
| 647 |
'use_ajax' => true, |
| 648 |
); |
| 649 |
} ); |
| 650 |
|
| 651 |
add_filter( 'accua_forms_use_draft_for_preview', '__return_true' ); |
| 652 |
add_filter( 'accua_forms_preview_suppress_auto_submit', '__return_true' ); |
| 653 |
// admin-ajax is an admin context, but the preview must mirror the |
| 654 |
// frontend (e.g. the colorpicker renders a native color input there). |
| 655 |
add_filter( 'accua_forms_preview_render_as_frontend', '__return_true' ); |
| 656 |
|
| 657 |
// Enqueue form styles before printing them |
| 658 |
accua_form_enqueue_scripts_and_styles(); |
| 659 |
|
| 660 |
// Same default-layout resolution as the frontend shortcode handler. |
| 661 |
$default_form_data = get_option( 'accua_forms_default_form_data', array() ); |
| 662 |
$layout = ! empty( $default_form_data['layout'] ) ? $default_form_data['layout'] : 'sidebyside'; |
| 663 |
|
| 664 |
$form = AccuaForm::create( '__accua-form__' . $preview_slug, array( |
| 665 |
'layout' => $layout, |
| 666 |
'title' => '', |
| 667 |
) ); |
| 668 |
$rendered = $form->render( true ); |
| 669 |
|
| 670 |
// Fields with no visible output (hidden inputs, captchas rendering an |
| 671 |
// HTML-comment placeholder when unconfigured) would show an empty box: |
| 672 |
// detect them and explain instead. Comments are stripped first; |
| 673 |
// wp_strip_all_tags() also drops script/style blocks with their content. |
| 674 |
// No iframe in the element list: the AJAX machinery prints its own |
| 675 |
// (invisible) submit-target iframe, and real captcha iframes are |
| 676 |
// injected client-side into markup that already carries a label. |
| 677 |
$stripped = preg_replace( '/<!--.*?-->/s', '', $rendered ); |
| 678 |
$has_visible = preg_match( '/<(?:select|textarea|button|label|legend|img|input(?![^>]*type=["\']?hidden))/i', $stripped ) |
| 679 |
|| trim( wp_strip_all_tags( $stripped ) ) !== ''; |
| 680 |
|
| 681 |
// The doctype matters: without it the iframe document renders in Quirks |
| 682 |
// Mode (jQuery is explicitly unsupported there, and layout metrics differ). |
| 683 |
echo '<!DOCTYPE html><html><head> |
| 684 |
<style> |
| 685 |
*, *::before, *::after { box-sizing: border-box; } |
| 686 |
/* The parent frame is sized to fit this document and animates height |
| 687 |
changes, so a scrollbar would only flash during the animation; |
| 688 |
fields-page.js re-enables scrolling if the content ever exceeds the |
| 689 |
frame height cap. */ |
| 690 |
html { overflow: hidden; } |
| 691 |
body { |
| 692 |
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; |
| 693 |
margin: 0; |
| 694 |
padding: 16px; |
| 695 |
background: #fff; |
| 696 |
font-size: 14px; |
| 697 |
line-height: 1.5; |
| 698 |
} |
| 699 |
.accua-field-preview-empty { color: #50575e; margin: 0 0 12px; } |
| 700 |
</style>'; |
| 701 |
accua_forms_prepare_emoji_styles_for_preview(); |
| 702 |
wp_print_styles(); |
| 703 |
wp_print_head_scripts(); |
| 704 |
echo '</head><body>'; |
| 705 |
if ( ! $has_visible ) { |
| 706 |
echo '<p class="accua-field-preview-empty">' . 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' ) . '</p>'; |
| 707 |
} |
| 708 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Form render manages its own escaping |
| 709 |
echo $rendered; |
| 710 |
wp_print_footer_scripts(); |
| 711 |
|
| 712 |
// The preview validates like a real form (the field is marked required |
| 713 |
// for demonstration) but must never actually submit: wrap the generated |
| 714 |
// submit handler so a passing validation shows a note instead. |
| 715 |
$preview_note = wp_json_encode( '<p>' . esc_html__( 'This is a preview: the form is not actually submitted.', 'contact-forms' ) . '</p>' ); |
| 716 |
echo '<script> |
| 717 |
jQuery(function() { |
| 718 |
var form = document.querySelector("form.accua-form"); |
| 719 |
if (!form) { return; } |
| 720 |
var orig = form.onsubmit; |
| 721 |
form.onsubmit = function() { |
| 722 |
var ret = true; |
| 723 |
if (orig) { try { ret = orig.call(this); } catch (e) { ret = false; } } |
| 724 |
if (ret !== false) { |
| 725 |
var summary = form.querySelector(".pfbc-validation-summary"); |
| 726 |
if (!summary) { |
| 727 |
summary = document.createElement("div"); |
| 728 |
summary.className = "pfbc-validation-summary"; |
| 729 |
form.insertBefore(summary, form.firstChild); |
| 730 |
} |
| 731 |
summary.className = "pfbc-validation-summary pfbc-validation-success"; |
| 732 |
summary.innerHTML = ' . |
| 733 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_json_encode() of an esc_html__'d string, printed into a script context. |
| 734 |
$preview_note . '; |
| 735 |
} |
| 736 |
return false; |
| 737 |
}; |
| 738 |
}); |
| 739 |
</script>'; |
| 740 |
echo '</body></html>'; |
| 741 |
die( '' ); |
| 742 |
} |
| 743 |
add_action( 'wp_ajax_accua_forms_field_preview', 'accua_forms_field_preview' ); |
| 744 |
|