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/utilities/charitable-utility-functions.php +547 -7 1.7.0 → 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,9 +29,11 @@
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 - 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 + }
34 36 }
35 37 }
36 38
37 39 if ( $a[ $key ] == $b[ $key ] ) {
@@ -86,8 +88,9 @@
86 88 /**
87 89 * Verify a nonce. This also just ensures that the nonce is set.
88 90 *
89 91 * @since 1.0.0
92 + * @version 1.8.9.1
90 93 *
91 94 * @param string $nonce The nonce name.
92 95 * @param string $action The nonce action.
93 96 * @param array $request_args Request arguments. If not set, will populate with $_GET.
@@ -94,8 +97,9 @@
94 97 * @return boolean
95 98 */
96 99 function charitable_verify_nonce( $nonce, $action, $request_args = array() ) {
97 100 if ( empty( $request_args ) ) {
101 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
98 102 $request_args = $_GET;
99 103 }
100 104
101 105 return isset( $request_args[ $nonce ] ) && wp_verify_nonce( $request_args[ $nonce ], $action );
@@ -130,9 +134,9 @@
130 134
131 135 /* Last try, guess timezone string manually */
132 136 if ( false === $timezone ) {
133 137
134 - $is_dst = date( 'I' );
138 + $is_dst = date( 'I' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
135 139
136 140 foreach ( timezone_abbreviations_list() as $abbr ) {
137 141 foreach ( $abbr as $city ) {
138 142 if ( $city['dst'] == $is_dst && $city['offset'] == $utc_offset ) {
@@ -172,8 +176,51 @@
172 176 return filter_var( $i, FILTER_VALIDATE_INT, array( 'min_range' => 1 ) );
173 177 }
174 178
175 179 /**
180 + * Ensure a string is a valid email..
181 + *
182 + * @since 1.7.0.9
183 + *
184 + * @param mixed $i Number received.
185 + * @return int|false
186 + */
187 +function charitable_validate_email( $i ) {
188 + return filter_var( $i, FILTER_VALIDATE_EMAIL );
189 +}
190 +
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 +/**
176 223 * Sanitize any checkbox value.
177 224 *
178 225 * @since 1.5.0
179 226 *
@@ -228,9 +275,11 @@
228 275 * We use WP_Locale to parse the month that the user has set.
229 276 *
230 277 * @global WP_Locale $wp_locale
231 278 *
232 - * @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
233 282 *
234 283 * @param string $date The date to be sanitized.
235 284 * @param string $return_format The date format to return. Default is U (timestamp).
236 285 * @return string|false
@@ -241,10 +290,70 @@
241 290 if ( empty( $date ) || ! $date ) {
242 291 return false;
243 292 }
244 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 +
245 346 list( $month, $day, $year ) = explode( ' ', $date );
246 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 +
247 356 $day = trim( $day, ',' );
248 357 $month = 1 + array_search( $month, array_values( $wp_locale->month ), true );
249 358 $time = mktime( 0, 0, 0, $month, (int) $day, (int) $year );
250 359
@@ -251,12 +360,98 @@
251 360 if ( 'U' === $return_format ) {
252 361 return $time;
253 362 }
254 363
255 - return date( $return_format, $time );
364 + return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
256 365 }
257 366
258 367 /**
368 + * Sanitizes a date passed in the format of yyyy/mm/dd.
369 + *
370 + * @since 1.7.0.8
371 + * @version 1.8.9.1
372 + *
373 + * @param string $date The date to be sanitized.
374 + * @param string $return_format The date format to return. Default is U (timestamp).
375 + * @return string|false
376 + */
377 +function charitable_sanitize_date_alt_format( $date, $return_format = 'U' ) {
378 + global $wp_locale;
379 +
380 + if ( empty( $date ) || ! $date ) {
381 + return false;
382 + }
383 +
384 + $date_array = explode( '/', $date );
385 +
386 + if ( count( $date_array ) !== 3 ) {
387 + return false;
388 + }
389 +
390 + $day = intval( $date_array[2] );
391 + $month = intval( $date_array[1] );
392 + $year = intval( $date_array[0] );
393 +
394 + $day = trim( $day, ',' );
395 + $month = trim( $month, ',' );
396 + $time = mktime( 0, 0, 0, $month, (int) $day, (int) $year );
397 +
398 + if ( 'U' === $return_format ) {
399 + return $time;
400 + }
401 +
402 + return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
403 +}
404 +
405 +/**
406 + * Sanitizes a date passed in the format of mm/dd/yyyy for form filters.
407 + *
408 + * @since 1.7.0.11
409 + * @version 1.8.9.1
410 + *
411 + * @param string $date The date to be sanitized.
412 + * @return string|false
413 + */
414 +function charitable_sanitize_date_filter_format( $date = false ) {
415 +
416 + if ( empty( $date ) || ! $date ) {
417 + return false;
418 + }
419 +
420 + // Convert the date to a timestamp.
421 + $timestamp = strtotime( $date );
422 +
423 + // If it's not empty and not equal to -1, then convert it to the mm/dd/yyyy format.
424 + $return_value = ( empty( $timestamp ) && -1 !== $timestamp ) ? '' : date( 'Y/m/d', $timestamp ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
425 +
426 + return $return_value;
427 +}
428 +
429 +/**
430 + * Sanitizes a date passed in the format of mm/dd/yyyy for form filters.
431 + *
432 + * @since 1.7.0.11
433 + * @version 1.8.9.1
434 + *
435 + * @param string $date The date to be sanitized.
436 + * @return string|false
437 + */
438 +function charitable_sanitize_date_export_format( $date = false ) {
439 +
440 + if ( empty( $date ) || ! $date ) {
441 + return false;
442 + }
443 +
444 + // Convert the date to a timestamp.
445 + $timestamp = strtotime( $date );
446 +
447 + // If it's not empty and not equal to -1, then convert it to the mm/dd/yyyy format.
448 + $return_value = ( empty( $timestamp ) && -1 !== $timestamp ) ? '' : date( 'Y/m/d', $timestamp ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
449 +
450 + return $return_value;
451 +}
452 +
453 +/**
259 454 * Return a string containing the correct number & type of placeholders.
260 455 *
261 456 * @since 1.5.0
262 457 *
@@ -294,8 +489,9 @@
294 489 *
295 490 * This is not fullproof. It uses a safelist of IP addresses.
296 491 *
297 492 * @since 1.6.14
493 + * @version 1.8.9.1
298 494 *
299 495 * @return boolean
300 496 */
301 497 function charitable_is_localhost() {
@@ -318,17 +514,361 @@
318 514 '::1',
319 515 )
320 516 );
321 517
322 - 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 );
323 520 }
324 521
325 522 /**
326 523 * Check whether we are currently using a block theme.
327 524 *
328 - * @since Charitable version 1.6.55
525 + * @since 1.6.55
329 526 *
330 527 * @return boolean
331 528 */
332 529 function charitable_is_block_theme() {
333 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 + }
334 874 }