PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.12.3
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.12.3
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 / donations / charitable-donation-functions.php

charitable-donation-functions.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.12.3, at includes/donations/charitable-donation-functions.php

875 lines 23.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Donation Functions.
4 *
5 * @package Charitable/Functions/Donation
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.0.0
10 * @version 1.6.60
11 * @version 1.8.9.2
12 */
13
14 // Exit if accessed directly.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * Returns the given donation.
21 *
22 * This will first attempt to retrieve it from the object cache to prevent duplicate objects.
23 *
24 * @since 1.0.0
25 *
26 * @param int $donation_id The donation ID.
27 * @param boolean $force Whether to force a non-cached donation object to be retrieved.
28 * @return Charitable_Donation|false
29 */
30 function charitable_get_donation( $donation_id, $force = false ) {
31 $donation = wp_cache_get( $donation_id, 'charitable_donation', $force );
32
33 if ( ! $donation ) {
34 $donation = charitable()->registry()->get( 'donation_factory' )->get_donation( $donation_id );
35 wp_cache_set( $donation_id, $donation, 'charitable_donation' );
36 }
37
38 return $donation;
39 }
40
41 /**
42 * Update the donation cache.
43 *
44 * @since 1.6.34
45 *
46 * @param Charitable_Donation|Charitable_Recurring_Donation $donation The donation object.
47 * @return mixed
48 */
49 function charitable_update_cached_donation( Charitable_Abstract_Donation $donation ) {
50 wp_cache_set( $donation->ID, $donation, 'charitable_donation' );
51 }
52
53 /**
54 * Given a donation ID and a key, return the submitted value.
55 *
56 * @since 1.5.0
57 *
58 * @param Charitable_Abstract_Donation $donation The donation ID.
59 * @param string $key The meta key.
60 * @return mixed
61 */
62 function charitable_get_donor_meta_value( Charitable_Abstract_Donation $donation, $key ) {
63 return $donation->get_donor()->get_donor_meta( $key );
64 }
65
66 /**
67 * Given a donation ID and a key, return the submitted value.
68 *
69 * @since 1.5.0
70 *
71 * @param Charitable_Abstract_Donation $donation The donation ID.
72 * @param string $key The meta key.
73 * @return mixed
74 */
75 function charitable_get_donation_meta_value( Charitable_Abstract_Donation $donation, $key ) {
76 return get_post_meta( $donation->ID, $key, true );
77 }
78
79 /**
80 * Sanitize a value for a donation field, based on the type of field.
81 *
82 * @since 1.6.7
83 *
84 * @param mixed $value The submitted value.
85 * @param string $key The meta key.
86 * @return mixed
87 */
88 function charitable_get_sanitized_donation_field_value( $value, $key ) {
89 $field = charitable()->donation_fields()->get_field( $key );
90 $form = ( false === $field->admin_form ) ? $field->donation_form : $field->admin_form;
91
92 if ( ! is_array( $form ) ) {
93 return $value;
94 }
95
96 if ( ! array_key_exists( 'options', $form ) ) {
97 return $value;
98 }
99
100 if ( is_array( $value ) ) {
101 $values = array();
102
103 foreach ( $value as $val ) {
104 $values[] = array_key_exists( $val, $form['options'] ) ? $form['options'][ $val ] : $val;
105 }
106
107 $value = implode( ', ', $values );
108 } else {
109 $value = array_key_exists( $value, $form['options'] ) ? $form['options'][ $value ] : $value;
110 }
111
112 return $value;
113 }
114
115 /**
116 * Return the date formatted for a form field value.
117 *
118 * @since 1.5.3
119 *
120 * @param Charitable_Abstract_Donation $donation The donation instance.
121 * @return string
122 */
123 function charitable_get_donation_date_for_form_value( Charitable_Abstract_Donation $donation ) {
124 return $donation->get_date( 'F j, Y' );
125 }
126
127 /**
128 * Get the donation date as a timestamp.
129 *
130 * @since 1.6.52
131 *
132 * @param Charitable_Abstract_Donation $donation The donation instance.
133 * @return int
134 */
135 function charitable_get_donation_date_as_timestamp( Charitable_Abstract_Donation $donation ) {
136 return strtotime( $donation->post_date );
137 }
138
139 /**
140 * Returns the donation for the current request.
141 *
142 * @since 1.0.0
143 *
144 * @return Charitable_Donation|false
145 */
146 function charitable_get_current_donation() {
147 return charitable_get_helper( 'request' )->get_current_donation();
148 }
149
150 /**
151 * Create a donation.
152 *
153 * @since 1.4.0
154 *
155 * @param array $args Values for the donation.
156 * @return int
157 */
158 function charitable_create_donation( array $args ) {
159 $donation_id = Charitable_Donation_Processor::get_instance()->save_donation( $args );
160
161 Charitable_Donation_Processor::destroy();
162
163 return $donation_id;
164 }
165
166 /**
167 * Find and return a donation based on the given donation key.
168 *
169 * @since 1.4.0
170 *
171 * @param string $donation_key The donation key to query by.
172 * @return int|null
173 */
174 function charitable_get_donation_by_key( $donation_key ) {
175 global $wpdb;
176
177 $sql = "SELECT post_id
178 FROM $wpdb->postmeta
179 WHERE meta_key = 'donation_key'
180 AND meta_value = %s";
181
182 return $wpdb->get_var( $wpdb->prepare( $sql, $donation_key ) ); // phpcs:ignore
183 }
184
185 /**
186 * Find and return a donation using a gateway transaction ID.
187 *
188 * @since 1.4.7
189 *
190 * @param string $transaction_id The transaction id to query by.
191 * @return int|null
192 */
193 function charitable_get_donation_by_transaction_id( $transaction_id ) {
194 global $wpdb;
195
196 $sql = "SELECT post_id
197 FROM $wpdb->postmeta
198 WHERE meta_key = '_gateway_transaction_id'
199 AND meta_value = %s";
200
201 return $wpdb->get_var( $wpdb->prepare( $sql, $transaction_id ) ); // phpcs:ignore
202 }
203
204 /**
205 * Return the IPN url for this gateway.
206 *
207 * IPNs in Charitable are structured in this way: charitable-listener=gateway
208 *
209 * @since 1.4.0
210 *
211 * @param string $gateway The gateway to get the ipn URL for.
212 * @return string
213 */
214 function charitable_get_ipn_url( $gateway ) {
215 return charitable_get_permalink( 'webhook_listener', array( 'gateway' => $gateway ) );
216 }
217
218 /**
219 * Checks for calls to our IPN.
220 *
221 * This method is called on the init hook.
222 *
223 * IPNs in Charitable are structured in this way: charitable-listener=gateway
224 *
225 * @deprecated 1.9.0
226 *
227 * @since 1.4.0
228 * @since 1.6.14 Deprecated. This is now handled by the webhook listener endpoint.
229 *
230 * @return boolean True if this is a call to our IPN. False otherwise.
231 */
232 function charitable_ipn_listener() {
233 charitable_get_deprecated()->deprecated_function(
234 __FUNCTION__,
235 '1.6.14',
236 "charitable()->endpoints()->get_endpoint( 'webhook_listener' )->process_incoming_webhook()"
237 );
238
239 return charitable()->endpoints()->get_endpoint( 'webhook_listener' )->process_incoming_webhook();
240 }
241
242 /**
243 * Checks if this is happening right after a donation.
244 *
245 * This method is called on the init hook.
246 *
247 * @since 1.4.0
248 *
249 * @return boolean Whether this is after a donation.
250 */
251 function charitable_is_after_donation() {
252 if ( is_admin() ) {
253 return false;
254 }
255
256 $processor = get_transient( 'charitable_donation_' . charitable_get_session()->get_session_id() );
257
258 if ( ! $processor ) {
259 return false;
260 }
261
262 /**
263 * Do something on a user's first page load after a donation has been made.
264 *
265 * @since 1.3.6
266 *
267 * @param Charitable_Donation_Processor $processor The instance of `Charitable_Donation_Processor`.
268 */
269 do_action( 'charitable_after_donation', $processor );
270
271 foreach ( $processor->get_campaign_donations_data() as $campaign_donation ) {
272 charitable_get_session()->remove_donation( $campaign_donation['campaign_id'] );
273 }
274
275 delete_transient( 'charitable_donation_' . charitable_get_session()->get_session_id() );
276
277 return true;
278 }
279
280 /**
281 * Returns whether the donation status is valid.
282 *
283 * @since 1.4.0
284 *
285 * @param string $status The status to check.
286 * @return boolean
287 */
288 function charitable_is_valid_donation_status( $status ) {
289 return array_key_exists( $status, charitable_get_valid_donation_statuses() );
290 }
291
292 /**
293 * Returns the donation statuses that signify a donation was complete.
294 *
295 * By default, this is just 'charitable-completed'. However, 'charitable-preapproval'
296 * is also counted.
297 *
298 * @since 1.4.0
299 *
300 * @return string[]
301 */
302 function charitable_get_approval_statuses() {
303 /**
304 * Filter the list of donation statuses that we consider "approved".
305 *
306 * All statuses must already be listed as valid donation statuses.
307 *
308 * @see charitable_get_valid_donation_statuses
309 *
310 * @since 1.0.0
311 *
312 * @param string[] $statuses List of statuses.
313 */
314 $statuses = apply_filters( 'charitable_approval_donation_statuses', array( 'charitable-completed' ) );
315
316 return array_filter( $statuses, 'charitable_is_valid_donation_status' );
317 }
318
319 /**
320 * Returns whether the passed status is an confirmed status.
321 *
322 * @since 1.4.0
323 *
324 * @param string|false $status The status to check.
325 * @return boolean
326 */
327 function charitable_is_approved_status( $status ) {
328 return false !== $status && in_array( $status, charitable_get_approval_statuses() ); // phpcs:ignore
329 }
330
331 /**
332 * Return array of valid donations statuses.
333 *
334 * @since 1.4.0
335 *
336 * @return array
337 */
338 function charitable_get_valid_donation_statuses() {
339 /**
340 * Filter the list of possible donation statuses.
341 *
342 * @since 1.0.0
343 *
344 * @param array $statuses The list of status as a key=>value array.
345 */
346 return apply_filters(
347 'charitable_donation_statuses',
348 array(
349 'charitable-completed' => __( 'Paid', 'charitable' ),
350 'charitable-pending' => __( 'Pending', 'charitable' ),
351 'charitable-failed' => __( 'Failed', 'charitable' ),
352 'charitable-cancelled' => __( 'Cancelled', 'charitable' ),
353 'charitable-refunded' => __( 'Refunded', 'charitable' ),
354 )
355 );
356 }
357
358 /**
359 * Cancel a donation.
360 *
361 * @since 1.4.0
362 *
363 * @global WP_Query $wp_query
364 * @return boolean True if the donation was cancelled. False otherwise.
365 */
366 function charitable_cancel_donation() {
367 global $wp_query;
368
369 if ( ! charitable_is_page( 'donation_cancel_page' ) ) {
370 return false;
371 }
372
373 if ( ! isset( $wp_query->query_vars['donation_id'] ) ) {
374 return false;
375 }
376
377 $donation = charitable_get_donation( $wp_query->query_vars['donation_id'] );
378
379 if ( ! $donation ) {
380 return false;
381 }
382
383 /* Donations can only be cancelled if they are currently pending. */
384 if ( 'charitable-pending' != $donation->get_status() ) { // phpcs:ignore
385 return false;
386 }
387
388 if ( ! $donation->is_from_current_user() ) {
389 return false;
390 }
391
392 $donation->update_status( 'charitable-cancelled' );
393
394 return true;
395 }
396
397 /**
398 * Load the donation form script.
399 *
400 * @since 1.4.0
401 *
402 * @return void
403 */
404 function charitable_load_donation_form_script() {
405 wp_enqueue_script( 'charitable-donation-form' );
406 }
407
408 /**
409 * Add a message to a donation's log.
410 *
411 * @since 1.0.0
412 *
413 * @param int $donation_id The donation ID.
414 * @param string $message The message to add to the log.
415 * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure.
416 */
417 function charitable_update_donation_log( $donation_id, $message ) {
418 return charitable_get_donation( $donation_id )->log()->add( $message );
419 }
420
421 /**
422 * Get a donation's log.
423 *
424 * @since 1.0.0
425 *
426 * @param int $donation_id The donation ID.
427 * @return array
428 */
429 function charitable_get_donation_log( $donation_id ) {
430 return charitable_get_donation( $donation_id )->log()->get_meta_log();
431 }
432
433 /**
434 * Get the gateway used for the donation.
435 *
436 * @since 1.0.0
437 *
438 * @param int $donation_id The donation ID.
439 * @return string
440 */
441 function charitable_get_donation_gateway( $donation_id ) {
442 return get_post_meta( $donation_id, 'donation_gateway', true );
443 }
444
445 /**
446 * Sanitize meta values before they are persisted to the database.
447 *
448 * @since 1.0.0
449 *
450 * @param mixed $value The value to sanitize.
451 * @param string $key The key of the meta field to be sanitized.
452 * @return mixed
453 */
454 function charitable_sanitize_donation_meta( $value, $key ) {
455 if ( 'donation_gateway' == $key ) { // phpcs:ignore
456 if ( empty( $value ) || ! $value ) {
457 $value = 'manual';
458 }
459 }
460
461 /**
462 * Deprecated hook for filtering donation meta.
463 *
464 * @since 1.0.0
465 * @since 1.5.0 Deprecated.
466 *
467 * @param mixed $value The value of the donation meta field.
468 */
469 $value = apply_filters( 'charitable_sanitize_donation_meta-' . $key, $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
470
471 /**
472 * Filter donation meta.
473 *
474 * This hook takes the form of charitable_sanitize_donation_meta_{key}.
475 * For example, to filter the `donation_gateway` meta, the hook would be:
476 *
477 * charitable_sanitize_donation_meta_doantion_gateway
478 *
479 * @since 1.5.0
480 *
481 * @param mixed $value The value of the donation meta field.
482 */
483 return apply_filters( 'charitable_sanitize_donation_meta_' . $key, $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
484 }
485
486 /**
487 * Flush the donations cache for every campaign receiving a donation.
488 *
489 * @since 1.0.0
490 *
491 * @param int $donation_id The donation ID.
492 * @return void
493 */
494 function charitable_flush_campaigns_donation_cache( $donation_id ) {
495 $campaign_donations = charitable_get_table( 'campaign_donations' )->get_donation_records( $donation_id );
496
497 foreach ( $campaign_donations as $campaign_donation ) {
498 Charitable_Campaign::flush_donations_cache( $campaign_donation->campaign_id );
499 }
500
501 wp_cache_delete( $donation_id, 'charitable_donation' );
502
503 /**
504 * Do something when the donation cache is flushed.
505 *
506 * @since 1.4.18
507 *
508 * @param int $donation_id The donation ID.
509 */
510 do_action( 'charitable_flush_donation_cache', $donation_id );
511 }
512
513 /**
514 * Return the minimum donation amount.
515 *
516 * @since 1.5.0
517 * @since 1.6.60 Added the $campaign_id argument.
518 * @since 1.7.0.3 Added the minimum check.
519 *
520 * @param int $campaign_id Optional campaign ID.
521 * @return float
522 */
523 function charitable_get_minimum_donation_amount( $campaign_id = 0 ) {
524
525 $minimum_amount = 0;
526
527 if ( 0 !== $campaign_id ) {
528
529 $minimum_amount_to_check = get_post_meta( $campaign_id, '_campaign_minimum_donation_amount', true );
530
531 if ( charitable_is_debug() ) {
532 error_log( print_r( $minimum_amount_to_check, true ) ); // phpcs:ignore
533 }
534
535 if ( false !== $minimum_amount_to_check && '' !== trim( $minimum_amount_to_check ) ) {
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 }
540 }
541 }
542
543 /**
544 * Filter the minimum donation amount.
545 *
546 * Note, the minimum donation amount should be expressed in a format
547 * where a period (.) is used as the decimal separator. i.e. 2.50 is
548 * two dollars and fifty cents.
549 *
550 * @since 1.5.0
551 * @since 1.6.60 Added the $campaign_id argument.
552 *
553 * @param float $minimum The minimum amount.
554 * @param int $campaign_id Campaign ID. This is 0 by default, meaning the global minimum should be returned.
555 */
556 return apply_filters( 'charitable_minimum_donation_amount', $minimum_amount, $campaign_id );
557 }
558
559 /**
560 * Return the maximum donation amount.
561 *
562 * @since 1.6.60
563 *
564 * @param int $campaign_id Optional campaign ID.
565 * @return float
566 */
567 function charitable_get_maximum_donation_amount( $campaign_id = 0 ) {
568 /**
569 * Filter the maximum donation amount.
570 *
571 * Note, the maximum donation amount should be expressed in a format
572 * where a period (.) is used as the decimal separator and no commas
573 * should be used for thousands. i.e. 9999.90 is nine thousand, nine hundred
574 * ninety-nine dollars and ninety cents.
575 *
576 * @since 1.6.60
577 *
578 * @param float|false $maximum The maximum amount, or false if there is no maximum.
579 * @param int $campaign_id Campaign ID. This is 0 by default, meaning the global maximum should be returned.
580 */
581 return apply_filters( 'charitable_maximum_donation_amount', false, $campaign_id );
582 }
583
584 /**
585 * Check whether the current user has access to a particular donation.
586 *
587 * @since 1.5.14
588 *
589 * @param int $donation_id The donation ID.
590 * @return boolean
591 */
592 function charitable_user_can_access_donation( $donation_id ) {
593 $donation = charitable_get_donation( $donation_id );
594
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 );
874 }
875