PluginProbe
Contact Forms by Cimatti / trunk
Contact Forms by Cimatti vtrunk
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
contact-forms / admin / fields-page.php

fields-page.php in Contact Forms by Cimatti trunk, at admin/fields-page.php

744 lines 36.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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">&larr; <?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 &#8220;slug&#8221; 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