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 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
← All changes | includes/utilities/charitable-utility-functions.php +457 -14 1.7.0.14 → 1.8.13 View file →
@@ -5,9 +5,9 @@
5 5 * Utility functions.
6 6 *
7 7 * @package Charitable/Functions/Utility
8 8 * @author David Bisset
9 - * @copyright Copyright (c) 2022, WP Charitable LLC
9 + * @copyright Copyright (c) 2023, WP Charitable LLC
10 10 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
11 11 * @since 1.0.0
12 12 * @version 1.6.55
13 13 */
@@ -29,10 +29,10 @@
29 29 */
30 30 function charitable_element_key_sort( $key, $a, $b ) {
31 31 foreach ( array( $a, $b ) as $item ) {
32 32 if ( ! array_key_exists( $key, $item ) ) {
33 - if ( defined( 'CHARITABLE_DEBUG' ) && CHARITABLE_DEBUG ) {
34 - error_log( sprintf( '%s missing from element: ' . json_encode( $item ), $key ) );
33 + if ( charitable_is_debug() ) {
34 + error_log( sprintf( '%s missing from element: ' . wp_json_encode( $item ), $key ) ); // phpcs:ignore
35 35 }
36 36 }
37 37 }
38 38
@@ -88,8 +88,9 @@
88 88 /**
89 89 * Verify a nonce. This also just ensures that the nonce is set.
90 90 *
91 91 * @since 1.0.0
92 + * @version 1.8.9.1
92 93 *
93 94 * @param string $nonce The nonce name.
94 95 * @param string $action The nonce action.
95 96 * @param array $request_args Request arguments. If not set, will populate with $_GET.
@@ -96,8 +97,9 @@
96 97 * @return boolean
97 98 */
98 99 function charitable_verify_nonce( $nonce, $action, $request_args = array() ) {
99 100 if ( empty( $request_args ) ) {
101 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
100 102 $request_args = $_GET;
101 103 }
102 104
103 105 return isset( $request_args[ $nonce ] ) && wp_verify_nonce( $request_args[ $nonce ], $action );
@@ -132,9 +134,9 @@
132 134
133 135 /* Last try, guess timezone string manually */
134 136 if ( false === $timezone ) {
135 137
136 - $is_dst = date( 'I' );
138 + $is_dst = date( 'I' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
137 139
138 140 foreach ( timezone_abbreviations_list() as $abbr ) {
139 141 foreach ( $abbr as $city ) {
140 142 if ( $city['dst'] == $is_dst && $city['offset'] == $utc_offset ) {
@@ -186,8 +188,39 @@
186 188 return filter_var( $i, FILTER_VALIDATE_EMAIL );
187 189 }
188 190
189 191 /**
192 + * Ensure a sort direction is one of the two values MySQL accepts.
193 + *
194 + * Sort directions cannot be passed to $wpdb->prepare() as placeholders, so any
195 + * value that is interpolated into the ORDER BY clause of a query has to be
196 + * validated against a strict whitelist instead. Anything that is not exactly
197 + * ASC or DESC (case insensitive) falls back to the given default.
198 + *
199 + * @since 1.8.12.2
200 + *
201 + * @param mixed $order The sort direction received.
202 + * @param string $fallback The direction to use when $order is not valid.
203 + * @return string Either 'ASC' or 'DESC'.
204 + */
205 +function charitable_validate_sort_direction( $order, $fallback = 'DESC' ) {
206 + /*
207 + * Only strings are considered. Anything else -- null, arrays, objects,
208 + * integers -- fails closed to the fallback rather than being coerced, so
209 + * there is no path on which caller-supplied bytes are returned.
210 + */
211 + $order = is_string( $order ) ? strtoupper( trim( $order ) ) : '';
212 +
213 + if ( in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
214 + return $order;
215 + }
216 +
217 + $fallback = is_string( $fallback ) ? strtoupper( trim( $fallback ) ) : '';
218 +
219 + return 'ASC' === $fallback ? 'ASC' : 'DESC';
220 +}
221 +
222 +/**
190 223 * Sanitize any checkbox value.
191 224 *
192 225 * @since 1.5.0
193 226 *
@@ -242,9 +275,11 @@
242 275 * We use WP_Locale to parse the month that the user has set.
243 276 *
244 277 * @global WP_Locale $wp_locale
245 278 *
246 - * @since 1.4.10
279 + * @since 1.4.10
280 + * @version 1.8.4.2 Fix bug where english dates weren't being found in $wp_locale.
281 + * @version 1.8.9.1
247 282 *
248 283 * @param string $date The date to be sanitized.
249 284 * @param string $return_format The date format to return. Default is U (timestamp).
250 285 * @return string|false
@@ -255,10 +290,70 @@
255 290 if ( empty( $date ) || ! $date ) {
256 291 return false;
257 292 }
258 293
294 + /*
295 + * Handle Y-m-d format dates (like "2027-01-31") directly. Ported from
296 + * Charitable Pro (1.8.13): the code below this block assumes a
297 + * space-separated, English-or-localized MONTH NAME date ("January 31,
298 + * 2027") and mis-parses a bare ISO date into garbage, which is what
299 + * charitable/update-campaign and charitable/create-campaign's end_date
300 + * input send (validated as ^\d{4}-\d{2}-\d{2}$ before it reaches here).
301 + */
302 + if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
303 + $date_parts = explode( '-', $date );
304 +
305 + if ( 3 === count( $date_parts ) ) {
306 + $year = (int) $date_parts[0];
307 + $month = (int) $date_parts[1];
308 + $day = (int) $date_parts[2];
309 +
310 + $time = mktime( 0, 0, 0, $month, $day, $year );
311 +
312 + if ( 'U' === $return_format ) {
313 + return $time;
314 + }
315 +
316 + return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
317 + }
318 +
319 + return false;
320 + }
321 +
322 + // If the month is in english, we need to ensure it's translated if the site is in another language.
323 + $english_to_local_month = array(
324 + 'january' => $wp_locale->get_month( 1 ),
325 + 'february' => $wp_locale->get_month( 2 ),
326 + 'march' => $wp_locale->get_month( 3 ),
327 + 'april' => $wp_locale->get_month( 4 ),
328 + 'may' => $wp_locale->get_month( 5 ),
329 + 'june' => $wp_locale->get_month( 6 ),
330 + 'july' => $wp_locale->get_month( 7 ),
331 + 'august' => $wp_locale->get_month( 8 ),
332 + 'september' => $wp_locale->get_month( 9 ),
333 + 'october' => $wp_locale->get_month( 10 ),
334 + 'november' => $wp_locale->get_month( 11 ),
335 + 'december' => $wp_locale->get_month( 12 ),
336 + );
337 +
338 + // a fix for a PHP warning to cover this particulr case.
339 + $date_format = 'Y-m-d H:i:s';
340 + $date_time = DateTime::createFromFormat( $date_format, $date );
341 +
342 + if ( 'Y-m-d 23:59:59' === $return_format && $date_time && $date_time->format( $date_format ) === $date ) {
343 + return $date;
344 + }
345 +
259 346 list( $month, $day, $year ) = explode( ' ', $date );
260 347
348 + // Normalize the month to lower case to match key.
349 + $month = strtolower( trim( $month ) );
350 +
351 + // Translate the month using the mapping.
352 + if ( array_key_exists( $month, $english_to_local_month ) ) {
353 + $month = $english_to_local_month[ $month ];
354 + }
355 +
261 356 $day = trim( $day, ',' );
262 357 $month = 1 + array_search( $month, array_values( $wp_locale->month ), true );
263 358 $time = mktime( 0, 0, 0, $month, (int) $day, (int) $year );
264 359
@@ -265,9 +360,9 @@
265 360 if ( 'U' === $return_format ) {
266 361 return $time;
267 362 }
268 363
269 - return date( $return_format, $time );
364 + return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
270 365 }
271 366
272 367 /**
273 368 * Sanitizes a date passed in the format of yyyy/mm/dd.
@@ -272,8 +367,9 @@
272 367 /**
273 368 * Sanitizes a date passed in the format of yyyy/mm/dd.
274 369 *
275 370 * @since 1.7.0.8
371 + * @version 1.8.9.1
276 372 *
277 373 * @param string $date The date to be sanitized.
278 374 * @param string $return_format The date format to return. Default is U (timestamp).
279 375 * @return string|false
@@ -290,11 +386,11 @@
290 386 if ( count( $date_array ) !== 3 ) {
291 387 return false;
292 388 }
293 389
294 - $day = intval( $date_array[ 2 ] );
295 - $month = intval( $date_array[ 1 ] );
296 - $year = intval( $date_array[ 0 ] );
390 + $day = intval( $date_array[2] );
391 + $month = intval( $date_array[1] );
392 + $year = intval( $date_array[0] );
297 393
298 394 $day = trim( $day, ',' );
299 395 $month = trim( $month, ',' );
300 396 $time = mktime( 0, 0, 0, $month, (int) $day, (int) $year );
@@ -302,9 +398,9 @@
302 398 if ( 'U' === $return_format ) {
303 399 return $time;
304 400 }
305 401
306 - return date( $return_format, $time );
402 + return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
307 403 }
308 404
309 405 /**
310 406 * Sanitizes a date passed in the format of mm/dd/yyyy for form filters.
@@ -309,8 +405,9 @@
309 405 /**
310 406 * Sanitizes a date passed in the format of mm/dd/yyyy for form filters.
311 407 *
312 408 * @since 1.7.0.11
409 + * @version 1.8.9.1
313 410 *
314 411 * @param string $date The date to be sanitized.
315 412 * @return string|false
316 413 */
@@ -323,9 +420,9 @@
323 420 // Convert the date to a timestamp.
324 421 $timestamp = strtotime( $date );
325 422
326 423 // If it's not empty and not equal to -1, then convert it to the mm/dd/yyyy format.
327 - $return_value = ( empty( $timestamp ) && -1 !== $timestamp ) ? '' : date( 'Y/m/d', $timestamp );
424 + $return_value = ( empty( $timestamp ) && -1 !== $timestamp ) ? '' : date( 'Y/m/d', $timestamp ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
328 425
329 426 return $return_value;
330 427 }
331 428
@@ -332,8 +429,9 @@
332 429 /**
333 430 * Sanitizes a date passed in the format of mm/dd/yyyy for form filters.
334 431 *
335 432 * @since 1.7.0.11
433 + * @version 1.8.9.1
336 434 *
337 435 * @param string $date The date to be sanitized.
338 436 * @return string|false
339 437 */
@@ -346,9 +444,9 @@
346 444 // Convert the date to a timestamp.
347 445 $timestamp = strtotime( $date );
348 446
349 447 // If it's not empty and not equal to -1, then convert it to the mm/dd/yyyy format.
350 - $return_value = ( empty( $timestamp ) && -1 !== $timestamp ) ? '' : date( 'Y/m/d', $timestamp );
448 + $return_value = ( empty( $timestamp ) && -1 !== $timestamp ) ? '' : date( 'Y/m/d', $timestamp ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
351 449
352 450 return $return_value;
353 451 }
354 452
@@ -391,8 +489,9 @@
391 489 *
392 490 * This is not fullproof. It uses a safelist of IP addresses.
393 491 *
394 492 * @since 1.6.14
493 + * @version 1.8.9.1
395 494 *
396 495 * @return boolean
397 496 */
398 497 function charitable_is_localhost() {
@@ -415,17 +514,361 @@
415 514 '::1',
416 515 )
417 516 );
418 517
419 - return in_array( $_SERVER['REMOTE_ADDR'], $safelist );
518 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
519 + return isset( $_SERVER['REMOTE_ADDR'] ) && in_array( $_SERVER['REMOTE_ADDR'], $safelist );
420 520 }
421 521
422 522 /**
423 523 * Check whether we are currently using a block theme.
424 524 *
425 - * @since Charitable version 1.6.55
525 + * @since 1.6.55
426 526 *
427 527 * @return boolean
428 528 */
429 529 function charitable_is_block_theme() {
430 530 return function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
531 +}
532 +
533 +/**
534 + * Santitize to ensure we are working with a hex color (especially when working with campaign templates).
535 + *
536 + * @since 1.8.0
537 + *
538 + * @param string $color The color to sanitize.
539 + * @param boolean $hash Whether to include the hash in the return value.
540 + *
541 + * @return boolean
542 + */
543 +function charitable_sanitize_hex( $color = '#FFFFFF', $hash = true ) {
544 +
545 + // Remove any spaces and special characters before and after the string.
546 + $color = trim( $color );
547 +
548 + // Remove any trailing '#' symbols from the color value.
549 + $color = str_replace( '#', '', $color );
550 +
551 + // If the string is 6 characters long then use it in pairs.
552 + if ( 3 === strlen( $color ) ) {
553 + $color = substr( $color, 0, 1 ) . substr( $color, 0, 1 ) . substr( $color, 1, 1 ) . substr( $color, 1, 1 ) . substr( $color, 2, 1 ) . substr( $color, 2, 1 );
554 + }
555 +
556 + $substr = array();
557 + for ( $i = 0; $i <= 5; $i++ ) {
558 + $default = ( 0 == $i ) ? 'F' : ( $substr[ $i - 1 ] );
559 + $substr[ $i ] = substr( $color, $i, 1 );
560 + $substr[ $i ] = ( false === $substr[ $i ] || ! ctype_xdigit( $substr[ $i ] ) ) ? $default : $substr[ $i ];
561 + }
562 + $hex = implode( '', $substr );
563 +
564 + return ( ! $hash ) ? $hex : '#' . $hex;
565 +}
566 +
567 +/**
568 + * Searches the database for transients stored there that match a specific prefix.
569 + * Full credit to Kellen Mace and Brad Parbs for this concept.
570 + *
571 + * @since 1.8.1
572 + *
573 + * @param string $prefix Prefix to search for.
574 + * @param string $postfix_separator Separator to use for the postfix.
575 + *
576 + * @return array|bool Nested array response for wpdb->get_results or false on failure.
577 + */
578 +function charitable_search_database_for_transients_by_prefix( $prefix = '', $postfix_separator = '_' ) {
579 +
580 + global $wpdb;
581 +
582 + // Add our prefix after concating our prefix with the _transient prefix.
583 + $prefix = $wpdb->esc_like( '_transient_' . $prefix . $postfix_separator );
584 +
585 + // Build up our SQL query.
586 + $sql = "SELECT `option_name` FROM $wpdb->options WHERE `option_name` LIKE '%s'";
587 +
588 + // Execute our query.
589 + $transients = $wpdb->get_results( $wpdb->prepare( $sql, $prefix . '%' ), ARRAY_A ); // phpcs:ignore
590 +
591 + // If if looks good, pass it back.
592 + if ( $transients && ! is_wp_error( $transients ) ) {
593 + return $transients;
594 + }
595 +
596 + // Otherise return false.
597 + return false;
598 +}
599 +
600 +/**
601 + * Expects a passed in multidimensional array of transient keys.
602 + *
603 + * Array(
604 + * array( 'option_name' => '_transient_blah_blah' ),
605 + * array( 'option_name' => 'transient_another_one' ),
606 + * )
607 + *
608 + * Can also pass in an array of transient names.
609 + *
610 + * @since 1.8.1
611 + *
612 + * @param array|string $transients Nested array of transients, keyed by option_name,
613 + * or array of names of transients.
614 + * @return array|bool Count of total vs deleted or false on failure.
615 + */
616 +function charitable_delete_transients_from_keys( $transients = false ) {
617 +
618 + if ( ! isset( $transients ) || false === $transients ) {
619 + return false;
620 + }
621 +
622 + // If we get a string key passed in, might as well use it correctly.
623 + if ( is_string( $transients ) ) {
624 + $transients = array( array( 'option_name' => $transients ) );
625 + }
626 +
627 + // If its not an array, we can't do anything.
628 + if ( ! is_array( $transients ) ) {
629 + return false;
630 + }
631 +
632 + $results = array();
633 +
634 + // Loop through our transients.
635 + foreach ( $transients as $transient ) {
636 +
637 + if ( is_array( $transient ) ) {
638 +
639 + // If we have an array, grab the first element.
640 + $transient = current( $transient );
641 + }
642 +
643 + // Remove that sucker.
644 + $results[ $transient ] = delete_transient( str_replace( '_transient_', '', $transient ) );
645 + }
646 +
647 + // Return an array of total number, and number deleted.
648 + return array(
649 + 'total' => count( $results ),
650 + 'deleted' => array_sum( $results ),
651 + );
652 +}
653 +
654 +/**
655 + * Check if AIOSEO Pro version is installed or not.
656 + *
657 + * @since 1.8.1
658 + *
659 + * @return bool
660 + */
661 +function charitable_is_installed_aioseo_pro() {
662 + $installed_plugins = get_plugins();
663 +
664 + if ( array_key_exists( 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php', $installed_plugins ) ) {
665 + return true;
666 + }
667 +
668 + return false;
669 +}
670 +
671 +/**
672 + * Check if MonsterInsights Pro version is installed or not.
673 + *
674 + * @since 1.8.1
675 + *
676 + * @return bool
677 + */
678 +function charitable_is_installed_mi_pro() {
679 + $installed_plugins = get_plugins();
680 +
681 + if ( array_key_exists( 'google-analytics-premium/googleanalytics-premium.php', $installed_plugins ) ) {
682 + return true;
683 + }
684 +
685 + return false;
686 +}
687 +
688 +/**
689 + * Get the secret key for the crypto functions.
690 + *
691 + * @since 1.8.7
692 + * @version 1.8.9.1
693 + *
694 + * @return string
695 + */
696 +function charitable_crypto_get_secret_key() {
697 +
698 + $secret_key = get_option( 'charitable_crypto_secret_key' );
699 +
700 + if ( charitable_is_debug() ) {
701 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
702 + error_log( 'charitable_crypto_get_secret_key: secret_key: ' . $secret_key );
703 + }
704 +
705 + // If we already have the secret, send it back.
706 + if ( false !== $secret_key ) {
707 + if ( charitable_is_debug() ) {
708 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
709 + error_log( 'charitable_crypto_get_secret_key: secret_key already exists: ' . $secret_key );
710 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
711 + error_log( 'charitable_crypto_get_secret_key: secret_key decoded: ' . base64_decode( $secret_key ) );
712 + }
713 + return base64_decode( $secret_key ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
714 + }
715 +
716 + // We don't have a secret, so let's generate one.
717 + $secret_key = sodium_crypto_secretbox_keygen();
718 + add_option( 'charitable_crypto_secret_key', base64_encode( $secret_key ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
719 +
720 + if ( charitable_is_debug() ) {
721 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
722 + error_log( 'charitable_crypto_get_secret_key: secret_key returned: ' . $secret_key );
723 + }
724 +
725 + return $secret_key;
726 +}
727 +
728 +/**
729 + * Encrypt a message.
730 + *
731 + * @since 1.6.1.2
732 + * @version 1.8.9.1
733 + *
734 + * @param string $message Message to encrypt.
735 + * @param string $key Encryption key.
736 + *
737 + * @return string
738 + */
739 +function charitable_crypto_encrypt( $message, $key = '' ) {
740 +
741 + if ( charitable_is_debug() ) {
742 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
743 + error_log( 'charitable_crypto_encrypt: message: ' . $message );
744 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
745 + error_log( 'charitable_crypto_encrypt: key: ' . $key );
746 + }
747 +
748 + // Create a nonce for this operation. It will be stored and recovered in the message itself.
749 + $nonce = random_bytes(
750 + SODIUM_CRYPTO_SECRETBOX_NONCEBYTES
751 + );
752 +
753 + if ( empty( $key ) ) {
754 + $key = charitable_crypto_get_secret_key();
755 + }
756 +
757 + if ( charitable_is_debug() ) {
758 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
759 + error_log( 'charitable_crypto_encrypt: key: ' . $key );
760 + }
761 +
762 + // Encrypt message and combine with nonce.
763 + $cipher = base64_encode( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
764 + $nonce .
765 + sodium_crypto_secretbox(
766 + $message,
767 + $nonce,
768 + $key
769 + )
770 + );
771 +
772 + try {
773 + sodium_memzero( $message );
774 + sodium_memzero( $key );
775 + if ( charitable_is_debug() ) {
776 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
777 + error_log( 'charitable_crypto_encrypt: cipher: ' . $cipher );
778 + }
779 + } catch ( \Exception $e ) {
780 + if ( charitable_is_debug() ) {
781 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
782 + error_log( 'charitable_crypto_encrypt: error: ' . $e->getMessage() );
783 + }
784 + return $cipher;
785 + }
786 +
787 + return $cipher;
788 +}
789 +
790 +/**
791 + * Decrypt a message.
792 + *
793 + * @since 1.6.1.2
794 + * @version 1.8.9.1
795 + *
796 + * @param string $encrypted Encrypted message.
797 + * @param string $key Encryption key.
798 + *
799 + * @return string
800 + */
801 +function charitable_crypto_decrypt( $encrypted, $key = '' ) {
802 +
803 + // Unpack base64 message.
804 + $decoded = base64_decode( $encrypted ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
805 +
806 + if ( false === $decoded ) {
807 + return false;
808 + }
809 +
810 + if ( mb_strlen( $decoded, '8bit' ) < ( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES ) ) {
811 + return false;
812 + }
813 +
814 + // Pull nonce and ciphertext out of unpacked message.
815 + $nonce = mb_substr( $decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit' );
816 + $ciphertext = mb_substr( $decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit' );
817 +
818 + if ( empty( $key ) ) {
819 + $key = charitable_crypto_get_secret_key();
820 + }
821 +
822 + // Decrypt it.
823 + $message = sodium_crypto_secretbox_open(
824 + $ciphertext,
825 + $nonce,
826 + $key
827 + );
828 +
829 + // Check for decrpytion failures.
830 + if ( false === $message ) {
831 + return false;
832 + }
833 +
834 + try {
835 + sodium_memzero( $ciphertext );
836 + sodium_memzero( $key );
837 + if ( charitable_is_debug() ) {
838 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
839 + error_log( 'charitable_crypto_decrypt: message: ' . $message );
840 + }
841 + } catch ( \Exception $e ) {
842 + if ( charitable_is_debug() ) {
843 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
844 + error_log( 'charitable_crypto_decrypt: error: ' . $e->getMessage() );
845 + }
846 + return $message;
847 + }
848 +
849 + return $message;
850 +}
851 +
852 +if ( ! function_exists( 'charitable_array_key_first' ) ) {
853 + /**
854 + * The `array_key_first` polyfill.
855 + *
856 + * @since 1.8.7
857 + *
858 + * @param array $arr Input array.
859 + *
860 + * @return mixed|null
861 + */
862 + function charitable_array_key_first( array $arr ) {
863 +
864 + if ( function_exists( 'array_key_first' ) ) {
865 + return array_key_first( $arr );
866 + }
867 +
868 + foreach ( $arr as $key => $unused ) {
869 + return $key;
870 + }
871 +
872 + return null;
873 + }
431 874 }