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
charitable / includes / utilities / charitable-utility-functions.php

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

875 lines 23.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 * 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 /*
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
346 list( $month, $day, $year ) = explode( ' ', $date );
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
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 );
359
360 if ( 'U' === $return_format ) {
361 return $time;
362 }
363
364 return date( $return_format, $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
365 }
366
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 /**
454 * Return a string containing the correct number & type of placeholders.
455 *
456 * @since 1.5.0
457 *
458 * @param int $count The number of placeholders to add.
459 * @param string $placeholder Type of placeholder to insert.
460 * @return string
461 */
462 function charitable_get_query_placeholders( $count = 1, $placeholder = '%s' ) {
463 $placeholders = array_fill( 0, $count, $placeholder );
464 return implode( ', ', $placeholders );
465 }
466
467 /**
468 * Return a list of pages in id=>title format for use in a select dropdown.
469 *
470 * @see get_pages
471 *
472 * @since 1.6.0
473 *
474 * @param array $args Optional arguments to be passed to get_pages.
475 * @return array
476 */
477 function charitable_get_pages_options( $args = array() ) {
478 $pages = get_pages( $args );
479
480 if ( ! $pages ) {
481 return array();
482 }
483
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 }
874 }
875