PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13.1
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13.1
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 / admin / charitable-core-admin-functions.php

charitable-core-admin-functions.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13.1, at includes/admin/charitable-core-admin-functions.php

1,919 lines 56.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Core Admin Functions
4 *
5 * General core functions available only within the admin area.
6 *
7 * @package Charitable/Functions/Admin
8 * @version 1.0.0
9 * @author David Bisset
10 * @copyright Copyright (c) 2023, WP Charitable LLC
11 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
12 */
13
14 // Exit if accessed directly.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * Load a view from the admin/views folder.
21 *
22 * If the view is not found, an Exception will be thrown.
23 *
24 * Example usage: charitable_admin_view('metaboxes/campaign-title');
25 *
26 * @since 1.0.0
27 *
28 * @param string $view The view to display.
29 * @param array $view_args Optional. Arguments to pass through to the view itself.
30 * @param bool $return_html Optional. Whether to return the HTML or echo it. Default is false.
31 *
32 * @return boolean True if the view exists and was rendered. False otherwise.
33 */
34 function charitable_admin_view( $view, $view_args = array(), $return_html = false ) {
35 $base_path = array_key_exists( 'base_path', $view_args ) ? $view_args['base_path'] : charitable()->get_path( 'admin' ) . 'views/';
36
37 /**
38 * Filter the path to the view.
39 *
40 * @since 1.0.0
41 *
42 * @param string $path The default path.
43 * @param string $view The view.
44 * @param array $view_args View args.
45 */
46 $filename = apply_filters( 'charitable_admin_view_path', $base_path . $view . '.php', $view, $view_args );
47
48 if ( ! is_readable( $filename ) ) {
49 charitable_get_deprecated()->doing_it_wrong(
50 __FUNCTION__,
51 sprintf(
52 /* translators: %s: Filename of passed view */
53 __( 'Passed view (%s) not found or is not readable.', 'charitable' ),
54 $filename
55 ),
56 '1.0.0'
57 );
58
59 return false;
60 }
61
62 ob_start();
63
64 include $filename;
65
66 if ( $return_html ) {
67 $output = ob_get_contents();
68 ob_end_clean();
69 return $output;
70 }
71
72 ob_end_flush();
73
74 return true;
75 }
76
77 /**
78 * Returns the Charitable_Settings helper.
79 *
80 * @since 1.0.0
81 *
82 * @return Charitable_Settings
83 */
84 function charitable_get_admin_settings() {
85 return Charitable_Settings::get_instance();
86 }
87
88 /**
89 * Returns the Charitable_Reports helper.
90 *
91 * @since 1.8.1
92 *
93 * @return Charitable_Reports
94 */
95 function charitable_get_admin_reports() {
96 return Charitable_Reports::get_instance();
97 }
98
99 /**
100 * Returns the Charitable_Dashboard_Legacy helper.
101 *
102 * @since 1.8.1
103 *
104 * @return Charitable_Dashboard_Legacy
105 */
106 function charitable_get_admin_dashboard() {
107 return Charitable_Dashboard_Legacy::get_instance();
108 }
109
110 /**
111 * Returns the Charitable_Tools helper.
112 *
113 * @since 1.8.1.6
114 *
115 * @return Charitable_Tools
116 */
117 function charitable_get_admin_tools() {
118 return Charitable_Tools::get_instance();
119 }
120
121 /**
122 * Returns the Charitable_Intergrations_WPCode helper.
123 *
124 * @since 1.8.1.6
125 *
126 * @return Charitable_Intergrations_WPCode
127 */
128 function charitable_get_intergration_wpcode() {
129 return Charitable_Intergrations_WPCode::get_instance();
130 }
131
132 /**
133 * Returns the Charitable_Tools_System_Info helper.
134 *
135 * @since 1.8.1.6
136 *
137 * @return Charitable_Tools_System_Info
138 */
139 function charitable_get_system_info() {
140 return Charitable_Tools_System_Info::get_instance();
141 }
142
143 /**
144 * Returns the Charitable_Admin_Notices helper.
145 *
146 * @since 1.4.6
147 *
148 * @return Charitable_Admin_Notices
149 */
150 function charitable_get_admin_notices() {
151 return charitable()->registry()->get( 'admin_notices' );
152 }
153
154 /**
155 * Returns whether we are currently viewing the Charitable settings area.
156 *
157 * @since 1.2.0
158 *
159 * @param string $tab Optional. If passed, the function will also check that we are on the given tab.
160 * @return boolean
161 */
162 function charitable_is_settings_view( $tab = '' ) {
163 if ( ! empty( $_POST ) ) { // phpcs:ignore
164 $is_settings = array_key_exists( 'option_page', $_POST ) && 'charitable_settings' === $_POST['option_page']; // phpcs:ignore
165
166 if ( ! $is_settings || empty( $tab ) ) {
167 return $is_settings;
168 }
169
170 return array_key_exists( 'charitable_settings', $_POST ) && array_key_exists( $tab, $_POST['charitable_settings'] ); // phpcs:ignore
171 }
172
173 $is_settings = isset( $_GET['page'] ) && 'charitable-settings' == $_GET['page']; // phpcs:ignore
174
175 if ( ! $is_settings || empty( $tab ) ) {
176 return $is_settings;
177 }
178
179 /* The general tab can be loaded when tab is not set. */
180 if ( 'general' == $tab ) {
181 return ! isset( $_GET['tab'] ) || 'general' == $_GET['tab']; // phpcs:ignore
182 }
183
184 return isset( $_GET['tab'] ) && $tab == $_GET['tab']; // phpcs:ignore
185 }
186
187 /**
188 * Print out the settings fields for a particular settings section.
189 *
190 * This is based on WordPress' do_settings_fields but allows the possibility
191 * of leaving out a field lable/title, for fullwidth fields.
192 *
193 * @see do_settings_fields
194 *
195 * @since 1.0.0
196 *
197 * @global $wp_settings_fields Storage array of settings fields and their pages/sections
198 *
199 * @param string $page Slug title of the admin page who's settings fields you want to show.
200 * @param string $section Slug title of the settings section who's fields you want to show.
201 * @return string
202 */
203 function charitable_do_settings_fields( $page, $section ) {
204 global $wp_settings_fields;
205
206 if ( ! isset( $wp_settings_fields[ $page ][ $section ] ) ) {
207 return;
208 }
209
210 foreach ( (array) $wp_settings_fields[ $page ][ $section ] as $field ) {
211
212 $class = '';
213
214 if ( ! empty( $field['args']['class'] ) ) {
215 $class = ' class="' . esc_attr( $field['args']['class'] ) . '"';
216 }
217
218 echo "<tr{$class}>"; // phpcs:ignore
219
220 if ( ! empty( $field['args']['label_for'] ) ) {
221 echo '<th scope="row"><label for="' . esc_attr( $field['args']['label_for'] ) . '">' . charitable_santitize_setting_labels( $field['title'] ) . '</label></th>'; // phpcs:ignore
222 echo '<td>';
223 call_user_func( $field['callback'], $field['args'] );
224 echo '</td>';
225 } elseif ( ! empty( $field['title'] ) ) {
226 if ( $field['args']['type'] === 'heading' && isset( $field['args']['help'] ) ) { // if this is a heading display a "help" as a subheading.
227 echo '<th scope="row" colspan="2"><h4>' . esc_html( $field['title'] ) . '</h4>';
228 echo '<p>' . sanitize_text_field( $field['args']['help'] ) . '</p>'; // phpcs:ignore
229 } else {
230 echo '<th scope="row"><h4>' . charitable_santitize_setting_labels( $field['title'] ) . '</h4>'; // phpcs:ignore
231 }
232 echo '</th>';
233 if ( $field['args']['type'] !== 'heading' ) {
234 echo '<td>';
235 call_user_func( $field['callback'], $field['args'] );
236 echo '</td>';
237 }
238 } else {
239 echo '<td colspan="2" class="charitable-fullwidth">';
240 call_user_func( $field['callback'], $field['args'] );
241 echo '</td>';
242 }
243
244 echo '</tr>';
245 }
246 }
247
248 /**
249 * Add new tab to the Charitable settings area.
250 *
251 * @since 1.3.0
252 *
253 * @param string[] $tabs The existing tabs.
254 * @param string $key The key for the new tab.
255 * @param string $name The name of the new tab.
256 * @param mixed[] $args Additional arguments for the new tab.
257 * @return string[]
258 */
259 function charitable_add_settings_tab( $tabs, $key, $name, $args = array() ) {
260 $defaults = array(
261 'index' => 3,
262 );
263
264 $args = wp_parse_args( $args, $defaults );
265 $keys = array_keys( $tabs );
266 $values = array_values( $tabs );
267
268 array_splice( $keys, $args['index'], 0, $key );
269 array_splice( $values, $args['index'], 0, $name );
270
271 return array_combine( $keys, $values );
272 }
273
274 /**
275 * Returns whether we are currently viewing the Charitable tools area.
276 *
277 * @since 1.8.1.6
278 *
279 * @param string $tab Optional. If passed, the function will also check that we are on the given tab.
280 * @return boolean
281 */
282 function charitable_is_tools_view( $tab = '' ) {
283
284 if ( ! empty( $_POST ) ) { // phpcs:ignore
285 $is_settings = array_key_exists( 'option_page', $_POST ) && 'charitable_tools' === $_POST['option_page']; // phpcs:ignore
286
287 if ( ! $is_settings || empty( $tab ) ) {
288 return $is_settings;
289 }
290
291 return array_key_exists( 'charitable_tools', $_POST ) && array_key_exists( $tab, $_POST['charitable_tools'] ); // phpcs:ignore
292 }
293
294 $is_settings = isset( $_GET['page'] ) && 'charitable-tools' === $_GET['page']; // phpcs:ignore
295
296 if ( ! $is_settings || empty( $tab ) ) {
297 return $is_settings;
298 }
299
300 /* Check for sub-tab format (e.g. 'import__charitable'). */
301 if ( false !== strpos( $tab, '__' ) ) {
302 $tab_parts = explode( '__', $tab );
303 $main_tab = $tab_parts[0];
304 $sub_tab = $tab_parts[1];
305
306 return isset( $_GET['tab'] ) && $main_tab == $_GET['tab'] // phpcs:ignore
307 && isset( $_GET['sub_tab'] ) && $sub_tab == $_GET['sub_tab']; // phpcs:ignore
308 }
309
310 /* The general tab can be loaded when tab is not set. */
311 if ( 'general' === $tab ) {
312 return ! isset( $_GET['tab'] ) || 'general' == $_GET['tab']; // phpcs:ignore
313 }
314
315 return isset( $_GET['tab'] ) && $tab == $_GET['tab']; // phpcs:ignore
316 }
317
318 /**
319 * Print out the settings fields for a particular settings section.
320 *
321 * This is based on WordPress' do_settings_fields but allows the possibility
322 * of leaving out a field lable/title, for fullwidth fields.
323 *
324 * @see do_settings_fields
325 *
326 * @since 1.8.1.6
327 *
328 * @global $wp_settings_fields Storage array of settings fields and their pages/sections
329 *
330 * @param string $page Slug title of the admin page who's settings fields you want to show.
331 * @param string $section Slug title of the settings section who's fields you want to show.
332 * @return string
333 */
334 function charitable_do_tools_fields( $page, $section ) {
335 global $wp_settings_fields;
336
337 if ( ! isset( $wp_settings_fields[ $page ][ $section ] ) ) {
338 return;
339 }
340
341 foreach ( (array) $wp_settings_fields[ $page ][ $section ] as $field ) {
342 $class = '';
343
344 if ( ! empty( $field['args']['class'] ) ) {
345 $class = ' class="' . esc_attr( $field['args']['class'] ) . '"';
346 }
347
348 echo "<tr{$class}>"; // phpcs:ignore
349
350 if ( ! empty( $field['args']['label_for'] ) ) {
351 echo '<th scope="row"><label for="' . esc_attr( $field['args']['label_for'] ) . '">' . charitable_santitize_setting_labels( $field['title'] ) . '</label></th>'; // phpcs:ignore
352 echo '<td>';
353 call_user_func( $field['callback'], $field['args'] );
354 echo '</td>';
355 } elseif ( ! empty( $field['title'] ) ) {
356 if ( $field['args']['type'] === 'heading' && isset( $field['args']['help'] ) ) { // if this is a heading display a "help" as a subheading.
357 echo '<th scope="row" colspan="2"><h4>' . esc_html( $field['title'] ) . '</h4>';
358 echo '<p>' . sanitize_text_field( $field['args']['help'] ) . '</p>'; // phpcs:ignore
359 } else {
360 echo '<th scope="row"><h4>' . charitable_santitize_setting_labels( $field['title'] ) . '</h4>'; // phpcs:ignore
361 }
362 echo '</th>';
363 if ( $field['args']['type'] !== 'heading' ) {
364 echo '<td>';
365 call_user_func( $field['callback'], $field['args'] );
366 echo '</td>';
367 }
368 } else {
369 echo '<td colspan="2" class="charitable-fullwidth">';
370 call_user_func( $field['callback'], $field['args'] );
371 echo '</td>';
372 }
373
374 echo '</tr>';
375 }
376 }
377
378 /**
379 * Return the donation actions class.
380 *
381 * @since 1.5.0
382 *
383 * @return Charitable_Donation_Admin_Actions
384 */
385 function charitable_get_donation_actions() {
386 return Charitable_Admin::get_instance()->get_donation_actions();
387 }
388
389 /**
390 * Adds the "Upgrade to Pro" menu item to the very end of the submenu.
391 *
392 * @since 1.7.0
393 */
394 function charitable_add_upgrade_item() {
395 global $submenu;
396
397 if ( charitable_is_pro() ) {
398 return;
399 }
400
401 $submenu['charitable'][99] = array( // phpcs:ignore
402 __( 'Upgrade to Pro', 'charitable' ),
403 'manage_options',
404 charitable_ga_url(
405 'https://wpcharitable.com/lite-vs-pro/',
406 urlencode( 'Admin Menu Link' ),
407 urlencode( 'Upgrade to Pro' )
408 ),
409 );
410 }
411 add_action( 'admin_menu', 'charitable_add_upgrade_item' );
412
413 /**
414 * Determines if Charitable allows legacy campaigns to be created.
415 *
416 * @since 1.8.2
417 *
418 * @return bool
419 */
420 function charitable_disable_legacy_campaigns() {
421
422 $disable_legacy_campaign = charitable_get_option( 'disable_campaign_legacy_mode', false ) ? true : false;
423 $disable_legacy_campaign = apply_filters( 'charitable_disable_legacy_campaign', $disable_legacy_campaign );
424
425 return $disable_legacy_campaign;
426 }
427
428 /**
429 * Outputs "please rate" text.
430 *
431 * @since 1.7.0
432 * @version 1.8.8.6
433 *
434 * @param string $footer_text Footer text.
435 * @return string
436 */
437 function charitable_add_footer_text( $footer_text ) {
438 if ( ! charitable_is_admin_screen() ) {
439 return $footer_text;
440 }
441
442 return sprintf(
443 /* translators: %1$s Opening strong tag, do not translate. %2$s Closing strong tag, do not translate. %3$s Opening anchor tag, do not translate. %4$s Closing anchor tag, do not translate. */
444 __( 'Please rate %1$sCharitable%2$s %3$s�
445 �
446 �
447 �
448 �
449 %4$s on %3$sWordPress.org%4$s to help us spread the word. Thank you from the Charitable team!', 'charitable' ),
450 '<strong>',
451 '</strong>',
452 '<a href="https://wpcharitable.com/charitable-admin-review/" rel="noopener noreferrer" target="_blank">',
453 '</a>'
454 );
455 }
456 add_filter( 'admin_footer_text', 'charitable_add_footer_text' );
457
458 /**
459 * Check if the promotion footer should be displayed.
460 *
461 * @since 1.8.7.2
462 *
463 * @return bool
464 */
465 function charitable_show_promotion_footer() {
466
467 // Some 3rd-party addons may use page slugs that start with `charitable-`
468 // so we should define exact pages we want the footer to be displayed on instead
469 // of targeting any page that looks like a WPForms page.
470 $plugin_pages = [
471 'charitable-dashboard',
472 'charitable-reports',
473 'charitable-tools',
474 'charitable-settings',
475 'charitable-about',
476 'charitable-education',
477 'charitable-growth-tools',
478 'charitable-intergration-wpcode',
479 'charitable-system-info',
480 'charitable-notices',
481 ];
482
483 $allowed_page = false;
484
485 // Is this a post_type page of 'campaign' or 'donation'?
486 if ( isset( $_REQUEST['post_type'] ) && in_array( $_REQUEST['post_type'], [ 'campaign', 'donation' ], true ) ) { // phpcs:ignore WordPress.Security.NonceVerification
487 $allowed_page = true;
488 }
489
490 if ( charitable_is_admin_screen() ) {
491 $allowed_page = true;
492 }
493
494 if ( ! $allowed_page ) {
495 // phpcs:ignore WordPress.Security.NonceVerification
496 $current_page = isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : '';
497
498 if ( ! in_array( $current_page, $plugin_pages, true ) ) {
499 $allowed_page = false;
500 }
501 }
502
503 return apply_filters( 'charitable_show_promotion_footer', $allowed_page );
504 }
505
506 /**
507 * Pre-footer promotion block, displayed on all Charitable admin pages except Form Builder.
508 *
509 * @since 1.8.7.2
510 * @version 1.8.8 - Updated plugin footer links.
511 */
512 function charitable_promote_footer() {
513
514 if ( ! charitable_show_promotion_footer() ) {
515 return;
516 }
517
518 $links = [
519 [
520 'url' => charitable_is_pro() ?
521 charitable_utm_link(
522 'https://charitable.com/account/support/',
523 'Plugin Footer',
524 'Contact Support'
525 ) : 'https://wordpress.org/support/plugin/charitable/',
526 'text' => __( 'Support', 'charitable' ),
527 'target' => '_blank',
528 ],
529 [
530 'url' => charitable_utm_link(
531 'https://wpcharitable.com/documentation/',
532 'Plugin Footer',
533 'Plugin Documentation'
534 ),
535 'text' => __( 'Docs', 'charitable' ),
536 'target' => '_blank',
537 ],
538 [
539 'url' => admin_url( 'admin.php?page=charitable-about' ),
540 'text' => __( 'Free Plugins', 'charitable' ),
541 ],
542 ];
543
544 echo charitable_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
545 'admin/templates/promotion',
546 [
547 'title' => sprintf(
548 /* translators: %s: Heart icon */
549 __( 'Made with %s by the Charitable Team', 'charitable' ),
550 '<span style="color: #d63638;">♥</span>'
551 ),
552 'links' => $links,
553 ],
554 true
555 );
556 }
557 add_action( 'in_admin_footer', 'charitable_promote_footer' );
558
559 /**
560 * Check if a screen is a plugin admin view.
561 * Returns the screen id if true, false (bool) if not.
562 *
563 * @since 1.7.0
564 * @since 1.8.1.5 Added another check and 'donation' to first check.
565 *
566 * @return string|bool
567 */
568 function charitable_is_admin_screen() {
569 $screen = \get_current_screen();
570
571 if ( ! $screen ) {
572 return false;
573 }
574
575 if (
576 'charitable' === $screen->post_type ||
577 'campaign' === $screen->post_type ||
578 'donation' === $screen->post_type ||
579 'charitable' === $screen->parent_file
580 ) {
581 return 'charitable';
582 }
583
584 if ( isset( $_GET['page'] ) ) { // phpcs:ignore
585 if ( 'charitable' === trim( $_GET['page'] ) ) { // phpcs:ignore
586 return 'charitable';
587 }
588 }
589
590 if ( isset( $_REQUEST['page'] ) && strpos( sanitize_text_field( $_REQUEST['page'] ), 'charitable' ) !== false ) { // phpcs:ignore
591 return 'charitable';
592 }
593
594 return false;
595 }
596
597 /**
598 * Appends UTM parameters to a given URL.
599 *
600 * @since 1.7.0
601 *
602 * @param string $base_url Base URL.
603 * @param string $utm_medium utm_medium parameter.
604 * @param string $utm_content Optional. utm_content parameter.
605 * @return string $url Full Google Analytics campaign URL.
606 */
607 function charitable_ga_url( $base_url, $utm_medium, $utm_content = false ) {
608 /**
609 * Filters the UTM campaign for generated links.
610 *
611 * @since 1.7.0
612 *
613 * @param string $utm_campaign
614 */
615 $utm_campaign = apply_filters( 'charitable_utm_campaign', 'WP+Charitable' );
616
617 $args = array(
618 'utm_source' => 'WordPress',
619 'utm_campaign' => $utm_campaign,
620 'utm_medium' => $utm_medium,
621 );
622
623 if ( ! empty( $utm_content ) ) {
624 $args['utm_content'] = $utm_content;
625 }
626
627 return esc_url( add_query_arg( $args, $base_url ) );
628 }
629
630 /**
631 * URL for upgrading to Pro (or another Pro licecnse).
632 *
633 * @since 1.7.0
634 *
635 * @param string $utm_medium utm_medium parameter.
636 * @param string $utm_content Optional. utm_content parameter.
637 * @return string
638 */
639 function charitable_pro_upgrade_url( $utm_medium, $utm_content = '' ) {
640 return apply_filters(
641 'charitable_upgrade_link',
642 charitable_ga_url(
643 'https://wpcharitable.com/lite-vs-pro/',
644 urlencode( $utm_medium ),
645 urlencode( $utm_content )
646 ),
647 $utm_medium,
648 $utm_content
649 );
650 }
651
652 /**
653 * Get the current installation license type (always lowercase).
654 *
655 * @since 1.7.0.3
656 *
657 * @return string|false
658 */
659 function charitable_get_license_type() {
660
661 $type = charitable_setting( 'type', '', 'charitable_license' );
662
663 if ( empty( $type ) || ! charitable()->is_pro() ) {
664 return false;
665 }
666
667 return strtolower( $type );
668 }
669
670 /**
671 * Get when WPCharitable was first installed.
672 *
673 * @since 1.6.0
674 *
675 * @param string $type Specific install type to check for.
676 *
677 * @return int|false Unix timestamp. False on failure.
678 */
679 function charitable_get_activated_timestamp( $type = '' ) {
680
681 $activated = (array) get_option( 'charitable_activated', [] );
682
683 if ( empty( $activated ) ) {
684 return false;
685 }
686
687 // When a passed install type is empty, then get it from a DB.
688 // If it is installed/activated first, it is saved first.
689 $type = empty( $type ) ? (string) array_keys( $activated )[0] : $type;
690
691 if ( ! empty( $activated[ $type ] ) ) {
692 return absint( $activated[ $type ] );
693 }
694
695 // Fallback.
696 $types = array_diff( [ 'lite', 'pro' ], [ $type ] );
697
698 foreach ( $types as $_type ) {
699 if ( ! empty( $activated[ $_type ] ) ) {
700 return absint( $activated[ $_type ] );
701 }
702 }
703
704 return false;
705 }
706
707 /**
708 * Get when WPCharitable was first installed.
709 *
710 * @since 8.1.0
711 *
712 * @param string $type Specific install type to check for.
713 *
714 * @return int|false Unix timestamp. False on failure.
715 */
716 function charitable_get_updated_timestamp( $type = '' ) {
717
718 $activated = (array) get_option( 'charitable_activated', [] );
719
720 if ( empty( $activated ) ) {
721 return false;
722 }
723
724 // When a passed install type is empty, then get it from a DB.
725 // If it is installed/activated first, it is saved first.
726 $type = empty( $type ) ? (string) array_keys( $activated )[0] : $type;
727
728 if ( ! empty( $activated[ $type ] ) ) {
729 return absint( $activated[ $type ] );
730 }
731
732 // Fallback.
733 $types = array_diff( [ 'lite', 'pro' ], [ $type ] );
734
735 foreach ( $types as $_type ) {
736 if ( ! empty( $activated[ $_type ] ) ) {
737 return absint( $activated[ $_type ] );
738 }
739 }
740
741 return false;
742 }
743
744 /**
745 * Determines whether the current request is a WP CLI request.
746 *
747 * @since 1.7.0.3
748 *
749 * @return bool
750 */
751 function charitable_doing_wp_cli() {
752
753 return defined( 'WP_CLI' ) && WP_CLI;
754 }
755
756 /**
757 * Modify the default USer-Agent generated by wp_remote_*() to include additional information.
758 *
759 * @since 1.7.0.3
760 *
761 * @return string
762 */
763 function charitable_get_default_user_agent() {
764
765 $wpcharitable_type = function_exists( 'charitable_is_pro' ) && charitable_is_pro() ? 'Paid' : 'Lite';
766
767 return 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ) . '; WPCharitable/' . $wpcharitable_type;
768 }
769
770 /**
771 * Get the template paths.
772 *
773 * @since 1.8.0
774 *
775 * @return array
776 */
777 function charitable_theme_template_paths() {
778
779 $template_dir = 'charitable';
780
781 $file_paths = array(
782 1 => trailingslashit( get_stylesheet_directory() ) . $template_dir,
783 10 => trailingslashit( get_template_directory() ) . $template_dir,
784 100 => trailingslashit( CHARITABLE_DIRECTORY_PATH ) . 'includes',
785 );
786
787 $file_paths = apply_filters( 'charitable_helpers_templates_get_theme_template_paths', $file_paths );
788
789 // Sort the file paths based on priority.
790 ksort( $file_paths, SORT_NUMERIC );
791
792 return array_map( 'trailingslashit', $file_paths );
793 }
794
795 /**
796 * Locate a template and return the path for inclusion.
797 *
798 * @since 1.7.0.3
799 *
800 * @param string $template_name Template name.
801 *
802 * @return string
803 */
804 function charitable_locate_template( $template_name ) {
805
806 // Trim off any slashes from the template name.
807 $template_name = ltrim( $template_name, '/' );
808
809 if ( empty( $template_name ) ) {
810 return apply_filters( 'charitable_helpers_templates_locate', '', $template_name );
811 }
812
813 $located = '';
814
815 // Try locating this template file by looping through the template paths.
816 foreach ( charitable_theme_template_paths() as $template_path ) {
817 if ( file_exists( $template_path . $template_name ) ) {
818 $located = $template_path . $template_name;
819 break;
820 }
821 }
822
823 return apply_filters( 'charitable_helpers_templates_locate', $located, $template_name );
824 }
825
826 /**
827 * Include a template.
828 * Use 'require' if $args are passed or 'load_template' if not.
829 *
830 * @since 1.7.0.3
831 *
832 * @param string $template_name Template name.
833 * @param array $args Arguments.
834 * @param bool $extract Extract arguments.
835 *
836 * @throws \RuntimeException If extract() tries to modify the scope.
837 */
838 function charitable_admin_include_html( $template_name, $args = array(), $extract = false ) {
839
840 $template_name .= '.php';
841
842 // Allow 3rd party plugins to filter template file from their plugin.
843 $located = apply_filters( 'charitable_helpers_templates_include_html_located', charitable_locate_template( $template_name ), $template_name, $args, $extract );
844 $args = apply_filters( 'charitable_helpers_templates_include_html_args', $args, $template_name, $extract );
845
846 if ( empty( $located ) || ! \is_readable( $located ) ) {
847 return;
848 }
849
850 // Load template WP way if no arguments were passed.
851 if ( empty( $args ) ) {
852 load_template( $located, false );
853 return;
854 }
855
856 $extract = apply_filters( 'charitable_helpers_templates_include_html_extract_args', $extract, $template_name, $args );
857
858 if ( $extract && is_array( $args ) ) {
859
860 // Extract variables for template, checking for conflicts to prevent scope modification.
861 $created_vars_count = 0;
862 foreach ( $args as $var_name => $var_value ) {
863 // Only create variable if it doesn't already exist (equivalent to EXTR_SKIP).
864 if ( ! isset( $$var_name ) ) {
865 $$var_name = $var_value;
866 $created_vars_count++;
867 }
868 }
869
870 // Protecting existing scope from modification.
871 if ( count( $args ) !== $created_vars_count ) {
872 throw new \RuntimeException( 'Extraction failed: variable names are clashing with the existing ones.' );
873 }
874 }
875
876 require $located;
877 }
878
879 /**
880 * Like self::include_html, but returns the HTML instead of including.
881 *
882 * @since 1.7.0.3
883 *
884 * @param string $template_name Template name.
885 * @param array $args Arguments.
886 * @param bool $extract Extract arguments.
887 *
888 * @return string
889 */
890 function charitable_admin_get_html( $template_name, $args = array(), $extract = false ) {
891 ob_start();
892 charitable_admin_include_html( $template_name, $args, $extract );
893 return ob_get_clean();
894 }
895
896 /**
897 * Include a template - alias to \charitable\Helpers\Template::get_html.
898 * Use 'require' if $args are passed or 'load_template' if not.
899 *
900 * @since 1.7.0.3
901 *
902 * @param string $template_name Template name.
903 * @param array $args Arguments.
904 * @param bool $extract Extract arguments.
905 *
906 * @throws \RuntimeException If extract() tries to modify the scope.
907 *
908 * @return string Compiled HTML.
909 */
910 function charitable_render( $template_name, $args = array(), $extract = false ) {
911 return charitable_admin_get_html( $template_name, $args, $extract );
912 }
913
914 /**
915 * Determine if the plugin/addon installations are allowed.
916 *
917 * @since 1.6.2.3
918 *
919 * @param string $type Should be `plugin` or `addon`.
920 *
921 * @return bool
922 */
923 function charitable_can_install( $type ) {
924
925 return charitable_can_do( 'install', $type );
926 }
927
928 /**
929 * Determine if the plugin/addon activations are allowed.
930 *
931 * @since 1.7.3
932 *
933 * @param string $type Should be `plugin` or `addon`.
934 *
935 * @return bool
936 */
937 function charitable_can_activate( $type ) {
938
939 return charitable_can_do( 'activate', $type );
940 }
941
942 /**
943 * Determine if the plugin/addon installations/activations are allowed.
944 *
945 * @since 1.7.3
946 * @since 1.8.4.3 // Removed 'Addons require additional license checks' comments.
947 *
948 * @internal Use charitable_can_activate() or charitable_can_install() instead.
949 *
950 * @param string $what Should be 'activate' or 'install'.
951 * @param string $type Should be `plugin` or `addon`.
952 *
953 * @return bool
954 */
955 function charitable_can_do( $what, $type ) {
956
957 if ( ! in_array( $what, [ 'install', 'activate' ], true ) ) {
958 return false;
959 }
960
961 if ( ! in_array( $type, [ 'plugin', 'addon' ], true ) ) {
962 return false;
963 }
964
965 $capability = $what . '_plugins';
966
967 if ( ! current_user_can( $capability ) ) {
968 return false;
969 }
970
971 // Determine whether file modifications are allowed and it is activation permissions checking.
972 if ( $what === 'install' && ! wp_is_file_mod_allowed( 'charitable_can_install' ) ) {
973 return false;
974 }
975
976 // All plugin checks are done.
977 if ( $type === 'plugin' ) {
978 return true;
979 }
980
981 return true;
982 }
983
984 /**
985 * Perform test connection to verify that the current web host can successfully
986 * make outbound SSL connections.
987 *
988 * @since 1.4.5
989 */
990 function charitable_verify_ssl() {
991
992 // Run a security check.
993 check_ajax_referer( 'charitable-admin-tools', 'nonce' );
994
995 // Check for permissions.
996 if ( ! charitable_current_user_can( 'manage_options' ) ) {
997 wp_send_json_error(
998 array(
999 'msg' => esc_html__( 'You do not have permission to perform this operation.', 'charitable' ),
1000 )
1001 );
1002 }
1003
1004 $response = wp_remote_post( 'https://wpcharitable.com/connection-test.php' );
1005
1006 if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
1007 wp_send_json_success(
1008 array(
1009 'msg' => esc_html__( 'Success! Your server can make SSL connections.', 'charitable' ),
1010 )
1011 );
1012 }
1013
1014 wp_send_json_error(
1015 array(
1016 'msg' => esc_html__( 'There was an error and the connection failed. Please contact your web host with the technical details below.', 'charitable' ),
1017 'debug' => '<pre>' . print_r( map_deep( $response, 'wp_strip_all_tags' ), true ) . '</pre>', // phpcs:ignore
1018 )
1019 );
1020 }
1021 add_action( 'wp_ajax_charitable_verify_ssl', 'charitable_verify_ssl' );
1022
1023 /**
1024 * Deactivate addon.
1025 *
1026 * @since 1.0.0
1027 * @since 1.6.2.3 Updated the permissions checking.
1028 */
1029 function charitable_deactivate_addon() {
1030
1031 // Run a security check.
1032 check_ajax_referer( 'charitable-admin', 'nonce' );
1033
1034 // Check for permissions.
1035 if ( ! current_user_can( 'deactivate_plugins' ) ) {
1036 wp_send_json_error( esc_html__( 'Plugin deactivation is disabled for you on this site.', 'charitable' ) );
1037 }
1038
1039 $type = empty( $_POST['type'] ) ? 'addon' : sanitize_key( $_POST['type'] );
1040
1041 if ( isset( $_POST['plugin'] ) ) {
1042 $plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
1043
1044 if ( charitable_is_debug() ) {
1045 error_log( 'charitable_deactivate_addon' ); // phpcs:ignore
1046 error_log( print_r( $plugin, true ) ); // phpcs:ignore
1047 error_log( print_r( $_POST, true ) ); // phpcs:ignore
1048 error_log( print_r( $type, true ) ); // phpcs:ignore
1049 }
1050
1051 deactivate_plugins( $plugin );
1052
1053 do_action( 'charitable_plugin_deactivated', $plugin );
1054
1055 if ( $type === 'plugin' ) {
1056 wp_send_json_success( esc_html__( 'Plugin deactivated.', 'charitable' ) );
1057 } else {
1058 wp_send_json_success( esc_html__( 'Addon deactivated.', 'charitable' ) );
1059 }
1060 }
1061
1062 wp_send_json_error( esc_html__( 'Could not deactivate the addon. Please deactivate from the Plugins page.', 'charitable' ) );
1063 }
1064 add_action( 'wp_ajax_charitable_deactivate_addon', 'charitable_deactivate_addon' );
1065
1066 /**
1067 * Activate addon.
1068 *
1069 * @since 1.0.0
1070 * @since 1.6.2.3 Updated the permissions checking.
1071 */
1072 function charitable_ajax_activate_addon() {
1073
1074 // Run a security check.
1075 check_ajax_referer( 'charitable-admin', 'nonce' );
1076
1077 // Check for permissions.
1078 if ( ! current_user_can( 'activate_plugins' ) ) {
1079 wp_send_json_error( esc_html__( 'Plugin activation is disabled for you on this site.', 'charitable' ) );
1080 }
1081
1082 $type = 'addon';
1083
1084 if ( isset( $_POST['plugin'] ) ) {
1085
1086 if ( ! empty( $_POST['type'] ) ) {
1087 $type = sanitize_key( $_POST['type'] );
1088 }
1089
1090 $plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
1091
1092 $activate = activate_plugins( $plugin );
1093
1094 /**
1095 * Fire after plugin activating via the Charitable installer.
1096 *
1097 * @since 1.6.3.1
1098 *
1099 * @param string $plugin Path to the plugin file relative to the plugins directory.
1100 */
1101 do_action( 'charitable_plugin_activated', $plugin );
1102
1103 if ( ! is_wp_error( $activate ) ) {
1104 if ( $type === 'plugin' ) {
1105 wp_send_json_success( esc_html__( 'Plugin activated.', 'charitable' ) );
1106 } else {
1107 wp_send_json_success( esc_html__( 'Addon activated.', 'charitable' ) );
1108 }
1109 }
1110 }
1111
1112 if ( $type === 'plugin' ) {
1113 wp_send_json_error( esc_html__( 'Could not activate the plugin. Please activate it on the Plugins page.', 'charitable' ) );
1114 }
1115
1116 wp_send_json_error( esc_html__( 'Could not activate the addon. Please activate it on the Plugins page.', 'charitable' ) );
1117 }
1118 add_action( 'wp_ajax_charitable_activate_addon', 'charitable_ajax_activate_addon' );
1119
1120
1121 /**
1122 * Installs an Charitable addon.
1123 *
1124 * @since 1.0.0
1125 * @version 1.8.6.2
1126 */
1127 function charitable_ajax_install_addon() {
1128
1129 // Run a security check first.
1130 check_admin_referer( 'charitable-admin', 'nonce' );
1131
1132 // Permission check.
1133 if ( ! current_user_can( 'install_plugins' ) ) {
1134 wp_send_json_error( esc_html__( 'Plugin install is disabled for you on this site.', 'charitable' ) );
1135 }
1136
1137 // Install the addon.
1138 if ( isset( $_POST['plugin'] ) ) {
1139 $download_url = esc_url_raw( wp_unslash( $_POST['plugin'] ) );
1140 global $hook_suffix;
1141
1142 // If the url doesn't start with 'https://www.wpcharitable.com/', then return an error.
1143 if ( strpos( $download_url, 'https://www.wpcharitable.com/' ) !== 0 ) {
1144 wp_send_json_error(
1145 array(
1146 'basename' => $download_url,
1147 'is_activated' => false,
1148 'msg' => esc_html__( 'Addon not installed. Invalid download URL.', 'charitable' ),
1149 )
1150 );
1151 }
1152
1153 if ( defined( 'CHARITABLE_DEBUG' ) && CHARITABLE_DEBUG ) {
1154 if ( charitable_is_debug() ) {
1155 error_log( 'charitable_ajax_install_addon' ); // phpcs:ignore
1156 error_log( print_r( $_POST, true ) ); // phpcs:ignore
1157 error_log( print_r( $download_url, true ) ); // phpcs:ignore
1158 }
1159 }
1160
1161 // Set the current screen to avoid undefined notices.
1162 set_current_screen();
1163
1164 // Prepare variables.
1165 $method = '';
1166 $url = add_query_arg(
1167 array(
1168 'page' => 'charitable-settings',
1169 ),
1170 admin_url( 'admin.php' )
1171 );
1172 $url = esc_url( $url );
1173
1174 // Start output bufferring to catch the filesystem form if credentials are needed.
1175 ob_start();
1176 $creds = request_filesystem_credentials( $url, $method, false, false, null );
1177 if ( false === $creds ) {
1178 $form = ob_get_clean();
1179 echo wp_json_encode( array( 'form' => $form ) );
1180 die;
1181 }
1182
1183 if ( charitable_is_debug() ) {
1184 error_log( 'charitable_ajax_install_addon creds' ); // phpcs:ignore
1185 error_log( print_r( $creds, true ) ); // phpcs:ignore
1186 }
1187
1188 // If we are not authenticated, make it happen now.
1189 if ( ! WP_Filesystem( $creds ) ) {
1190 ob_start();
1191 request_filesystem_credentials( $url, $method, true, false, null );
1192 $form = ob_get_clean();
1193 echo wp_json_encode( array( 'form' => $form ) );
1194 die;
1195 }
1196
1197 // Do not allow WordPress to search/download translations, as this will break JS output.
1198 remove_action( 'upgrader_process_complete', [ 'Language_Pack_Upgrader', 'async_upgrade' ], 20 );
1199
1200 // We do not need any extra credentials if we have gotten this far, so let's install the plugin.
1201 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
1202 require_once plugin_dir_path( CHARITABLE_DIRECTORY_PATH ) . 'charitable/includes/utilities/Skin.php';
1203
1204 // Create the plugin upgrader with our custom skin.
1205 $skin = new Charitable_Skin();
1206 $installer = new Plugin_Upgrader( $skin );
1207 $installer->install( $download_url );
1208
1209 // Flush the cache and return the newly installed plugin basename.
1210 wp_cache_flush();
1211
1212 if ( $installer->plugin_info() ) {
1213 $plugin_basename = $installer->plugin_info();
1214 $plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
1215 // attempt to activate the installed addon, save the user a step.
1216 $activate = activate_plugins( $plugin_basename );
1217 if ( ! is_wp_error( $activate ) ) {
1218 $response = array(
1219 'basename' => $plugin_basename,
1220 'is_activated' => true,
1221 'msg' => esc_html__( 'Addon installed and activated.', 'charitable' ),
1222 );
1223
1224 // When Charitable Pro is what was just installed and activated, point
1225 // the user at the Charitable dashboard. Activating Pro auto-deactivates
1226 // Lite, which means the addons screen they're currently on (served by
1227 // Lite) no longer exists in the menu — without a redirect they'd land
1228 // on a 404 / blank admin page once the AJAX returns. Pro's main file
1229 // lives at charitable-pro/charitable.php, so we match by directory.
1230 if ( 0 === strpos( $plugin_basename, 'charitable-pro/' ) ) {
1231 $response['redirect'] = admin_url( 'admin.php?page=charitable-dashboard' );
1232 }
1233
1234 wp_send_json_success( $response );
1235 } else {
1236 wp_send_json_success(
1237 array(
1238 'basename' => $plugin_basename,
1239 'msg' => esc_html__( 'Addon installed.', 'charitable' ),
1240 )
1241 );
1242 }
1243
1244 die;
1245 }
1246 }
1247
1248 // Send back a response.
1249 echo wp_json_encode( true );
1250 die;
1251 }
1252 add_action( 'wp_ajax_charitable_install_addon', 'charitable_ajax_install_addon' );
1253
1254 /**
1255 * Keep an option updated to know what/when a warning went out for a third party plugin, theme, etc.
1256 *
1257 * @since 1.7.0.8
1258 */
1259 function charitable_update_third_party_warning_option() {
1260
1261 $third_party_warnings = get_option( 'charitable_third_party_warnings' );
1262 $updated = false;
1263
1264 // If the option is empty or wiped (which is possible), initialize it with an empty array.
1265 $third_party_warnings = ( false === $third_party_warnings ) ? array( 'plugins' => array() ) : $third_party_warnings = json_decode( $third_party_warnings, ARRAY_A );
1266
1267 if ( is_plugin_active( 'code-snippets/code-snippets.php' ) && ! array_key_exists( 'code-snippets/code-snippets.php', $third_party_warnings['plugins'] ) ) {
1268 $third_party_warnings['plugins']['code-snippets/code-snippets.php'] = 'noted';
1269 $updated = true;
1270 } elseif ( ! is_plugin_active( 'code-snippets/code-snippets.php' ) && array_key_exists( 'code-snippets/code-snippets.php', $third_party_warnings['plugins'] ) && $third_party_warnings['plugins']['code-snippets/code-snippets.php'] === 'noted' ) {
1271 unset( $third_party_warnings['plugins']['code-snippets/code-snippets.php'] );
1272 $updated = true;
1273 }
1274
1275 $third_party_warnings = apply_filters( 'charitable_update_third_party_warning_option', $third_party_warnings );
1276
1277 if ( $updated ) {
1278 $result = update_option( 'charitable_third_party_warnings', wp_json_encode( $third_party_warnings ) );
1279 }
1280 }
1281 add_action( 'admin_init', 'charitable_update_third_party_warning_option' );
1282
1283 /**
1284 * Get an option related to third party warning ( null, noted, dismissed )
1285 *
1286 * @since 1.7.0.8
1287 *
1288 * @param string $plugin_path Plugin path.
1289 * @param string $category Category.
1290 */
1291 function charitable_get_third_party_warning_option( $plugin_path = false, $category = 'plugins' ) {
1292
1293 if ( false === $plugin_path ) {
1294 return false;
1295 }
1296
1297 $third_party_warnings = get_option( 'charitable_third_party_warnings' );
1298
1299 if ( false === $third_party_warnings ) {
1300 return false;
1301 }
1302
1303 $third_party_warnings = json_decode( $third_party_warnings, ARRAY_A );
1304
1305 if ( ! isset( $third_party_warnings[ $category ][ $plugin_path ] ) ) {
1306 return false;
1307 }
1308
1309 return esc_html( $third_party_warnings[ $category ][ $plugin_path ] );
1310 }
1311
1312 /**
1313 * Get an option related to third party warning ( null, noted, dismissed )
1314 *
1315 * @since 1.7.0.8
1316 *
1317 * @param string $plugin_path Plugin path.
1318 * @param string $value Value.
1319 * @param string $category Category.
1320 */
1321 function charitable_set_third_party_warning_option( $plugin_path = false, $value = false, $category = 'plugins' ) {
1322
1323 if ( false === $plugin_path ) {
1324 return;
1325 }
1326
1327 $third_party_warnings = get_option( 'charitable_third_party_warnings' );
1328
1329 if ( false === $third_party_warnings ) {
1330 return;
1331 }
1332
1333 $third_party_warnings = json_decode( $third_party_warnings, ARRAY_A );
1334
1335 $third_party_warnings[ $category ][ $plugin_path ] = $value;
1336
1337 $result = update_option( 'charitable_third_party_warnings', wp_json_encode( $third_party_warnings ) );
1338
1339 return $result;
1340 }
1341
1342 /**
1343 * Return the latest versions of Charitable plugins.
1344 *
1345 * @since 1.8.0
1346 *
1347 * @param array $licenses Licenses.
1348 * @param string $update_url Update URL.
1349 *
1350 * @return array
1351 */
1352 function charitable_get_addons_data_from_server( $licenses = false, $update_url = 'https://wpcharitable.com' ) {
1353
1354 $versions = get_transient( '_charitable_plugin_versions' );
1355
1356 if ( false === $versions ) {
1357
1358 if ( get_transient( '_charitable_plugin_versions_failed' ) ) {
1359 return false;
1360 }
1361
1362 if ( false === $licenses ) {
1363
1364 $licenses = array();
1365
1366 foreach ( charitable_get_licenses() as $license ) {
1367 if ( isset( $license['license'] ) ) {
1368 $licenses[] = $license['license'];
1369 }
1370 }
1371 }
1372
1373 /*
1374 * No licence means the response cannot be acted on: `install` is a licence-keyed download URL
1375 * and `license` is plan-tier data used only to pick an upgrade prompt. Serve the static
1376 * catalogue instead, which costs no licence key and no home_url() on the wire.
1377 */
1378 if ( empty( $licenses ) ) {
1379 return Charitable_Addons_Catalogue::get_instance()->get();
1380 }
1381
1382 $response = wp_remote_post(
1383 $update_url . '/edd-api/versions-v3/',
1384 array(
1385 'sslverify' => false,
1386 'timeout' => 15,
1387 'body' => array(
1388 'licenses' => $licenses,
1389 'url' => home_url(),
1390 ),
1391 )
1392 );
1393
1394 $response_body = wp_remote_retrieve_body( $response );
1395 $response_code = wp_remote_retrieve_response_code( $response );
1396
1397 // Bail out early if there are any errors. Cached briefly, because the campaign builder calls
1398 // this once per payment and marketing panel and a failure sets no transient to short-circuit
1399 // them: at a 15 second timeout an outage meant ~30 serial requests on one page load.
1400 if ( (int) $response_code !== 200 || is_wp_error( $response_body ) ) {
1401 set_transient( '_charitable_plugin_versions_failed', 1, 15 * MINUTE_IN_SECONDS );
1402 return false;
1403 }
1404
1405 $versions = json_decode( $response_body, true );
1406
1407 set_transient( '_charitable_plugin_versions', $versions, DAY_IN_SECONDS );
1408 delete_transient( '_charitable_plugin_versions_failed' );
1409
1410 } // end if
1411
1412 return $versions;
1413 }
1414
1415 if ( ! function_exists( 'charitable_get_licenses' ) ) {
1416 /**
1417 * Return the list of licenses.
1418 *
1419 * Note: The licenses are not necessarily valid. If a user enters an invalid
1420 * license, the license will be stored but it will be flagged as invalid.
1421 *
1422 * @since 1.8.0
1423 *
1424 * @return array[]
1425 */
1426 function charitable_get_licenses() {
1427 return charitable_get_option( 'licenses', array() );
1428 }
1429 }
1430
1431 /**
1432 * Get license label from plan id.
1433 *
1434 * @since 1.8.0
1435 *
1436 * @param boolean $plan_id Plan ID.
1437 * @return string
1438 */
1439 function charitable_get_license_label_from_plan_id( $plan_id = false ) {
1440
1441 if ( ! $plan_id ) {
1442 $settings = get_option( 'charitable_settings' );
1443 $plan_id = ! empty( $settings['licenses']['charitable-v2']['plan_id'] ) ? intval( $settings['licenses']['charitable-v2']['plan_id'] ) : false;
1444 }
1445
1446 if ( ! $plan_id ) {
1447 $plan_name = 'Lite';
1448 }
1449
1450 switch ( $plan_id ) {
1451 case 1:
1452 $plan_name = 'Basic';
1453 break;
1454 case 2:
1455 $plan_name = 'Plus';
1456 break;
1457 case 3:
1458 $plan_name = 'Pro';
1459 break;
1460 case 4:
1461 $plan_name = 'Elite';
1462 break;
1463 default:
1464 $plan_name = 'Lite';
1465 break;
1466 }
1467
1468 return $plan_name;
1469 }
1470
1471 /**
1472 * Get license label from plan id.
1473 *
1474 * @since 1.8.0
1475 *
1476 * @param boolean $plan_id Plan ID.
1477 * @return string
1478 */
1479 function charitable_get_license_slug_from_plan_id( $plan_id = false ) {
1480
1481 return sanitize_title( charitable_get_license_label_from_plan_id( $plan_id ) );
1482 }
1483
1484
1485 /**
1486 * Hide non-Charitable warnings.
1487 *
1488 * @since 1.8.1.5
1489 * @version 1.8.8.6
1490 *
1491 * @return void
1492 */
1493 function hide_non_charitable_warnings() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Function name is hooked via add_action and changing it would break existing functionality.
1494
1495 // Bail if we're not on a charitable screen (another type of check).
1496 if ( false === charitable_is_admin_screen() ) {
1497 return;
1498 }
1499
1500 charitable_filter_admin_notices( 'user_admin_notices', 'charitable' );
1501 charitable_filter_admin_notices( 'admin_notices', 'charitable' );
1502 charitable_filter_admin_notices( 'all_admin_notices', 'charitable' );
1503 }
1504
1505 /**
1506 * Filter notices.
1507 *
1508 * @since 1.8.1.5
1509 *
1510 * @param string $notice_type Notice type.
1511 * @param string $prefix Prefix.
1512 *
1513 * @return void
1514 */
1515 function charitable_filter_admin_notices( $notice_type, $prefix ) {
1516 global $wp_filter;
1517 if ( ! empty( $wp_filter[ $notice_type ]->callbacks ) && is_array( $wp_filter[ $notice_type ]->callbacks ) ) {
1518 foreach ( $wp_filter[ $notice_type ]->callbacks as $priority => $hooks ) {
1519 foreach ( $hooks as $name => $arr ) {
1520 if ( is_object( $arr['function'] ) && $arr['function'] instanceof Closure ) {
1521 unset( $wp_filter[ $notice_type ]->callbacks[ $priority ][ $name ] );
1522 continue;
1523 }
1524 if ( ! empty( $arr['function'][0] ) && is_object( $arr['function'][0] ) && false !== strpos( strtolower( get_class( $arr['function'][0] ) ), $prefix ) ) {
1525 continue;
1526 }
1527 if ( ! empty( $name ) && false === strpos( $name, $prefix ) ) {
1528 unset( $wp_filter[ $notice_type ]->callbacks[ $priority ][ $name ] );
1529 }
1530 }
1531 }
1532 }
1533 }
1534
1535 add_action( 'admin_print_scripts', 'hide_non_charitable_warnings' );
1536 add_action( 'admin_head', 'hide_non_charitable_warnings', PHP_INT_MAX );
1537
1538
1539 /**
1540 * Remove a dashboard notification via AJAX.
1541 *
1542 * @since 1.8.2
1543 *
1544 * @return void
1545 */
1546 function charitable_disable_dashboard_notification_ajax() {
1547
1548 // Run a security check.
1549 check_ajax_referer( 'charitable-admin', 'nonce' );
1550
1551 $notification_id = isset( $_POST['notification_id'] ) ? sanitize_text_field( wp_unslash( $_POST['notification_id'] ) ) : false;
1552
1553 if ( ! $notification_id ) {
1554 wp_send_json_error( array( 'message' => esc_html__( 'Invalid notification ID.', 'charitable' ) ) );
1555 }
1556
1557 $notifications = (array) get_option( 'charitable_dashboard_notifications', array() );
1558
1559 if ( empty( $notifications ) ) {
1560 wp_send_json_error( array( 'message' => esc_html__( 'No notifications found.', 'charitable' ) ) );
1561 }
1562
1563 if ( ! empty( $notifications[ $notification_id ] ) ) {
1564 // add a 'dismissed' key to the notification with the current time.
1565 $notifications[ $notification_id ]['dismissed'] = time();
1566 update_option( 'charitable_dashboard_notifications', $notifications );
1567
1568 /**
1569 * Fires after a dashboard rail notification has been dismissed.
1570 *
1571 * Listeners can attach side effects keyed off the dismissed slug —
1572 * e.g. the resume-setup-wizard module clears
1573 * `charitable_started_onboarding` when its banner is dismissed,
1574 * so the underlying onboarding state is also turned off, not just
1575 * the banner.
1576 *
1577 * @since 1.8.10.5
1578 *
1579 * @param string $notification_id Slug of the dismissed notification.
1580 */
1581 do_action( 'charitable_dashboard_notification_dismissed', $notification_id );
1582
1583 wp_send_json_success( array( 'message' => esc_html__( 'Notification removed.', 'charitable' ) ) );
1584 } else {
1585 wp_send_json_error( array( 'message' => esc_html__( 'Notification not found.', 'charitable' ) ) );
1586 }
1587 }
1588
1589 add_action( 'wp_ajax_charitable_disable_dashboard_notification', 'charitable_disable_dashboard_notification_ajax' );
1590
1591 /**
1592 * Get a checkbox wrapped with markup to be displayed as a toggle.
1593 *
1594 * @since 1.8.2
1595 *
1596 * @param bool $checked Is it checked or not.
1597 * @param string $name The name for the input.
1598 * @param string $description Field description (optional).
1599 * @param string|int $value Field value (optional).
1600 * @param string $label Field label (optional).
1601 *
1602 * @return string
1603 */
1604 function charitable_get_checkbox_toggle( $checked, $name, $description = '', $value = '', $label = '' ) {
1605 $markup = '<label class="wpcode-checkbox-toggle">';
1606
1607 $markup .= '<input type="checkbox" ' . checked( $checked, true, false ) . ' name="' . esc_attr( $name ) . '" id="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '" />';
1608 $markup .= '<span class="wpcode-checkbox-toggle-slider"></span>';
1609 $markup .= '</label>';
1610 if ( ! empty( $label ) ) {
1611 $markup .= '<label class="wpcode-checkbox-toggle-label" for="' . esc_attr( $name ) . '">' . esc_html( $label ) . '</label>';
1612 }
1613
1614 if ( ! empty( $description ) ) {
1615 $markup .= '<p class="description">' . wp_kses_post( $description ) . '</p>';
1616 }
1617
1618 return $markup;
1619 }
1620
1621 /**
1622 * Gets the unique site ID or token.
1623 * This is generated from the home URL and two random pieces of data
1624 * to create a hashed site ID that can anonymize data.
1625 *
1626 * @since 1.8.4
1627 *
1628 * @return string
1629 */
1630 function charitable_get_site_token() {
1631 $wpchar_site_token = get_option( 'wpchar_telemetry_uuid' );
1632 if ( empty( $wpchar_site_token ) ) {
1633 $home_url = get_home_url();
1634 $uuid = wp_generate_uuid4();
1635 $today = gmdate( 'now' );
1636 $wpchar_site_token = md5( $home_url . $uuid . $today );
1637 update_option( 'wpchar_telemetry_uuid', $wpchar_site_token, false );
1638 }
1639 return $wpchar_site_token;
1640 }
1641
1642 if ( ! function_exists( 'charitable_onboarding_app_url' ) ) :
1643
1644 /**
1645 * Base URL of the centrally hosted onboarding wizard, without a trailing slash.
1646 *
1647 * Filterable so a development site can be pointed at a local instance of the
1648 * wizard. The wizard has no staging environment, so without this the only way
1649 * to exercise the hand-off between it and a site is against production.
1650 *
1651 * add_filter( 'charitable_onboarding_app_url', function () {
1652 * return 'http://127.0.0.1:8131';
1653 * } );
1654 *
1655 * Unfiltered the return value is unchanged from the previously hardcoded
1656 * literal, so this is a no-op in production.
1657 *
1658 * @since 1.8.12.4
1659 *
1660 * @return string
1661 */
1662 function charitable_onboarding_app_url() {
1663 return untrailingslashit( apply_filters( 'charitable_onboarding_app_url', 'https://app.wpcharitable.com' ) );
1664 }
1665
1666 endif;
1667
1668 if ( ! function_exists( 'charitable_get_onboarding_url' ) ) :
1669
1670 /**
1671 * Get the URL for the first screen of the onboarding process.
1672 *
1673 * @since 1.8.4
1674 *
1675 * @return string
1676 */
1677 function charitable_get_onboarding_url() {
1678
1679 $current_site_url = get_site_url();
1680
1681 return add_query_arg(
1682 array(
1683 'token' => charitable_get_site_token(),
1684 'version' => charitable()->get_version(),
1685 'utm_campaign' => 'onboarding_charitable_lite',
1686 'email' => get_option( 'admin_email' ),
1687 // generate a random session id to prevent caching.
1688 'sessionid' => wp_rand( 10000000, 99999999 ),
1689 'return' => rawurlencode(
1690 base64_encode( get_admin_url( null, 'admin.php') ) // phpcs:ignore
1691 ),
1692 'update_to_pro_url' => 'https://app.charitable.com/upgrade-free-to-pro?api_token=REPLACE_API_TOKEN&license_key=REPLACE_LICENSE_KEY&oth=11ecbadab9561202d33b5ffb8405f9cb9b783af17b52c4b16e16bd8fbbd6cdccbd2a5445c2cb456cb11cdd555471c19e5e2ad446450df2f4e0fc70e410a814d4&endpoint=&siteurl=' . $current_site_url . '/wp-admin/',
1693 ),
1694 charitable_onboarding_app_url() . '/setup-wizard-charitable_lite'
1695 );
1696 }
1697
1698 endif;
1699
1700 if ( ! function_exists( 'charitable_get_usage_tracking_setting' ) ) :
1701
1702 /**
1703 * Get the usage tracking setting.
1704 *
1705 * @since 1.8.4
1706 *
1707 * @return string
1708 */
1709 function charitable_get_usage_tracking_setting() {
1710 return (int) get_option( 'charitable_usage_tracking', false );
1711 }
1712
1713 endif;
1714
1715 if ( ! function_exists( 'charitable_update_usage_tracking_setting' ) ) :
1716
1717 /**
1718 * Update the usage tracking setting.
1719 *
1720 * @since 1.8.4
1721 *
1722 * @param int $value The new value (0 or 1).
1723 */
1724 function charitable_update_usage_tracking_setting( $value ) {
1725
1726 // update options.
1727 update_option( 'charitable_usage_tracking', $value );
1728
1729 // update Charitable settings to match.
1730 $settings = get_option( 'charitable_settings' );
1731 $settings['charitable_usage_tracking'] = $value ? true : false;
1732 update_option( 'charitable_settings', $settings );
1733 }
1734
1735 endif;
1736
1737 /**
1738 * Santitize setting labels to allow only <div> and <span> tags and their respective attributes.
1739 *
1740 * @since 1.8.4
1741 *
1742 * @param mixed $content The content to sanitize.
1743 *
1744 * @return string
1745 */
1746 function charitable_santitize_setting_labels( $content ) {
1747 $allowed_tags = array(
1748 'div' => array(
1749 'class' => array(),
1750 'id' => array(),
1751 ),
1752 'span' => array(
1753 'class' => array(),
1754 'id' => array(),
1755 ),
1756 );
1757
1758 return wp_kses( $content, $allowed_tags );
1759 }
1760
1761 /**
1762 * Check if the pro plugin is installed.
1763 *
1764 * @since 1.8.5.3
1765 *
1766 * @return bool
1767 */
1768 function charitable_is_pro_installed() {
1769 $pro_plugin_path = 'charitable-pro/charitable.php';
1770 if ( ! file_exists( WP_PLUGIN_DIR . '/' . $pro_plugin_path ) ) {
1771 return false;
1772 }
1773
1774 return true;
1775 }
1776
1777 /**
1778 * Display admin bar when active.
1779 *
1780 * @since 1.8.7.5
1781 * @version 1.8.9.1
1782 *
1783 * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar instance, passed by reference.
1784 *
1785 * @return bool
1786 */
1787 function charitable_show_test_mode_notice_in_admin_bar( $wp_admin_bar ) {
1788 $is_test_mode = charitable_get_option( 'test_mode' ) ? true : false;
1789
1790 // Allow filtering to disable the admin bar notice
1791 $show_notice = apply_filters( 'charitable_show_test_mode_admin_bar_notice', true );
1792
1793 if (
1794 ! current_user_can( 'manage_options' ) ||
1795 ! $is_test_mode ||
1796 ! $show_notice
1797 ) {
1798 return false;
1799 }
1800
1801 // Add the main site admin menu item.
1802 $wp_admin_bar->add_menu(
1803 [
1804 'id' => 'charitable-test-notice',
1805 'href' => admin_url( 'admin.php?page=charitable-settings&tab=gateways' ),
1806 'parent' => 'top-secondary',
1807 'title' => '<span class="charitable-test-mode-badge">' . esc_html__( 'Charitable Test Mode Active', 'charitable' ) . '</span>',
1808 'meta' => [
1809 'class' => 'charitable-test-mode-active',
1810 'onclick' => 'sessionStorage.setItem("charitable_from_admin_bar", "true");',
1811 ],
1812 ]
1813 );
1814
1815 return true;
1816 }
1817
1818 add_action( 'admin_bar_menu', 'charitable_show_test_mode_notice_in_admin_bar', 1000, 1 );
1819
1820 /**
1821 * Render the global upgrade CTA section.
1822 *
1823 * This function provides a consistent upgrade CTA that can be used across all admin pages.
1824 * It uses the same styling and content as the dashboard v2 upgrade section.
1825 *
1826 * @since 1.8.8
1827 *
1828 * @param string $css_class Optional. Additional CSS classes to add to the section.
1829 * @param bool $echo Optional. Whether to echo the output or return it. Default is true.
1830 * @return string|void The HTML output if $echo is false, otherwise void.
1831 */
1832 function charitable_render_global_upgrade_cta( $css_class = '', $echo = true ) {
1833 // Don't show upgrade CTA if user already has Pro license
1834 if ( charitable_is_pro() ) {
1835 return $echo ? '' : '';
1836 }
1837
1838 $output = '';
1839
1840 $output .= '<section class="charitable-dashboard-v2-section charitable-global-upgrade-cta ' . esc_attr( $css_class ) . '">';
1841 $output .= '<h4 class="charitable-dashboard-v2-upgrade-title">' . esc_html__( 'Upgrade to Pro to Unlock Powerful Donation Features', 'charitable' ) . '</h4>';
1842 $output .= '<div class="charitable-dashboard-v2-upgrade-features">';
1843 $output .= '<div class="charitable-dashboard-v2-upgrade-column">';
1844 $output .= charitable_render_global_upgrade_features_left();
1845 $output .= '</div>';
1846 $output .= '<div class="charitable-dashboard-v2-upgrade-column">';
1847 $output .= charitable_render_global_upgrade_features_right();
1848 $output .= '</div>';
1849 $output .= '</div>';
1850 $output .= '<div class="charitable-dashboard-v2-upgrade-actions">';
1851 $output .= '<a href="' . esc_url( charitable_utm_link( 'https://wpcharitable.com/lite-vs-pro/', 'Reports Upgrade Section', 'Upgrade To Pro Button' ) ) . '" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-upgrade-button">' . esc_html__( 'Upgrade To Pro', 'charitable' ) . '</a>';
1852 $output .= '<a href="' . esc_url( charitable_utm_link( 'https://wpcharitable.com/lite-vs-pro/', 'Reports Upgrade Section', 'Learn More About Features Link' ) ) . '" target="_blank" rel="noopener noreferrer" class="charitable-dashboard-v2-learn-more-link">' . esc_html__( 'Learn more about all features →', 'charitable' ) . '</a>';
1853 $output .= '</div>';
1854 $output .= '</section>';
1855
1856 if ( $echo ) {
1857 echo $output; // phpcs:ignore
1858 } else {
1859 return $output;
1860 }
1861 }
1862
1863 /**
1864 * Render left column upgrade features for global CTA.
1865 *
1866 * @since 1.8.8
1867 *
1868 * @return string The HTML output for left column features.
1869 */
1870 function charitable_render_global_upgrade_features_left() {
1871 $features = array(
1872 __( 'Recurring Donations', 'charitable' ),
1873 __( 'Peer-to-Peer Fundraising', 'charitable' ),
1874 __( 'Donor Database', 'charitable' ),
1875 __( 'Donor Dashboard', 'charitable' ),
1876 __( 'PDF & Annual Receipts', 'charitable' )
1877 );
1878
1879 $output = '';
1880 foreach ( $features as $feature ) {
1881 $output .= '<div class="charitable-dashboard-v2-upgrade-feature">';
1882 $output .= '<svg class="charitable-dashboard-v2-upgrade-checkmark" width="18" height="18" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">';
1883 $output .= '<path fill-rule="evenodd" clip-rule="evenodd" d="M9.0688 1.3573C13.798 1.3573 17.6374 5.19676 17.6374 9.92591C17.6374 14.6551 13.798 18.4945 9.0688 18.4945C4.33966 18.4945 0.500198 14.6551 0.500198 9.92591C0.500198 5.19676 4.33966 1.3573 9.0688 1.3573ZM13.5778 7.82078C13.5778 7.67488 13.5197 7.52981 13.4146 7.42562L12.6234 6.63444C12.5192 6.52938 12.3733 6.47118 12.2283 6.47118C12.0824 6.47118 11.9373 6.52938 11.8322 6.63444L8.01537 10.4548L6.30536 8.7396C6.20026 8.6345 6.05523 8.57631 5.90933 8.57631C5.7643 8.57631 5.6184 8.6345 5.51421 8.7396L4.72303 9.53075C4.61793 9.63498 4.55978 9.78088 4.55978 9.92591C4.55978 10.0709 4.61793 10.2168 4.72303 10.3211L7.62021 13.2174C7.72444 13.3225 7.87034 13.3806 8.01537 13.3806C8.16127 13.3806 8.3063 13.3225 8.4114 13.2174L13.4146 8.2159C13.5197 8.11171 13.5778 7.96581 13.5778 7.82078Z" fill="#59A56D"/>';
1884 $output .= '</svg>';
1885 $output .= '<span class="charitable-dashboard-v2-upgrade-feature-text">' . esc_html( $feature ) . '</span>';
1886 $output .= '</div>';
1887 }
1888
1889 return $output;
1890 }
1891
1892 /**
1893 * Render right column upgrade features for global CTA.
1894 *
1895 * @since 1.8.8
1896 *
1897 * @return string The HTML output for right column features.
1898 */
1899 function charitable_render_global_upgrade_features_right() {
1900 $features = array(
1901 __( 'Anonymous Giving', 'charitable' ),
1902 __( 'Fee Coverage Option', 'charitable' ),
1903 __( 'Email Marketing Integration', 'charitable' ),
1904 __( 'Built-In Analytics & Reports', 'charitable' ),
1905 __( 'Flexible Payment Methods', 'charitable' )
1906 );
1907
1908 $output = '';
1909 foreach ( $features as $feature ) {
1910 $output .= '<div class="charitable-dashboard-v2-upgrade-feature">';
1911 $output .= '<svg class="charitable-dashboard-v2-upgrade-checkmark" width="18" height="18" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">';
1912 $output .= '<path fill-rule="evenodd" clip-rule="evenodd" d="M9.0688 1.3573C13.798 1.3573 17.6374 5.19676 17.6374 9.92591C17.6374 14.6551 13.798 18.4945 9.0688 18.4945C4.33966 18.4945 0.500198 14.6551 0.500198 9.92591C0.500198 5.19676 4.33966 1.3573 9.0688 1.3573ZM13.5778 7.82078C13.5778 7.67488 13.5197 7.52981 13.4146 7.42562L12.6234 6.63444C12.5192 6.52938 12.3733 6.47118 12.2283 6.47118C12.0824 6.47118 11.9373 6.52938 11.8322 6.63444L8.01537 10.4548L6.30536 8.7396C6.20026 8.6345 6.05523 8.57631 5.90933 8.57631C5.7643 8.57631 5.6184 8.6345 5.51421 8.7396L4.72303 9.53075C4.61793 9.63498 4.55978 9.78088 4.55978 9.92591C4.55978 10.0709 4.61793 10.2168 4.72303 10.3211L7.62021 13.2174C7.72444 13.3225 7.87034 13.3806 8.01537 13.3806C8.16127 13.3806 8.3063 13.3225 8.4114 13.2174L13.4146 8.2159C13.5197 8.11171 13.5778 7.96581 13.5778 7.82078Z" fill="#59A56D"/>';
1913 $output .= '</svg>';
1914 $output .= '<span class="charitable-dashboard-v2-upgrade-feature-text">' . esc_html( $feature ) . '</span>';
1915 $output .= '</div>';
1916 }
1917
1918 return $output;
1919 }