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 +617 -22 1.6.7 → 1.8.13 View file →
@@ -4,16 +4,19 @@
4 4 *
5 5 * Utility functions.
6 6 *
7 7 * @package Charitable/Functions/Utility
8 - * @version 1.0.0
9 - * @author Eric Daams
10 - * @copyright Copyright (c) 2018, Studio 164a
8 + * @author David Bisset
9 + * @copyright Copyright (c) 2023, WP Charitable LLC
11 10 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
11 + * @since 1.0.0
12 + * @version 1.6.55
12 13 */
13 14
14 15 // Exit if accessed directly.
15 -if ( ! defined( 'ABSPATH' ) ) { exit; }
16 +if ( ! defined( 'ABSPATH' ) ) {
17 + exit;
18 +}
16 19
17 20 /**
18 21 * Orders an array by a particular key.
19 22 *
@@ -25,10 +28,12 @@
25 28 * @return int
26 29 */
27 30 function charitable_element_key_sort( $key, $a, $b ) {
28 31 foreach ( array( $a, $b ) as $item ) {
29 - if ( ! array_key_exists( $key, $item ) ) {
30 - error_log( sprintf( '%s missing from element: ' . json_encode( $item ), $key ) );
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 + }
31 36 }
32 37 }
33 38
34 39 if ( $a[ $key ] == $b[ $key ] ) {
@@ -47,9 +52,9 @@
47 52 * @param array $b Element to compare against.
48 53 * @return int
49 54 */
50 55 function charitable_priority_sort( $a, $b ) {
51 - return charitable_element_key_sort( 'priority', $a, $b );
56 + return charitable_element_key_sort( 'priority', $a, $b );
52 57 }
53 58
54 59 /**
55 60 * Orders an array by the time key.
@@ -70,13 +75,13 @@
70 75 * Full credit to Pippin Williamson and the EDD team.
71 76 *
72 77 * @since 1.0.0
73 78 *
74 - * @param string $function Name of the function.
75 - * @return bool Whether or not function is disabled.
79 + * @param string $function Name of the function.
80 + * @return boolean Whether or not function is disabled.
76 81 */
77 82 function charitable_is_func_disabled( $function ) {
78 - $disabled = explode( ',', ini_get( 'disable_functions' ) );
83 + $disabled = explode( ',', ini_get( 'disable_functions' ) );
79 84
80 85 return in_array( $function, $disabled );
81 86 }
82 87
@@ -83,16 +88,18 @@
83 88 /**
84 89 * Verify a nonce. This also just ensures that the nonce is set.
85 90 *
86 91 * @since 1.0.0
92 + * @version 1.8.9.1
87 93 *
88 - * @param string $nonce
89 - * @param string $action
90 - * @param array $request_args
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.
91 97 * @return boolean
92 98 */
93 99 function charitable_verify_nonce( $nonce, $action, $request_args = array() ) {
94 100 if ( empty( $request_args ) ) {
101 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
95 102 $request_args = $_GET;
96 103 }
97 104
98 105 return isset( $request_args[ $nonce ] ) && wp_verify_nonce( $request_args[ $nonce ], $action );
@@ -127,9 +134,9 @@
127 134
128 135 /* Last try, guess timezone string manually */
129 136 if ( false === $timezone ) {
130 137
131 - $is_dst = date( 'I' );
138 + $is_dst = date( 'I' ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
132 139
133 140 foreach ( timezone_abbreviations_list() as $abbr ) {
134 141 foreach ( $abbr as $city ) {
135 142 if ( $city['dst'] == $is_dst && $city['offset'] == $utc_offset ) {
@@ -169,8 +176,51 @@
169 176 return filter_var( $i, FILTER_VALIDATE_INT, array( 'min_range' => 1 ) );
170 177 }
171 178
172 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 +/**
173 223 * Sanitize any checkbox value.
174 224 *
175 225 * @since 1.5.0
176 226 *
@@ -189,9 +239,9 @@
189 239 * If there are three or more items, it will return a string like this: "x, y and z".
190 240 *
191 241 * @since 1.3.0
192 242 *
193 - * @param string[] $list
243 + * @param string[] $list The list.
194 244 * @return string
195 245 */
196 246 function charitable_list_to_sentence_part( $list ) {
197 247 $list = array_values( $list );
@@ -200,14 +250,24 @@
200 250 return $list[0];
201 251 }
202 252
203 253 if ( 2 == count( $list ) ) {
204 - return sprintf( _x( '%s and %s', 'x and y', 'charitable' ), $list[0], $list[1] );
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 + );
205 260 }
206 261
207 262 $last = array_pop( $list );
208 263
209 - return sprintf( _x( '%s and %s', 'x and y', 'charitable' ), implode( ', ', $list ), $last );
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 + );
210 270 }
211 271
212 272 /**
213 273 * Sanitizes a date passed in the format of January 29, 2009.
@@ -215,9 +275,11 @@
215 275 * We use WP_Locale to parse the month that the user has set.
216 276 *
217 277 * @global WP_Locale $wp_locale
218 278 *
219 - * @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
220 282 *
221 283 * @param string $date The date to be sanitized.
222 284 * @param string $return_format The date format to return. Default is U (timestamp).
223 285 * @return string|false
@@ -228,24 +290,168 @@
228 290 if ( empty( $date ) || ! $date ) {
229 291 return false;
230 292 }
231 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 +
232 346 list( $month, $day, $year ) = explode( ' ', $date );
233 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 +
234 356 $day = trim( $day, ',' );
357 + $month = 1 + array_search( $month, array_values( $wp_locale->month ), true );
358 + $time = mktime( 0, 0, 0, $month, (int) $day, (int) $year );
235 359
236 - $month = 1 + array_search( $month, array_values( $wp_locale->month ) );
360 + if ( 'U' === $return_format ) {
361 + return $time;
362 + }
237 363
238 - $time = mktime( 0, 0, 0, $month, $day, $year );
364 + return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
365 +}
239 366
240 - if ( 'U' == $return_format ) {
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 ) {
241 399 return $time;
242 400 }
243 401
244 - return date( $return_format, $time );
402 + return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
245 403 }
246 404
247 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 +/**
248 454 * Return a string containing the correct number & type of placeholders.
249 455 *
250 456 * @since 1.5.0
251 457 *
@@ -275,5 +481,394 @@
275 481 return array();
276 482 }
277 483
278 484 return array_combine( wp_list_pluck( $pages, 'ID' ), wp_list_pluck( $pages, 'post_title' ) );
485 +}
486 +
487 +/**
488 + * Checks whether this is localhost.
489 + *
490 + * This is not fullproof. It uses a safelist of IP addresses.
491 + *
492 + * @since 1.6.14
493 + * @version 1.8.9.1
494 + *
495 + * @return boolean
496 + */
497 +function charitable_is_localhost() {
498 +
499 + if ( false !== ( defined( 'CHARITABLE_FORCE_NO_LOCALHOST_WITH_STRIPE_CONNECT' ) && CHARITABLE_FORCE_NO_LOCALHOST_WITH_STRIPE_CONNECT ) ) {
500 + return false;
501 + }
502 +
503 + /**
504 + * Filter list of localhost IP addresses.
505 + *
506 + * @since 1.6.14
507 + *
508 + * @param array $ip_addresses The list of IP addresses.
509 + */
510 + $safelist = apply_filters(
511 + 'charitable_localhost_ips',
512 + array(
513 + '127.0.0.1',
514 + '::1',
515 + )
516 + );
517 +
518 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
519 + return isset( $_SERVER['REMOTE_ADDR'] ) && in_array( $_SERVER['REMOTE_ADDR'], $safelist );
520 +}
521 +
522 +/**
523 + * Check whether we are currently using a block theme.
524 + *
525 + * @since 1.6.55
526 + *
527 + * @return boolean
528 + */
529 +function charitable_is_block_theme() {
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 + }
279 874 }