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