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

847 lines 22.4 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 * 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 /**
223 * Sanitize any checkbox value.
224 *
225 * @since 1.5.0
226 *
227 * @param mixed $value Value set for checkbox, or false.
228 * @return boolean
229 */
230 function charitable_sanitize_checkbox( $value = false ) {
231 return intval( true == $value || 'on' == $value );
232 }
233
234 /**
235 * Format an array of strings as a sentence part.
236 *
237 * If there is one item in the string, this will just return that item.
238 * If there are two items, it will return a string like this: "x and y".
239 * If there are three or more items, it will return a string like this: "x, y and z".
240 *
241 * @since 1.3.0
242 *
243 * @param string[] $list The list.
244 * @return string
245 */
246 function charitable_list_to_sentence_part( $list ) {
247 $list = array_values( $list );
248
249 if ( 1 == count( $list ) ) {
250 return $list[0];
251 }
252
253 if ( 2 == count( $list ) ) {
254 return sprintf(
255 /* translators: %1$s: first list item; %2$s: second list item. */
256 _x( '%1$s and %2$s', 'x and y', 'charitable' ),
257 $list[0],
258 $list[1]
259 );
260 }
261
262 $last = array_pop( $list );
263
264 return sprintf(
265 /* translators: %1$s: all list items except last, comma-separated; %2$s: second list item. */
266 _x( '%1$s and %2$s', 'x and y', 'charitable' ),
267 implode( ', ', $list ),
268 $last
269 );
270 }
271
272 /**
273 * Sanitizes a date passed in the format of January 29, 2009.
274 *
275 * We use WP_Locale to parse the month that the user has set.
276 *
277 * @global WP_Locale $wp_locale
278 *
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
282 *
283 * @param string $date The date to be sanitized.
284 * @param string $return_format The date format to return. Default is U (timestamp).
285 * @return string|false
286 */
287 function charitable_sanitize_date( $date, $return_format = 'U' ) {
288 global $wp_locale;
289
290 if ( empty( $date ) || ! $date ) {
291 return false;
292 }
293
294 // If the month is in english, we need to ensure it's translated if the site is in another language.
295 $english_to_local_month = array(
296 'january' => $wp_locale->get_month( 1 ),
297 'february' => $wp_locale->get_month( 2 ),
298 'march' => $wp_locale->get_month( 3 ),
299 'april' => $wp_locale->get_month( 4 ),
300 'may' => $wp_locale->get_month( 5 ),
301 'june' => $wp_locale->get_month( 6 ),
302 'july' => $wp_locale->get_month( 7 ),
303 'august' => $wp_locale->get_month( 8 ),
304 'september' => $wp_locale->get_month( 9 ),
305 'october' => $wp_locale->get_month( 10 ),
306 'november' => $wp_locale->get_month( 11 ),
307 'december' => $wp_locale->get_month( 12 ),
308 );
309
310 // a fix for a PHP warning to cover this particulr case.
311 $date_format = 'Y-m-d H:i:s';
312 $date_time = DateTime::createFromFormat( $date_format, $date );
313
314 if ( 'Y-m-d 23:59:59' === $return_format && $date_time && $date_time->format( $date_format ) === $date ) {
315 return $date;
316 }
317
318 list( $month, $day, $year ) = explode( ' ', $date );
319
320 // Normalize the month to lower case to match key.
321 $month = strtolower( trim( $month ) );
322
323 // Translate the month using the mapping.
324 if ( array_key_exists( $month, $english_to_local_month ) ) {
325 $month = $english_to_local_month[ $month ];
326 }
327
328 $day = trim( $day, ',' );
329 $month = 1 + array_search( $month, array_values( $wp_locale->month ), true );
330 $time = mktime( 0, 0, 0, $month, (int) $day, (int) $year );
331
332 if ( 'U' === $return_format ) {
333 return $time;
334 }
335
336 return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
337 }
338
339 /**
340 * Sanitizes a date passed in the format of yyyy/mm/dd.
341 *
342 * @since 1.7.0.8
343 * @version 1.8.9.1
344 *
345 * @param string $date The date to be sanitized.
346 * @param string $return_format The date format to return. Default is U (timestamp).
347 * @return string|false
348 */
349 function charitable_sanitize_date_alt_format( $date, $return_format = 'U' ) {
350 global $wp_locale;
351
352 if ( empty( $date ) || ! $date ) {
353 return false;
354 }
355
356 $date_array = explode( '/', $date );
357
358 if ( count( $date_array ) !== 3 ) {
359 return false;
360 }
361
362 $day = intval( $date_array[2] );
363 $month = intval( $date_array[1] );
364 $year = intval( $date_array[0] );
365
366 $day = trim( $day, ',' );
367 $month = trim( $month, ',' );
368 $time = mktime( 0, 0, 0, $month, (int) $day, (int) $year );
369
370 if ( 'U' === $return_format ) {
371 return $time;
372 }
373
374 return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
375 }
376
377 /**
378 * Sanitizes a date passed in the format of mm/dd/yyyy for form filters.
379 *
380 * @since 1.7.0.11
381 * @version 1.8.9.1
382 *
383 * @param string $date The date to be sanitized.
384 * @return string|false
385 */
386 function charitable_sanitize_date_filter_format( $date = false ) {
387
388 if ( empty( $date ) || ! $date ) {
389 return false;
390 }
391
392 // Convert the date to a timestamp.
393 $timestamp = strtotime( $date );
394
395 // If it's not empty and not equal to -1, then convert it to the mm/dd/yyyy format.
396 $return_value = ( empty( $timestamp ) && -1 !== $timestamp ) ? '' : date( 'Y/m/d', $timestamp ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
397
398 return $return_value;
399 }
400
401 /**
402 * Sanitizes a date passed in the format of mm/dd/yyyy for form filters.
403 *
404 * @since 1.7.0.11
405 * @version 1.8.9.1
406 *
407 * @param string $date The date to be sanitized.
408 * @return string|false
409 */
410 function charitable_sanitize_date_export_format( $date = false ) {
411
412 if ( empty( $date ) || ! $date ) {
413 return false;
414 }
415
416 // Convert the date to a timestamp.
417 $timestamp = strtotime( $date );
418
419 // If it's not empty and not equal to -1, then convert it to the mm/dd/yyyy format.
420 $return_value = ( empty( $timestamp ) && -1 !== $timestamp ) ? '' : date( 'Y/m/d', $timestamp ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
421
422 return $return_value;
423 }
424
425 /**
426 * Return a string containing the correct number & type of placeholders.
427 *
428 * @since 1.5.0
429 *
430 * @param int $count The number of placeholders to add.
431 * @param string $placeholder Type of placeholder to insert.
432 * @return string
433 */
434 function charitable_get_query_placeholders( $count = 1, $placeholder = '%s' ) {
435 $placeholders = array_fill( 0, $count, $placeholder );
436 return implode( ', ', $placeholders );
437 }
438
439 /**
440 * Return a list of pages in id=>title format for use in a select dropdown.
441 *
442 * @see get_pages
443 *
444 * @since 1.6.0
445 *
446 * @param array $args Optional arguments to be passed to get_pages.
447 * @return array
448 */
449 function charitable_get_pages_options( $args = array() ) {
450 $pages = get_pages( $args );
451
452 if ( ! $pages ) {
453 return array();
454 }
455
456 return array_combine( wp_list_pluck( $pages, 'ID' ), wp_list_pluck( $pages, 'post_title' ) );
457 }
458
459 /**
460 * Checks whether this is localhost.
461 *
462 * This is not fullproof. It uses a safelist of IP addresses.
463 *
464 * @since 1.6.14
465 * @version 1.8.9.1
466 *
467 * @return boolean
468 */
469 function charitable_is_localhost() {
470
471 if ( false !== ( defined( 'CHARITABLE_FORCE_NO_LOCALHOST_WITH_STRIPE_CONNECT' ) && CHARITABLE_FORCE_NO_LOCALHOST_WITH_STRIPE_CONNECT ) ) {
472 return false;
473 }
474
475 /**
476 * Filter list of localhost IP addresses.
477 *
478 * @since 1.6.14
479 *
480 * @param array $ip_addresses The list of IP addresses.
481 */
482 $safelist = apply_filters(
483 'charitable_localhost_ips',
484 array(
485 '127.0.0.1',
486 '::1',
487 )
488 );
489
490 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
491 return isset( $_SERVER['REMOTE_ADDR'] ) && in_array( $_SERVER['REMOTE_ADDR'], $safelist );
492 }
493
494 /**
495 * Check whether we are currently using a block theme.
496 *
497 * @since 1.6.55
498 *
499 * @return boolean
500 */
501 function charitable_is_block_theme() {
502 return function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
503 }
504
505 /**
506 * Santitize to ensure we are working with a hex color (especially when working with campaign templates).
507 *
508 * @since 1.8.0
509 *
510 * @param string $color The color to sanitize.
511 * @param boolean $hash Whether to include the hash in the return value.
512 *
513 * @return boolean
514 */
515 function charitable_sanitize_hex( $color = '#FFFFFF', $hash = true ) {
516
517 // Remove any spaces and special characters before and after the string.
518 $color = trim( $color );
519
520 // Remove any trailing '#' symbols from the color value.
521 $color = str_replace( '#', '', $color );
522
523 // If the string is 6 characters long then use it in pairs.
524 if ( 3 === strlen( $color ) ) {
525 $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 );
526 }
527
528 $substr = array();
529 for ( $i = 0; $i <= 5; $i++ ) {
530 $default = ( 0 == $i ) ? 'F' : ( $substr[ $i - 1 ] );
531 $substr[ $i ] = substr( $color, $i, 1 );
532 $substr[ $i ] = ( false === $substr[ $i ] || ! ctype_xdigit( $substr[ $i ] ) ) ? $default : $substr[ $i ];
533 }
534 $hex = implode( '', $substr );
535
536 return ( ! $hash ) ? $hex : '#' . $hex;
537 }
538
539 /**
540 * Searches the database for transients stored there that match a specific prefix.
541 * Full credit to Kellen Mace and Brad Parbs for this concept.
542 *
543 * @since 1.8.1
544 *
545 * @param string $prefix Prefix to search for.
546 * @param string $postfix_separator Separator to use for the postfix.
547 *
548 * @return array|bool Nested array response for wpdb->get_results or false on failure.
549 */
550 function charitable_search_database_for_transients_by_prefix( $prefix = '', $postfix_separator = '_' ) {
551
552 global $wpdb;
553
554 // Add our prefix after concating our prefix with the _transient prefix.
555 $prefix = $wpdb->esc_like( '_transient_' . $prefix . $postfix_separator );
556
557 // Build up our SQL query.
558 $sql = "SELECT `option_name` FROM $wpdb->options WHERE `option_name` LIKE '%s'";
559
560 // Execute our query.
561 $transients = $wpdb->get_results( $wpdb->prepare( $sql, $prefix . '%' ), ARRAY_A ); // phpcs:ignore
562
563 // If if looks good, pass it back.
564 if ( $transients && ! is_wp_error( $transients ) ) {
565 return $transients;
566 }
567
568 // Otherise return false.
569 return false;
570 }
571
572 /**
573 * Expects a passed in multidimensional array of transient keys.
574 *
575 * Array(
576 * array( 'option_name' => '_transient_blah_blah' ),
577 * array( 'option_name' => 'transient_another_one' ),
578 * )
579 *
580 * Can also pass in an array of transient names.
581 *
582 * @since 1.8.1
583 *
584 * @param array|string $transients Nested array of transients, keyed by option_name,
585 * or array of names of transients.
586 * @return array|bool Count of total vs deleted or false on failure.
587 */
588 function charitable_delete_transients_from_keys( $transients = false ) {
589
590 if ( ! isset( $transients ) || false === $transients ) {
591 return false;
592 }
593
594 // If we get a string key passed in, might as well use it correctly.
595 if ( is_string( $transients ) ) {
596 $transients = array( array( 'option_name' => $transients ) );
597 }
598
599 // If its not an array, we can't do anything.
600 if ( ! is_array( $transients ) ) {
601 return false;
602 }
603
604 $results = array();
605
606 // Loop through our transients.
607 foreach ( $transients as $transient ) {
608
609 if ( is_array( $transient ) ) {
610
611 // If we have an array, grab the first element.
612 $transient = current( $transient );
613 }
614
615 // Remove that sucker.
616 $results[ $transient ] = delete_transient( str_replace( '_transient_', '', $transient ) );
617 }
618
619 // Return an array of total number, and number deleted.
620 return array(
621 'total' => count( $results ),
622 'deleted' => array_sum( $results ),
623 );
624 }
625
626 /**
627 * Check if AIOSEO Pro version is installed or not.
628 *
629 * @since 1.8.1
630 *
631 * @return bool
632 */
633 function charitable_is_installed_aioseo_pro() {
634 $installed_plugins = get_plugins();
635
636 if ( array_key_exists( 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php', $installed_plugins ) ) {
637 return true;
638 }
639
640 return false;
641 }
642
643 /**
644 * Check if MonsterInsights Pro version is installed or not.
645 *
646 * @since 1.8.1
647 *
648 * @return bool
649 */
650 function charitable_is_installed_mi_pro() {
651 $installed_plugins = get_plugins();
652
653 if ( array_key_exists( 'google-analytics-premium/googleanalytics-premium.php', $installed_plugins ) ) {
654 return true;
655 }
656
657 return false;
658 }
659
660 /**
661 * Get the secret key for the crypto functions.
662 *
663 * @since 1.8.7
664 * @version 1.8.9.1
665 *
666 * @return string
667 */
668 function charitable_crypto_get_secret_key() {
669
670 $secret_key = get_option( 'charitable_crypto_secret_key' );
671
672 if ( charitable_is_debug() ) {
673 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
674 error_log( 'charitable_crypto_get_secret_key: secret_key: ' . $secret_key );
675 }
676
677 // If we already have the secret, send it back.
678 if ( false !== $secret_key ) {
679 if ( charitable_is_debug() ) {
680 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
681 error_log( 'charitable_crypto_get_secret_key: secret_key already exists: ' . $secret_key );
682 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
683 error_log( 'charitable_crypto_get_secret_key: secret_key decoded: ' . base64_decode( $secret_key ) );
684 }
685 return base64_decode( $secret_key ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
686 }
687
688 // We don't have a secret, so let's generate one.
689 $secret_key = sodium_crypto_secretbox_keygen();
690 add_option( 'charitable_crypto_secret_key', base64_encode( $secret_key ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
691
692 if ( charitable_is_debug() ) {
693 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
694 error_log( 'charitable_crypto_get_secret_key: secret_key returned: ' . $secret_key );
695 }
696
697 return $secret_key;
698 }
699
700 /**
701 * Encrypt a message.
702 *
703 * @since 1.6.1.2
704 * @version 1.8.9.1
705 *
706 * @param string $message Message to encrypt.
707 * @param string $key Encryption key.
708 *
709 * @return string
710 */
711 function charitable_crypto_encrypt( $message, $key = '' ) {
712
713 if ( charitable_is_debug() ) {
714 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
715 error_log( 'charitable_crypto_encrypt: message: ' . $message );
716 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
717 error_log( 'charitable_crypto_encrypt: key: ' . $key );
718 }
719
720 // Create a nonce for this operation. It will be stored and recovered in the message itself.
721 $nonce = random_bytes(
722 SODIUM_CRYPTO_SECRETBOX_NONCEBYTES
723 );
724
725 if ( empty( $key ) ) {
726 $key = charitable_crypto_get_secret_key();
727 }
728
729 if ( charitable_is_debug() ) {
730 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
731 error_log( 'charitable_crypto_encrypt: key: ' . $key );
732 }
733
734 // Encrypt message and combine with nonce.
735 $cipher = base64_encode( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
736 $nonce .
737 sodium_crypto_secretbox(
738 $message,
739 $nonce,
740 $key
741 )
742 );
743
744 try {
745 sodium_memzero( $message );
746 sodium_memzero( $key );
747 if ( charitable_is_debug() ) {
748 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
749 error_log( 'charitable_crypto_encrypt: cipher: ' . $cipher );
750 }
751 } catch ( \Exception $e ) {
752 if ( charitable_is_debug() ) {
753 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
754 error_log( 'charitable_crypto_encrypt: error: ' . $e->getMessage() );
755 }
756 return $cipher;
757 }
758
759 return $cipher;
760 }
761
762 /**
763 * Decrypt a message.
764 *
765 * @since 1.6.1.2
766 * @version 1.8.9.1
767 *
768 * @param string $encrypted Encrypted message.
769 * @param string $key Encryption key.
770 *
771 * @return string
772 */
773 function charitable_crypto_decrypt( $encrypted, $key = '' ) {
774
775 // Unpack base64 message.
776 $decoded = base64_decode( $encrypted ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
777
778 if ( false === $decoded ) {
779 return false;
780 }
781
782 if ( mb_strlen( $decoded, '8bit' ) < ( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES ) ) {
783 return false;
784 }
785
786 // Pull nonce and ciphertext out of unpacked message.
787 $nonce = mb_substr( $decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit' );
788 $ciphertext = mb_substr( $decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit' );
789
790 if ( empty( $key ) ) {
791 $key = charitable_crypto_get_secret_key();
792 }
793
794 // Decrypt it.
795 $message = sodium_crypto_secretbox_open(
796 $ciphertext,
797 $nonce,
798 $key
799 );
800
801 // Check for decrpytion failures.
802 if ( false === $message ) {
803 return false;
804 }
805
806 try {
807 sodium_memzero( $ciphertext );
808 sodium_memzero( $key );
809 if ( charitable_is_debug() ) {
810 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
811 error_log( 'charitable_crypto_decrypt: message: ' . $message );
812 }
813 } catch ( \Exception $e ) {
814 if ( charitable_is_debug() ) {
815 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
816 error_log( 'charitable_crypto_decrypt: error: ' . $e->getMessage() );
817 }
818 return $message;
819 }
820
821 return $message;
822 }
823
824 if ( ! function_exists( 'charitable_array_key_first' ) ) {
825 /**
826 * The `array_key_first` polyfill.
827 *
828 * @since 1.8.7
829 *
830 * @param array $arr Input array.
831 *
832 * @return mixed|null
833 */
834 function charitable_array_key_first( array $arr ) {
835
836 if ( function_exists( 'array_key_first' ) ) {
837 return array_key_first( $arr );
838 }
839
840 foreach ( $arr as $key => $unused ) {
841 return $key;
842 }
843
844 return null;
845 }
846 }
847