PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.11.3
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.11.3
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 / utilities / charitable-utility-functions.php

charitable-utility-functions.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.11.3, at includes/utilities/charitable-utility-functions.php

816 lines 21.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Utility Functions.
4 *
5 * Utility functions.
6 *
7 * @package Charitable/Functions/Utility
8 * @author David Bisset
9 * @copyright Copyright (c) 2023, WP Charitable LLC
10 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
11 * @since 1.0.0
12 * @version 1.6.55
13 */
14
15 // Exit if accessed directly.
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 /**
21 * Orders an array by a particular key.
22 *
23 * @since 1.5.0
24 *
25 * @param strign $key The key to sort by.
26 * @param array $a First element.
27 * @param array $b Element to compare against.
28 * @return int
29 */
30 function charitable_element_key_sort( $key, $a, $b ) {
31 foreach ( array( $a, $b ) as $item ) {
32 if ( ! array_key_exists( $key, $item ) ) {
33 if ( charitable_is_debug() ) {
34 error_log( sprintf( '%s missing from element: ' . wp_json_encode( $item ), $key ) ); // phpcs:ignore
35 }
36 }
37 }
38
39 if ( $a[ $key ] == $b[ $key ] ) {
40 return 0;
41 }
42
43 return $a[ $key ] < $b[ $key ] ? -1 : 1;
44 }
45
46 /**
47 * Orders an array by the priority key.
48 *
49 * @since 1.0.0
50 *
51 * @param array $a First element.
52 * @param array $b Element to compare against.
53 * @return int
54 */
55 function charitable_priority_sort( $a, $b ) {
56 return charitable_element_key_sort( 'priority', $a, $b );
57 }
58
59 /**
60 * Orders an array by the time key.
61 *
62 * @since 1.5.0
63 *
64 * @param array $a First element.
65 * @param array $b Element to compare against.
66 * @return int
67 */
68 function charitable_timestamp_sort( $a, $b ) {
69 return charitable_element_key_sort( 'time', $a, $b );
70 }
71
72 /**
73 * Checks whether function is disabled.
74 *
75 * Full credit to Pippin Williamson and the EDD team.
76 *
77 * @since 1.0.0
78 *
79 * @param string $function Name of the function.
80 * @return boolean Whether or not function is disabled.
81 */
82 function charitable_is_func_disabled( $function ) {
83 $disabled = explode( ',', ini_get( 'disable_functions' ) );
84
85 return in_array( $function, $disabled );
86 }
87
88 /**
89 * Verify a nonce. This also just ensures that the nonce is set.
90 *
91 * @since 1.0.0
92 * @version 1.8.9.1
93 *
94 * @param string $nonce The nonce name.
95 * @param string $action The nonce action.
96 * @param array $request_args Request arguments. If not set, will populate with $_GET.
97 * @return boolean
98 */
99 function charitable_verify_nonce( $nonce, $action, $request_args = array() ) {
100 if ( empty( $request_args ) ) {
101 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
102 $request_args = $_GET;
103 }
104
105 return isset( $request_args[ $nonce ] ) && wp_verify_nonce( $request_args[ $nonce ], $action );
106 }
107
108 /**
109 * Retrieve the timezone id.
110 *
111 * Credit: Pippin Williamson & the rest of the EDD team.
112 *
113 * @since 1.0.0
114 *
115 * @return string
116 */
117 function charitable_get_timezone_id() {
118 $timezone = get_option( 'timezone_string' );
119
120 /* If site timezone string exists, return it */
121 if ( $timezone ) {
122 return $timezone;
123 }
124
125 $utc_offset = 3600 * get_option( 'gmt_offset', 0 );
126
127 /* Get UTC offset, if it isn't set return UTC */
128 if ( ! $utc_offset ) {
129 return 'UTC';
130 }
131
132 /* Attempt to guess the timezone string from the UTC offset */
133 $timezone = timezone_name_from_abbr( '', $utc_offset );
134
135 /* Last try, guess timezone string manually */
136 if ( false === $timezone ) {
137
138 $is_dst = date( 'I' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
139
140 foreach ( timezone_abbreviations_list() as $abbr ) {
141 foreach ( $abbr as $city ) {
142 if ( $city['dst'] == $is_dst && $city['offset'] == $utc_offset ) {
143 return $city['timezone_id'];
144 }
145 }
146 }
147 }
148
149 /* If we still haven't figured out the timezone, fall back to UTC */
150 return 'UTC';
151 }
152
153 /**
154 * Given an array and a separate array of keys, returns a new array that only contains the
155 * elements in the original array with the specified keys.
156 *
157 * @since 1.5.0
158 *
159 * @param array $original_array The original array we need to pull a subset from.
160 * @param array $subset_keys The keys to use for our subset.
161 * @return array
162 */
163 function charitable_array_subset( array $original_array, $subset_keys ) {
164 return array_intersect_key( $original_array, array_flip( $subset_keys ) );
165 }
166
167 /**
168 * Ensure a number is a positive integer.
169 *
170 * @since 1.0.0
171 *
172 * @param mixed $i Number received.
173 * @return int|false
174 */
175 function charitable_validate_absint( $i ) {
176 return filter_var( $i, FILTER_VALIDATE_INT, array( 'min_range' => 1 ) );
177 }
178
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 * Sanitize any checkbox value.
193 *
194 * @since 1.5.0
195 *
196 * @param mixed $value Value set for checkbox, or false.
197 * @return boolean
198 */
199 function charitable_sanitize_checkbox( $value = false ) {
200 return intval( true == $value || 'on' == $value );
201 }
202
203 /**
204 * Format an array of strings as a sentence part.
205 *
206 * If there is one item in the string, this will just return that item.
207 * If there are two items, it will return a string like this: "x and y".
208 * If there are three or more items, it will return a string like this: "x, y and z".
209 *
210 * @since 1.3.0
211 *
212 * @param string[] $list The list.
213 * @return string
214 */
215 function charitable_list_to_sentence_part( $list ) {
216 $list = array_values( $list );
217
218 if ( 1 == count( $list ) ) {
219 return $list[0];
220 }
221
222 if ( 2 == count( $list ) ) {
223 return sprintf(
224 /* translators: %1$s: first list item; %2$s: second list item. */
225 _x( '%1$s and %2$s', 'x and y', 'charitable' ),
226 $list[0],
227 $list[1]
228 );
229 }
230
231 $last = array_pop( $list );
232
233 return sprintf(
234 /* translators: %1$s: all list items except last, comma-separated; %2$s: second list item. */
235 _x( '%1$s and %2$s', 'x and y', 'charitable' ),
236 implode( ', ', $list ),
237 $last
238 );
239 }
240
241 /**
242 * Sanitizes a date passed in the format of January 29, 2009.
243 *
244 * We use WP_Locale to parse the month that the user has set.
245 *
246 * @global WP_Locale $wp_locale
247 *
248 * @since 1.4.10
249 * @version 1.8.4.2 Fix bug where english dates weren't being found in $wp_locale.
250 * @version 1.8.9.1
251 *
252 * @param string $date The date to be sanitized.
253 * @param string $return_format The date format to return. Default is U (timestamp).
254 * @return string|false
255 */
256 function charitable_sanitize_date( $date, $return_format = 'U' ) {
257 global $wp_locale;
258
259 if ( empty( $date ) || ! $date ) {
260 return false;
261 }
262
263 // If the month is in english, we need to ensure it's translated if the site is in another language.
264 $english_to_local_month = array(
265 'january' => $wp_locale->get_month( 1 ),
266 'february' => $wp_locale->get_month( 2 ),
267 'march' => $wp_locale->get_month( 3 ),
268 'april' => $wp_locale->get_month( 4 ),
269 'may' => $wp_locale->get_month( 5 ),
270 'june' => $wp_locale->get_month( 6 ),
271 'july' => $wp_locale->get_month( 7 ),
272 'august' => $wp_locale->get_month( 8 ),
273 'september' => $wp_locale->get_month( 9 ),
274 'october' => $wp_locale->get_month( 10 ),
275 'november' => $wp_locale->get_month( 11 ),
276 'december' => $wp_locale->get_month( 12 ),
277 );
278
279 // a fix for a PHP warning to cover this particulr case.
280 $date_format = 'Y-m-d H:i:s';
281 $date_time = DateTime::createFromFormat( $date_format, $date );
282
283 if ( 'Y-m-d 23:59:59' === $return_format && $date_time && $date_time->format( $date_format ) === $date ) {
284 return $date;
285 }
286
287 list( $month, $day, $year ) = explode( ' ', $date );
288
289 // Normalize the month to lower case to match key.
290 $month = strtolower( trim( $month ) );
291
292 // Translate the month using the mapping.
293 if ( array_key_exists( $month, $english_to_local_month ) ) {
294 $month = $english_to_local_month[ $month ];
295 }
296
297 $day = trim( $day, ',' );
298 $month = 1 + array_search( $month, array_values( $wp_locale->month ), true );
299 $time = mktime( 0, 0, 0, $month, (int) $day, (int) $year );
300
301 if ( 'U' === $return_format ) {
302 return $time;
303 }
304
305 return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
306 }
307
308 /**
309 * Sanitizes a date passed in the format of yyyy/mm/dd.
310 *
311 * @since 1.7.0.8
312 * @version 1.8.9.1
313 *
314 * @param string $date The date to be sanitized.
315 * @param string $return_format The date format to return. Default is U (timestamp).
316 * @return string|false
317 */
318 function charitable_sanitize_date_alt_format( $date, $return_format = 'U' ) {
319 global $wp_locale;
320
321 if ( empty( $date ) || ! $date ) {
322 return false;
323 }
324
325 $date_array = explode( '/', $date );
326
327 if ( count( $date_array ) !== 3 ) {
328 return false;
329 }
330
331 $day = intval( $date_array[2] );
332 $month = intval( $date_array[1] );
333 $year = intval( $date_array[0] );
334
335 $day = trim( $day, ',' );
336 $month = trim( $month, ',' );
337 $time = mktime( 0, 0, 0, $month, (int) $day, (int) $year );
338
339 if ( 'U' === $return_format ) {
340 return $time;
341 }
342
343 return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
344 }
345
346 /**
347 * Sanitizes a date passed in the format of mm/dd/yyyy for form filters.
348 *
349 * @since 1.7.0.11
350 * @version 1.8.9.1
351 *
352 * @param string $date The date to be sanitized.
353 * @return string|false
354 */
355 function charitable_sanitize_date_filter_format( $date = false ) {
356
357 if ( empty( $date ) || ! $date ) {
358 return false;
359 }
360
361 // Convert the date to a timestamp.
362 $timestamp = strtotime( $date );
363
364 // If it's not empty and not equal to -1, then convert it to the mm/dd/yyyy format.
365 $return_value = ( empty( $timestamp ) && -1 !== $timestamp ) ? '' : date( 'Y/m/d', $timestamp ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
366
367 return $return_value;
368 }
369
370 /**
371 * Sanitizes a date passed in the format of mm/dd/yyyy for form filters.
372 *
373 * @since 1.7.0.11
374 * @version 1.8.9.1
375 *
376 * @param string $date The date to be sanitized.
377 * @return string|false
378 */
379 function charitable_sanitize_date_export_format( $date = false ) {
380
381 if ( empty( $date ) || ! $date ) {
382 return false;
383 }
384
385 // Convert the date to a timestamp.
386 $timestamp = strtotime( $date );
387
388 // If it's not empty and not equal to -1, then convert it to the mm/dd/yyyy format.
389 $return_value = ( empty( $timestamp ) && -1 !== $timestamp ) ? '' : date( 'Y/m/d', $timestamp ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
390
391 return $return_value;
392 }
393
394 /**
395 * Return a string containing the correct number & type of placeholders.
396 *
397 * @since 1.5.0
398 *
399 * @param int $count The number of placeholders to add.
400 * @param string $placeholder Type of placeholder to insert.
401 * @return string
402 */
403 function charitable_get_query_placeholders( $count = 1, $placeholder = '%s' ) {
404 $placeholders = array_fill( 0, $count, $placeholder );
405 return implode( ', ', $placeholders );
406 }
407
408 /**
409 * Return a list of pages in id=>title format for use in a select dropdown.
410 *
411 * @see get_pages
412 *
413 * @since 1.6.0
414 *
415 * @param array $args Optional arguments to be passed to get_pages.
416 * @return array
417 */
418 function charitable_get_pages_options( $args = array() ) {
419 $pages = get_pages( $args );
420
421 if ( ! $pages ) {
422 return array();
423 }
424
425 return array_combine( wp_list_pluck( $pages, 'ID' ), wp_list_pluck( $pages, 'post_title' ) );
426 }
427
428 /**
429 * Checks whether this is localhost.
430 *
431 * This is not fullproof. It uses a safelist of IP addresses.
432 *
433 * @since 1.6.14
434 * @version 1.8.9.1
435 *
436 * @return boolean
437 */
438 function charitable_is_localhost() {
439
440 if ( false !== ( defined( 'CHARITABLE_FORCE_NO_LOCALHOST_WITH_STRIPE_CONNECT' ) && CHARITABLE_FORCE_NO_LOCALHOST_WITH_STRIPE_CONNECT ) ) {
441 return false;
442 }
443
444 /**
445 * Filter list of localhost IP addresses.
446 *
447 * @since 1.6.14
448 *
449 * @param array $ip_addresses The list of IP addresses.
450 */
451 $safelist = apply_filters(
452 'charitable_localhost_ips',
453 array(
454 '127.0.0.1',
455 '::1',
456 )
457 );
458
459 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
460 return isset( $_SERVER['REMOTE_ADDR'] ) && in_array( $_SERVER['REMOTE_ADDR'], $safelist );
461 }
462
463 /**
464 * Check whether we are currently using a block theme.
465 *
466 * @since 1.6.55
467 *
468 * @return boolean
469 */
470 function charitable_is_block_theme() {
471 return function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
472 }
473
474 /**
475 * Santitize to ensure we are working with a hex color (especially when working with campaign templates).
476 *
477 * @since 1.8.0
478 *
479 * @param string $color The color to sanitize.
480 * @param boolean $hash Whether to include the hash in the return value.
481 *
482 * @return boolean
483 */
484 function charitable_sanitize_hex( $color = '#FFFFFF', $hash = true ) {
485
486 // Remove any spaces and special characters before and after the string.
487 $color = trim( $color );
488
489 // Remove any trailing '#' symbols from the color value.
490 $color = str_replace( '#', '', $color );
491
492 // If the string is 6 characters long then use it in pairs.
493 if ( 3 === strlen( $color ) ) {
494 $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 );
495 }
496
497 $substr = array();
498 for ( $i = 0; $i <= 5; $i++ ) {
499 $default = ( 0 == $i ) ? 'F' : ( $substr[ $i - 1 ] );
500 $substr[ $i ] = substr( $color, $i, 1 );
501 $substr[ $i ] = ( false === $substr[ $i ] || ! ctype_xdigit( $substr[ $i ] ) ) ? $default : $substr[ $i ];
502 }
503 $hex = implode( '', $substr );
504
505 return ( ! $hash ) ? $hex : '#' . $hex;
506 }
507
508 /**
509 * Searches the database for transients stored there that match a specific prefix.
510 * Full credit to Kellen Mace and Brad Parbs for this concept.
511 *
512 * @since 1.8.1
513 *
514 * @param string $prefix Prefix to search for.
515 * @param string $postfix_separator Separator to use for the postfix.
516 *
517 * @return array|bool Nested array response for wpdb->get_results or false on failure.
518 */
519 function charitable_search_database_for_transients_by_prefix( $prefix = '', $postfix_separator = '_' ) {
520
521 global $wpdb;
522
523 // Add our prefix after concating our prefix with the _transient prefix.
524 $prefix = $wpdb->esc_like( '_transient_' . $prefix . $postfix_separator );
525
526 // Build up our SQL query.
527 $sql = "SELECT `option_name` FROM $wpdb->options WHERE `option_name` LIKE '%s'";
528
529 // Execute our query.
530 $transients = $wpdb->get_results( $wpdb->prepare( $sql, $prefix . '%' ), ARRAY_A ); // phpcs:ignore
531
532 // If if looks good, pass it back.
533 if ( $transients && ! is_wp_error( $transients ) ) {
534 return $transients;
535 }
536
537 // Otherise return false.
538 return false;
539 }
540
541 /**
542 * Expects a passed in multidimensional array of transient keys.
543 *
544 * Array(
545 * array( 'option_name' => '_transient_blah_blah' ),
546 * array( 'option_name' => 'transient_another_one' ),
547 * )
548 *
549 * Can also pass in an array of transient names.
550 *
551 * @since 1.8.1
552 *
553 * @param array|string $transients Nested array of transients, keyed by option_name,
554 * or array of names of transients.
555 * @return array|bool Count of total vs deleted or false on failure.
556 */
557 function charitable_delete_transients_from_keys( $transients = false ) {
558
559 if ( ! isset( $transients ) || false === $transients ) {
560 return false;
561 }
562
563 // If we get a string key passed in, might as well use it correctly.
564 if ( is_string( $transients ) ) {
565 $transients = array( array( 'option_name' => $transients ) );
566 }
567
568 // If its not an array, we can't do anything.
569 if ( ! is_array( $transients ) ) {
570 return false;
571 }
572
573 $results = array();
574
575 // Loop through our transients.
576 foreach ( $transients as $transient ) {
577
578 if ( is_array( $transient ) ) {
579
580 // If we have an array, grab the first element.
581 $transient = current( $transient );
582 }
583
584 // Remove that sucker.
585 $results[ $transient ] = delete_transient( str_replace( '_transient_', '', $transient ) );
586 }
587
588 // Return an array of total number, and number deleted.
589 return array(
590 'total' => count( $results ),
591 'deleted' => array_sum( $results ),
592 );
593 }
594
595 /**
596 * Check if AIOSEO Pro version is installed or not.
597 *
598 * @since 1.8.1
599 *
600 * @return bool
601 */
602 function charitable_is_installed_aioseo_pro() {
603 $installed_plugins = get_plugins();
604
605 if ( array_key_exists( 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php', $installed_plugins ) ) {
606 return true;
607 }
608
609 return false;
610 }
611
612 /**
613 * Check if MonsterInsights Pro version is installed or not.
614 *
615 * @since 1.8.1
616 *
617 * @return bool
618 */
619 function charitable_is_installed_mi_pro() {
620 $installed_plugins = get_plugins();
621
622 if ( array_key_exists( 'google-analytics-premium/googleanalytics-premium.php', $installed_plugins ) ) {
623 return true;
624 }
625
626 return false;
627 }
628
629 /**
630 * Get the secret key for the crypto functions.
631 *
632 * @since 1.8.7
633 * @version 1.8.9.1
634 *
635 * @return string
636 */
637 function charitable_crypto_get_secret_key() {
638
639 $secret_key = get_option( 'charitable_crypto_secret_key' );
640
641 if ( charitable_is_debug() ) {
642 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
643 error_log( 'charitable_crypto_get_secret_key: secret_key: ' . $secret_key );
644 }
645
646 // If we already have the secret, send it back.
647 if ( false !== $secret_key ) {
648 if ( charitable_is_debug() ) {
649 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
650 error_log( 'charitable_crypto_get_secret_key: secret_key already exists: ' . $secret_key );
651 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
652 error_log( 'charitable_crypto_get_secret_key: secret_key decoded: ' . base64_decode( $secret_key ) );
653 }
654 return base64_decode( $secret_key ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
655 }
656
657 // We don't have a secret, so let's generate one.
658 $secret_key = sodium_crypto_secretbox_keygen();
659 add_option( 'charitable_crypto_secret_key', base64_encode( $secret_key ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
660
661 if ( charitable_is_debug() ) {
662 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
663 error_log( 'charitable_crypto_get_secret_key: secret_key returned: ' . $secret_key );
664 }
665
666 return $secret_key;
667 }
668
669 /**
670 * Encrypt a message.
671 *
672 * @since 1.6.1.2
673 * @version 1.8.9.1
674 *
675 * @param string $message Message to encrypt.
676 * @param string $key Encryption key.
677 *
678 * @return string
679 */
680 function charitable_crypto_encrypt( $message, $key = '' ) {
681
682 if ( charitable_is_debug() ) {
683 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
684 error_log( 'charitable_crypto_encrypt: message: ' . $message );
685 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
686 error_log( 'charitable_crypto_encrypt: key: ' . $key );
687 }
688
689 // Create a nonce for this operation. It will be stored and recovered in the message itself.
690 $nonce = random_bytes(
691 SODIUM_CRYPTO_SECRETBOX_NONCEBYTES
692 );
693
694 if ( empty( $key ) ) {
695 $key = charitable_crypto_get_secret_key();
696 }
697
698 if ( charitable_is_debug() ) {
699 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
700 error_log( 'charitable_crypto_encrypt: key: ' . $key );
701 }
702
703 // Encrypt message and combine with nonce.
704 $cipher = base64_encode( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
705 $nonce .
706 sodium_crypto_secretbox(
707 $message,
708 $nonce,
709 $key
710 )
711 );
712
713 try {
714 sodium_memzero( $message );
715 sodium_memzero( $key );
716 if ( charitable_is_debug() ) {
717 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
718 error_log( 'charitable_crypto_encrypt: cipher: ' . $cipher );
719 }
720 } catch ( \Exception $e ) {
721 if ( charitable_is_debug() ) {
722 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
723 error_log( 'charitable_crypto_encrypt: error: ' . $e->getMessage() );
724 }
725 return $cipher;
726 }
727
728 return $cipher;
729 }
730
731 /**
732 * Decrypt a message.
733 *
734 * @since 1.6.1.2
735 * @version 1.8.9.1
736 *
737 * @param string $encrypted Encrypted message.
738 * @param string $key Encryption key.
739 *
740 * @return string
741 */
742 function charitable_crypto_decrypt( $encrypted, $key = '' ) {
743
744 // Unpack base64 message.
745 $decoded = base64_decode( $encrypted ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
746
747 if ( false === $decoded ) {
748 return false;
749 }
750
751 if ( mb_strlen( $decoded, '8bit' ) < ( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES ) ) {
752 return false;
753 }
754
755 // Pull nonce and ciphertext out of unpacked message.
756 $nonce = mb_substr( $decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit' );
757 $ciphertext = mb_substr( $decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit' );
758
759 if ( empty( $key ) ) {
760 $key = charitable_crypto_get_secret_key();
761 }
762
763 // Decrypt it.
764 $message = sodium_crypto_secretbox_open(
765 $ciphertext,
766 $nonce,
767 $key
768 );
769
770 // Check for decrpytion failures.
771 if ( false === $message ) {
772 return false;
773 }
774
775 try {
776 sodium_memzero( $ciphertext );
777 sodium_memzero( $key );
778 if ( charitable_is_debug() ) {
779 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
780 error_log( 'charitable_crypto_decrypt: message: ' . $message );
781 }
782 } catch ( \Exception $e ) {
783 if ( charitable_is_debug() ) {
784 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
785 error_log( 'charitable_crypto_decrypt: error: ' . $e->getMessage() );
786 }
787 return $message;
788 }
789
790 return $message;
791 }
792
793 if ( ! function_exists( 'charitable_array_key_first' ) ) {
794 /**
795 * The `array_key_first` polyfill.
796 *
797 * @since 1.8.7
798 *
799 * @param array $arr Input array.
800 *
801 * @return mixed|null
802 */
803 function charitable_array_key_first( array $arr ) {
804
805 if ( function_exists( 'array_key_first' ) ) {
806 return array_key_first( $arr );
807 }
808
809 foreach ( $arr as $key => $unused ) {
810 return $key;
811 }
812
813 return null;
814 }
815 }
816