PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.7.0.2
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.7.0.2
1.8.13.1 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 All 212 releases
charitable / includes / abstracts / abstract-class-charitable-form.php

abstract-class-charitable-form.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.7.0.2, at includes/abstracts/abstract-class-charitable-form.php

796 lines 20.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * A base class to be extended by specific form classes.
4 *
5 * @package Charitable/Classes/Charitable_Form
6 * @author David Bisset
7 * @copyright Copyright (c) 2022, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 * @version 1.6.54
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Form' ) ) :
19
20 /**
21 * Charitable_Form
22 *
23 * @since 1.0.0
24 */
25 abstract class Charitable_Form {
26
27 /**
28 * Temporary, unique ID of this form.
29 *
30 * @since 1.0.0
31 *
32 * @var string
33 */
34 protected $id;
35
36 /**
37 * Nonce action.
38 *
39 * @since 1.0.0
40 *
41 * @var string
42 */
43 protected $nonce_action = 'charitable_form';
44
45 /**
46 * Nonce name.
47 *
48 * @since 1.0.0
49 *
50 * @var string
51 */
52 protected $nonce_name = '_charitable_form_nonce';
53
54 /**
55 * Form action.
56 *
57 * @since 1.0.0
58 *
59 * @var string
60 */
61 protected $form_action;
62
63 /**
64 * Errors with the form submission.
65 *
66 * @since 1.0.0
67 *
68 * @var array
69 */
70 protected $errors = array();
71
72 /**
73 * Submitted values.
74 *
75 * @since 1.0.0
76 *
77 * @var array
78 */
79 protected $submitted;
80
81 /**
82 * Form View.
83 *
84 * @since 1.5.0
85 *
86 * @var Charitable_Form_View_Interface
87 */
88 protected $view;
89
90 /**
91 * Set up callbacks for actions and filters.
92 *
93 * @since 1.0.0
94 *
95 * @return void
96 */
97 protected function attach_hooks_and_filters() {
98 charitable_get_deprecated()->doing_it_wrong(
99 __METHOD__,
100 sprintf(
101 /* translators: %s: class name */
102 __( 'None of the callbacks are required as of Charitable 1.5; use `Charitable_Public_Form_View` instead. Issue encountered in %s class.' ),
103 get_class( $this )
104 ),
105 '1.5.0'
106 );
107
108 add_action( 'charitable_form_before_fields', array( $this, 'render_error_notices' ) );
109 add_action( 'charitable_form_before_fields', array( $this, 'add_hidden_fields' ) );
110 add_action( 'charitable_form_field', array( $this, 'render_field' ), 10, 6 );
111 add_filter( 'charitable_form_field_increment', array( $this, 'increment_index' ), 10, 2 );
112 }
113
114 /**
115 * Set the Form View object.
116 *
117 * @since 1.5.0
118 *
119 * @param Charitable_Form_View_Interface $form_view An object implementing the `Charitable_Form_View_Interface`.
120 * @return void
121 */
122 public function set_form_view( Charitable_Form_View_Interface $form_view ) {
123 $this->view = $form_view;
124 }
125
126 /**
127 * Return the Form_View object for this form.
128 *
129 * @since 1.5.0
130 *
131 * @return Charitable_Form_View_Interface
132 */
133 public function view() {
134 if ( ! isset( $this->view ) ) {
135 $this->view = new Charitable_Public_Form_View( $this );
136 }
137
138 return $this->view;
139 }
140
141 /**
142 * Compares the ID of the form passed by the action and the current form object to ensure they're the same.
143 *
144 * @since 1.0.0
145 *
146 * @param string $id Current form ID.
147 * @return boolean
148 */
149 public function is_current_form( $id ) {
150 return $id === $this->id;
151 }
152
153 /**
154 * Return the form fields.
155 *
156 * @since 1.5.0
157 *
158 * @return array
159 */
160 public function get_fields() {
161 return array();
162 }
163
164 /**
165 * Return the form action.
166 *
167 * @since 1.3.1
168 *
169 * @return string
170 */
171 public function get_form_action() {
172 return $this->form_action;
173 }
174
175 /**
176 * Return the form ID.
177 *
178 * @since 1.0.0
179 *
180 * @return string
181 */
182 public function get_form_identifier() {
183 return $this->id;
184 }
185
186 /**
187 * Retrieve hidden fields.
188 *
189 * @since 1.5.0
190 *
191 * @return array
192 */
193 public function get_hidden_fields() {
194 /**
195 * Filter hidden fields.
196 *
197 * @since 1.5.0
198 *
199 * @param array $fields The hidden fields as a key=>value array.
200 * @param Charitable_Form $form This instance of `Charitable_Form`.
201 */
202 return apply_filters(
203 'charitable_form_hidden_fields',
204 array(
205 $this->nonce_name => wp_create_nonce( $this->nonce_action ),
206 '_wp_http_referer' => wp_unslash( $_SERVER['REQUEST_URI'] ),
207 'charitable_action' => $this->form_action,
208 ),
209 $this
210 );
211 }
212
213 /**
214 * Output the nonce.
215 *
216 * @since 1.0.0
217 *
218 * @return void
219 */
220 public function nonce_field() {
221 wp_nonce_field( $this->nonce_action, $this->nonce_name );
222 }
223
224 /**
225 * Validate nonce data passed by the submitted form.
226 *
227 * @since 1.0.0
228 *
229 * @return boolean
230 */
231 public function validate_nonce() {
232 $submitted = $this->get_submitted_values();
233 $validated = isset( $submitted[ $this->nonce_name ] ) && wp_verify_nonce( $submitted[ $this->nonce_name ], $this->nonce_action );
234
235 if ( ! $validated ) {
236 charitable_get_notices()->add_error( __( 'Unable to submit form. Please try again.', 'charitable' ) );
237
238 if ( defined( 'CHARITABLE_DEBUG' ) && CHARITABLE_DEBUG ) {
239 error_log(
240 sprintf(
241 /* translators: %1$s: nonce name; %2$s: nonce action; %3$s: submitted nonce */
242 __( 'Nonce verification failed for nonce with name "%1$s" and action "%2$s". Submitted nonce is "%3$s".', 'charitable' ),
243 $this->nonce_name,
244 $this->nonce_action,
245 array_key_exists( $this->nonce_name, $submitted ) ? $submitted[ $this->nonce_name ] : ''
246 )
247 );
248 }
249 }
250
251 return $validated;
252 }
253
254 /**
255 * Make sure that the honeypot field is empty.
256 *
257 * @since 1.4.3
258 *
259 * @return boolean
260 */
261 public function validate_honeypot() {
262 $submitted = $this->get_submitted_values();
263
264 if ( ! isset( $submitted['charitable_form_id'] ) ) {
265 return true;
266 }
267
268 $form_id = $submitted['charitable_form_id'];
269
270 return array_key_exists( $form_id, $submitted ) && 0 === strlen( $submitted[ $form_id ] );
271 }
272
273 /**
274 * Callback method used to filter out non-required fields.
275 *
276 * @since 1.0.0
277 *
278 * @param array $field Field definition.
279 * @return array
280 */
281 public function filter_required_fields( $field ) {
282 return isset( $field['required'] ) && true == $field['required'];
283 }
284
285 /**
286 * Filters array returning just the required fields.
287 *
288 * @since 1.0.0
289 *
290 * @param array[] $fields Array of form fields.
291 * @return array[]
292 */
293 public function get_required_fields( $fields ) {
294 $required_fields = array_filter( $fields, array( $this, 'filter_required_fields' ) );
295
296 return $required_fields;
297 }
298
299 /**
300 * Check the passed fields to ensure that all required fields have been submitted.
301 *
302 * @since 1.0.0
303 *
304 * @param array $fields Array of form fields.
305 * @param array $submitted Submitted values.
306 * @return boolean
307 */
308 public function check_required_fields( $fields, $submitted = array() ) {
309 if ( empty( $submitted ) ) {
310 $submitted = $this->get_submitted_values();
311 }
312
313 $ret = true;
314 $missing = array();
315
316 foreach ( $this->get_required_fields( $fields ) as $key => $field ) {
317 /* We already have a value for this field. */
318 if ( ! empty( $field['value'] ) && 'checkbox' !== $field['type'] ) {
319 continue;
320 }
321
322 $exists = isset( $submitted[ $key ] );
323
324 /* Verify that a value was provided. */
325 if ( $exists ) {
326 $value = $submitted[ $key ];
327 $exists = ! empty( $value ) || ( is_string( $value ) && strlen( $value ) );
328 }
329
330 /* If a value was not provided, check if it's in the $_FILES array. */
331 if ( ! $exists ) {
332 $exists = ( 'picture' == $field['type'] && isset( $_FILES[ $key ] ) && ! empty( $_FILES[ $key ]['name'] ) );
333 }
334
335 $exists = apply_filters( 'charitable_required_field_exists', $exists, $key, $field, $submitted, $this );
336
337 if ( ! $exists ) {
338
339 $label = isset( $field['label'] ) ? $field['label'] : $key;
340
341 $missing[] = $label;
342 }
343 }//end foreach
344
345 $missing = apply_filters( 'charitable_form_missing_fields', $missing, $this, $fields, $submitted );
346
347 if ( count( $missing ) ) {
348
349 $missing_fields = implode( '</li><li>', $missing );
350
351 charitable_get_notices()->add_error(
352 sprintf(
353 '<p>%s</p><ul class="error-list"><li>%s</li></ul>',
354 __( 'There were problems with your form submission. The following required fields were not filled out:', 'charitable' ),
355 $missing_fields
356 )
357 );
358
359 $ret = false;
360 }
361
362 return $ret;
363 }
364
365 /**
366 * Organize fields by data type, also filtering out unused parameters (we just need the key and the type).
367 *
368 * @since 1.0.0
369 *
370 * @param string $key Key of the field to sort.
371 * @param array $field Field definition.
372 * @param array $ret Return value that we're carrying.
373 * @return array[]
374 */
375 public function sort_field_by_data_type( $key, $field, $ret ) {
376 /* Filter out paragraphs and fields without a type. */
377 if ( ! isset( $field['type'] ) || 'paragraph' == $field['type'] ) {
378 return $ret;
379 }
380
381 /**
382 * Group the fields by data type.
383 *
384 * Where fields don't have a data_type specified, we group
385 * them as 'internal', since we need them to be grouped into a
386 * data type for the data processor to work as expected.
387 *
388 * @since 1.6.51 Added 'internal' data type as fallback.
389 */
390 $data_type = isset( $field['data_type'] ) ? $field['data_type'] : 'internal';
391 $ret[ $data_type ][ $key ] = $field['type'];
392
393 return $ret;
394 }
395
396 /**
397 * Returns the submitted values.
398 *
399 * Use this method instead of accessing the raw $_POST array to take
400 * advantage of the filter on the values.
401 *
402 * @since 1.0.0
403 *
404 * @param array $fields Merged array of all the fields in the form.
405 * @return array
406 */
407 public function get_submitted_values() {
408 if ( ! isset( $this->submitted ) ) {
409 $submitted = $_POST;
410 $fields = $this->get_fields_by_data_type();
411 $options = $this->get_valid_field_options();
412
413 /* Process the submitted data. This handles field sanitization. */
414 $data_processor = new Charitable_Data_Processor( $submitted, $fields, $options );
415
416 foreach ( $data_processor->output() as $data_type_data ) {
417 $submitted = array_merge( $submitted, $data_type_data );
418 }
419
420 /**
421 * Filter the submitted values.
422 *
423 * @since 1.0.0
424 *
425 * @param array $data The submitted data.
426 * @param Charitable_Form $form The form class.
427 */
428 $this->submitted = apply_filters( 'charitable_form_submitted_values', $submitted, $this );
429 }
430
431 return $this->submitted;
432 }
433
434 /**
435 * Returns the submitted value for a particular field.
436 *
437 * @since 1.0.0
438 *
439 * @param string $key The key to search for.
440 * @return mixed Submitted value if set. NULL if value was not set.
441 */
442 public function get_submitted_value( $key ) {
443 $submitted = $this->get_submitted_values();
444 return isset( $submitted[ $key ] ) ? $submitted[ $key ] : null;
445 }
446
447 /**
448 * Uploads a file and attaches it to the given post.
449 *
450 * @since 1.0.0
451 *
452 * @param string $file_key Key of the file input.
453 * @param int $post_id Post ID.
454 * @param array $post_data Overwrite some of the attachment. Optional.
455 * @param array $overrides Override the wp_handle_upload() behavior. Optional.
456 * @return int|WP_Error ID of the attachment or a WP_Error object on failure.
457 */
458 public function upload_post_attachment( $file_key, $post_id, $post_data = array(), $overrides = array() ) {
459 require_once( ABSPATH . 'wp-admin/includes/image.php' );
460 require_once( ABSPATH . 'wp-admin/includes/file.php' );
461 require_once( ABSPATH . 'wp-admin/includes/media.php' );
462
463 $overrides = $this->get_file_overrides( $file_key, $overrides );
464
465 return media_handle_upload( $file_key, $post_id, $post_data, $overrides );
466 }
467
468 /**
469 * Upload a file.
470 *
471 * @since 1.0.0
472 *
473 * @param string $file_key Reference to a single element of `$_FILES`. Call the
474 * function once for each uploaded file.
475 * @param array $overrides Optional. An associative array of names=>values to
476 * override default variables. Default false.
477 * @return array|WP_Error On success, returns an associative array of file attributes.
478 * On failure, returns $overrides['upload_error_handler'](&$file, $message )
479 * or array( 'error'=>$message ).
480 */
481 public function upload_file( $file_key, $overrides = array() ) {
482 require_once( ABSPATH . 'wp-admin/includes/file.php' );
483
484 $overrides = $this->get_file_overrides( $file_key, $overrides );
485 $file = wp_handle_upload( $_FILES[ $file_key ], $overrides );
486
487 if ( isset( $file['error'] ) ) {
488 return new WP_Error( 'upload_error', $file['error'] );
489 }
490
491 return $file;
492 }
493
494 /**
495 * Return overrides array for use with upload_file() and upload_post_attachment() methods.
496 *
497 * @since 1.0.0
498 *
499 * @param string $file_key Reference to a single element of `$_FILES`. Call the
500 * function once for each uploaded file.
501 * @param array $overrides Optional. An associative array of names=>values to
502 * override default variables. Default false.
503 * @return array
504 */
505 protected function get_file_overrides( $file_key, $overrides = array() ) {
506 $allowed_mimes = array(
507 'jpg|jpeg|jpe' => 'image/jpeg',
508 'gif' => 'image/gif',
509 'png' => 'image/png',
510 'bmp' => 'image/bmp',
511 'tif|tiff' => 'image/tiff',
512 'ico' => 'image/x-icon',
513 );
514
515 $defaults = array(
516 'test_form' => false,
517 'mimes' => apply_filters( 'charitable_file_' . $file_key . '_allowed_mimes', $allowed_mimes ),
518 );
519
520 $overrides = wp_parse_args( $overrides, $defaults );
521
522 return $overrides;
523 }
524
525 /**
526 * Returns all fields as a merged array.
527 *
528 * @since 1.6.51
529 *
530 * @return array[]
531 */
532 public function get_merged_fields() {
533 $fields = array();
534
535 foreach ( $this->get_fields() as $key => $section ) {
536 if ( isset( $section['fields'] ) ) {
537 $fields = array_merge( $fields, $section['fields'] );
538 } else {
539 $fields[ $key ] = $section;
540 }
541 }
542
543 return $fields;
544 }
545
546 /**
547 * Return the form fields, organized by data type.
548 *
549 * @since 1.6.51
550 *
551 * @return array
552 */
553 protected function get_fields_by_data_type() {
554 $fields = array();
555
556 foreach ( $this->get_merged_fields() as $key => $field ) {
557 $fields = $this->sort_field_by_data_type( $key, $field, $fields );
558 }
559
560 return $fields;
561 }
562
563 /**
564 * Get an array of valid field options for fields in the form.
565 *
566 * @since 1.6.51
567 *
568 * @return array
569 */
570 protected function get_valid_field_options() {
571 $options = array();
572
573 foreach ( $this->get_merged_fields() as $key => $field ) {
574 /* The field has a specific set of options. */
575 if ( isset( $field['options'] ) ) {
576 $options[ $key ] = array_keys( $field['options'] );
577 /* Checkbox fields without a value set should be either 1 or nothing. */
578 } elseif ( isset( $field['type'] ) && 'checkbox' === $field['type'] ) {
579 $checked_value = isset( $field['value'] ) ? $field['value'] : 1;
580 $options[ $key ] = array( $checked_value );
581 }
582 }
583
584 return $options;
585 }
586
587 /**
588 * Return the template name used for this field.
589 *
590 * @deprecated 1.8.0
591 *
592 * @since 1.0.0
593 * @since 1.5.0 Deprecated.
594 *
595 * @param array $field Field definition.
596 * @return string
597 */
598 public function get_template_name( $field ) {
599 charitable_get_deprecated()->deprecated_function(
600 __METHOD__,
601 '1.5.0',
602 'Charitable_Public_Form_View::get_template_name()'
603 );
604
605 return $form->view()->get_template_name( $field );
606 }
607
608 /**
609 * Checks whether a template is valid.
610 *
611 * @deprecated 1.8.0
612 *
613 * @since 1.0.0
614 * @since 1.5.0 Deprecated. Implemented by Form View.
615 *
616 * @param mixed $template Template we're checking.
617 * @return boolean
618 */
619 protected function is_valid_template( $template ) {
620 charitable_get_deprecated()->deprecated_function(
621 __METHOD__,
622 '1.5.0',
623 'Charitable_Public_Form_View::is_valid_template()'
624 );
625
626 return $this->view()->is_valid_template( $template );
627 }
628
629 /**
630 * Whether the given field type can use the default field template.
631 *
632 * @deprecated 1.8.0
633 *
634 * @since 1.0.0
635 * @since 1.5.0 Deprecated. Implemented by Form View.
636 *
637 * @param string $field_type Type of field.
638 * @return boolean
639 */
640 protected function use_default_field_template( $field_type ) {
641 charitable_get_deprecated()->deprecated_function(
642 __METHOD__,
643 '1.5.0',
644 'Charitable_Public_Form_View::use_default_field_template()'
645 );
646
647 return $this->view()->use_default_field_template( $field_type );
648 }
649
650 /**
651 * Set how much the index should be incremented by.
652 *
653 * @deprecated 1.8.0
654 *
655 * @since 1.0.0
656 * @since 1.5.0 Deprecated. Implemented by Form View.
657 *
658 * @param int $increment The number the index should be incremented by.
659 * @param array $field The field definition.
660 * @return int
661 */
662 public function increment_index( $increment, $field ) {
663 charitable_get_deprecated()->deprecated_function(
664 __METHOD__,
665 '1.5.0',
666 'Charitable_Public_Form_View::increment_index()'
667 );
668
669 /**
670 * Remove form's hooked filter.
671 *
672 * Before 1.5, forms used the filter to set the increment level. For
673 * backwards-compatibility purposes, we still provide this method in the
674 * form class, but it calls the Form View. This method shoud
675 * default in the form abstract, but remove it when this function
676 * is called directly.
677 */
678 remove_filter( 'charitable_form_field_increment', array( $this, 'increment_index' ), 10, 2 );
679
680 return $this->view()->increment_index( $field );
681 }
682
683 /**
684 * Display error notices at the start of the form, if there are any.
685 *
686 * @deprecated 1.8.0
687 *
688 * @since 1.0.0
689 * @since 1.5.0 Deprecated. Notices are rendered by the Form View now.
690 *
691 * @param Charitable_Form $form Form object.
692 * @return boolean Whether the notices were rendered.
693 */
694 public function render_error_notices( $form ) {
695 charitable_get_deprecated()->deprecated_function(
696 __METHOD__,
697 '1.5.0',
698 'Charitable_Public_Form_View::render_notices()'
699 );
700
701 if ( ! $form->is_current_form( $this->id ) ) {
702 return false;
703 }
704
705 return $this->view()->render_notices();
706 }
707
708 /**
709 * Adds hidden fields to the start of the donation form.
710 *
711 * @deprecated 1.8.0
712 *
713 * @since 1.0.0
714 * @since 1.5.0 Deprecated. Hidden fields are rendered by the Form View now.
715 *
716 * @param Charitable_Form $form The form object.
717 * @return boolean Whether the output is added.
718 */
719 public function add_hidden_fields( $form ) {
720 charitable_get_deprecated()->deprecated_function(
721 __METHOD__,
722 '1.5.0',
723 'Charitable_Public_Form_View::render_hidden_fields()'
724 );
725
726 if ( ! $form->is_current_form( $this->id ) ) {
727 return false;
728 }
729
730 return $this->view()->render_hidden_fields();
731 }
732
733 /**
734 * Render a form field.
735 *
736 * @deprecated 1.8.0
737 *
738 * @since 1.0.0
739 * @since 1.5.0 Deprecated. Use `Charitable_Public_Form_View` instead.
740 * @since 1.5.4 Added $from_template parameter for backwards compatibility purposes.
741 * Anytime this hook is called from a template using the pre-Charitable 1.5
742 * approach of rendering fields (i.e. do_action( 'charitable_form_field' )),
743 * there will not be a sixth parameter, so this will default to true.
744 *
745 * @param array $field Field definition.
746 * @param string $key Field key.
747 * @param Charitable_Form $form The form object.
748 * @param int $index The current index.
749 * @param string $namespace Namespace for the form field's name attribute.
750 * @param boolean $from_template Whether the method was called from a template file.
751 * @return boolean False if the field was not rendered. True otherwise.
752 */
753 public function render_field( $field, $key, $form, $index = 0, $namespace = null, $from_template = true ) {
754 if ( ! $from_template ) {
755 return false;
756 }
757
758 charitable_get_deprecated()->deprecated_function(
759 __METHOD__,
760 '1.5.0',
761 'Charitable_Public_Form_View::render_field()'
762 );
763
764 if ( ! $form->is_current_form( $this->id ) ) {
765 return false;
766 }
767
768 /**
769 * This was not evoked by the form view so it's likely that notices,
770 * hidden fields and the honeypot also haven't been rendered.
771 */
772 if ( ! $this->view()->rendered_notices ) {
773 $this->view()->render_notices();
774 }
775
776 if ( ! $this->view()->rendered_honeypot ) {
777 $this->view()->render_honeypot();
778 }
779
780 if ( ! $this->view()->rendered_hidden_fields ) {
781 $this->view()->render_hidden_fields();
782 }
783
784 return $form->view()->render_field(
785 $field,
786 $key,
787 array(
788 'index' => $index,
789 'namespace' => $namespace,
790 )
791 );
792 }
793 }
794
795 endif;
796