PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
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
← All changes | includes/donations/charitable-donation-functions.php +294 -10 1.7.0.7 → 1.8.13 View file →
@@ -3,12 +3,13 @@
3 3 * Charitable Donation Functions.
4 4 *
5 5 * @package Charitable/Functions/Donation
6 6 * @author David Bisset
7 - * @copyright Copyright (c) 2022, WP Charitable LLC
7 + * @copyright Copyright (c) 2023, WP Charitable LLC
8 8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 9 * @since 1.0.0
10 10 * @version 1.6.60
11 + * @version 1.8.9.2
11 12 */
12 13
13 14 // Exit if accessed directly.
14 15 if ( ! defined( 'ABSPATH' ) ) {
@@ -177,9 +178,9 @@
177 178 FROM $wpdb->postmeta
178 179 WHERE meta_key = 'donation_key'
179 180 AND meta_value = %s";
180 181
181 - return $wpdb->get_var( $wpdb->prepare( $sql, $donation_key ) );
182 + return $wpdb->get_var( $wpdb->prepare( $sql, $donation_key ) ); // phpcs:ignore
182 183 }
183 184
184 185 /**
185 186 * Find and return a donation using a gateway transaction ID.
@@ -196,9 +197,9 @@
196 197 FROM $wpdb->postmeta
197 198 WHERE meta_key = '_gateway_transaction_id'
198 199 AND meta_value = %s";
199 200
200 - return $wpdb->get_var( $wpdb->prepare( $sql, $transaction_id ) );
201 + return $wpdb->get_var( $wpdb->prepare( $sql, $transaction_id ) ); // phpcs:ignore
201 202 }
202 203
203 204 /**
204 205 * Return the IPN url for this gateway.
@@ -273,9 +274,8 @@
273 274
274 275 delete_transient( 'charitable_donation_' . charitable_get_session()->get_session_id() );
275 276
276 277 return true;
277 -
278 278 }
279 279
280 280 /**
281 281 * Returns whether the donation status is valid.
@@ -324,9 +324,9 @@
324 324 * @param string|false $status The status to check.
325 325 * @return boolean
326 326 */
327 327 function charitable_is_approved_status( $status ) {
328 - return false !== $status && in_array( $status, charitable_get_approval_statuses() );
328 + return false !== $status && in_array( $status, charitable_get_approval_statuses() ); // phpcs:ignore
329 329 }
330 330
331 331 /**
332 332 * Return array of valid donations statuses.
@@ -380,9 +380,9 @@
380 380 return false;
381 381 }
382 382
383 383 /* Donations can only be cancelled if they are currently pending. */
384 - if ( 'charitable-pending' != $donation->get_status() ) {
384 + if ( 'charitable-pending' != $donation->get_status() ) { // phpcs:ignore
385 385 return false;
386 386 }
387 387
388 388 if ( ! $donation->is_from_current_user() ) {
@@ -451,9 +451,9 @@
451 451 * @param string $key The key of the meta field to be sanitized.
452 452 * @return mixed
453 453 */
454 454 function charitable_sanitize_donation_meta( $value, $key ) {
455 - if ( 'donation_gateway' == $key ) {
455 + if ( 'donation_gateway' == $key ) { // phpcs:ignore
456 456 if ( empty( $value ) || ! $value ) {
457 457 $value = 'manual';
458 458 }
459 459 }
@@ -465,9 +465,9 @@
465 465 * @since 1.5.0 Deprecated.
466 466 *
467 467 * @param mixed $value The value of the donation meta field.
468 468 */
469 - $value = apply_filters( 'charitable_sanitize_donation_meta-' . $key, $value );
469 + $value = apply_filters( 'charitable_sanitize_donation_meta-' . $key, $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
470 470
471 471 /**
472 472 * Filter donation meta.
473 473 *
@@ -479,9 +479,9 @@
479 479 * @since 1.5.0
480 480 *
481 481 * @param mixed $value The value of the donation meta field.
482 482 */
483 - return apply_filters( 'charitable_sanitize_donation_meta_' . $key, $value );
483 + return apply_filters( 'charitable_sanitize_donation_meta_' . $key, $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
484 484 }
485 485
486 486 /**
487 487 * Flush the donations cache for every campaign receiving a donation.
@@ -527,12 +527,18 @@
527 527 if ( 0 !== $campaign_id ) {
528 528
529 529 $minimum_amount_to_check = get_post_meta( $campaign_id, '_campaign_minimum_donation_amount', true );
530 530
531 + if ( charitable_is_debug() ) {
532 + error_log( print_r( $minimum_amount_to_check, true ) ); // phpcs:ignore
533 + }
534 +
531 535 if ( false !== $minimum_amount_to_check && '' !== trim( $minimum_amount_to_check ) ) {
532 536 $minimum_amount = charitable_sanitize_amount( $minimum_amount_to_check, true ); // converts a format like $1.05 to 1.05, per the dev note below.
537 + if ( charitable_is_debug() ) {
538 + error_log( print_r( $minimum_amount, true ) ); // phpcs:ignore
539 + }
533 540 }
534 -
535 541 }
536 542
537 543 /**
538 544 * Filter the minimum donation amount.
@@ -586,5 +592,283 @@
586 592 function charitable_user_can_access_donation( $donation_id ) {
587 593 $donation = charitable_get_donation( $donation_id );
588 594
589 595 return $donation && $donation->is_from_current_user();
596 +}
597 +
598 +/**
599 + * Log a donation form error.
600 + *
601 + * @since 1.8.9.2
602 + *
603 + * @param string $error_type The type of error (validation_failure, ajax_failure, security_failure, email_failure).
604 + * @param string $error_details Specific error details or code.
605 + * @param array $context Additional context data for the error.
606 + *
607 + * @return int|false The activity ID on success, false on failure.
608 + */
609 +function charitable_log_form_error( $error_type, $error_details, $context = array() ) {
610 + // Check if logging is enabled
611 + if ( ! apply_filters( 'charitable_form_error_logging_enabled', true ) ) {
612 + return false;
613 + }
614 +
615 + // Basic spam protection - rate limiting per IP
616 + $ip_address = charitable_get_user_ip_address();
617 + if ( ! charitable_check_basic_rate_limit( $ip_address ) ) {
618 + return false; // Rate limit exceeded
619 + }
620 +
621 + // Basic burst protection - total errors per minute
622 + if ( ! charitable_check_basic_burst_limit() ) {
623 + return false; // Burst limit exceeded
624 + }
625 +
626 + // Get the activity logger instance (defensive: avoid fatal if class not loaded, e.g. cron/frontend edge cases).
627 + if ( ! class_exists( 'Charitable_Admin_Activities' ) ) {
628 + return false;
629 + }
630 + $activity_logger = Charitable_Admin_Activities::get_instance();
631 + if ( ! is_object( $activity_logger ) || ! method_exists( $activity_logger, 'log_form_error' ) ) {
632 + return false;
633 + }
634 +
635 + // Prepare context with current request data
636 + $default_context = array(
637 + 'url' => isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( $_SERVER['REQUEST_URI'] ) : '',
638 + 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ) : '',
639 + 'ip_address' => $ip_address,
640 + );
641 +
642 + $context = array_merge( $default_context, $context );
643 +
644 + return $activity_logger->log_form_error( $error_type, $error_details, $context );
645 +}
646 +
647 +/**
648 + * Get user IP address safely.
649 + *
650 + * @since 1.8.9.2
651 + *
652 + * @return string User IP address.
653 + */
654 +function charitable_get_user_ip_address() {
655 + $ip_address = '';
656 +
657 + // Check for IP from shared internet
658 + if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
659 + $ip_address = $_SERVER['HTTP_CLIENT_IP'];
660 + }
661 + // Check for IP passed from proxy (may be comma-separated list: client, proxy1, proxy2)
662 + elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
663 + $ip_address = trim( strtok( $_SERVER['HTTP_X_FORWARDED_FOR'], ',' ) );
664 + }
665 + // Check for IP from remote address
666 + elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
667 + $ip_address = $_SERVER['REMOTE_ADDR'];
668 + }
669 +
670 + // Validate and sanitize IP (prevents using invalid/comma-separated value)
671 + $ip_address = filter_var( $ip_address, FILTER_VALIDATE_IP );
672 +
673 + return $ip_address ? $ip_address : '';
674 +}
675 +
676 +/**
677 + * Get recent donation form errors.
678 + *
679 + * @since 1.8.9.2
680 + *
681 + * @param int $limit The number of errors to retrieve.
682 + *
683 + * @return array Array of error records.
684 + */
685 +function charitable_get_recent_form_errors( $limit = 50 ) {
686 + if ( ! class_exists( 'Charitable_Admin_Activities' ) ) {
687 + return array();
688 + }
689 + $activity_logger = Charitable_Admin_Activities::get_instance();
690 + if ( ! is_object( $activity_logger ) || ! method_exists( $activity_logger, 'get_recent_form_errors' ) ) {
691 + return array();
692 + }
693 + return $activity_logger->get_recent_form_errors( $limit );
694 +}
695 +
696 +/**
697 + * Get donation form error statistics.
698 + *
699 + * @since 1.8.9.2
700 + *
701 + * @param int $days Number of days to look back.
702 + *
703 + * @return array Statistics array.
704 + */
705 +function charitable_get_form_error_stats( $days = 7 ) {
706 + if ( ! class_exists( 'Charitable_Admin_Activities' ) ) {
707 + return array();
708 + }
709 + $activity_logger = Charitable_Admin_Activities::get_instance();
710 + if ( ! is_object( $activity_logger ) || ! method_exists( $activity_logger, 'get_form_error_stats' ) ) {
711 + return array();
712 + }
713 + return $activity_logger->get_form_error_stats( $days );
714 +}
715 +
716 +/**
717 + * Clean up old donation form error logs.
718 + *
719 + * @since 1.8.9.2
720 + *
721 + * @param int $days Number of days to retain.
722 + *
723 + * @return int Number of records deleted.
724 + */
725 +function charitable_cleanup_old_form_errors( $days = 7 ) {
726 + // Allow customization of retention period
727 + $days = apply_filters( 'charitable_form_error_retention_days', $days );
728 +
729 + if ( ! class_exists( 'Charitable_Admin_Activities' ) ) {
730 + return 0;
731 + }
732 + $activity_logger = Charitable_Admin_Activities::get_instance();
733 + if ( ! is_object( $activity_logger ) || ! method_exists( $activity_logger, 'cleanup_old_form_errors' ) ) {
734 + return 0;
735 + }
736 + return $activity_logger->cleanup_old_form_errors( $days );
737 +}
738 +
739 +/**
740 + * Schedule daily cleanup of old donation form error logs.
741 + *
742 + * @since 1.8.9.2
743 + *
744 + * @return void
745 + */
746 +function charitable_schedule_error_log_cleanup() {
747 + if ( ! wp_next_scheduled( 'charitable_daily_error_log_cleanup' ) ) {
748 + // Schedule daily cleanup at 3 AM local time
749 + wp_schedule_event( strtotime( '03:00:00' ), 'daily', 'charitable_daily_error_log_cleanup' );
750 + }
751 +}
752 +
753 +/**
754 + * Register the deactivation hook for error log cleanup.
755 + *
756 + * @since 1.8.9.2
757 + *
758 + * @return void
759 + */
760 +function charitable_register_error_log_deactivation_hook() {
761 + $charitable = charitable();
762 + if ( ! $charitable || ! is_callable( array( $charitable, 'get_path' ) ) ) {
763 + return;
764 + }
765 + register_deactivation_hook( $charitable->get_path(), 'charitable_cleanup_error_log_cron_on_deactivation' );
766 +}
767 +
768 +/**
769 + * Clean up scheduled cron jobs on plugin deactivation.
770 + *
771 + * @since 1.8.9.2
772 + *
773 + * @return void
774 + */
775 +function charitable_cleanup_error_log_cron_on_deactivation() {
776 + // Clear all scheduled events for this hook (avoids orphaned events if scheduled multiple times).
777 + wp_clear_scheduled_hook( 'charitable_daily_error_log_cleanup' );
778 +}
779 +
780 +/**
781 + * Basic rate limiting check per IP address.
782 + *
783 + * @since 1.8.9.2
784 + *
785 + * @param string $ip_address IP address to check.
786 + *
787 + * @return bool True if within rate limit, false if exceeded.
788 + */
789 +function charitable_check_basic_rate_limit( $ip_address ) {
790 + $rate_limit = apply_filters( 'charitable_error_rate_limit_per_ip', 10 ); // 10 errors per minute per IP
791 + $time_window = apply_filters( 'charitable_error_rate_limit_window', 60 ); // 60 seconds
792 +
793 + if ( empty( $ip_address ) ) {
794 + return true; // Can't rate limit without IP
795 + }
796 +
797 + $transient_key = 'charitable_error_rate_' . md5( $ip_address );
798 + $current_count = get_transient( $transient_key );
799 +
800 + if ( false === $current_count ) {
801 + // First error from this IP in this time window
802 + set_transient( $transient_key, 1, $time_window );
803 + return true;
804 + }
805 +
806 + if ( $current_count >= $rate_limit ) {
807 + // Rate limit exceeded
808 + return false;
809 + }
810 +
811 + // Increment counter
812 + set_transient( $transient_key, $current_count + 1, $time_window );
813 + return true;
814 +}
815 +
816 +/**
817 + * Basic burst protection check.
818 + *
819 + * @since 1.8.9.2
820 + *
821 + * @return bool True if within burst limit, false if exceeded.
822 + */
823 +function charitable_check_basic_burst_limit() {
824 + $burst_limit = apply_filters( 'charitable_error_burst_limit', 50 ); // 50 errors per minute system-wide
825 + $time_window = apply_filters( 'charitable_error_burst_window', 60 ); // 60 seconds
826 +
827 + $transient_key = 'charitable_error_burst_count';
828 + $current_count = get_transient( $transient_key );
829 +
830 + if ( false === $current_count ) {
831 + // First error in this time window
832 + set_transient( $transient_key, 1, $time_window );
833 + return true;
834 + }
835 +
836 + if ( $current_count >= $burst_limit ) {
837 + // Burst limit exceeded
838 + return false;
839 + }
840 +
841 + // Increment counter
842 + set_transient( $transient_key, $current_count + 1, $time_window );
843 + return true;
844 +}
845 +
846 +/**
847 + * Get current spam protection status.
848 + *
849 + * @since 1.8.9.2
850 + *
851 + * @return array Status information.
852 + */
853 +function charitable_get_spam_protection_status() {
854 + $ip_address = charitable_get_user_ip_address();
855 + $ip_transient_key = 'charitable_error_rate_' . md5( $ip_address );
856 + $burst_transient_key = 'charitable_error_burst_count';
857 +
858 + $ip_count = get_transient( $ip_transient_key );
859 + $burst_count = get_transient( $burst_transient_key );
860 +
861 + $rate_limit = apply_filters( 'charitable_error_rate_limit_per_ip', 10 );
862 + $burst_limit = apply_filters( 'charitable_error_burst_limit', 50 );
863 +
864 + return array(
865 + 'ip_address' => $ip_address,
866 + 'ip_errors_this_minute' => $ip_count ?: 0,
867 + 'ip_rate_limit' => $rate_limit,
868 + 'ip_rate_limit_exceeded' => ( $ip_count >= $rate_limit ),
869 + 'total_errors_this_minute' => $burst_count ?: 0,
870 + 'burst_limit' => $burst_limit,
871 + 'burst_limit_exceeded' => ( $burst_count >= $burst_limit ),
872 + 'logging_enabled' => apply_filters( 'charitable_form_error_logging_enabled', true ),
873 + );
590 874 }