PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13.1
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13.1
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / includes / admin / class-charitable-admin.php

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

1,842 lines 60.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class that sets up the Charitable Admin functionality.
4 *
5 * @package Charitable/Classes/Charitable_Admin
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 * @version 1.8.4
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Admin' ) ) :
19
20 /**
21 * Charitable_Admin
22 *
23 * @final
24 * @since 1.0.0
25 */
26 final class Charitable_Admin {
27
28 /**
29 * The single instance of this class.
30 *
31 * @var Charitable_Admin|null
32 */
33 private static $instance = null;
34
35 /**
36 * Donation actions class.
37 *
38 * @var Charitable_Donation_Admin_Actions
39 */
40 private $donation_actions;
41
42 /**
43 * Set up the class.
44 *
45 * Note that the only way to instantiate an object is with the charitable_start method,
46 * which can only be called during the start phase. In other words, don't try
47 * to instantiate this object.
48 *
49 * @since 1.0.0
50 * @version 1.8.3 added third party plugin area.
51 */
52 protected function __construct() {
53 $this->load_dependencies();
54
55 $this->donation_actions = new Charitable_Donation_Admin_Actions();
56
57 // Initialize About page.
58 new Charitable_About();
59
60 do_action( 'charitable_admin_loaded' );
61
62 add_action( 'wp_ajax_charitable_lite_settings_upgrade', array( $this, 'dismiss_lite_cta' ) );
63 add_action( 'wp_ajax_charitable_lite_reports_upgrade', array( $this, 'dismiss_lite_reports_cta' ) );
64
65 // Addon management AJAX handlers.
66 add_action( 'wp_ajax_charitable_install_addon', array( $this, 'install_addon' ) );
67 add_action( 'wp_ajax_charitable_activate_addon', array( $this, 'activate_addon' ) );
68 add_action( 'wp_ajax_charitable_deactivate_addon', array( $this, 'deactivate_addon' ) );
69
70 // Dedicated About/Addons handlers to avoid conflicts with legacy hooks.
71 add_action( 'wp_ajax_charitable_addons_install_wporg', array( $this, 'install_addon' ) );
72 add_action( 'wp_ajax_charitable_addons_activate', array( $this, 'activate_addon' ) );
73 add_action( 'wp_ajax_charitable_addons_deactivate', array( $this, 'deactivate_addon' ) );
74
75 add_filter( 'post_row_actions', array( $this, 'campaign_action_row' ), 10, 2 );
76 add_filter( 'get_edit_post_link', array( $this, 'campaign_link' ), 99, 3 );
77 add_filter( 'preview_post_link', array( $this, 'campaign_preview_link' ), 10, 2 );
78
79 add_filter( 'admin_init', array( $this, 'update_dashboard_url' ), 10 );
80
81 /* third party plugins */
82 add_filter( 'elementor/settings/controls/checkbox_list_cpt/post_type_objects', array( $this, 'elementor_post_types' ), 10, 1 );
83 }
84
85 /**
86 * Redirect from the old dashboard to the new, unless legacy campaigns are being used and defined.
87 *
88 * @since 1.8.1
89 * @version 1.8.4
90 *
91 * @return void
92 */
93 public function update_dashboard_url() {
94 global $pagenow;
95
96 if ( function_exists( 'charitable_use_legacy_dashboard' ) && charitable_use_legacy_dashboard() ) {
97 return;
98 }
99
100 if ( $pagenow === 'admin.php' && isset( $_GET['page'] ) && $_GET['page'] === 'charitable' ) { // phpcs:ignore
101
102 // Check and see if this has to do with onboarding.
103
104 if ( isset( $_GET['wpchar_lite'] ) && isset( $_GET['setup'] ) ) { // phpcs:ignore
105
106 switch ( sanitize_text_field( wp_unslash( $_GET['setup'] ) ) ) { // phpcs:ignore
107
108 case 'welcome':
109 if ( empty( $_GET['wpchar_lite'] ) || 'lite' !== $_GET['wpchar_lite'] ) { // phpcs:ignore
110 wp_safe_redirect( admin_url( 'admin.php?page=charitable-dashboard' ) );
111 exit;
112 }
113
114 break;
115
116 case 'return':
117 // The wizard finishes by POSTing the user's choices from the
118 // hosted app straight into this admin, cross-site and
119 // top-level. Browsers drop that body when a SameSite=Lax auth
120 // cookie forces a trip through wp-login.php, or when a
121 // canonical / http-to-https / security-plugin redirect turns
122 // the POST into a GET. The user is logged in the whole time;
123 // the cookie simply did not ride along.
124
125 // Applying onboarding choices writes plugin options and
126 // third-party activation flags, so it is an administrator
127 // action. This previously ran for any user who could reach
128 // wp-admin.
129 if ( ! charitable_current_user_can( 'administrator' ) ) {
130 wp_safe_redirect( admin_url( 'admin.php?page=charitable-dashboard' ) );
131 exit;
132 }
133
134 $charitable_return_data = wp_unslash( $_POST ); // phpcs:ignore
135
136 if ( empty( $charitable_return_data ) && is_admin() ) {
137 // Body dropped in transit. Recover the choices with a
138 // server-to-server read: no browser, no cookies, no
139 // redirects, so nothing that drops the POST applies.
140 $charitable_return_data = $this->pull_onboarding_return_data();
141 }
142
143 if ( empty( $charitable_return_data ) ) {
144 wp_safe_redirect( admin_url( 'admin.php?page=charitable&wpchar_lite=lite&setup=welcome&resume=true&lostconnection=1' ) );
145 exit;
146 }
147
148 $this->process_onboarding_return_data( $charitable_return_data );
149
150 wp_safe_redirect( admin_url( 'admin.php?page=charitable-setup&setup=1' ) );
151 exit;
152
153 case 'cancelled':
154 // Either user is going "back" to the checklist page or (if the checklist is disabled) the welcome page.
155 // https://mydomain.com/wp-admin/admin.php?page=charitable&wpchar_lite=lite&setup=cancelled.
156 $checklist_class = Charitable_Checklist::get_instance();
157 $checklist_possible = $checklist_class->is_checklist_skipped() || $checklist_class->is_checklist_completed();
158 $welcome_go_back_url = $checklist_possible ? admin_url( 'admin.php?page=charitable-dashboard' ) : admin_url( 'admin.php?page=charitable-setup-checklist' );
159
160 // Clear the transient.
161 delete_transient( 'charitable_activation_redirect' );
162
163 // Clear the option.
164 delete_option( 'charitable_started_onboarding' );
165
166 wp_safe_redirect( $welcome_go_back_url );
167 exit;
168
169 default:
170 wp_safe_redirect( admin_url( 'admin.php?page=charitable-dashboard' ) );
171 exit;
172
173 }
174 } else {
175
176 wp_safe_redirect( admin_url( 'admin.php?page=charitable-dashboard' ) );
177 exit;
178
179 }
180 }
181 }
182
183 /**
184 * Apply a set of onboarding choices handed back by the setup wizard.
185 *
186 * Extracted verbatim from the `setup=return` branch so the same logic
187 * serves both routes into it: the cross-site POST the wizard sends, and
188 * the server-to-server pull used when that POST body is dropped. Keeping
189 * one implementation is the point - a second decoder would drift.
190 *
191 * Expects the same keys and encodings as the POST: `meta`, `pm`,
192 * `features`, `plugins` and `license_key` base64-encoded, and `template`
193 * plus the `campaign_*` fields plain. Every field is optional.
194 *
195 * @since 1.8.12.4
196 *
197 * @param array $data Onboarding fields, already unslashed.
198 * @return void
199 */
200 private function process_onboarding_return_data( $data ) {
201
202 if ( ! is_array( $data ) ) {
203 return;
204 }
205
206 $charitable_field = function ( $key ) use ( $data ) {
207 return isset( $data[ $key ] ) && ! is_array( $data[ $key ] )
208 ? sanitize_text_field( $data[ $key ] )
209 : '';
210 };
211
212 $plugins = '' !== $charitable_field( 'plugins' ) ? base64_decode( $charitable_field( 'plugins' ) ) : '';
213 $features = '' !== $charitable_field( 'features' ) ? base64_decode( $charitable_field( 'features' ) ) : '';
214 $meta = '' !== $charitable_field( 'meta' ) ? base64_decode( $charitable_field( 'meta' ) ) : '';
215 $pm = '' !== $charitable_field( 'pm' ) ? base64_decode( $charitable_field( 'pm' ) ) : '';
216 $license_key = '' !== $charitable_field( 'license_key' ) ? base64_decode( $charitable_field( 'license_key' ) ) : '';
217
218 // process meta.
219 if ( ! empty( $meta ) ) {
220 $meta = str_replace( '\\"', '"', $meta );
221 if ( ! empty( $meta ) ) {
222 $meta = json_decode( $meta, true );
223 }
224 }
225 // process plugins.
226 if ( ! empty( $plugins ) ) {
227 $plugins = json_decode( $plugins );
228 $plugins = str_replace( array( '"', '[', ']' ), '', $plugins );
229 if ( ! empty( $plugins ) ) {
230 $plugins = explode( ',', $plugins );
231 }
232 }
233
234 // process payment methods.
235 // NOTE: the inner guard tests $plugins, not $pm. That is how this has
236 // always behaved, so it is preserved here rather than quietly changed
237 // during a refactor whose whole value is being behaviour-identical.
238 // Worth fixing on its own, separately.
239 if ( ! empty( $pm ) ) {
240 $pm = json_decode( $pm );
241 if ( $pm ) {
242 $pm = str_replace( array( '"', '[', ']' ), '', $pm );
243 if ( ! empty( $plugins ) ) {
244 $pm = explode( ',', $pm );
245 }
246 }
247 }
248
249 // process features.
250 if ( ! empty( $features ) ) {
251 $features = json_decode( $features );
252 $features = str_replace( array( '"', '[', ']' ), '', $features );
253 if ( ! empty( $features ) ) {
254 $features = explode( ',', $features );
255 }
256 }
257
258 // clean license (remove quotes).
259 $license_key = str_replace( '"', '', $license_key );
260
261 // process campaign.
262 $campaign = array(
263 'template' => $charitable_field( 'template' ),
264 'title' => $charitable_field( 'campaign_title' ),
265 'description' => $charitable_field( 'campaign_description' ),
266 'goal' => $charitable_field( 'campaign_goal' ),
267 'end_date' => $charitable_field( 'campaign_end_date' ),
268 );
269
270 // store server side setups.
271 $serverside = new Charitable_Setup();
272
273 $serverside->store_meta( $meta );
274 $serverside->store_plugins( $plugins );
275 $serverside->store_features( $features );
276 $serverside->store_payment_methods( $pm );
277 $serverside->store_campaign( $campaign );
278 // Guarded, so a payload without a licence key never overwrites one
279 // already stored. The pull payload deliberately never carries one.
280 if ( $license_key ) {
281 $serverside->store_license_key( $license_key );
282 }
283
284 // If this is a select few addons that we need to disable their auto-activation, do so.
285 if ( ! empty( $plugins ) && is_array( $plugins ) ) {
286 foreach ( $plugins as $plugin ) {
287 if ( $plugin === 'all-in-one-seo-pack' ) {
288 update_option( 'aioseo_activation_redirect', true );
289 } elseif ( $plugin === 'wp-mail-smtp' ) {
290 update_option( 'wp_mail_smtp_activation_prevent_redirect', true );
291 }
292 }
293 }
294
295 // add a transient to indicate that the server side onboarding has moved to the plugin.
296 set_transient( 'charitable_ss_onboarding', 1, 0 );
297
298 delete_option( 'charitable_started_onboarding' );
299 }
300
301 /**
302 * Read the onboarding choices back from the wizard, server to server.
303 *
304 * Used only when the wizard's cross-site POST arrived with no body. A
305 * plain server-side GET has no browser, no cookies and no redirects, so
306 * none of the mechanisms that drop that POST can affect it.
307 *
308 * Returns an empty array on any doubt at all - disabled, missing
309 * helpers, transport error, non-200, unparseable body, or a payload that
310 * does not say `status: ok`. The caller then falls back to exactly the
311 * behaviour that shipped before this existed.
312 *
313 * @since 1.8.12.4
314 *
315 * @return array Onboarding fields, or an empty array.
316 */
317 private function pull_onboarding_return_data() {
318
319 // Field kill switch: both of these turn the fallback off without
320 // needing a release, reverting to the previous behaviour exactly.
321 if ( ! apply_filters( 'charitable_onboarding_pull_enabled', true ) ) {
322 return array();
323 }
324
325 if ( defined( 'CHARITABLE_ONBOARDING_DISABLE_PULL' ) && CHARITABLE_ONBOARDING_DISABLE_PULL ) {
326 return array();
327 }
328
329 if ( ! function_exists( 'charitable_onboarding_app_url' ) || ! function_exists( 'charitable_get_site_token' ) ) {
330 return array();
331 }
332
333 $site_token = charitable_get_site_token();
334
335 if ( empty( $site_token ) ) {
336 return array();
337 }
338
339 $response = wp_remote_get(
340 charitable_onboarding_app_url() . '/setup-wizard-charitable_lite/pull/' . rawurlencode( $site_token ),
341 array(
342 'timeout' => 10,
343 'sslverify' => true,
344 )
345 );
346
347 if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
348 return array();
349 }
350
351 $body = json_decode( wp_remote_retrieve_body( $response ), true );
352
353 if ( ! is_array( $body ) || empty( $body['status'] ) || 'ok' !== $body['status'] ) {
354 return array();
355 }
356
357 return $body;
358 }
359
360 /**
361 * Returns and/or create the single instance of this class.
362 *
363 * @since 1.2.0
364 *
365 * @return Charitable_Admin
366 */
367 public static function get_instance() {
368 if ( is_null( self::$instance ) ) {
369 self::$instance = new self();
370 }
371
372 return self::$instance;
373 }
374
375 /**
376 * Include admin-only files.
377 *
378 * @since 1.0.0
379 *
380 * @return void
381 */
382 private function load_dependencies() {
383 $admin_dir = charitable()->get_path( 'includes' ) . 'admin/';
384
385 require_once $admin_dir . 'charitable-core-admin-functions.php';
386 require_once $admin_dir . 'campaigns/charitable-admin-campaign-hooks.php';
387 require_once $admin_dir . 'dashboard/class-charitable-dashboard-ajax.php';
388 require_once $admin_dir . 'charitable-dashboard-hooks.php';
389 require_once $admin_dir . 'dashboard-widgets/charitable-dashboard-widgets-hooks.php';
390 require_once $admin_dir . 'donations/charitable-admin-donation-hooks.php';
391 require_once $admin_dir . 'settings/charitable-settings-admin-hooks.php';
392 // Security hooks are now loaded in charitable.php to ensure CAPTCHA works on frontend.
393 // require_once $admin_dir . 'security/charitable-security-hooks.php';
394 require_once $admin_dir . 'activities/charitable-admin-activity-hooks.php';
395 require_once $admin_dir . 'reports/charitable-core-reports-functions.php';
396 require_once $admin_dir . 'reports/charitable-admin-reports-hooks.php';
397 require_once $admin_dir . 'plugins/class-charitable-admin-plugins-third-party.php';
398 require_once $admin_dir . 'tools/charitable-tools-admin-hooks.php';
399 require_once $admin_dir . 'growth-tools/charitable-growth-tools-admin-hooks.php';
400 require_once $admin_dir . 'onboarding/charitable-onboarding-admin-hooks.php';
401 require_once $admin_dir . 'tracking/charitable-tracking-admin-hooks.php';
402 require_once $admin_dir . 'smtp/charitable-smtp-admin-hooks.php';
403 require_once $admin_dir . 'class-charitable-about.php';
404 require_once $admin_dir . 'charitable-admin-addons-functions.php';
405 require_once $admin_dir . 'privacy-compliance/charitable-privacy-compliance-admin-hooks.php';
406 require_once $admin_dir . 'intergrations/charitable-privacy-compliance-admin-hooks.php';
407 require_once $admin_dir . 'intergrations/charitable-backups-admin-hooks.php';
408 require_once $admin_dir . 'intergrations/charitable-seo-admin-hooks.php';
409 require_once $admin_dir . 'intergrations/charitable-automation-admin-hooks.php';
410 }
411
412 /**
413 * Get Charitable_Donation_Admin_Actions class.
414 *
415 * @since 1.5.0
416 *
417 * @return Charitable_Donation_Admin_Actions
418 */
419 public function get_donation_actions() {
420 return $this->donation_actions;
421 }
422
423 /**
424 * Do an admin action.
425 *
426 * @since 1.5.0
427 *
428 * @return boolean|WP_Error WP_Error in case of error. Mixed results if the action was performed.
429 */
430 public function maybe_do_admin_action() {
431 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
432 if ( ! array_key_exists( 'charitable_admin_action', $_GET ) ) {
433 return false;
434 }
435
436 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Check for required keys only
437 if ( count( array_diff( array( 'action_type', '_nonce', 'object_id' ), array_keys( $_GET ) ) ) ) {
438 return new WP_Error( __( 'Action could not be executed.', 'charitable' ) );
439 }
440
441 if ( ! isset( $_GET['_nonce'] ) || ! wp_verify_nonce( $_GET['_nonce'], 'donation_action' ) ) { // phpcs:ignore.
442 return new WP_Error( __( 'Action could not be executed. Nonce check failed.', 'charitable' ) );
443 }
444
445 if ( ! isset( $_GET['action_type'] ) || 'donation' !== $_GET['action_type'] ) {
446 return new WP_Error( __( 'Action from an unknown action type executed.', 'charitable' ) );
447 }
448
449 return $this->donation_actions->do_action( esc_html( $_GET['charitable_admin_action'] ), esc_html( $_GET['object_id'] ) ); // phpcs:ignore.
450 }
451
452 /**
453 * Loads admin-only scripts and stylesheets.
454 *
455 * @since 1.0.0
456 * @version 1.8.8.6
457 *
458 * @return void
459 */
460 public function admin_enqueue_scripts() {
461
462 $min = charitable_get_min_suffix(); // 1.8.3
463 $version = charitable_is_break_cache() ? charitable()->get_version() . '.' . time() : charitable()->get_version();
464
465 $assets_dir = charitable()->get_path( 'assets', false );
466
467 $localized_vars = array(
468 'suggested_amount_placeholder' => __( 'Amount', 'charitable' ),
469 'suggested_amount_description_placeholder' => __( 'Optional Description', 'charitable' ),
470 'ajax_url' => admin_url( 'admin-ajax.php' ),
471 'nonce' => wp_create_nonce( 'charitable_admin_nonce' ),
472 'heads_up' => __( 'Heads Up!', 'charitable' ),
473 'square_legacy_modal_message' => __( 'Square (Legacy) is currently active. Activating the Square (Core) payment method will deactivate the Square (Legacy) payment and you will need to reconnect your Square account.<br><br>Click <strong><span style="color: #ff6b35;">Ok</span></strong> to deactivate Square (Legacy) and activate Square (Core) payment. Click <strong><span style="color: #000000;">Cancel</span></strong> to keep Square (Legacy) payment active.</strong><br><br><a href="https://wpcharitable.com/documentation/square-legacy" target="_blank">Read our documentation</a>', 'charitable' ),
474 'square_core_modal_message' => __( 'Square (Core) is currently active. Activating the Square (Legacy) payment method will deactivate the Square (Core) payment and you will need to reconnect your Square account.<br><br>Click <strong><span style="color: #ff6b35;">Ok</span></strong> to deactivate Square (Core) and activate Square (Legacy) payment. Click <strong><span style="color: #000000;">Cancel</span></strong> to keep Square (Core) payment active.<br><br><a href="https://wpcharitable.com/documentation/square-legacy" target="_blank">Read our documentation</a>', 'charitable' ),
475 'oops' => __( 'Oops!', 'charitable' ),
476 'square_legacy_switch_error' => __( 'There was an error switching the payment gateways. Please try again.', 'charitable' ),
477 'ok' => __( 'OK', 'charitable' ),
478 'cancel' => __( 'Cancel', 'charitable' ),
479 'square_legacy_active' => charitable_get_helper( 'gateways' )->is_active_gateway( 'square' ),
480 'square_core_active' => charitable_get_helper( 'gateways' )->is_active_gateway( 'square_core' ),
481 );
482
483 /* Menu styles are loaded everywhere in the WordPress dashboard. */
484 wp_register_style(
485 'charitable-admin-menu',
486 $assets_dir . 'css/admin/charitable-admin-menu' . $min . '.css',
487 array(),
488 $version
489 );
490
491 wp_enqueue_style( 'charitable-admin-menu' );
492
493 wp_register_script(
494 'charitable-admin-pages-all',
495 $assets_dir . 'js/admin/charitable-admin-pages' . $min . '.js',
496 array( 'jquery' ),
497 $version,
498 false
499 );
500
501 wp_enqueue_script( 'charitable-admin-pages-all' );
502
503 /* Admin page styles are registered but only enqueued when necessary. */
504 wp_register_style(
505 'charitable-admin-pages',
506 $assets_dir . 'css/admin/charitable-admin-pages' . $min . '.css',
507 array(),
508 $version
509 );
510
511 /* The following styles are only loaded on Charitable screens. */
512 $screen = get_current_screen();
513
514 /* This check covers the category and tags pages, only load the main admin css (upgrade banner, etc.) */
515 if ( ! is_null( $screen ) && ( in_array( $screen->id, array( 'edit-campaign_category', 'edit-campaign_tag' ), true ) || ( in_array( $screen->base, array( 'edit-tags', 'edit-categories' ), true ) ) ) ) {
516
517 wp_register_style(
518 'charitable-admin',
519 $assets_dir . 'css/admin/charitable-admin-legacy' . $min . '.css',
520 array(),
521 $version
522 );
523
524 wp_enqueue_style( 'charitable-admin' );
525
526 wp_register_script(
527 'charitable-admin-notice',
528 $assets_dir . 'js/admin/charitable-admin-notice' . $min . '.js',
529 array( 'jquery' ),
530 $version,
531 false
532 );
533
534 }
535
536 // v 1.8.0.
537 wp_register_style(
538 'charitable-admin-2.0',
539 $assets_dir . 'css/admin/charitable-admin' . $min . '.css',
540 array(),
541 $version
542 );
543
544 wp_enqueue_style( 'charitable-admin-2.0' );
545
546 wp_enqueue_style(
547 'charitable-notifications-v2',
548 $assets_dir . 'css/admin/notifications-v2' . $min . '.css',
549 array(),
550 $version
551 );
552
553 wp_register_style(
554 'charitable-admin',
555 $assets_dir . 'css/admin/charitable-admin-legacy' . $min . '.css',
556 array(),
557 $version
558 );
559
560 wp_enqueue_style( 'charitable-admin' );
561
562 if ( ! is_null( $screen ) && in_array( $screen->id, charitable_get_charitable_screens() ) ) {
563
564 $dependencies = array( 'jquery-ui-datepicker', 'jquery-ui-tabs', 'jquery-ui-sortable' );
565
566 if ( 'donation' === $screen->id ) {
567 wp_register_script(
568 'accounting',
569 $assets_dir . 'js/libraries/accounting' . $min . '.js',
570 array( 'jquery' ),
571 $version,
572 true
573 );
574
575 $dependencies[] = 'accounting';
576 $localized_vars = array_merge(
577 $localized_vars,
578 array(
579 'currency_format_num_decimals' => esc_attr( charitable_get_currency_helper()->get_decimals() ),
580 'currency_format_decimal_sep' => esc_attr( charitable_get_currency_helper()->get_decimal_separator() ),
581 'currency_format_thousand_sep' => esc_attr( charitable_get_currency_helper()->get_thousands_separator() ),
582 'currency_format' => esc_attr( charitable_get_currency_helper()->get_accounting_js_format() ),
583 )
584 );
585 }
586
587 wp_register_script(
588 'charitable-admin',
589 $assets_dir . 'js/admin/charitable-admin' . $min . '.js',
590 $dependencies,
591 $version,
592 false
593 );
594
595 wp_enqueue_script( 'charitable-admin' );
596
597 // v 1.8.0.
598
599 wp_enqueue_style(
600 'jquery-confirm',
601 charitable()->get_path( 'directory', false ) . 'assets/lib/jquery.confirm/jquery-confirm.min.css',
602 null,
603 '3.3.4'
604 );
605
606 wp_enqueue_style(
607 'charitable-font-awesome',
608 charitable()->get_path( 'directory', false ) . 'assets/lib/font-awesome/font-awesome.min.css',
609 null,
610 '4.7.0'
611 );
612
613 wp_enqueue_script(
614 'jquery-confirm',
615 charitable()->get_path( 'directory', false ) . 'assets/lib/jquery.confirm/jquery-confirm.min.js',
616 array( 'jquery' ),
617 '3.3.4',
618 false
619 );
620
621 wp_enqueue_style(
622 'lity',
623 charitable()->get_path( 'directory', false ) . 'assets/lib/lity/lity.min.css',
624 null,
625 '3.0.0'
626 );
627
628 // Add custom CSS for lity image sizing
629 wp_add_inline_style( 'lity', '
630 .lity-image img {
631 max-width: 90vw !important;
632 max-height: 90vh !important;
633 width: auto !important;
634 height: auto !important;
635 margin: 0 auto !important;
636 }
637 .lity-image .lity-container {
638 max-width: 90vw !important;
639 max-height: 90vh !important;
640 }
641 ' );
642
643 wp_enqueue_script(
644 'lity',
645 charitable()->get_path( 'directory', false ) . 'assets/lib/lity/lity.min.js',
646 array( 'jquery' ),
647 '3.0.0',
648 false
649 );
650
651 wp_register_script(
652 'charitable-admin-2.0',
653 $assets_dir . 'js/admin/charitable-admin-2.0' . $min . '.js',
654 $dependencies,
655 $version,
656 false
657 );
658
659 wp_enqueue_script( 'charitable-admin-2.0' );
660
661 /**
662 * Filter the admin Javascript vars.
663 *
664 * @since 1.0.0
665 *
666 * @param array $localized_vars The vars.
667 */
668 $localized_vars = (array) apply_filters( 'charitable_localized_javascript_vars', $localized_vars );
669
670 wp_localize_script( 'charitable-admin', 'CHARITABLE', $localized_vars );
671
672 // Localize script for charitable-admin-2.0.js
673 wp_localize_script( 'charitable-admin-2.0', 'charitable_admin', array(
674 'autoshow_plugin_notifications' => charitable_get_autoshow_plugin_notifications(),
675 ) );
676
677 /* color picker for admin settings */
678 wp_enqueue_script( 'wp-color-picker', false, false, false, true ); // phpcs:ignore.
679 wp_enqueue_style( 'wp-color-picker' );
680
681 } // end if
682
683 wp_enqueue_script(
684 'charitable-admin-utils',
685 charitable()->get_path( 'directory', false ) . "assets/js/admin/charitable-admin-utils{$min}.js",
686 array( 'jquery' ),
687 charitable()->get_version(),
688 false
689 );
690
691 // Register these scripts only for reports and dashboard pages.
692 if ( ! is_null( $screen ) && ( $screen->id === 'charitable_page_charitable-reports' || $screen->id === 'charitable_page_charitable-dashboard' ) ) {
693
694 // Specific styles for the "overview" and main reporting tabs.
695 if ( empty( $_GET['tab'] ) || ( ! empty( $_GET['tab'] ) && charitable_reports_allow_tab_load_scripts( strtolower( sanitize_text_field( wp_unslash( $_GET['tab'] ) ) ) ) ) ) { // phpcs:ignore
696
697 wp_register_script(
698 'charitable-apex-charts',
699 charitable()->get_path( 'assets', false ) . 'js/libraries/apexcharts.min.js',
700 array( 'jquery' ),
701 $version,
702 true
703 );
704
705 /**
706 * Use WordPress core's Moment.js library.
707 *
708 * WordPress core includes Moment.js and exposes it via wp_enqueue_script('moment').
709 * This library is required as a dependency for daterangepicker.min.js, which powers
710 * the date range picker functionality in the reporting interface.
711 *
712 * @see https://github.com/moment/moment
713 * @since 1.0.0
714 * @version 1.8.8.6
715 */
716 wp_enqueue_script( 'moment' );
717
718 wp_register_script(
719 'charitable-report-date-range-picker',
720 charitable()->get_path( 'assets', false ) . 'js/libraries/daterangepicker.min.js',
721 array( 'jquery', 'moment' ),
722 $version,
723 true
724 );
725
726 wp_register_script(
727 'charitable-reporting',
728 $assets_dir . 'js/admin/charitable-admin-reporting' . $min . '.js',
729 array( 'jquery', 'charitable-apex-charts', 'charitable-report-date-range-picker' ),
730 $version,
731 true
732 );
733
734 wp_enqueue_style(
735 'charitable-report-date-range-picker',
736 charitable()->get_path( 'assets', false ) . 'css/libraries/daterangepicker.css',
737 null,
738 '4.7.0'
739 );
740
741 wp_enqueue_script( 'charitable-apex-charts' );
742 wp_enqueue_script( 'charitable-report-date-range-picker' );
743 wp_enqueue_script( 'charitable-reporting' );
744
745 } elseif ( ! empty( $_GET['tab'] ) && 'analytics' === $_GET['tab'] ) { // phpcs:ignore
746
747 // this loads a specific script for the analytics tab.
748 do_action( 'charitable_admin_enqueue_analytics_scripts' );
749
750 }
751 }
752
753 // Register v2 dashboard specific assets
754 if ( ! is_null( $screen ) && $screen->id === 'charitable_page_charitable-dashboard' && ! charitable_use_legacy_dashboard() ) {
755
756 // Register ApexCharts for the v2 dashboard chart
757 wp_register_script(
758 'charitable-apex-charts',
759 charitable()->get_path( 'assets', false ) . 'js/libraries/apexcharts.min.js',
760 array( 'jquery' ),
761 $version,
762 true
763 );
764
765 // Enqueue dashboard specific CSS
766 wp_enqueue_style(
767 'charitable-admin-dashboard',
768 charitable()->get_path( 'assets', false ) . 'css/admin/charitable-admin-dashboard.css',
769 array(),
770 charitable()->get_version()
771 );
772
773 // Enqueue dashboard specific JavaScript with ApexCharts dependency
774 wp_enqueue_script(
775 'charitable-admin-dashboard',
776 $assets_dir . 'js/admin/charitable-admin-dashboard.js',
777 array( 'jquery', 'charitable-apex-charts' ),
778 charitable()->get_version(),
779 true
780 );
781
782 // Enqueue ApexCharts
783 wp_enqueue_script( 'charitable-apex-charts' );
784 }
785
786 // Register these scripts only for checklist, onboarding, and similar pages.
787 if ( ! is_null( $screen ) && ( $screen->id === 'charitable_page_charitable-setup-checklist' ) ) {
788
789 // v 1.8.1.10.
790
791 wp_register_script(
792 'charitable-onboarding',
793 $assets_dir . 'js/admin/charitable-admin-onboarding' . $min . '.js',
794 array( 'jquery' ),
795 $version,
796 true
797 );
798
799 wp_enqueue_script( 'charitable-onboarding' );
800
801 }
802
803 wp_register_script(
804 'charitable-admin-plugins',
805 $assets_dir . 'js/plugins/charitable-admin-plugins' . $min . '.js',
806 array( 'jquery' ),
807 $version,
808 true
809 );
810
811 wp_enqueue_script( 'charitable-admin-plugins' );
812
813 wp_register_script(
814 'charitable-admin-notice',
815 $assets_dir . 'js/admin/charitable-admin-notice' . $min . '.js',
816 array( 'jquery' ),
817 $version,
818 false
819 );
820
821 wp_register_script(
822 'charitable-admin-media',
823 $assets_dir . 'js/admin/charitable-admin-media' . $min . '.js',
824 array( 'jquery' ),
825 $version,
826 false
827 );
828
829 wp_register_script(
830 'lean-modal',
831 $assets_dir . 'js/libraries/leanModal' . $min . '.js',
832 array( 'jquery' ),
833 $version,
834 true
835 );
836
837 wp_register_style(
838 'lean-modal-css',
839 $assets_dir . 'css/modal' . $min . '.css',
840 array(),
841 $version
842 );
843
844 wp_register_script(
845 'charitable-admin-tables',
846 $assets_dir . 'js/admin/charitable-admin-tables' . $min . '.js',
847 array( 'jquery', 'lean-modal' ),
848 $version,
849 true
850 );
851
852 wp_register_script(
853 'select2',
854 $assets_dir . 'js/libraries/select2' . $min . '.js',
855 array( 'jquery' ),
856 $version,
857 true
858 );
859
860 wp_register_style(
861 'select2-css',
862 $assets_dir . 'css/libraries/select2' . $min . '.css',
863 array(),
864 $version
865 );
866
867 do_action( 'after_charitable_admin_enqueue_scripts', $min, $version, $assets_dir ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Hook name is already in use by other code (charitable-admin-hooks.php). Changing it would break existing functionality.
868 }
869
870 /**
871 * Set admin body classes.
872 *
873 * @since 1.5.0
874 * @version 1.8.1.6
875 * @version 1.8.1.16 Added pro/lite check.
876 * @version 1.8.3
877 * @version 1.8.5 Added dashboard and reports classes.
878 *
879 * @param string $classes Existing list of classes.
880 * @return string
881 */
882 public function set_body_class( $classes ) {
883 $screen = get_current_screen();
884
885 if ( 'donation' === $screen->post_type && ( 'add' === $screen->action || isset( $_GET['show_form'] ) ) ) { // phpcs:ignore.
886 $classes .= ' charitable-admin-donation-form';
887 }
888
889 if ( isset( $_GET['page'] ) && ( 'charitable-tools' === $_GET['page'] || 'charitable-settings' === $_GET['page'] ) ) { // phpcs:ignore.
890 $classes .= ' charitable-admin-settings';
891 }
892
893 if ( isset( $_GET['page'] ) && 'charitable-settings' === $_GET['page'] && isset( $_GET['tab'] ) && '' !== $_GET['tab'] ) { // phpcs:ignore.
894 $classes .= ' charitable-admin-settings-' . esc_attr( $_GET['tab'] ); // phpcs:ignore.
895 }
896
897 if ( isset( $_GET['page'] ) && 'charitable-settings' === $_GET['page'] && isset( $_GET['tab'] ) && 'gateways' === $_GET['tab'] && empty( $_GET['group'] ) ) { // phpcs:ignore.
898 $classes .= ' charitable-admin-settings-gateways-main'; // phpcs:ignore.
899 }
900
901 if ( isset( $_GET['page'] ) && 'charitable-reports' === $_GET['page'] ) { // phpcs:ignore.
902 $classes .= ' charitable-admin-reports';
903 }
904
905 if ( isset( $_GET['page'] ) && 'charitable-dashboard' === $_GET['page'] && ! isset( $_GET['tab'] ) ) { // phpcs:ignore.
906 $classes .= ' charitable-admin-reports-dashboard';
907 }
908
909 if ( isset( $_GET['page'] ) && 'charitable-reports' === $_GET['page'] && isset( $_GET['tab'] ) && '' !== $_GET['tab'] ) { // phpcs:ignore.
910 $classes .= ' charitable-admin-reports charitable-admin-reports-' . esc_attr( $_GET['tab'] ); // phpcs:ignore.
911 }
912
913 if ( function_exists( 'charitable_use_legacy_dashboard' ) && charitable_use_legacy_dashboard() ) {
914 $classes .= ' charitable_legacy_dashboard';
915 }
916
917 if ( charitable_show_promotion_footer() ) {
918 $classes .= ' charitable-admin-promotion-footer';
919 }
920
921 $classes .= function_exists( 'charitable_is_pro' ) && charitable_is_pro() ? ' charitable-pro' : ' charitable';
922
923 return $classes;
924 }
925
926 /**
927 * Add notices to the dashboard.
928 *
929 * @since 1.4.0
930 * @version 1.8.3 Added check for PHP Version deprecation.
931 *
932 * @return void
933 */
934 public function add_notices() {
935 if ( charitable_is_debug( 'square' ) ) {
936 error_log( 'Charitable_Admin::add_notices() - Starting to render admin notices' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
937 }
938
939 // Render notices.
940 charitable_get_admin_notices()->render();
941
942 if ( charitable_is_debug( 'square' ) ) {
943 error_log( 'Charitable_Admin::add_notices() - Current admin notices: ' . print_r( charitable_get_admin_notices()->get_notices(), true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
944 }
945
946 // Check for PHP version deprecation.
947 if ( version_compare( PHP_VERSION, '7.4', '<' ) ) {
948 $this->php_notice_deprecated();
949 }
950
951 // Add third party notices.
952 $this->add_third_party_notices();
953
954 // Add version update notices.
955 $this->add_version_update_notices();
956 }
957
958 /**
959 * Add notices for potential third party warnings.
960 *
961 * @since 1.7.0.8
962 */
963 public function add_third_party_notices() {
964 if ( ! current_user_can( 'manage_options' ) ) {
965 return;
966 }
967
968 // Allow this feature to be disabled in case unforeseen problems come up.
969 if ( false === apply_filters( 'charitable_donations_export_args', true ) ) {
970 return;
971 }
972
973 $notices = array(
974 /* translators: %s: link */
975 'code-snippets/code-snippets.php' => sprintf(
976 // Translators: %s: link.
977 __( "You appear to be using a code snippet plugin. Please <a href='%s' target='_blank'>review best practices</a> when using snippets to avoid problems when upgrading or deactivating Charitable.", 'charitable' ),
978 'https://wpcharitable.com/code-snippet-best-practices/'
979 ),
980 );
981
982 // Allow this list to be altered by a third party or charitable addon.
983 $notices = apply_filters( 'charitable_admin_third_party_notices', $notices );
984
985 $helper = charitable_get_admin_notices();
986
987 foreach ( $notices as $notice => $message ) {
988 if ( false === charitable_get_third_party_warning_option( 'code-snippets/code-snippets.php' ) || 'dismissed' === charitable_get_third_party_warning_option( 'code-snippets/code-snippets.php' ) ) {
989 continue;
990 }
991
992 $helper->add_third_party_warning( $message, $notice );
993 }
994 }
995
996 /**
997 * Add version update notices to the dashboard.
998 *
999 * @since 1.4.6 (deprecated in 1.8.0)
1000 *
1001 * @return void
1002 */
1003 public function add_version_update_notices() {
1004 if ( ! current_user_can( 'manage_options' ) ) {
1005 return;
1006 }
1007
1008 $notices = array(
1009 'release-150' => sprintf(
1010 /* translators: %s: link */
1011 __( "Charitable 1.5 is packed with new features and improvements. <a href='%s' target='_blank'>Find out what's new</a>.", 'charitable' ),
1012 'https://wpcharitable.com/charitable-1-5-release-notes/?utm_source=WordPress&utm_campaign=WP+Charitable&utm_medium=Admin+Notice&utm_content=Version+One+Five+Whats+New'
1013 ),
1014 'release-160' => sprintf(
1015 /* translators: %s: link */
1016 __( 'Charitable 1.6 introduces important new user privacy features and other improvements. <a href="%s" target="_blank">Find out what\'s new</a>.', 'charitable' ),
1017 'https://www.wpcharitable.com/charitable-1-6-user-privacy-gdpr-better-refunds-and-a-new-shortcode/?utm_source=WordPress&utm_campaign=WP+Charitable&utm_medium=Admin+Notice&utm_content=Version+One+Six+Whats+New'
1018 ),
1019 );
1020
1021 $helper = charitable_get_admin_notices();
1022
1023 foreach ( $notices as $notice => $message ) {
1024 if ( ! get_transient( 'charitable_' . $notice . '_notice' ) ) {
1025 continue;
1026 }
1027
1028 $helper->add_version_update( $message, $notice );
1029 }
1030 }
1031
1032 /**
1033 * Add php decpreation notices.
1034 *
1035 * @since 1.8.3
1036 *
1037 * @return void
1038 */
1039 public function php_notice_deprecated() {
1040 $medium = charitable_is_pro() ? 'proplugin' : 'liteplugin';
1041 ?>
1042 <div class="notice notice-error">
1043 <p>
1044 <?php
1045 echo wp_kses(
1046 sprintf(
1047 // Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - Opening HTML link tag, 4 - Closing HTML link tag.
1048 __( 'Your site is running an %1$soutdated version%2$s of PHP that is no longer supported and may cause issues with %3$s. Please contact your web hosting provider to update your PHP version or switch to a %4$srecommended WordPress hosting company%5$s.', 'charitable' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
1049 '<strong>',
1050 '</strong>',
1051 '<strong>Charitable</strong>',
1052 '<a href="https://www.wpbeginner.com/wordpress-hosting/" target="_blank" rel="noopener noreferrer">',
1053 '</a>'
1054 ),
1055 array(
1056 'a' => array(
1057 'href' => array(),
1058 'target' => array(),
1059 'rel' => array(),
1060 ),
1061 'strong' => array(),
1062 )
1063 );
1064 ?>
1065 <br><br>
1066 <?php
1067 echo wp_kses(
1068 sprintf(
1069 // phpcs:ignore Generic.Files.LineLength.MaxExceeded
1070 // Translators: 1 - Opening HTML bold tag, 2 - Closing HTML bold tag, 3 - The PHP version, 4 - The current year, 5 - The short plugin name ("Charitable"), 6 - Opening HTML link tag, 7 - Closing HTML link tag.
1071 __( '%1$sNote:%2$s Support for PHP %3$s will be discontinued in %4$s. After this, if no further action is taken, %5$s functionality will be disabled. %6$sRead more for additional information.%7$s', 'charitable' ), // phpcs:ignore Generic.Files.LineLength.MaxExceeded
1072 '<strong>',
1073 '</strong>',
1074 PHP_VERSION,
1075 gmdate( 'Y' ),
1076 'Charitable',
1077 '<a href="https://wpcharitable.com/documentation/supported-php-versions-for-charitable/?utm_source=WordPress&utm_medium=' . $medium . '&utm_campaign=outdated-php-notice" target="_blank" rel="noopener noreferrer">', // phpcs:ignore Generic.Files.LineLength.MaxExceeded
1078 '</a>'
1079 ),
1080 array(
1081 'a' => array(
1082 'href' => array(),
1083 'target' => array(),
1084 'rel' => array(),
1085 ),
1086 'strong' => array(),
1087 )
1088 );
1089 ?>
1090 </p>
1091 </div>
1092
1093 <?php
1094 // In case this is on plugin activation.
1095 if ( isset( $_GET['activate'] ) ) { // phpcs:ignore HM.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Recommended
1096 unset( $_GET['activate'] );
1097 }
1098 }
1099
1100 /**
1101 * Dismiss a notice.
1102 *
1103 * @since 1.4.6
1104 *
1105 * @return void
1106 */
1107 public function dismiss_notice() {
1108 if ( ! isset( $_POST['notice'] ) ) { // phpcs:ignore.
1109 wp_send_json_error();
1110 }
1111
1112 $notice = sanitize_text_field( wp_unslash( $_POST['notice'] ) ); // phpcs:ignore.
1113
1114 // Handle Square connection error notice.
1115 if ( 'square_connection_error' === $notice ) {
1116 delete_option( 'charitable_square_connection_error_notice' );
1117 wp_send_json_success();
1118 return;
1119 }
1120
1121 $ret = ( 'noted' === charitable_get_third_party_warning_option( 'code-snippets/code-snippets.php' ) ) ? charitable_set_third_party_warning_option( 'code-snippets/code-snippets.php', 'dismissed' ) : delete_transient( 'charitable_' . $notice . '_notice', true );
1122
1123 do_action( 'charitable_dismiss_notice', $_POST ); // phpcs:ignore.
1124
1125 if ( ! $ret ) {
1126 wp_send_json_error( $ret );
1127 }
1128
1129 wp_send_json_success();
1130 }
1131
1132 /**
1133 * Dismiss a banner.
1134 *
1135 * @since 1.7.0
1136 *
1137 * @return void
1138 */
1139 public function dismiss_banner() {
1140 if ( ! isset( $_POST['banner_id'] ) ) { // phpcs:ignore.
1141 wp_send_json_error();
1142 }
1143
1144 $ret = set_transient( 'charitable_' . esc_attr( $_POST['banner_id'] ) . '_banner', 1, WEEK_IN_SECONDS ); // phpcs:ignore.
1145
1146 if ( ! $ret ) {
1147 wp_send_json_error( $ret );
1148 }
1149
1150 wp_send_json_success();
1151 }
1152
1153 /**
1154 * Dismiss a banner.
1155 *
1156 * @since 1.8.0
1157 *
1158 * @return void
1159 */
1160 public function dismiss_list_banner() {
1161 if ( ! isset( $_POST['banner_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
1162 wp_send_json_error();
1163 }
1164
1165 $ret = set_transient( 'charitable_' . esc_attr( $_POST['banner_id'] ) . '_list_banner', 1, WEEK_IN_SECONDS ); // phpcs:ignore.
1166
1167 if ( ! $ret ) {
1168 wp_send_json_error( $ret );
1169 }
1170
1171 wp_send_json_success();
1172 }
1173
1174 /**
1175 * Dismiss a five star rating.
1176 *
1177 * @since 1.7.0
1178 *
1179 * @return void
1180 */
1181 public function dismiss_five_star_rating() {
1182 if ( ! isset( $_POST['banner_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
1183 wp_send_json_error();
1184 }
1185
1186 $check = get_transient( 'charitable_' . esc_attr( $_POST['banner_id'] ) . '_banner' ); // phpcs:ignore.
1187
1188 if ( ! $check ) {
1189 $ret = set_transient( 'charitable_' . esc_attr( $_POST['banner_id'] ) . '_banner', true ); // phpcs:ignore.
1190 if ( ! $ret ) {
1191 wp_send_json_error( $ret );
1192 }
1193 }
1194
1195 wp_send_json_success();
1196 }
1197
1198 /**
1199 * Dismiss dashboard notices.
1200 *
1201 * @since 1.8.1.6
1202 *
1203 * @return void
1204 */
1205 public function dismiss_dashboard_notices() {
1206 if ( ! isset( $_POST['notice_ids'] ) ) { // phpcs:ignore.
1207 wp_send_json_error();
1208 }
1209
1210 $ret = delete_transient( 'charitable_dashboard_notices' );
1211
1212 // take a comma delimited $_POST value and turn it into an array that is properly filtered only for alphanumeric values.
1213 $notices = array_filter( array_map( 'sanitize_text_field', explode( ',', $_POST['notice_ids'] ) ) ); // phpcs:ignore
1214
1215 if ( ! empty( $notices ) && in_array( 'donation-security', $notices, true ) ) {
1216 delete_transient( 'charitable_donation_security_checks' );
1217 }
1218
1219 if ( ! $ret ) {
1220 wp_send_json_error( $ret );
1221 }
1222
1223 wp_send_json_success();
1224 }
1225
1226 /**
1227 * Dismiss guide tool notices.
1228 *
1229 * @since 1.8.1.6
1230 *
1231 * @return void
1232 */
1233 public function dismiss_growth_tools_notices() {
1234
1235 // check the nonce via ajax.
1236 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'charitable_dismiss_growth_tools' ) ) { // phpcs:ignore
1237 wp_send_json_error();
1238 }
1239
1240 $notice_type = isset( $_POST['notice_type'] ) ? sanitize_text_field( wp_unslash( $_POST['notice_type'] ) ) : ''; // phpcs:ignore
1241
1242 if ( ! $notice_type ) {
1243 wp_send_json_error();
1244 }
1245
1246 $charitable_growth_tool_notices = get_option( 'charitable_growth_tool_notices' );
1247
1248 if ( ! is_array( $charitable_growth_tool_notices ) ) {
1249 $charitable_growth_tool_notices = array();
1250 }
1251
1252 $charitable_growth_tool_notices['dismiss'][ $notice_type ] = time();
1253
1254 $ret = update_option( 'charitable_growth_tool_notices', $charitable_growth_tool_notices );
1255
1256 if ( ! $ret ) {
1257 wp_send_json_error( $ret );
1258 }
1259
1260 wp_send_json_success();
1261 }
1262
1263 /**
1264 * Dismiss a five star rating.
1265 *
1266 * @since 1.7.0
1267 *
1268 * @return void
1269 */
1270 public function dismiss_lite_cta() {
1271 if ( ! isset( $_POST['charitable_action'] ) ) { // phpcs:ignore.
1272 wp_send_json_error();
1273 }
1274
1275 $updated = update_option( 'charitable_lite_settings_upgrade', true );
1276
1277 if ( $updated ) {
1278 wp_send_json_success();
1279 } else {
1280 wp_send_json_error();
1281 }
1282 }
1283
1284 /**
1285 * Dismiss a five star rating.
1286 *
1287 * @since 1.8.1.6
1288 *
1289 * @return void
1290 */
1291 public function dismiss_lite_reports_cta() {
1292 if ( ! isset( $_POST['charitable_action'] ) ) { // phpcs:ignore.
1293 wp_send_json_error();
1294 }
1295
1296 $updated = update_option( 'charitable_lite_reports_upgrade', true );
1297
1298 if ( $updated ) {
1299 wp_send_json_success();
1300 } else {
1301 wp_send_json_error();
1302 }
1303 }
1304
1305 /**
1306 * Adds one or more classes to the body tag in the dashboard.
1307 *
1308 * @since 1.0.0
1309 *
1310 * @param string $classes Current body classes.
1311 * @return string Altered body classes.
1312 */
1313 public function add_admin_body_class( $classes ) {
1314 $screen = get_current_screen();
1315
1316 if ( in_array( $screen->post_type, array( Charitable::DONATION_POST_TYPE, Charitable::CAMPAIGN_POST_TYPE ) ) ) {
1317 $classes .= ' post-type-charitable';
1318 }
1319
1320 // Add `charitable-settings-page-{tab}` so per-tab CSS (e.g. Payment
1321 // Gateways tab layout overrides in charitable-admin.css) can scope
1322 // itself without affecting other settings tabs.
1323 if ( isset( $_GET['page'] ) && 'charitable-settings' === $_GET['page'] && isset( $_GET['tab'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1324 $classes .= ' charitable-settings-page-' . sanitize_html_class( wp_unslash( $_GET['tab'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1325 }
1326
1327 return $classes;
1328 }
1329
1330 /**
1331 * Add custom links to the plugin actions.
1332 *
1333 * @since 1.0.0
1334 *
1335 * @param string[] $links Plugin action links.
1336 * @return string[]
1337 */
1338 public function add_plugin_action_links( $links ) {
1339 $links[] = '<a href="' . admin_url( 'admin.php?page=charitable-settings' ) . '">' . __( 'Settings', 'charitable' ) . '</a>';
1340 return $links;
1341 }
1342
1343 /**
1344 * Add Extensions link to the plugin row meta.
1345 *
1346 * @since 1.2.0
1347 *
1348 * @param string[] $links Plugin action links.
1349 * @param string $file The plugin file.
1350 * @return string[] $links
1351 */
1352 public function add_plugin_row_meta( $links, $file ) {
1353 if ( plugin_basename( charitable()->get_path() ) != $file ) {
1354 return $links;
1355 }
1356
1357 // Use an updated UTM.
1358 $extensions_link = charitable_ga_url(
1359 'https://wpcharitable.com/extensions/',
1360 urlencode( 'Admin Plugin Page' ),
1361 urlencode( 'Extensions' )
1362 );
1363
1364 $links[] = '<a href="' . $extensions_link . '" target="_blank" rel="noopener">' . __( 'Extensions', 'charitable' ) . '</a>';
1365
1366 return $links;
1367 }
1368
1369 /**
1370 * Remove the jQuery UI styles added by Ninja Forms.
1371 *
1372 * @since 1.2.0
1373 *
1374 * @param string $context Media buttons context.
1375 * @return string
1376 */
1377 public function remove_jquery_ui_styles_nf( $context ) {
1378 wp_dequeue_style( 'jquery-smoothness' );
1379 return $context;
1380 }
1381
1382 /**
1383 * Export donations.
1384 *
1385 * @since 1.3.0
1386 *
1387 * @return false|void Returns false if the export failed. Exits otherwise.
1388 */
1389 public function export_donations() {
1390
1391 if ( ! isset( $_GET['_charitable_export_nonce'] ) || ! wp_verify_nonce( $_GET['_charitable_export_nonce'], 'charitable_export_donations' ) ) { // phpcs:ignore
1392 return false;
1393 }
1394
1395 /**
1396 * Filter the donation export arguments.
1397 *
1398 * @since 1.3.0
1399 *
1400 * @param array $args Export arguments.
1401 */
1402 $export_args = apply_filters(
1403 'charitable_donations_export_args',
1404 array(
1405 'start_date' => isset( $_GET['start_date'] ) ? sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) : '',
1406 'end_date' => isset( $_GET['end_date'] ) ? sanitize_text_field( wp_unslash( $_GET['end_date'] ) ) : '',
1407 'status' => isset( $_GET['status'] ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : '',
1408 'campaign_id' => isset( $_GET['campaign_id'] ) ? sanitize_text_field( wp_unslash( $_GET['campaign_id'] ) ) : '',
1409 'report_type' => isset( $_GET['report_type'] ) ? sanitize_text_field( wp_unslash( $_GET['report_type'] ) ) : '',
1410 )
1411 );
1412
1413 /**
1414 * Filter the export class name.
1415 *
1416 * @since 1.3.0
1417 *
1418 * @param string $report_type The type of report.
1419 * @param array $args Export arguments.
1420 */
1421 $export_class = apply_filters( 'charitable_donations_export_class', 'Charitable_Export_Donations', esc_attr( $_GET['report_type'] ), $export_args ); // phpcs:ignore
1422
1423 new $export_class( $export_args );
1424
1425 die();
1426 }
1427
1428 /**
1429 * Export campaigns.
1430 *
1431 * @since 1.6.0
1432 *
1433 * @return false|void Returns false if the export failed. Exits otherwise.
1434 */
1435 public function export_campaigns() {
1436
1437 if ( ! isset( $_GET['_charitable_export_nonce'] ) || ! wp_verify_nonce( $_GET['_charitable_export_nonce'], 'charitable_export_campaigns' ) ) { // phpcs:ignore
1438 return false;
1439 }
1440
1441 /**
1442 * Filter the campaign export arguments.
1443 *
1444 * @since 1.6.0
1445 *
1446 * @param array $args Export arguments.
1447 */
1448 $export_args = apply_filters(
1449 'charitable_campaigns_export_args',
1450 array(
1451 'start_date_to' => isset( $_GET['start_date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['start_date_to'] ) ) : '',
1452 'end_date_from' => isset( $_GET['end_date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['end_date_from'] ) ) : '',
1453 'end_date_to' => isset( $_GET['end_date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['end_date_to'] ) ) : '',
1454 'status' => isset( $_GET['status'] ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : '',
1455 'report_type' => isset( $_GET['report_type'] ) ? sanitize_text_field( wp_unslash( $_GET['report_type'] ) ) : '',
1456 )
1457 );
1458
1459 /**
1460 * Filter the export class name.
1461 *
1462 * @since 1.6.0
1463 *
1464 * @param string $report_type The type of report.
1465 * @param array $args Export arguments.
1466 */
1467 $export_class = apply_filters( 'charitable_campaigns_export_class', 'Charitable_Export_Campaigns', esc_attr( $_GET['report_type'] ), $export_args ); // phpcs:ignore
1468
1469 new $export_class( $export_args );
1470
1471 die();
1472 }
1473
1474 /**
1475 * Returns an array of screen IDs where the Charitable scripts should be loaded.
1476 * (Deprecated in 1.8.1.15, use charitable_get_charitable_Screens() in core functions instead).
1477 *
1478 * @uses charitable_admin_screens
1479 *
1480 * @since 1.0.0
1481 *
1482 * @return array
1483 */
1484 public function get_charitable_screens() {
1485 /**
1486 * Filter admin screens where Charitable styles & scripts should be loaded.
1487 *
1488 * @since 1.8.0
1489 * @since 1.8.1 Added `charitable_page_charitable-dashboard` and 'charitable_page_charitable-reports' to the list of screens.
1490 * @since 1.8.1.6 Added 'charitable_page_charitable-tools' and 'charitable_page_charitable-growth-tool' to the list of screens.
1491 * @since 1.8.5 Added 'charitable_page_charitable-donors' to the list of screens.
1492 *
1493 * @param string[] $screens List of screen ids.
1494 */
1495 return apply_filters(
1496 'charitable_admin_screens',
1497 array(
1498 'campaign',
1499 'donation',
1500 'charitable_page_charitable-reports',
1501 'charitable_page_charitable-dashboard',
1502 'charitable_page_charitable-settings',
1503 'charitable_page_charitable-tools',
1504 'charitable_page_charitable-growth-tools',
1505 'edit-campaign',
1506 'edit-donation',
1507 'dashboard',
1508 'toplevel_page_charitable',
1509 'charitable_page_charitable-addons',
1510 'charitable_page_charitable-donors',
1511 )
1512 );
1513 }
1514
1515 /**
1516 * Updates the "action row" of campaigns to add the ability to edit with builder.
1517 *
1518 * @since 1.8.0
1519 *
1520 * @param array $actions Actions.
1521 * @param array $post Post data.
1522 *
1523 * @return array
1524 */
1525 public function campaign_action_row( $actions, $post ) {
1526
1527 $actions_first = array();
1528
1529 $suffix = ( charitable_is_debug() || charitable_is_script_debug() ) ? '#charitabledebug' : false;
1530
1531 // Check for the post type.
1532
1533 if ( is_object( $post ) && isset( $post->post_type ) && 'campaign' === $post->post_type && ( ! isset( $_GET['post_status'] ) || ( isset( $_GET['post_status'] ) && 'trash' !== strtolower( $_GET['post_status'] ) ) ) ) { // phpcs:ignore
1534
1535 $post_id = intval( $post->ID );
1536
1537 // Determine if this is a "legacy" campaign.
1538 $is_legacy_campaign = charitable_is_campaign_legacy( $post_id );
1539
1540 if ( isset( $actions['edit'] ) && 0 !== $post_id ) {
1541
1542 $actions_show_id = array( 'id' => esc_html__( 'ID: ', 'charitable' ) . $post_id );
1543 $actions_edit_legacy = array( 'edit' => $actions['edit'] );
1544
1545 unset( $actions['edit'] );
1546
1547 }
1548
1549 if ( defined( 'CHARITABLE_BUILDER_SHOW_LEGACY_EDIT_LINKS' ) && CHARITABLE_BUILDER_SHOW_LEGACY_EDIT_LINKS === true ) {
1550
1551 // Add "edit" link that goes to the builder. The campaign can still be edited w/ legacy via direct link (example https://website.test/wp-admin/post.php?post=246&action=edit).
1552 $actions_edit_legacy = array( 'Edit' => '<a href="' . admin_url( 'post.php?post=' . $post_id . '&action=edit' ) . '">' . esc_html__( 'Legacy Edit', 'charitable' ) . '</a>' );
1553
1554 $actions = ( false === $is_legacy_campaign )
1555 ? $actions_show_id + $actions_edit_legacy + array( 'charitable_builder' => '<a href="' . admin_url( 'admin.php?page=charitable-campaign-builder&campaign_id=' . $post_id ) . $suffix . '">' . esc_html__( 'Edit With Builder', 'charitable' ) . '</a>' ) + $actions
1556 : $actions_show_id + $actions_edit_legacy + $actions;
1557
1558 } else {
1559
1560 $actions = ( false === $is_legacy_campaign )
1561 ? $actions_show_id + array( 'charitable_builder' => '<a href="' . admin_url( 'admin.php?page=charitable-campaign-builder&campaign_id=' . $post_id ) . $suffix . '">' . esc_html__( 'Edit', 'charitable' ) . '</a>' ) + $actions
1562 : $actions_show_id + $actions_edit_legacy + $actions;
1563
1564 }
1565 }
1566
1567 return $actions;
1568 }
1569
1570 /**
1571 * Updates the "post link" of campaigns to add the ability to edit with builder.
1572 *
1573 * @since 1.8.0
1574 * @since 1.8.1 added check for get_current_screen().
1575 *
1576 * @param string $link The current link URL.
1577 * @param integer $post_id Post ID.
1578 * @param array $context Context.
1579 *
1580 * @return array
1581 */
1582 public function campaign_link( $link, $post_id, $context ) { // phpcs:ignore
1583
1584 if ( ! function_exists( 'get_current_screen' ) ) {
1585 return $link;
1586 }
1587
1588 $screen = get_current_screen();
1589 $post_id = intval( $post_id );
1590
1591 if ( 0 === $post_id || ! isset( $screen->id ) || $screen->id !== 'edit-campaign' ) {
1592 return $link;
1593 }
1594
1595 // Determine if this is a "legacy" campaign.
1596 $is_legacy_campaign = charitable_is_campaign_legacy( $post_id );
1597
1598 $suffix = ( charitable_is_debug() || charitable_is_script_debug() ) ? '#charitabledebug' : false;
1599
1600 $new_link = ( false === $is_legacy_campaign )
1601 ? admin_url( 'admin.php?page=charitable-campaign-builder&campaign_id=' . $post_id ) . $suffix
1602 : esc_url( $link );
1603
1604 return $new_link;
1605 }
1606
1607 /**
1608 * Filters the URL used for a campaign preview.
1609 *
1610 * @since 1.8.0
1611 *
1612 * @param string $preview_link URL used for the post preview.
1613 * @param WP_Post $post Post object.
1614 */
1615 public function campaign_preview_link( $preview_link, $post ) {
1616
1617 if ( ! empty( $post ) && 'campaign' === $post->post_type ) {
1618
1619 $preview_link = add_query_arg(
1620 array(
1621 'charitable_campaign_preview' => $post->ID,
1622 ),
1623 $preview_link
1624 );
1625
1626 }
1627
1628 return $preview_link;
1629 }
1630
1631 /**
1632 * Adjustments needed for Elementor.
1633 * Last tested with Elementor Lite 3.24.7.
1634 *
1635 * @since 1.8.3
1636 *
1637 * @param array $post_types_objects The post types.
1638 *
1639 * @return void
1640 */
1641 public function elementor_post_types( $post_types_objects = array() ) {
1642
1643 // If Elementor is not installed, ignore.
1644 if ( ! defined( 'ELEMENTOR_VERSION' ) ) {
1645 return;
1646 }
1647
1648 // Check for the Charitable Elementor integration override.
1649 if ( defined( 'CHARITABLE_ELEMENTOR_ALLOW_POST_TYPE' ) && CHARITABLE_ELEMENTOR_ALLOW_POST_TYPE ) {
1650 return;
1651 }
1652
1653 $post_types_to_remove = apply_filters( 'charitable_elementor_post_types_to_remove', array( 'campaign' ) );
1654
1655 if ( empty( $post_types_to_remove ) || ! is_array( $post_types_to_remove ) ) {
1656 return $post_types_objects;
1657 }
1658
1659 // Remove all the post types from the object array.
1660 foreach ( $post_types_to_remove as $post_type ) {
1661 if ( isset( $post_types_objects[ $post_type ] ) ) {
1662 unset( $post_types_objects[ $post_type ] );
1663 }
1664 }
1665
1666 return $post_types_objects;
1667 }
1668
1669 /**
1670 * Install addon via AJAX.
1671 *
1672 * @since 1.8.7.6
1673 */
1674 public function install_addon() {
1675 // phpcs:disable WordPress.Security.NonceVerification.Missing
1676 // Check nonce.
1677 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- wp_verify_nonce handles validation
1678 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'charitable_admin_addons' ) ) {
1679 wp_send_json_error( 'Invalid nonce' );
1680 }
1681
1682 // Check permissions.
1683 if ( ! charitable_can_install( 'plugin' ) ) {
1684 wp_send_json_error( 'Insufficient permissions' );
1685 }
1686
1687 $plugin = isset( $_POST['plugin'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) : '';
1688 $type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '';
1689 // phpcs:enable WordPress.Security.NonceVerification.Missing
1690
1691 if ( empty( $plugin ) ) {
1692 wp_send_json_error( 'Plugin not specified' );
1693 }
1694
1695 // Ensure filesystem access (request credentials if needed) similar to legacy flow.
1696 require_once ABSPATH . 'wp-admin/includes/file.php';
1697 $method = '';
1698 $url = esc_url( admin_url( 'admin.php?page=charitable-about' ) );
1699
1700 ob_start();
1701 $creds = request_filesystem_credentials( $url, $method, false, false, null );
1702 if ( false === $creds ) {
1703 $form = ob_get_clean();
1704 wp_send_json_success( array( 'form' => $form ) );
1705 }
1706
1707 if ( ! WP_Filesystem( $creds ) ) {
1708 ob_start();
1709 request_filesystem_credentials( $url, $method, true, false, null );
1710 $form = ob_get_clean();
1711 wp_send_json_success( array( 'form' => $form ) );
1712 }
1713
1714 // Snapshot installed plugins BEFORE the install so we can identify what
1715 // was actually unzipped. The legacy slug-derivation in
1716 // charitable_get_plugin_basename_from_slug() reduces a download URL like
1717 // `…/charitable-pro-plugin-1.8.13.5.zip` to the slug
1718 // `charitable-pro-plugin-1.8.13.5`, then expects an installed plugin
1719 // directory whose name *starts* with that slug — which doesn't hold for
1720 // any addon whose zip filename includes a version suffix and whose
1721 // directory does not (e.g. Charitable Pro lives at `charitable-pro/`).
1722 // The before/after diff sidesteps that mismatch entirely.
1723 if ( ! function_exists( 'get_plugins' ) ) {
1724 require_once ABSPATH . 'wp-admin/includes/plugin.php';
1725 }
1726 $plugins_before = array_keys( get_plugins() );
1727
1728 // Download and install the plugin.
1729 $result = charitable_install_wporg_plugin( $plugin );
1730
1731 if ( is_wp_error( $result ) ) {
1732 wp_send_json_error( $result->get_error_message() );
1733 }
1734
1735 // Force a fresh re-scan of the plugins directory and compute the diff.
1736 wp_cache_delete( 'plugins', 'plugins' );
1737 $plugins_after = array_keys( get_plugins() );
1738 $newly_installed = array_values( array_diff( $plugins_after, $plugins_before ) );
1739
1740 if ( ! empty( $newly_installed ) ) {
1741 $plugin_basename = $newly_installed[0];
1742 } else {
1743 // Fallback to the legacy slug-derived guess if for some reason no new
1744 // plugin was detected (e.g. the zip overwrote an existing install).
1745 $plugin_basename = charitable_get_plugin_basename_from_slug( $plugin );
1746 }
1747
1748 wp_send_json_success( array(
1749 'msg' => __( 'Plugin installed successfully', 'charitable' ),
1750 'basename' => $plugin_basename,
1751 'is_activated' => false,
1752 ) );
1753 }
1754
1755 /**
1756 * Activate addon via AJAX.
1757 *
1758 * @since 1.8.7.6
1759 */
1760 public function activate_addon() {
1761 // phpcs:disable WordPress.Security.NonceVerification.Missing
1762 // Check nonce.
1763 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- wp_verify_nonce handles validation
1764 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'charitable_admin_addons' ) ) {
1765 wp_send_json_error( 'Invalid nonce' );
1766 }
1767
1768 // Check permissions.
1769 if ( ! charitable_can_activate( 'plugin' ) ) {
1770 wp_send_json_error( 'Insufficient permissions' );
1771 }
1772
1773 $plugin = isset( $_POST['plugin'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) : '';
1774 // phpcs:enable WordPress.Security.NonceVerification.Missing
1775
1776 if ( empty( $plugin ) ) {
1777 wp_send_json_error( 'Plugin not specified' );
1778 }
1779
1780 // Normalize plugin basename if a folder only or slug was passed.
1781 if ( false === strpos( $plugin, '/' ) ) {
1782 $plugin = charitable_get_plugin_basename_from_slug( $plugin );
1783 }
1784
1785 // Activate the plugin.
1786 $result = charitable_activate_wporg_plugin( $plugin );
1787
1788 if ( is_wp_error( $result ) ) {
1789 wp_send_json_error( $result->get_error_message() );
1790 }
1791
1792 $response = array( 'msg' => __( 'Plugin activated successfully', 'charitable' ) );
1793
1794 // Activating Charitable Pro auto-deactivates Charitable Lite, which means
1795 // the screen the user came from (page=charitable-addons, served by Lite)
1796 // no longer exists in the menu. Redirect them to the Charitable dashboard
1797 // so they don't land on a 404 / blank admin page after the AJAX returns.
1798 if ( 0 === strpos( $plugin, 'charitable-pro/' ) ) {
1799 $response['redirect'] = admin_url( 'admin.php?page=charitable-dashboard' );
1800 }
1801
1802 wp_send_json_success( $response );
1803 }
1804
1805 /**
1806 * Deactivate addon via AJAX.
1807 *
1808 * @since 1.8.7.6
1809 */
1810 public function deactivate_addon() {
1811 // phpcs:disable WordPress.Security.NonceVerification.Missing
1812 // Check nonce.
1813 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- wp_verify_nonce handles validation
1814 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'charitable_admin_addons' ) ) {
1815 wp_send_json_error( 'Invalid nonce' );
1816 }
1817
1818 // Check permissions.
1819 if ( ! charitable_can_activate( 'plugin' ) ) {
1820 wp_send_json_error( 'Insufficient permissions' );
1821 }
1822
1823 $plugin = isset( $_POST['plugin'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) : '';
1824 // phpcs:enable WordPress.Security.NonceVerification.Missing
1825
1826 if ( empty( $plugin ) ) {
1827 wp_send_json_error( 'Plugin not specified' );
1828 }
1829
1830 // Deactivate the plugin.
1831 $result = charitable_deactivate_wporg_plugin( $plugin );
1832
1833 if ( is_wp_error( $result ) ) {
1834 wp_send_json_error( $result->get_error_message() );
1835 }
1836
1837 wp_send_json_success( 'Plugin deactivated successfully' );
1838 }
1839 }
1840
1841 endif;
1842