PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.11
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.11
1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 1.7.0.9 All 211 releases
charitable / includes / admin / campaign-builder / fields / class-base.php

class-base.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.11, at includes/admin/campaign-builder/fields/class-base.php

649 lines 18.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Base case for fields.
4 *
5 * @package Charitable
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.8.0
10 * @version 1.8.0
11 */
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 if ( ! class_exists( 'Charitable_Builder_Field' ) ) :
18
19 /**
20 * Base field template.
21 *
22 * @since 1.8.0
23 */
24 abstract class Charitable_Builder_Field {
25
26 /**
27 * Full name of the field type, eg "Paragraph Text".
28 *
29 * @since 1.8.0
30 *
31 * @var string
32 */
33 public $name;
34
35 /**
36 * Type of the field, eg "textarea".
37 *
38 * @since 1.8.0
39 *
40 * @var string
41 */
42 public $type;
43
44 /**
45 * Defines if the field is able to be edited by the user.
46 *
47 * @since 1.8.0
48 *
49 * @var string
50 */
51 public $can_be_edited = false;
52
53 /**
54 * Defines if the field is able to be deleted by the user.
55 *
56 * @since 1.8.0
57 *
58 * @var string
59 */
60 public $can_be_deleted = true;
61
62 /**
63 * Defines if the field is able to be duplicated by the user.
64 *
65 * @since 1.8.0
66 *
67 * @var string
68 */
69 public $can_be_duplicated = true;
70
71 /**
72 * Defines the label when the user hovers over the edit button in the preview.
73 *
74 * @since 1.8.0
75 *
76 * @var string
77 */
78 public $edit_label = '';
79
80 /**
81 * Defines the type.
82 *
83 * @since 1.8.0
84 *
85 * @var string
86 */
87 public $edit_type = '';
88
89 /**
90 * Field group the field belongs to.
91 *
92 * @since 1.8.0
93 *
94 * @var string
95 */
96 public $edit_section = 'standard';
97
98 /**
99 * Field group the field belongs to.
100 *
101 * @since 1.8.0
102 *
103 * @var string
104 */
105 public $group = 'standard';
106
107 /**
108 * Name of field that requires a quill WYSIWYG JS applied to it.
109 *
110 * @since 1.8.0
111 *
112 * @var string
113 */
114 public $html_field = '';
115
116 /**
117 * Defines the field edit for editting.
118 *
119 * @since 1.8.0
120 *
121 * @var string
122 */
123 public $edit_field_id = '';
124
125 /**
126 * Defines any default tooltip text.
127 *
128 * @since 1.8.0
129 *
130 * @var string
131 */
132 public $tooltip = '';
133
134 /**
135 * Defines default alignment (left/center/right) for the field element.
136 *
137 * @since 1.8.0
138 *
139 * @var string
140 */
141 public $align_default = 'left';
142
143 /**
144 * Defines a charavter limit in the preview window, intended for textareas (like campaign description).
145 *
146 * @since 1.8.0
147 *
148 * @var string
149 */
150 public $preview_character_limit = 550;
151
152 /**
153 * Font Awesome Icon used for the editor button, eg "fa-list".
154 *
155 * @since 1.8.0
156 *
157 * @var mixed
158 */
159 public $icon = false;
160
161 /**
162 * Priority order the field button should show inside the "Add Fields" tab.
163 *
164 * @since 1.8.0
165 *
166 * @var int
167 */
168 public $order = 1;
169
170 /**
171 * Placeholder to hold default value(s) for some field types.
172 *
173 * @since 1.8.0
174 *
175 * @var mixed
176 */
177 public $defaults;
178
179 /**
180 * Current campaign ID in the admin builder.
181 *
182 * @since 1.8.0
183 *
184 * @var int|bool
185 */
186 public $form_id;
187
188 /**
189 * Current field ID.
190 *
191 * @since 1.8.0
192 *
193 * @var int
194 */
195 public $field_id;
196
197 /**
198 * Current form data.
199 *
200 * @since 1.8.0
201 *
202 * @var array
203 */
204 public $form_data;
205
206 /**
207 * Current field data.
208 *
209 * @since 1.8.0
210 *
211 * @var array
212 */
213 public $field_data;
214
215 /**
216 * Total number of fields of this type that can exist in a single campaign template.
217 *
218 * @since 1.8.0
219 *
220 * @var string
221 */
222 public $max_allowed = false;
223
224 /**
225 * Primary class constructor.
226 *
227 * @since 1.8.0
228 *
229 * @param bool $init Pass false to allow to shortcut the whole initialization, if needed.
230 */
231 public function __construct( $init = true ) {
232
233 if ( ! $init ) {
234 return;
235 }
236
237 // The form ID is to be accessed in the builder.
238 $this->form_id = isset( $_GET['campaign_id'] ) ? (int) $_GET['campaign_id'] : false; // phpcs:ignore WordPress.Security.NonceVerification
239
240 // Bootstrap.
241 $this->init();
242
243 // Add fields tab.
244 add_filter( 'charitable_builder_design_buttons', [ $this, 'field_button' ], 15 );
245
246 // Field options tab.
247 add_action( "charitable_builder_fields_options_{$this->type}", [ $this, 'field_options' ], 10 );
248
249 // Preview fields.
250 add_action( "charitable_builder_fields_previews_{$this->type}", [ $this, 'field_preview' ], 10, 3 );
251
252 // AJAX Add new field.
253 add_action( "wp_ajax_charitable_new_field_{$this->type}", [ $this, 'field_new' ] );
254
255 // Display field input elements on front-end.
256 add_action( "charitable_display_field_{$this->type}", [ $this, 'field_display' ], 10, 3 );
257
258 // Display field on back-end.
259 add_filter( "charitable_pro_admin_entries_edit_is_field_displayable_{$this->type}", '__return_true', 9 );
260
261 // Validation on submit.
262 add_action( "charitable_process_validate_{$this->type}", [ $this, 'validate' ], 10, 3 );
263
264 // Format.
265 add_action( "charitable_process_format_{$this->type}", [ $this, 'format' ], 10, 3 );
266
267 // Prefill.
268 add_filter( 'charitable_field_properties', [ $this, 'field_prefill_value_property' ], 10, 3 );
269
270 // Change the choice's value while saving entries.
271 add_filter( 'charitable_process_before_form_data', [ $this, 'field_fill_empty_choices' ] );
272
273 // Change field name for ajax error.
274 add_filter( 'charitable_process_ajax_error_field_name', [ $this, 'ajax_error_field_name' ], 10, 4 );
275
276 // Add HTML line breaks before all newlines in Entry Preview.
277 add_filter( "charitable_pro_fields_entry_preview_get_field_value_{$this->type}_field_after", 'nl2br', 100 );
278 }
279
280 /**
281 * All systems go. Used by subclasses. Required.
282 *
283 * @since 1.8.0
284 */
285 abstract public function init();
286
287 /**
288 * Create a new field in the admin AJAX editor.
289 *
290 * @since 1.8.0
291 */
292 public function field_new() {
293
294 // Run a security check.
295 if ( ! check_ajax_referer( 'charitable-builder', 'nonce', false ) ) {
296 wp_send_json_error( esc_html__( 'Your session expired. Please reload the builder.', 'charitable' ) );
297 }
298
299 // phpcs:disable WordPress.Security.NonceVerification.Missing
300 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- wp_verify_nonce handles validation
301 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'charitable-builder' ) ) {
302 wp_send_json_error( esc_html__( 'A security or cache check has failed. Please reload the builder.', 'charitable' ) );
303 }
304
305 // Check for permissions.
306 if ( ! charitable_current_user_can( 'edit_campaigns' ) ) {
307 wp_send_json_error( esc_html__( 'You are not allowed to perform this action.', 'charitable' ) );
308 }
309
310 // Check for field type to add.
311 if ( empty( $_POST['type'] ) ) {
312 wp_send_json_error( esc_html__( 'No field type found', 'charitable' ) );
313 }
314
315 // Grab field data.
316 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by sanitize_text_field
317 $campaign_title = ! empty( $_POST['campaign_title'] ) && '' !== trim( wp_unslash( $_POST['campaign_title'] ) ) ? sanitize_text_field( wp_unslash( $_POST['campaign_title'] ) ) : '';
318 // phpcs:enable WordPress.Security.NonceVerification.Missing
319
320 $field_args = ! empty( $_POST['defaults'] ) && is_array( $_POST['defaults'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['defaults'] ) ) : [];
321 $field_type = sanitize_key( $_POST['type'] );
322 $field_id = 0 === absint( $_POST['field_id'] ) ? 1 : absint( $_POST['field_id'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
323 $field = [
324 'id' => $field_id,
325 'type' => $field_type,
326 'label' => $this->name,
327 'description' => '',
328 ];
329 $field = wp_parse_args( $field_args, $field );
330 $field = apply_filters( 'charitable_field_new_default', $field );
331 $field_required = apply_filters( 'charitable_field_new_required', '', $field );
332 $field_class = apply_filters( 'charitable_field_new_class', '', $field );
333 // $field_max = 0 === absint( $_POST['field_max'] ) ? 1 : absint( $_POST['field_max'] );
334 // $field_helper_hide = ! empty( $_COOKIE['charitable_field_helper_hide'] );
335
336 $campaign_data = array();
337
338 // Field types that default to required.
339 if ( ! empty( $field_required ) ) {
340 $field_required = 'required';
341 $field['required'] = '1';
342 }
343
344 $campaign_data = get_post_meta( isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0, 'campaign_settings_v2', true );
345
346 // If $campaign_data is false made it an array.
347 if ( ! is_array( $campaign_data ) ) {
348 $campaign_data = array();
349 }
350
351 // If there isn't any data, we might still have the campaign title at least from the form (probably not saved).
352 if ( empty( $campaign_data['title'] ) && '' !== $campaign_title ) {
353 $campaign_data['title'] = $campaign_title;
354 }
355
356 // Build Preview.
357 ob_start();
358 $this->field_preview( $field, $campaign_data, $field_id );
359 $prev = ob_get_clean();
360 $preview = sprintf(
361 '<div class="charitable-field charitable-field-%1$s %2$s %3$s %4$s %5$s %6$s" id="charitable-field-%7$d" data-field-id="%7$d" data-field-type="%8$s" data-field-max="%9$d">',
362 esc_attr( $field_type ),
363 esc_attr( $field_required ),
364 esc_attr( $field_class ),
365 $this->can_be_edited ? 'charitable-can-edit' : 'charitable-no-edit',
366 $this->can_be_duplicated ? 'charitable-can-duplicate' : 'charitable-no-duplicate',
367 $this->can_be_deleted ? 'charitable-can-delete' : 'charitable-no-delete',
368 absint( $field['id'] ),
369 esc_attr( $field_type ),
370 absint( $this->max_allowed )
371 );
372
373 if ( apply_filters( 'charitable_field_new_display_edit_button', true, $field ) ) {
374 if ( $this->can_be_edited ) :
375 $edit_label = ! empty( $this->edit_label ) ? esc_attr( $this->edit_label ) : esc_attr__( 'Edit Field', 'charitable' );
376 $preview .= sprintf( '<a href="#" class="charitable-field-edit" data-type="' . $this->edit_type . '" data-section="' . $this->edit_section . '" title="%s"><i class="fa fa-pencil"></i></a>', $edit_label );
377 endif;
378 }
379
380 if ( apply_filters( 'charitable_field_new_display_duplicate_button', true, $field ) ) {
381 if ( $this->can_be_duplicated ) :
382 $preview .= sprintf( '<a href="#" class="charitable-field-duplicate" title="%s"><i class="fa fa-files-o" aria-hidden="true"></i></a>', esc_attr__( 'Duplicate Field', 'charitable' ) );
383 endif;
384 }
385
386 if ( apply_filters( 'charitable_field_new_display_delete_button', true, $field ) ) {
387 if ( $this->can_be_deleted ) :
388 $preview .= sprintf( '<a href="#" class="charitable-field-delete" title="%s"><i class="fa fa-trash-o"></i></a>', esc_attr__( 'Delete Field', 'charitable' ) );
389 endif;
390 }
391
392 $preview .= $prev;
393 $preview .= '</div>';
394
395 // Build Options.
396 $class = apply_filters( 'charitable_builder_field_option_class', '', $field );
397 $options = sprintf(
398 '<div class="charitable-field-option charitable-field-option-%1$s %2$s" id="charitable-field-option-%3$d" data-field-id="%3$d">',
399 sanitize_html_class( $field['type'] ),
400 charitable_sanitize_classes( $class ),
401 absint( $field['id'] )
402 );
403
404 $options .= sprintf(
405 '<input type="hidden" name="fields[%1$d][id]" value="%1$d" class="charitable-field-option-hidden-id">',
406 absint( $field['id'] )
407 );
408 $options .= sprintf(
409 '<input type="hidden" name="fields[%d][type]" value="%s" class="charitable-field-option-hidden-type">',
410 absint( $field['id'] ),
411 esc_attr( $field['type'] )
412 );
413
414 ob_start();
415 $this->field_options( $field );
416 $options .= ob_get_clean();
417 $options .= '</div>';
418
419 $edit_field_html = $this->settings_display( $field_id, $campaign_data );
420
421 // Prepare to return compiled results.
422 wp_send_json_success(
423 [
424 'form_id' => isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0,
425 'field_id' => absint( $field_id ),
426 'field' => $field,
427 'preview' => $preview,
428 'options' => $options,
429 'max' => $this->max_allowed,
430 'edit_field_html' => $edit_field_html,
431 'wysiwyg' => $this->html_field,
432 ]
433 );
434 }
435
436 /**
437 * Create the button for the 'Add Fields' tab, inside the form editor.
438 *
439 * @since 1.8.0
440 *
441 * @param array $fields List of form fields with their data.
442 *
443 * @return array
444 */
445 public function field_button( $fields ) {
446
447 // Don't add if it is already added.
448 if ( isset( $fields[ $this->edit_section ]['fields'][ $this->type ] ) ) {
449 return $fields;
450 }
451
452 // Add field information to fields array.
453 $fields[ $this->edit_section ]['fields'][ $this->type ] = [
454 'order' => $this->order,
455 'name' => $this->name,
456 'type' => $this->type,
457 'icon' => $this->icon,
458 ];
459
460 // Wipe hands clean.
461 return $fields;
462 }
463
464 /**
465 * Create the field options panel. Used by subclasses.
466 *
467 * @since 1.8.0
468 *
469 * @param array $field Field data and settings.
470 */
471 abstract public function field_options( $field );
472
473 /**
474 * Create the field preview. Used by subclasses.
475 *
476 * @since 1.8.0
477 *
478 * @param array $field Field data and settings.
479 * @param array $campaign_data Campaign data and settings.
480 * @param integer $field_id Field ID.
481 */
482 abstract public function field_preview( $field, $campaign_data, $field_id );
483
484 /**
485 * Display the field input elements on the frontend.
486 *
487 * @since 1.8.0
488 *
489 * @param array $field Field data and settings.
490 * @param array $field_atts Field attributes.
491 * @param array $form_data Form data and settings.
492 */
493 abstract public function field_display( $field, $field_atts, $form_data );
494
495 /**
496 * Create the settings display panel. Used by subclasses.
497 *
498 * @since 1.8.0
499 *
500 * @param array $field campaign_data Campaign data and settings.
501 */
502 abstract public function settings_display( $field, $campaign_data );
503
504 /**
505 * Display field input errors if present.
506 *
507 * @since 1.8.0
508 *
509 * @param string $key Input key.
510 * @param array $field Field data and settings.
511 */
512 public function field_display_error( $key, $field ) {
513
514 // Need an error.
515 if ( empty( $field['properties']['error']['value'][ $key ] ) ) {
516 return;
517 }
518
519 printf(
520 '<label class="charitable-error" for="%s">%s</label>',
521 esc_attr( $field['properties']['inputs'][ $key ]['id'] ),
522 esc_html( $field['properties']['error']['value'][ $key ] )
523 );
524 }
525
526 /**
527 * Wrap field HTML title - a universal title function!
528 *
529 * @since 1.8.0
530 *
531 * @param string $title Field title.
532 */
533 public function field_title( $title = false ) {
534
535 return '<h6 class="charitable-preview-field-title"><span>' . $title . '</span></h6>';
536 }
537
538 /**
539 * Wrap field HTML with a container that allows a (mostly) universal way of aligning and controlling visuals of the field in the preview area.
540 *
541 * @since 1.8.0
542 *
543 * @param string $html Field HTML.
544 * @param array $field_data Field data and settings.
545 */
546 public function field_wrapper( $html = '', $field_data = false ) {
547
548 $align = isset( $field_data['align'] ) ? esc_attr( $field_data['align'] ) : $this->align_default;
549 $columns = isset( $field_data['columns'] ) ? intval( $field_data['columns'] ) : 1;
550 $width_percentage = isset( $field_data['width_percentage'] ) ? absint( $field_data['width_percentage'] ) : 100;
551
552 $css_classes = array(
553 'charitable-preview-field-container',
554 'charitable-preview-align-' . $align,
555 'charitable-preview-columns-' . $columns,
556 );
557
558 $html = '<div class="' . implode( ' ', $css_classes ) . '" style="width: ' . $width_percentage . '%"><span class="charitable-preview-field-indicator charitable-hidden"></span>' . $html . '</div>';
559
560 return $html;
561 }
562
563 /**
564 * Wrap field HTML with a container that allows a (mostly) universal way of aligning and controlling visuals of the field in the preview area.
565 *
566 * @since 1.8.0
567 *
568 * @param string $html Field HTML.
569 * @param array $field_data Field data and settings.
570 */
571 public function field_display_wrapper( $html, $field_data = false ) {
572
573 $align = isset( $field_data['align'] ) ? esc_attr( $field_data['align'] ) : $this->align_default;
574 $columns = isset( $field_data['columns'] ) ? intval( $field_data['columns'] ) : 1;
575 $width_percentage = isset( $field_data['width_percentage'] ) ? absint( $field_data['width_percentage'] ) : 100;
576 $custom_css_class = isset( $field_data['css_class'] ) && '' !== trim( $field_data['css_class'] ) ? esc_attr( $field_data['css_class'] ) : false;
577
578 $css_classes = array(
579 'charitable-campaign-field-container',
580 'charitable-campaign-align-' . $align,
581 'charitable-campaign-columns-' . $columns,
582 );
583
584 if ( $custom_css_class ) {
585 $css_classes[] = $custom_css_class;
586 }
587
588 $html = '<div class="charitable-campaign-field charitable-campaign-field-' . $this->type . ' ' . implode( ' ', $css_classes ) . '"><div class="charitable-campaign-field-inner" style="width: ' . $width_percentage . '%">' . $html . '</div></div>';
589
590 return $html;
591 }
592
593 /**
594 * Displays a preview area because the field can't be totally functional in a preview state (say on a preview campaign page)
595 *
596 * @since 1.8.0
597 *
598 * @param string $field_type Field type.
599 * @param array $field_data Any field data.
600 * @param array $campaign_data Form data and settings.
601 */
602 public function show_preview( $field_type, $field_data = false, $campaign_data = false ) {
603
604 $field_label = ucwords( str_replace( '-', ' ', $field_type ) );
605
606 $html = '<div class="charitable-campaign-builder-field-no-preview">';
607 /* translators: %s is the field label */
608 $html .= '<p>' . sprintf( esc_html__( 'The %s field cannot be rendered in preview mode.', 'charitable' ), $field_label ) . '</p></div>';
609
610 return apply_filters( 'charitable_campaign_builder_preview_html', $html, $field_type, $field_data, $campaign_data );
611 }
612
613 /**
614 * Keep a number within a certain range.
615 *
616 * @param integer $n The number.
617 * @param integer $min Min.
618 * @param integer $max Max.
619 * @return integer
620 */
621 public function keep_in_range( $n, $min = 0, $max = 100 ) {
622 return max( min( $max, $n ), $min );
623 }
624
625 /**
626 * Get field name for ajax error message.
627 *
628 * @since 1.8.0
629 *
630 * @param string $name Field name for error triggered.
631 * @param array $field Field settings.
632 * @param array $props List of properties.
633 * @param string $error Error message.
634 *
635 * @return string
636 */
637 public function ajax_error_field_name( $name, $field, $props, $error ) { // phpcs:ignore
638
639 if ( $name ) {
640 return $name;
641 }
642 $input = isset( $props['inputs']['primary'] ) ? $props['inputs']['primary'] : end( $props['inputs'] );
643
644 return (string) isset( $input['attr']['name'] ) ? $input['attr']['name'] : '';
645 }
646 }
647
648 endif;
649