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 +1000 -93 1.7.0.9 → 1.8.13 View file →
@@ -3,12 +3,12 @@
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 David Bisset
10 - * @copyright Copyright (c) 2022, WP Charitable LLC
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.
@@ -26,8 +26,10 @@
26 26 * @since 1.0.0
27 27 *
28 28 * @param string $view The view to display.
29 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 + *
30 32 * @return boolean True if the view exists and was rendered. False otherwise.
31 33 */
32 34 function charitable_admin_view( $view, $view_args = array(), $return_html = false ) {
33 35 $base_path = array_key_exists( 'base_path', $view_args ) ? $view_args['base_path'] : charitable()->get_path( 'admin' ) . 'views/';
@@ -58,9 +60,9 @@
58 60 }
59 61
60 62 ob_start();
61 63
62 - include( $filename );
64 + include $filename;
63 65
64 66 if ( $return_html ) {
65 67 $output = ob_get_contents();
66 68 ob_end_clean();
@@ -83,8 +85,63 @@
83 85 return Charitable_Settings::get_instance();
84 86 }
85 87
86 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 +/**
87 144 * Returns the Charitable_Admin_Notices helper.
88 145 *
89 146 * @since 1.4.6
90 147 *
@@ -102,19 +159,19 @@
102 159 * @param string $tab Optional. If passed, the function will also check that we are on the given tab.
103 160 * @return boolean
104 161 */
105 162 function charitable_is_settings_view( $tab = '' ) {
106 - if ( ! empty( $_POST ) ) {
107 - $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
108 165
109 166 if ( ! $is_settings || empty( $tab ) ) {
110 167 return $is_settings;
111 168 }
112 169
113 - 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
114 171 }
115 172
116 - $is_settings = isset( $_GET['page'] ) && 'charitable-settings' == $_GET['page'];
173 + $is_settings = isset( $_GET['page'] ) && 'charitable-settings' == $_GET['page']; // phpcs:ignore
117 174
118 175 if ( ! $is_settings || empty( $tab ) ) {
119 176 return $is_settings;
120 177 }
@@ -120,12 +177,12 @@
120 177 }
121 178
122 179 /* The general tab can be loaded when tab is not set. */
123 180 if ( 'general' == $tab ) {
124 - return ! isset( $_GET['tab'] ) || 'general' == $_GET['tab'];
181 + return ! isset( $_GET['tab'] ) || 'general' == $_GET['tab']; // phpcs:ignore
125 182 }
126 183
127 - return isset( $_GET['tab'] ) && $tab == $_GET['tab'];
184 + return isset( $_GET['tab'] ) && $tab == $_GET['tab']; // phpcs:ignore
128 185 }
129 186
130 187 /**
131 188 * Print out the settings fields for a particular settings section.
@@ -138,10 +195,10 @@
138 195 * @since 1.0.0
139 196 *
140 197 * @global $wp_settings_fields Storage array of settings fields and their pages/sections
141 198 *
142 - * @param string $page Slug title of the admin page who's settings fields you want to show.
143 - * @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.
144 201 * @return string
145 202 */
146 203 function charitable_do_settings_fields( $page, $section ) {
147 204 global $wp_settings_fields;
@@ -150,8 +207,9 @@
150 207 return;
151 208 }
152 209
153 210 foreach ( (array) $wp_settings_fields[ $page ][ $section ] as $field ) {
211 +
154 212 $class = '';
155 213
156 214 if ( ! empty( $field['args']['class'] ) ) {
157 215 $class = ' class="' . esc_attr( $field['args']['class'] ) . '"';
@@ -156,21 +214,21 @@
156 214 if ( ! empty( $field['args']['class'] ) ) {
157 215 $class = ' class="' . esc_attr( $field['args']['class'] ) . '"';
158 216 }
159 217
160 - echo "<tr{$class}>";
218 + echo "<tr{$class}>"; // phpcs:ignore
161 219
162 220 if ( ! empty( $field['args']['label_for'] ) ) {
163 - 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
164 222 echo '<td>';
165 223 call_user_func( $field['callback'], $field['args'] );
166 224 echo '</td>';
167 225 } elseif ( ! empty( $field['title'] ) ) {
168 - if ( $field['args']['type'] === 'heading' && isset( $field['args']['help'] ) ) { // if this is a heading display a "help" as a subheading
169 - echo '<th scope="row" colspan="2"><h4>' . $field['title'] . '</h4>';
170 - echo '<p>' . $field['args']['help'] . '</p>';
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
171 229 } else {
172 - echo '<th scope="row"><h4>' . $field['title'] . '</h4>';
230 + echo '<th scope="row"><h4>' . charitable_santitize_setting_labels( $field['title'] ) . '</h4>'; // phpcs:ignore
173 231 }
174 232 echo '</th>';
175 233 if ( $field['args']['type'] !== 'heading' ) {
176 234 echo '<td>';
@@ -191,12 +249,12 @@
191 249 * Add new tab to the Charitable settings area.
192 250 *
193 251 * @since 1.3.0
194 252 *
195 - * @param string[] $tabs
196 - * @param string $key
197 - * @param string $name
198 - * @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.
199 257 * @return string[]
200 258 */
201 259 function charitable_add_settings_tab( $tabs, $key, $name, $args = array() ) {
202 260 $defaults = array(
@@ -213,8 +271,112 @@
213 271 return array_combine( $keys, $values );
214 272 }
215 273
216 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 +/**
217 379 * Return the donation actions class.
218 380 *
219 381 * @since 1.5.0
220 382 *
@@ -220,12 +382,11 @@
220 382 *
221 383 * @return Charitable_Donation_Admin_Actions
222 384 */
223 385 function charitable_get_donation_actions() {
224 - return Charitable_Admin::get_instance()->get_donation_actions();
386 + return Charitable_Admin::get_instance()->get_donation_actions();
225 387 }
226 388
227 -
228 389 /**
229 390 * Adds the "Upgrade to Pro" menu item to the very end of the submenu.
230 391 *
231 392 * @since 1.7.0
@@ -236,25 +397,40 @@
236 397 if ( charitable_is_pro() ) {
237 398 return;
238 399 }
239 400
240 - $submenu['charitable'][99] = array(
241 - __( 'Upgrade to Pro', 'chariable' ),
401 + $submenu['charitable'][99] = array( // phpcs:ignore
402 + __( 'Upgrade to Pro', 'charitable' ),
242 403 'manage_options',
243 404 charitable_ga_url(
244 405 'https://wpcharitable.com/lite-vs-pro/',
245 - urlencode( 'Admin Menu Link'),
406 + urlencode( 'Admin Menu Link' ),
246 407 urlencode( 'Upgrade to Pro' )
247 - )
408 + ),
248 409 );
249 410 }
250 -add_action('admin_menu', 'charitable_add_upgrade_item');
411 +add_action( 'admin_menu', 'charitable_add_upgrade_item' );
251 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() {
252 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 +
253 428 /**
254 429 * Outputs "please rate" text.
255 430 *
256 431 * @since 1.7.0
432 + * @version 1.8.8.6
257 433 *
258 434 * @param string $footer_text Footer text.
259 435 * @return string
260 436 */
@@ -264,12 +440,12 @@
264 440 }
265 441
266 442 return sprintf(
267 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. */
268 - __( 'Please rate %1$sWP Charitable%2$s %3$s★★★★★%4$s on %3$sWordPress.org%4$s to help us spread the word. Thank you from the WP Charitable team!', 'stripe' ),
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' ),
269 445 '<strong>',
270 446 '</strong>',
271 - '<a href="https://wordpress.org/support/plugin/charitable/reviews/?filter=5#new-post" rel="noopener noreferrer" target="_blank">',
447 + '<a href="https://wpcharitable.com/charitable-admin-review/" rel="noopener noreferrer" target="_blank">',
272 448 '</a>'
273 449 );
274 450 }
275 451 add_filter( 'admin_footer_text', 'charitable_add_footer_text' );
@@ -274,12 +450,114 @@
274 450 }
275 451 add_filter( 'admin_footer_text', 'charitable_add_footer_text' );
276 452
277 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 +/**
278 555 * Check if a screen is a plugin admin view.
279 556 * Returns the screen id if true, false (bool) if not.
280 557 *
281 558 * @since 1.7.0
559 + * @since 1.8.1.5 Added another check and 'donation' to first check.
282 560 *
283 561 * @return string|bool
284 562 */
285 563 function charitable_is_admin_screen() {
@@ -284,28 +562,29 @@
284 562 */
285 563 function charitable_is_admin_screen() {
286 564 $screen = \get_current_screen();
287 565
566 + if ( ! $screen ) {
567 + return false;
568 + }
569 +
288 570 if (
289 571 'charitable' === $screen->post_type ||
290 - 'campaign' === $screen->post_type ||
572 + 'campaign' === $screen->post_type ||
573 + 'donation' === $screen->post_type ||
291 574 'charitable' === $screen->parent_file
292 575 ) {
293 576 return 'charitable';
294 577 }
295 578
296 - if ( isset( $_GET['page'] ) ) {
297 - if ( 'charitable' == $_GET['page'] ) {
579 + if ( isset( $_GET['page'] ) ) { // phpcs:ignore
580 + if ( 'charitable' === trim( $_GET['page'] ) ) { // phpcs:ignore
298 581 return 'charitable';
299 582 }
583 + }
300 584
301 - // if ( 'charitable_settings' == $_GET['page'] ) {
302 - // return 'charitable_settings';
303 - // }
304 -
305 - // if ( 'charitable_system_status' == $_GET['page'] ) {
306 - // return 'charitable_system_status';
307 - // }
585 + if ( isset( $_REQUEST['page'] ) && strpos( sanitize_text_field( $_REQUEST['page'] ), 'charitable' ) !== false ) { // phpcs:ignore
586 + return 'charitable';
308 587 }
309 588
310 589 return false;
311 590 }
@@ -323,15 +602,15 @@
323 602 function charitable_ga_url( $base_url, $utm_medium, $utm_content = false ) {
324 603 /**
325 604 * Filters the UTM campaign for generated links.
326 605 *
327 - * @since 3.0.0
606 + * @since 1.7.0
328 607 *
329 608 * @param string $utm_campaign
330 609 */
331 610 $utm_campaign = apply_filters( 'charitable_utm_campaign', 'WP+Charitable' );
332 611
333 - $args = array(
612 + $args = array(
334 613 'utm_source' => 'WordPress',
335 614 'utm_campaign' => $utm_campaign,
336 615 'utm_medium' => $utm_medium,
337 616 );
@@ -420,28 +699,42 @@
420 699 return false;
421 700 }
422 701
423 702 /**
424 - * Check permissions for currently logged in user, taken from Charitable.
425 - * Both short (e.g. 'view_own_forms') or long (e.g. 'charitable_view_own_forms') capability name can be used.
426 - * Only Charitable capabilities get processed.
703 + * Get when WPCharitable was first installed.
427 704 *
428 - * @since 1.7.0.3
705 + * @since 8.1.0
429 706 *
430 - * @param array|string $caps Capability name(s).
431 - * @param int $id ID of the specific object to check against if capability is a "meta" cap. "Meta"
432 - * capabilities, e.g. 'edit_post', 'edit_user', etc., are capabilities used by
433 - * map_meta_cap() to map to other "primitive" capabilities, e.g. 'edit_posts',
434 - * edit_others_posts', etc. Accessed via func_get_args() and passed to
435 - * WP_User::has_cap(), then map_meta_cap().
707 + * @param string $type Specific install type to check for.
436 708 *
437 - * @return bool
709 + * @return int|false Unix timestamp. False on failure.
438 710 */
439 -function charitable_current_user_can( $caps = [], $id = 0 ) {
711 +function charitable_get_updated_timestamp( $type = '' ) {
440 712
441 - $user_can = current_user_can( $caps , $id );
713 + $activated = (array) get_option( 'charitable_activated', [] );
442 714
443 - return apply_filters( 'charitable_current_user_can', $user_can, $caps, $id );
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;
444 737 }
445 738
446 739 /**
447 740 * Determines whether the current request is a WP CLI request.
@@ -468,9 +761,15 @@
468 761
469 762 return 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ) . '; WPCharitable/' . $wpcharitable_type;
470 763 }
471 764
472 -
765 +/**
766 + * Get the template paths.
767 + *
768 + * @since 1.8.0
769 + *
770 + * @return array
771 + */
473 772 function charitable_theme_template_paths() {
474 773
475 774 $template_dir = 'charitable';
476 775
@@ -476,9 +775,9 @@
476 775
477 776 $file_paths = array(
478 777 1 => trailingslashit( get_stylesheet_directory() ) . $template_dir,
479 778 10 => trailingslashit( get_template_directory() ) . $template_dir,
480 - 100 => trailingslashit( CHARTIABLE_DIRECTORY_PATH ) . 'includes',
779 + 100 => trailingslashit( CHARITABLE_DIRECTORY_PATH ) . 'includes',
481 780 );
482 781
483 782 $file_paths = apply_filters( 'charitable_helpers_templates_get_theme_template_paths', $file_paths );
484 783
@@ -552,9 +851,17 @@
552 851 $extract = apply_filters( 'charitable_helpers_templates_include_html_extract_args', $extract, $template_name, $args );
553 852
554 853 if ( $extract && is_array( $args ) ) {
555 854
556 - $created_vars_count = extract( $args, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract
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 + }
557 864
558 865 // Protecting existing scope from modification.
559 866 if ( count( $args ) !== $created_vars_count ) {
560 867 throw new \RuntimeException( 'Extraction failed: variable names are clashing with the existing ones.' );
@@ -630,8 +937,9 @@
630 937 /**
631 938 * Determine if the plugin/addon installations/activations are allowed.
632 939 *
633 940 * @since 1.7.3
941 + * @since 1.8.4.3 // Removed 'Addons require additional license checks' comments.
634 942 *
635 943 * @internal Use charitable_can_activate() or charitable_can_install() instead.
636 944 *
637 945 * @param string $what Should be 'activate' or 'install'.
@@ -664,14 +972,8 @@
664 972 if ( $type === 'plugin' ) {
665 973 return true;
666 974 }
667 975
668 - // Addons require additional license checks.
669 - // $license = get_option( 'charitable_license', [] );
670 -
671 - // // Allow addons installation if license is not expired, enabled and valid.
672 - // return empty( $license['is_expired'] ) && empty( $license['is_disabled'] ) && empty( $license['is_invalid'] );
673 -
674 976 return true;
675 977 }
676 978
677 979 /**
@@ -682,12 +984,12 @@
682 984 */
683 985 function charitable_verify_ssl() {
684 986
685 987 // Run a security check.
686 - check_ajax_referer( 'charitable-admin', 'nonce' );
988 + check_ajax_referer( 'charitable-admin-tools', 'nonce' );
687 989
688 990 // Check for permissions.
689 - if ( ! charitable_current_user_can() ) {
991 + if ( ! charitable_current_user_can( 'manage_options' ) ) {
690 992 wp_send_json_error(
691 993 array(
692 994 'msg' => esc_html__( 'You do not have permission to perform this operation.', 'charitable' ),
693 995 )
@@ -706,9 +1008,9 @@
706 1008
707 1009 wp_send_json_error(
708 1010 array(
709 1011 'msg' => esc_html__( 'There was an error and the connection failed. Please contact your web host with the technical details below.', 'charitable' ),
710 - 'debug' => '<pre>' . print_r( map_deep( $response, 'wp_strip_all_tags' ), true ) . '</pre>',
1012 + 'debug' => '<pre>' . print_r( map_deep( $response, 'wp_strip_all_tags' ), true ) . '</pre>', // phpcs:ignore
711 1013 )
712 1014 );
713 1015 }
714 1016 add_action( 'wp_ajax_charitable_verify_ssl', 'charitable_verify_ssl' );
@@ -733,13 +1035,13 @@
733 1035
734 1036 if ( isset( $_POST['plugin'] ) ) {
735 1037 $plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
736 1038
737 - if ( defined( 'CHARITABLE_DEBUG' ) && CHARITABLE_DEBUG ) {
738 - error_log( 'charitable_deactivate_addon' );
739 - error_log( print_r( $plugin, true ) );
740 - error_log( print_r( $_POST, true ) );
741 - error_log( print_r( $type, true ) );
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
742 1044 }
743 1045
744 1046 deactivate_plugins( $plugin );
745 1047
@@ -779,9 +1081,9 @@
779 1081 if ( ! empty( $_POST['type'] ) ) {
780 1082 $type = sanitize_key( $_POST['type'] );
781 1083 }
782 1084
783 - $plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
1085 + $plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
784 1086
785 1087 $activate = activate_plugins( $plugin );
786 1088
787 1089 /**
@@ -813,9 +1115,10 @@
813 1115
814 1116 /**
815 1117 * Installs an Charitable addon.
816 1118 *
817 - * @since 1.0.0
1119 + * @since 1.0.0
1120 + * @version 1.8.6.2
818 1121 */
819 1122 function charitable_ajax_install_addon() {
820 1123
821 1124 // Run a security check first.
@@ -820,17 +1123,35 @@
820 1123
821 1124 // Run a security check first.
822 1125 check_admin_referer( 'charitable-admin', 'nonce' );
823 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 +
824 1132 // Install the addon.
825 1133 if ( isset( $_POST['plugin'] ) ) {
826 1134 $download_url = esc_url_raw( wp_unslash( $_POST['plugin'] ) );
827 1135 global $hook_suffix;
828 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 +
829 1148 if ( defined( 'CHARITABLE_DEBUG' ) && CHARITABLE_DEBUG ) {
830 - error_log( 'charitable_ajax_install_addon' );
831 - error_log( print_r( $_POST, true ) );
832 - error_log( print_r( $download_url, true ) );
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 + }
833 1154 }
834 1155
835 1156 // Set the current screen to avoid undefined notices.
836 1157 set_current_screen();
@@ -853,11 +1174,11 @@
853 1174 echo wp_json_encode( array( 'form' => $form ) );
854 1175 die;
855 1176 }
856 1177
857 - if ( defined( 'CHARITABLE_DEBUG' ) && CHARITABLE_DEBUG ) {
858 - error_log( 'charitable_ajax_install_addon creds' );
859 - error_log( print_r( $creds, true ) );
1178 + if ( charitable_is_debug() ) {
1179 + error_log( 'charitable_ajax_install_addon creds' ); // phpcs:ignore
1180 + error_log( print_r( $creds, true ) ); // phpcs:ignore
860 1181 }
861 1182
862 1183 // If we are not authenticated, make it happen now.
863 1184 if ( ! WP_Filesystem( $creds ) ) {
@@ -872,9 +1193,9 @@
872 1193 remove_action( 'upgrader_process_complete', [ 'Language_Pack_Upgrader', 'async_upgrade' ], 20 );
873 1194
874 1195 // We do not need any extra credentials if we have gotten this far, so let's install the plugin.
875 1196 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
876 - require_once plugin_dir_path( CHARTIABLE_DIRECTORY_PATH ) . 'charitable/includes/utilities/Skin.php';
1197 + require_once plugin_dir_path( CHARITABLE_DIRECTORY_PATH ) . 'charitable/includes/utilities/Skin.php';
877 1198
878 1199 // Create the plugin upgrader with our custom skin.
879 1200 $skin = new Charitable_Skin();
880 1201 $installer = new Plugin_Upgrader( $skin );
@@ -885,16 +1206,37 @@
885 1206
886 1207 if ( $installer->plugin_info() ) {
887 1208 $plugin_basename = $installer->plugin_info();
888 1209 $plugin = sanitize_text_field( wp_unslash( $_POST['plugin'] ) );
889 - // attempt to activate the installed addon, save the user a step
1210 + // attempt to activate the installed addon, save the user a step.
890 1211 $activate = activate_plugins( $plugin_basename );
891 1212 if ( ! is_wp_error( $activate ) ) {
892 - wp_send_json_success( array( 'basename' => $plugin_basename, 'is_activated' => true, 'msg' => esc_html__( 'Addon installed and activated.', 'charitable' ) ) );
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 );
893 1230 } else {
894 - wp_send_json_success( array( 'basename' => $plugin_basename, 'msg' => esc_html__( 'Addon installed.', 'charitable' ) ) );
1231 + wp_send_json_success(
1232 + array(
1233 + 'basename' => $plugin_basename,
1234 + 'msg' => esc_html__( 'Addon installed.', 'charitable' ),
1235 + )
1236 + );
895 1237 }
896 - // wp_send_json_success( esc_html__( 'Addon installed.', 'charitable' ) );
1238 +
897 1239 die;
898 1240 }
899 1241 }
900 1242
@@ -900,9 +1242,8 @@
900 1242
901 1243 // Send back a response.
902 1244 echo wp_json_encode( true );
903 1245 die;
904 -
905 1246 }
906 1247 add_action( 'wp_ajax_charitable_install_addon', 'charitable_ajax_install_addon' );
907 1248
908 1249 /**
@@ -920,9 +1261,9 @@
920 1261
921 1262 if ( is_plugin_active( 'code-snippets/code-snippets.php' ) && ! array_key_exists( 'code-snippets/code-snippets.php', $third_party_warnings['plugins'] ) ) {
922 1263 $third_party_warnings['plugins']['code-snippets/code-snippets.php'] = 'noted';
923 1264 $updated = true;
924 - } else if ( ! 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' ) {
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' ) {
925 1266 unset( $third_party_warnings['plugins']['code-snippets/code-snippets.php'] );
926 1267 $updated = true;
927 1268 }
928 1269
@@ -928,11 +1269,10 @@
928 1269
929 1270 $third_party_warnings = apply_filters( 'charitable_update_third_party_warning_option', $third_party_warnings );
930 1271
931 1272 if ( $updated ) {
932 - $result = update_option( 'charitable_third_party_warnings', json_encode( $third_party_warnings ) );
1273 + $result = update_option( 'charitable_third_party_warnings', wp_json_encode( $third_party_warnings ) );
933 1274 }
934 -
935 1275 }
936 1276 add_action( 'admin_init', 'charitable_update_third_party_warning_option' );
937 1277
938 1278 /**
@@ -938,8 +1278,11 @@
938 1278 /**
939 1279 * Get an option related to third party warning ( null, noted, dismissed )
940 1280 *
941 1281 * @since 1.7.0.8
1282 + *
1283 + * @param string $plugin_path Plugin path.
1284 + * @param string $category Category.
942 1285 */
943 1286 function charitable_get_third_party_warning_option( $plugin_path = false, $category = 'plugins' ) {
944 1287
945 1288 if ( false === $plugin_path ) {
@@ -958,9 +1301,8 @@
958 1301 return false;
959 1302 }
960 1303
961 1304 return esc_html( $third_party_warnings[ $category ][ $plugin_path ] );
962 -
963 1305 }
964 1306
965 1307 /**
966 1308 * Get an option related to third party warning ( null, noted, dismissed )
@@ -965,8 +1307,12 @@
965 1307 /**
966 1308 * Get an option related to third party warning ( null, noted, dismissed )
967 1309 *
968 1310 * @since 1.7.0.8
1311 + *
1312 + * @param string $plugin_path Plugin path.
1313 + * @param string $value Value.
1314 + * @param string $category Category.
969 1315 */
970 1316 function charitable_set_third_party_warning_option( $plugin_path = false, $value = false, $category = 'plugins' ) {
971 1317
972 1318 if ( false === $plugin_path ) {
@@ -982,9 +1328,570 @@
982 1328 $third_party_warnings = json_decode( $third_party_warnings, ARRAY_A );
983 1329
984 1330 $third_party_warnings[ $category ][ $plugin_path ] = $value;
985 1331
986 - $result = update_option( 'charitable_third_party_warnings', json_encode( $third_party_warnings ) );
1332 + $result = update_option( 'charitable_third_party_warnings', wp_json_encode( $third_party_warnings ) );
987 1333
988 1334 return $result;
1335 +}
989 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;
990 1897 }