PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.12.2
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.12.2
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 / onboarding / class-charitable-setup.php

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

2,021 lines 59.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Setup.
4 *
5 * @package Charitable/Classes/Charitable_Setup
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.8.4
10 * @version 1.8.4
11 * @version 1.8.9.7
12 */
13
14 // Exit if accessed directly.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 if ( ! class_exists( 'Charitable_Setup' ) ) :
20
21 /**
22 * Charitable_Setup
23 *
24 * @final
25 * @since 1.8.4
26 */
27 class Charitable_Setup {
28
29 /**
30 * The steps.
31 *
32 * @var array
33 */
34 private $steps = array( 'meta', 'plugins_installed', 'plugins_activated', 'payment_methods', 'features', 'campaign' );
35
36 /**
37 * The plugin meta.
38 *
39 * @var array
40 */
41 private $plugin_meta = array(
42 'wp-mail-smtp' => array(
43 'name' => 'WP Mail SMTP',
44 'basename' => 'wp-mail-smtp/wp_mail_smtp.php',
45 ),
46 'optinmonster' => array(
47 'name' => 'OptinMonster',
48 'basename' => 'optinmonster/optin-monster-wp-api.php',
49 ),
50 'wpforms' => array(
51 'name' => 'WPForms',
52 'basename' => 'wpforms/wpforms.php',
53 ),
54 'google-analytics-for-wordpress' => array(
55 'name' => 'MonsterInsights',
56 'basename' => 'google-analytics-for-wordpress/googleanalytics.php',
57 ),
58 'all-in-one-seo-pack' => array(
59 'name' => 'All in One SEO',
60 'basename' => 'all-in-one-seo-pack/all_in_one_seo_pack.php',
61 ),
62 'duplicator' => array(
63 'name' => 'Duplicator',
64 'basename' => 'duplicator/duplicator.php',
65 ),
66 );
67
68 /**
69 * The feature meta.
70 *
71 * @since 1.8.4
72 *
73 * @var array
74 */
75 private $feature_meta = array(
76 'ambassadors' => array(
77 'name' => 'Ambassadors',
78 ),
79 'recurring' => array(
80 'name' => 'Recurring Donations',
81 ),
82 'fee-relief' => array(
83 'name' => 'Fee Relief',
84 ),
85 );
86
87 /**
88 * The private app_server_url property.
89 *
90 * @since 1.8.4
91 *
92 * @var string
93 */
94 private $app_server_url = 'https://app.wpcharitable.com/';
95
96 /**
97 * The private $stripe_redirect_url property.
98 *
99 * @var string
100 */
101 private $stripe_redirect_url = '';
102
103 /**
104 * The single instance of this class.
105 *
106 * @var Charitable_Setup|null
107 */
108 private static $instance = null;
109
110 /**
111 * Create object instance.
112 *
113 * @since 1.8.4
114 */
115 public function __construct() {
116
117 add_action( 'plugins_loaded', [ $this, 'hooks' ] );
118
119 $this->stripe_redirect_url = admin_url( 'admin.php?page=charitable-setup&setup=stripe-connect' );
120 }
121
122 /**
123 * Run things upon init.
124 *
125 * @since 1.8.4
126 *
127 * @return void
128 */
129 public function init() {
130 }
131
132 /**
133 * The hooks.
134 *
135 * @since 1.8.4
136 */
137 public function hooks() {
138
139 // Register the resume-banner dismiss listener unconditionally — it
140 // must fire during admin-ajax, which the guards below bail out of.
141 add_action( 'charitable_dashboard_notification_dismissed', [ $this, 'maybe_clear_onboarding_state_on_dismiss' ] );
142
143 // If user is in admin ajax or doing cron, return.
144 if ( wp_doing_ajax() || wp_doing_cron() ) {
145 return;
146 }
147
148 // If user cannot manage_options, return.
149 if ( ! charitable_current_user_can( 'administrator' ) ) {
150 return;
151 }
152
153 // Third party plugins.
154 add_filter( 'duplicator_disable_onboarding_redirect', '__return_true' );
155
156 // Resume onboarding.
157 add_action( 'init', [ $this, 'resume_onboarding' ] );
158 add_action( 'init', [ $this, 'cancel_onboarding' ] );
159
160 // Init.
161 add_action( 'admin_init', [ $this, 'redirect' ], 9999 );
162 }
163
164 /**
165 * Cancels the onboarding process.
166 *
167 * @since 1.8.4
168 *
169 * @return void
170 */
171 public function cancel_onboarding() {
172
173 if ( ! isset( $_GET['charitable_onboarding'] ) || 'cancel' !== $_GET['charitable_onboarding'] ) { // phpcs:ignore
174 return;
175 }
176
177 // Clear the transient.
178 delete_transient( 'charitable_activation_redirect' );
179
180 // Clear the option.
181 delete_option( 'charitable_started_onboarding' );
182
183 // If the user was going to the checklist, send them there.
184 $checklist_class = Charitable_Checklist::get_instance();
185 $redirect_url = ! $checklist_class->maybe_load_checklist_assets() ? admin_url( 'admin.php?page=charitable-dashboard' ) : admin_url( 'admin.php?page=charitable-setup-checklist' );
186
187 wp_safe_redirect( $redirect_url );
188 exit;
189 }
190
191 /**
192 * If the option 'charitable_start_onboarding' is set, then we will start the onboarding process, assuming the user lost connection, WP site logged out by the time app.wpcharitable.com came back, etc.
193 *
194 * @since 1.8.4
195 * @since 1.8.9.2 Added WordPress context awareness and smart page detection.
196 */
197 public function resume_onboarding() {
198
199 // Don't redirect during WordPress maintenance operations.
200 if ( defined( 'DOING_AUTOSAVE' ) || wp_is_json_request() ||
201 defined( 'IFRAME_REQUEST' ) || ( function_exists( 'wp_doing_heartbeat' ) && wp_doing_heartbeat() ) ) {
202 return;
203 }
204
205 // Check if we should consider redirection.
206 if ( false === (bool) get_option( 'charitable_started_onboarding', false ) ) {
207 return;
208 }
209
210 // Allow developers to disable resume onboarding via filter.
211 if ( apply_filters( 'charitable_disable_activation_redirect', false ) ) {
212 return;
213 }
214
215 // Don't redirect when the user is on any Charitable admin page
216 // OTHER than the welcome destination (`?page=charitable`).
217 // Charitable's sub-pages — `charitable-dashboard`, `charitable-settings-checklist`,
218 // etc. — each have their own menu_slug, so strict equality keeps the
219 // redirect scoped to `?page=charitable` itself (which IS the welcome
220 // screen) rather than yanking the user back from every sub-page.
221 if ( ! empty( $_GET['page'] ) && is_string( $_GET['page'] ) && 'charitable' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
222 return;
223 }
224
225 // Don't redirect from standard WordPress admin scripts that don't
226 // use a `?page=` parameter (e.g. the WP Dashboard, CPT lists, media library).
227 $current_page = basename( $_SERVER['PHP_SELF'] ?? '' );
228 $wp_admin_pages = array( 'index.php', 'edit.php', 'post-new.php', 'upload.php', 'users.php', 'edit-tags.php', 'edit-comments.php', 'themes.php', 'plugins.php', 'tools.php', 'options-general.php' );
229 if ( in_array( $current_page, $wp_admin_pages, true ) ) {
230 return;
231 }
232
233 // Don't redirect if user is accessing post type or taxonomy pages.
234 if ( isset( $_GET['post_type'] ) || isset( $_GET['taxonomy'] ) ) {
235 return;
236 }
237
238 // Make sure we aren't already on the setup welcome page.
239 if ( isset( $_GET['wpchar_lite'] ) && 'lite' === $_GET['wpchar_lite'] ) { // phpcs:ignore
240 return;
241 }
242
243 // Don't redirect if user is trying to cancel onboarding - let cancel_onboarding() method handle it.
244 if ( isset( $_GET['charitable_onboarding'] ) && 'cancel' === $_GET['charitable_onboarding'] ) { // phpcs:ignore
245 return;
246 }
247
248 // Build the URL for going back to the onboarding process.
249 $onboarding_url = add_query_arg( array( 'resume' => 'true' ), charitable_get_onboarding_url() );
250
251 wp_safe_redirect( admin_url( 'admin.php?page=charitable&wpchar_lite=lite&setup=welcome&resume=true' ) );
252 exit;
253 }
254
255 /**
256 * Clear the in-progress onboarding flag when the user dismisses the
257 * dashboard "resume setup wizard" banner.
258 *
259 * Triggered by the existing notification rail's dismiss AJAX. The
260 * action fires for ALL dismissed notifications, so we filter on slug
261 * here.
262 *
263 * Net effect: dismissing the banner ends the resume prompt everywhere.
264 * The banner is gone (the rail's existing dismissed-key filter handles
265 * that), and the `?page=charitable` resume redirect also defuses
266 * because it keys off the same option we delete here.
267 *
268 * @since 1.8.10.5
269 *
270 * @param string $notification_id Slug of the dismissed notification.
271 *
272 * @return void
273 */
274 public function maybe_clear_onboarding_state_on_dismiss( $notification_id ) {
275
276 if ( 'resume_setup_wizard' !== $notification_id ) {
277 return;
278 }
279
280 delete_option( 'charitable_started_onboarding' );
281 }
282
283 /**
284 * Onboarding welcome screen redirect.
285 *
286 * This function checks if a new install or update has just occurred. If so,
287 * then we redirect the user to the appropriate page.
288 *
289 * @since 1.8.1.12
290 * @version 1.8.3 tweak what is an initial install.
291 * @version 1.8.4 add charitable_started_onboarding option.
292 * @version 1.8.9.2 Added WordPress filter and context awareness.
293 */
294 public function redirect() {
295
296 // Don't redirect during WordPress maintenance operations.
297 if ( defined( 'DOING_AUTOSAVE' ) || wp_is_json_request() ||
298 defined( 'IFRAME_REQUEST' ) || ( function_exists( 'wp_doing_heartbeat' ) && wp_doing_heartbeat() ) ) {
299 return;
300 }
301
302 // Check if we should consider redirection.
303 if ( ! get_transient( 'charitable_activation_redirect' ) ) {
304 return;
305 }
306
307 // If we are redirecting, clear the transient so it only happens once.
308 delete_transient( 'charitable_activation_redirect' );
309
310 // Check option to disable welcome redirect.
311 if ( get_option( 'charitable_activation_redirect', false ) ) {
312 return;
313 }
314
315 // Allow developers to disable activation redirect via filter.
316 if ( apply_filters( 'charitable_disable_activation_redirect', false ) ) {
317 return;
318 }
319
320 // Only do this for single site installs.
321 if ( isset( $_GET['activate-multi'] ) || is_network_admin() ) { // phpcs:ignore WordPress.Security.NonceVerification
322 return;
323 }
324
325 // Check if this is an update or first install.
326 $upgrade = get_option( 'charitable_version_upgraded_from', [] );
327
328 if ( ! $upgrade || 1 === count( $upgrade ) ) {
329 // Initial install.
330 update_option( 'charitable_started_onboarding', 1 );
331 wp_safe_redirect( admin_url( 'admin.php?page=charitable&wpchar_lite=lite&setup=welcome&f=1' ) );
332 exit;
333 }
334 }
335
336 /**
337 * Enqueue assets.
338 *
339 * @since 1.8.4
340 */
341 public function enqueue_scripts() {
342
343 $min = charitable_get_min_suffix();
344 $version = charitable()->get_version();
345 $assets_dir = charitable()->get_path( 'assets', false );
346
347 /* The following styles are only loaded on Charitable screens. */
348 $screen = get_current_screen();
349
350 if ( $this->is_setup_page() ) {
351
352 wp_enqueue_script(
353 'campaign-admin-congrats-wizard-confetti',
354 charitable()->get_path( 'directory', false ) . 'assets/js/libraries/confetti.min.js',
355 [ 'jquery' ],
356 $version,
357 true
358 );
359
360 }
361
362 if ( $this->is_setup_page() || $this->is_welcome_page() ) {
363
364 wp_register_style(
365 'charitable-admin-user-onboarding',
366 $assets_dir . 'css/admin/charitable-admin-user-onboarding' . $min . '.css',
367 array(),
368 $version
369 );
370
371 wp_enqueue_style( 'charitable-admin-user-onboarding' );
372
373 if ( $this->is_setup_page() ) :
374
375 wp_register_script(
376 'charitable-admin-user-onboarding',
377 $assets_dir . 'js/admin/charitable-admin-user-onboarding' . $min . '.js',
378 array( 'jquery', 'campaign-admin-congrats-wizard-confetti' ),
379 $version,
380 true
381 );
382
383 wp_enqueue_script( 'charitable-admin-user-onboarding' );
384
385 wp_localize_script(
386 'charitable-admin-user-onboarding',
387 'charitable_setup',
388 $this->get_localized_strings()
389 );
390
391 endif;
392
393 }
394 }
395
396 /**
397 * Is this the setup page?
398 *
399 * @since 1.8.4
400 *
401 * @return bool
402 */
403 public function is_setup_page() {
404 return is_admin() && isset( $_GET['page'] ) && 'charitable-setup' === $_GET['page']; // phpcs:ignore
405 }
406
407 /**
408 * Is this the welcome page?
409 *
410 * @since 1.8.4
411 *
412 * @return bool
413 */
414 public function is_welcome_page() {
415 return is_admin() && isset( $_GET['page'] ) && 'charitable' === $_GET['page'] && isset( $_GET['wpchar_lite'] ) && 'lite' === $_GET['wpchar_lite'] && isset( $_GET['setup'] ) && ( 'cancelled' === $_GET['setup'] || 'welcome' === $_GET['setup'] || 'return' === $_GET['setup'] ); // phpcs:ignore
416 }
417
418 /**
419 * Maybe process the next step.
420 *
421 * @since 1.8.4
422 *
423 * @return void
424 */
425 public function maybe_process_step() {
426
427 // security check.
428 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'charitable-setup' ) ) { // phpcs:ignore
429 wp_send_json_error( array( 'message' => 'Invalid nonce.' ) );
430 }
431
432 $current_step = (string) $this->get_current_setup_step();
433
434 if ( 'meta' === $current_step ) {
435 $meta = get_option( 'charitable_setup_meta', array() );
436 $processed_meta = $this->process_meta( $meta );
437
438 // we are also going to process the payment options (enable them, leaving a possible Stripe connect for later near the end).
439 $payment_methods = $this->retrieve_payment_methods();
440 $processed_pm = $this->process_payment_methods( $payment_methods );
441 }
442
443 // return json success.
444 wp_send_json_success(
445 array(
446 'status' => 'success',
447 'processed_step' => $current_step,
448 'next_step' => $this->get_next_step(),
449 )
450 );
451 exit;
452 }
453
454 /**
455 * Get the next setup step by looking at the private $steps property and returning the next value.
456 *
457 * @param string $current_step The current step.
458 *
459 * @since 1.8.4
460 *
461 * @return string
462 */
463 public function get_next_step( $current_step = '' ) {
464
465 if ( ! $current_step ) {
466 $current_step = $this->get_current_setup_step();
467 }
468
469 $steps = $this->steps;
470
471 $next_step = '';
472
473 foreach ( $steps as $key => $step ) {
474 if ( $step === $current_step ) {
475 $next_step = $steps[ $key + 1 ];
476 break;
477 }
478 }
479
480 return $next_step;
481 }
482
483 /**
484 * Get the current setup step.
485 * Possible values: meta, plugins, payment_methods, features.
486 *
487 * @since 1.8.4
488 *
489 * @return int
490 */
491 public function get_current_setup_step() {
492
493 return 'meta';
494 }
495
496 /**
497 * Register the page.
498 *
499 * @since 1.3.0
500 *
501 * @return void
502 */
503 public function register_page() {
504 add_dashboard_page(
505 __( 'Setup Charitable', 'charitable' ),
506 __( 'Setup Charitable', 'charitable' ),
507 'manage_charitable_settings',
508 'charitable-setup',
509 array( $this, 'render_page' )
510 );
511 }
512
513 /**
514 * Remove the page from the dashboard menu.
515 *
516 * @since 1.3.0
517 *
518 * @return void
519 */
520 public function remove_page_from_menu() {
521 remove_submenu_page( 'index.php', 'charitable-setup' );
522 }
523
524 /**
525 * Render the page.
526 *
527 * @since 1.3.0
528 *
529 * @return void
530 */
531 public function render_page() {
532
533 $stripe_connect_url = '';
534 $stripe_connected = false;
535
536 $stripe_returned = isset( $_GET['setup'] ) && 'stripe-connect' === $_GET['setup'] ? true : false; // phpcs:ignore
537
538 if ( ! $stripe_returned ) {
539
540 $payment_methods = $this->retrieve_payment_methods();
541
542 if ( in_array( 'stripe', $payment_methods, true ) && class_exists( 'Charitable_Gateway_Stripe_AM' ) ) {
543 $stripe_gateway = new Charitable_Gateway_Stripe_AM();
544 $stripe_connect_url = ! $stripe_gateway->maybe_stripe_connected() ? $stripe_gateway->get_stripe_connect_url( $this->stripe_redirect_url ) : '';
545 $stripe_connected = $stripe_gateway->maybe_stripe_connected();
546 }
547 }
548
549 // The "continue" or "return" URL should the checklist by default, but on the chance the checklist is completed, we'll send them to the dashboard.
550 $checklist_class = Charitable_Checklist::get_instance();
551 $checklist_possible = $checklist_class->is_checklist_skipped() || $checklist_class->is_checklist_completed();
552 $completed_url = $checklist_possible ? admin_url( 'admin.php?page=charitable-dashboard' ) : admin_url( 'admin.php?page=charitable-setup-checklist' );
553
554 // If the checklist is not an option, check and see if they created a campaign. If so, send them to that draft campaign.
555 $campaign_admin_url = false !== get_option( 'charitable_setup_campaign_created', false )
556 ? admin_url( 'admin.php?page=charitable-campaign-builder&campaign_id=' . intval( get_option( 'charitable_setup_campaign_created', 0 ) ) . '&view=design' )
557 : false;
558
559 charitable_admin_view(
560 'onboarding/setup',
561 array(
562 'page' => $this,
563 'stripe_connect_url' => $stripe_connect_url,
564 'stripe_connected' => $stripe_connected,
565 'stripe_returned' => $stripe_returned,
566 'completed_url' => $completed_url,
567 'campaign_admin_url' => $campaign_admin_url,
568 )
569 );
570 }
571
572 /**
573 * Store the meta.
574 *
575 * @param array $meta The meta.
576 *
577 * @since 1.8.4
578 */
579 public function store_meta( $meta = array() ) {
580
581 update_option( 'charitable_setup_meta', $meta, false );
582 }
583
584 /**
585 * Store plugin data.
586 *
587 * @param array $plugins The plugins.
588 *
589 * @since 1.8.4
590 */
591 public function store_plugins( $plugins = array() ) {
592
593 update_option( 'charitable_setup_plugins', $plugins, false );
594 }
595
596 /**
597 * Retrieve the plugin data that was stored.
598 *
599 * @since 1.8.4
600 *
601 * @return array
602 */
603 public function retrieve_plugins() {
604
605 $plugins = get_option( 'charitable_setup_plugins', array() );
606
607 if ( ! is_array( $plugins ) ) {
608 return array();
609 }
610
611 $plugins = array_map( 'sanitize_text_field', $plugins );
612
613 if ( empty( $plugins ) ) {
614 return array();
615 }
616
617 return $plugins;
618 }
619
620 /**
621 * Return plugins that need to be installed potentially.
622 *
623 * @since 1.8.4
624 *
625 * @return array
626 */
627 public function plugins_to_install() {
628
629 $plugins = $this->retrieve_plugins();
630
631 if ( empty( $plugins ) ) {
632 return array();
633 }
634
635 $plugins = array_map(
636 function ( $plugin ) {
637 // is this plugin already installed in the WordPress site?
638 if ( ! $this->is_plugin_installed( $plugin ) ) {
639 return $plugin;
640 }
641 },
642 $plugins
643 );
644
645 // remove any null values and remove keys.
646 $plugins = array_values( array_filter( $plugins ) );
647
648 return $plugins;
649 }
650
651 /**
652 * Return plugins that need to be activated potentially.
653 *
654 * @since 1.8.4
655 *
656 * @return array
657 */
658 public function plugins_to_activate() {
659
660 $plugins = $this->retrieve_plugins();
661
662 if ( empty( $plugins ) ) {
663 return array();
664 }
665
666 // check if the plugin is installed and activated. If not, add it to the list of plugins to activate.
667 $plugins = array_map(
668 function ( $plugin ) {
669 // is this plugin already installed in the WordPress site?
670 if ( $this->is_plugin_installed( $plugin ) ) {
671 // is this plugin already activated?
672 if ( ! is_plugin_active( $plugin ) ) {
673 return $plugin;
674 }
675 }
676 },
677 $plugins
678 );
679
680 // remove any null values and reindex the array.
681 $plugins = array_values( array_filter( $plugins ) );
682
683 return $plugins;
684 }
685
686 /**
687 * Returns stored "features".
688 *
689 * @since 1.8.4
690 *
691 * @return array
692 */
693 public function features_to_install() {
694
695 $features = get_option( 'charitable_setup_features', array() );
696
697 if ( ! is_array( $features ) ) {
698 return array();
699 }
700
701 $features = array_map( 'sanitize_text_field', $features );
702
703 if ( empty( $features ) ) {
704 return array();
705 }
706
707 return $features;
708 }
709
710 /**
711 * Activate "features" via ajax call.
712 *
713 * @since 1.8.4
714 *
715 * @return void
716 */
717 public function ajax_activate_feature() {
718
719 // security check.
720 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'charitable-admin' ) ) { // phpcs:ignore
721 wp_send_json_error( array( 'message' => 'Invalid nonce.' ) );
722 }
723
724 $plugin_basename = isset( $_POST['slug'] ) ? 'charitable-' . sanitize_text_field( $_POST['slug'] ) : ''; // phpcs:ignore
725
726 if ( ! $plugin_basename ) {
727 wp_send_json_error( array( 'message' => 'Invalid feature.' ) );
728 }
729
730 // override a few slug/names here.
731 if ( $plugin_basename === 'charitable-recurring' ) {
732 $plugin_basename = 'charitable-recurring-donations';
733 }
734
735 $is_pro = charitable_is_pro();
736
737 if ( ! $is_pro ) {
738 wp_send_json_error(
739 array(
740 'message' => 'Invalid access.',
741 'is_pro' => $is_pro,
742 'error' => 'test',
743 )
744 );
745 }
746
747 $plugin_slug = $plugin_basename . '/' . $plugin_basename . '.php';
748
749 // is this plugin already installed?
750 if ( is_plugin_active( $plugin_basename ) ) {
751 wp_send_json_success(
752 array(
753 'message' => 'Already installed and activated',
754 'error' => 'feature-plugin-success',
755 )
756 );
757 exit;
758 }
759
760 // is this plugin installed and just needs to be activated?
761 if ( $this->is_plugin_installed( $plugin_slug ) ) {
762 $activate = activate_plugin( $plugin_slug );
763 if ( is_wp_error( $activate ) ) {
764 wp_send_json_error(
765 array(
766 'message' => $activate->get_error_message(),
767 'error' => 'feature-plugin-error',
768 )
769 );
770 exit;
771 } else {
772 wp_send_json_success(
773 array(
774 'message' => "Plugin '{$plugin_slug}' activated successfully.",
775 'error' => 'feature-plugin-success',
776 )
777 );
778 exit;
779 }
780 }
781
782 // ok, so it's not installed at all? Let's install it - and activate it.
783
784 // check if the feature is already installed and if not install it.
785 $addons = charitable_get_addons_data_from_server();
786 $download_url = '';
787
788 if ( $addons ) {
789
790 foreach ( (array) $addons as $i => $addon ) {
791
792 if ( ! isset( $addon['slug'] ) || $addon['slug'] === '' || strtolower( $addon['slug'] ) === 'auto draft' ) {
793 continue;
794 }
795
796 if ( $addon['slug'] === $plugin_basename ) {
797 $addon_slug = $addon['slug'];
798 $download_url = ! empty( $addon['install'] ) ? esc_url( $addon['install'] ) : false;
799 break;
800 }
801
802 }
803
804 }
805
806 if ( ! $download_url ) {
807 wp_send_json_error(
808 array(
809 'message' => 'Feature not found.',
810 'error' => 'feature-not-found',
811 )
812 );
813 }
814
815 // we need to convery &#038; to & in the download_url.
816 $download_url = str_replace( '&#038;', '&', $download_url );
817
818 // Prepare variables.
819 $method = '';
820 $url = add_query_arg(
821 array(
822 'page' => 'charitable-settings',
823 ),
824 admin_url( 'admin.php' )
825 );
826 $url = esc_url( $url );
827
828 // Start output bufferring to catch the filesystem form if credentials are needed.
829 ob_start();
830 $creds = request_filesystem_credentials( $url, $method, false, false, null );
831 if ( false === $creds ) {
832 $form = ob_get_clean();
833 echo wp_json_encode( array( 'form' => $form ) );
834 die;
835 }
836
837 // If we are not authenticated, make it happen now.
838 if ( ! WP_Filesystem( $creds ) ) {
839 ob_start();
840 request_filesystem_credentials( $url, $method, true, false, null );
841 $form = ob_get_clean();
842 echo wp_json_encode( array( 'form' => $form ) );
843 die;
844 }
845
846 // Do not allow WordPress to search/download translations, as this will break JS output.
847 remove_action( 'upgrader_process_complete', [ 'Language_Pack_Upgrader', 'async_upgrade' ], 20 );
848
849 // We do not need any extra credentials if we have gotten this far, so let's install the plugin.
850 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
851 require_once plugin_dir_path( CHARITABLE_DIRECTORY_PATH ) . 'charitable/includes/utilities/Skin.php';
852
853 // Create the plugin upgrader with our custom skin.
854 $skin = new Charitable_Skin();
855 $installer = new Plugin_Upgrader( $skin );
856 $installer->install( $download_url );
857
858 // Flush the cache and return the newly installed plugin basename.
859 wp_cache_flush();
860
861 if ( $installer->plugin_info() ) {
862 $plugin_basename = $installer->plugin_info();
863 $plugin = isset( $_POST['plugin'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) : '';
864 // attempt to activate the installed addon, save the user a step.
865 $activate = activate_plugins( $plugin_basename );
866 if ( is_plugin_active( $plugin_basename ) ) {
867 wp_send_json_success(
868 array(
869 'message' => "Plugin '{$plugin_slug}' activated successfully.",
870 'error' => 'feature-plugin-success',
871 )
872 );
873 } else {
874 wp_send_json_error(
875 array(
876 'message' => "Failed to activate plugin '{$plugin_slug}'.",
877 'error' => 'feature-plugin-error',
878 )
879 );
880 }
881 exit;
882 } else {
883
884 wp_send_json_error(
885 array(
886 'message' => "Failed issue with installing plugin '{$plugin_slug}'.",
887 'error' => 'feature-plugin-error',
888 )
889 );
890
891 }
892 }
893
894 /**
895 * Get license key.
896 *
897 * @param string $license_key The license key.
898 *
899 * @since 1.8.4
900 */
901 public function get_license_key( $license_key = '' ) { // phpcs:ignore
902
903 return get_option( 'charitable_setup_license_key', '' );
904 }
905
906 /**
907 * Store license key.
908 *
909 * @param string $license_key The license key.
910 *
911 * @since 1.8.4
912 */
913 public function store_license_key( $license_key = '' ) {
914
915 update_option( 'charitable_setup_license_key', $license_key, '' );
916 }
917
918 /**
919 * Store payment methods.
920 *
921 * @param array $payment_methods The payment methods.
922 *
923 * @since 1.8.4
924 */
925 public function store_payment_methods( $payment_methods = array() ) {
926
927 update_option( 'charitable_setup_payment_methods', $payment_methods, false );
928 }
929
930 /**
931 * Store features.
932 *
933 * @param array $features The features.
934 *
935 * @since 1.8.4
936 */
937 public function store_features( $features = array() ) {
938
939 update_option( 'charitable_setup_features', $features, false );
940 }
941
942 /**
943 * Store campaign info for setup.
944 *
945 * @param array $campaign The campaign.
946
947 * @since 1.8.4
948 */
949 public function store_campaign( $campaign = array() ) {
950
951 update_option( 'charitable_setup_campaign', $campaign, false );
952 }
953
954 /**
955 * Determines if we can skip building a new campaign.
956 *
957 * @since 1.8.4
958 *
959 * @return bool
960 */
961 public function maybe_skip_campaign_setup() {
962
963 // Check for permissions.
964 if ( ! charitable_current_user_can( 'edit_campaigns' ) ) {
965 return false;
966 }
967
968 $campaign_data = $this->retrieve_campaign();
969
970 if ( ! $campaign_data ) {
971 return true;
972 }
973
974 return false;
975 }
976
977 /**
978 * Processing opt-in tracking requests.
979 *
980 * @since 1.8.4
981 *
982 * @return void
983 */
984 public function ajax_process_tracking() {
985
986 // security check.
987 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'charitable-admin' ) ) { // phpcs:ignore
988 wp_send_json_error(
989 array(
990 'message' => 'Invalid nonce.',
991 'error' => 'invalid-nonce',
992 )
993 );
994 }
995
996 // Check for permissions.
997 if ( ! charitable_current_user_can( 'administrator' ) ) {
998 wp_send_json_error(
999 array(
1000 'message' => 'You are not allowed to perform this action.',
1001 'error' => 'permission-denied',
1002 )
1003 );
1004 }
1005
1006 // checking for the action.
1007 if ( ! isset( $_POST['action'] ) ) {
1008 wp_send_json_error(
1009 array(
1010 'message' => 'Invalid tracking action.',
1011 'error' => 'invalid-tracking-action',
1012 )
1013 );
1014 }
1015
1016 if ( ! isset( $_POST['tracking_action'] ) || ! in_array( $_POST['tracking_action'], array( 'opt-in', 'opt-out' ), true ) ) {
1017 wp_send_json_error(
1018 array(
1019 'message' => 'Invalid tracking action.',
1020 'error' => 'invalid-tracking-action',
1021 )
1022 );
1023 }
1024
1025 // setup variables.
1026 $opt_in_tracking = empty( $_POST['tracking_action'] ) || $_POST['tracking_action'] === 'opt-out' ? '0' : '1';
1027 $email = update_option( 'charitable_email_signup', false ) ? get_option( 'charitable_email_signup' ) : get_option( 'admin_email' );
1028
1029 // assemble a url to the setup wizard using WordPress function.
1030 $tracking_update_url = add_query_arg(
1031 array(
1032 'email' => is_email( $email ) ? $email : false,
1033 'opt_in_track' => $opt_in_tracking,
1034 'site_token' => charitable_get_site_token(),
1035 'return' => rawurlencode(
1036 base64_encode( get_admin_url( null, 'admin.php') ) // phpcs:ignore
1037 ),
1038 ),
1039 $this->app_server_url . 'optin-charitable_tracking'
1040 );
1041
1042 // Send the POST request.
1043 $response = wp_remote_get(
1044 $tracking_update_url,
1045 [
1046 'timeout' => 45,
1047 'sslverify' => false,
1048 ]
1049 );
1050
1051 // Get the http status code.
1052 $response_code = wp_remote_retrieve_response_code( $response );
1053
1054 if ( 200 !== $response_code ) {
1055 wp_send_json_error(
1056 array(
1057 'message' => 'Error updating tracking.',
1058 'error' => 'tracking-error',
1059 )
1060 );
1061 exit;
1062 }
1063
1064 if ( is_wp_error( $response ) ) {
1065 // Handle the error.
1066 $error_message = $response->get_error_message();
1067 wp_send_json_error(
1068 array(
1069 'message' => 'Error updating tracking: ' . $error_message,
1070 'error' => 'tracking-error',
1071 )
1072 );
1073 exit;
1074 }
1075
1076 $response_args = json_decode( $response['body'] );
1077 $return_url = isset( $response_args->return_url ) ? $response_args->return_url : false;
1078 $site_token = isset( $response_args->site_token ) ? $response_args->site_token : false;
1079 $opt_in_tracking = isset( $response_args->opt_in_tracking ) ? intval( $response_args->opt_in_tracking ) : false;
1080
1081 // the site token should match the site token we have.
1082 if ( $site_token !== charitable_get_site_token() ) {
1083 wp_send_json_error(
1084 array(
1085 'message' => 'Error updating tracking: Invalid site token.',
1086 'error' => 'tracking-error',
1087 )
1088 );
1089 exit;
1090 }
1091
1092 // flip the $opt_in_tracking variable... if it's opt-in, we want to opt-out and vice versa.
1093 $opt_in_tracking_setting = ( 0 === intval( $opt_in_tracking ) ) ? false : true;
1094
1095 charitable_update_usage_tracking_setting( $opt_in_tracking_setting );
1096
1097 // redirect to the return url.
1098 wp_send_json_success(
1099 array(
1100 'text' => $opt_in_tracking === 0 ? 'Allow And Continue' : 'Opt Out',
1101 'return' => $return_url,
1102 'tracking_action' => $opt_in_tracking === 0 ? '' : 'joined',
1103 'opt_in_tracking' => $opt_in_tracking === 0 ? false : true,
1104 )
1105 );
1106
1107 exit;
1108 }
1109
1110 /**
1111 * Process campaign.
1112 *
1113 * @since 1.8.4
1114 *
1115 * @return void
1116 */
1117 public function ajax_process_campaign() {
1118
1119 // security check.
1120 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'charitable-setup' ) ) { // phpcs:ignore
1121 wp_send_json_error(
1122 array(
1123 'message' => 'Invalid nonce.',
1124 'error' => 'invalid-nonce',
1125 )
1126 );
1127 }
1128
1129 // Check for permissions.
1130 if ( ! charitable_current_user_can( 'edit_campaigns' ) ) {
1131 wp_send_json_error(
1132 array(
1133 'message' => 'You are not allowed to perform this action.',
1134 'error' => 'permission-denied',
1135 )
1136 );
1137 }
1138
1139 $campaign_data = $this->retrieve_campaign();
1140
1141 if ( ! $campaign_data ) {
1142 wp_send_json_error(
1143 array(
1144 'message' => 'No campaign.',
1145 'error' => 'invalid-campaign',
1146 'processed_campaign' => 'false',
1147 )
1148 );
1149 }
1150
1151 // Remove any previous one, if it exists.
1152 delete_option( 'charitable_setup_campaign_created' );
1153
1154 $campaign_template = isset( $campaign_data['template'] ) ? sanitize_text_field( $campaign_data['template'] ) : ''; // example: animal-sanctuary.
1155 $campaign_title = isset( $campaign_data['title'] ) ? sanitize_text_field( $campaign_data['title'] ) : '';
1156 $campaign_description = isset( $campaign_data['description'] ) ? sanitize_text_field( $campaign_data['description'] ) : '';
1157 $campaign_goal = isset( $campaign_data['goal'] ) ? sanitize_text_field( $campaign_data['goal'] ) : '0';
1158
1159 // if $campaign_end_date is a validated date, then we can convert it to a proper date format "2025-01-25 23:59:59". If it's not a valid date, then we can set it to 0.
1160 $campaign_end_date = isset( $campaign_data['end_date'] ) ? sanitize_text_field( $campaign_data['end_date'] ) : 0;
1161 $campaign_end_date = strtotime( $campaign_end_date ) ? gmdate( 'Y-m-d H:i:s', strtotime( $campaign_end_date ) ) : 0;
1162
1163 // Generate the campaign in the datbase so we can get the database ID.
1164 $campaign_id = charitable_create_campaign(
1165 array(
1166 'title' => $campaign_title,
1167 'content' => $campaign_description,
1168 'creator' => get_current_user_id(),
1169 'end_date' => $campaign_end_date,
1170 'goal' => $campaign_goal,
1171 'status' => 'draft',
1172 )
1173 );
1174
1175 // Process the campaign, which has it's roots back to legacy campaigns.
1176 $processor = new Charitable_Campaign_Processor();
1177 $processor->campaign_id = $campaign_id;
1178
1179 // taken from class-charitable-campaign-processor.php.
1180 $campaign_meta_keys = array(
1181 'goal' => $campaign_goal,
1182 'end_date' => $campaign_end_date,
1183 );
1184 foreach ( $campaign_meta_keys as $meta_key => $meta_value ) {
1185 $processor->save_meta_field( $meta_value, $meta_key );
1186 }
1187
1188 $campaign_data = array(
1189 'title' => $campaign_title,
1190 );
1191
1192 // get the template settings.
1193 $builder_template = new Charitable_Campaign_Builder_Templates();
1194 $template_settings = $builder_template->get_template_data( $campaign_template, $campaign_data );
1195
1196 // Update (overwrite) the campaign settings with the new values.
1197 $campaign_settings_v2 = [];
1198
1199 $campaign_settings_v2['layout'] = $this->transform_layout_structure( $template_settings['layout'] ); // Updated.
1200
1201 $campaign_settings_v2['id'] = $campaign_id;
1202 $campaign_settings_v2['template_id'] = $template_settings['meta']['slug'];
1203 $campaign_settings_v2['template_label'] = $template_settings['meta']['label'];
1204 $campaign_settings_v2['color_base_primary'] = $template_settings['meta']['colors']['primary'];
1205 $campaign_settings_v2['color_base_secondary'] = $template_settings['meta']['colors']['secondary'];
1206 $campaign_settings_v2['color_base_tertiary'] = $template_settings['meta']['colors']['tertiary'];
1207 $campaign_settings_v2['color_base_button'] = $template_settings['meta']['colors']['button_bg'];
1208
1209 $campaign_settings_v2['layout']['advanced']['theme_color_primary'] = $template_settings['meta']['colors']['primary'];
1210 $campaign_settings_v2['layout']['advanced']['theme_color_secondary'] = $template_settings['meta']['colors']['secondary'];
1211 $campaign_settings_v2['layout']['advanced']['theme_color_tertiary'] = $template_settings['meta']['colors']['tertiary'];
1212 $campaign_settings_v2['layout']['advanced']['theme_color_button'] = $template_settings['meta']['colors']['button_bg'];
1213
1214 $campaign_settings_v2['layout']['advanced']['tab_style'] = $template_settings['advanced']['tab_style'];
1215 $campaign_settings_v2['layout']['advanced']['tab_size'] = $template_settings['advanced']['tab_size'];
1216 $campaign_settings_v2['layout']['advanced']['show_field_names'] = 'hide'; // $template_settings['advanced']['show_field_names'];
1217 $campaign_settings_v2['layout']['advanced']['preview_mode'] = $template_settings['advanced']['preview_mode'];
1218
1219 $campaign_settings_v2['post_status'] = 'draft';
1220 $campaign_settings_v2['post_status_label'] = 'Draft';
1221 $campaign_settings_v2['title'] = $campaign_title;
1222
1223 $campaign_settings_v2['fields'] = $this->campaign_map_fields( $template_settings );
1224 $campaign_settings_v2['tabs'] = $this->get_tabs_from_template_for_campaign_settings( $template_settings );
1225 $campaign_settings_v2['field_id'] = count( $campaign_settings_v2['fields'] ); // ?????
1226
1227 $campaign_settings_v2['settings'] = [];
1228 $campaign_settings_v2['settings']['general']['description'] = $campaign_description;
1229 $campaign_settings_v2['settings']['general']['extended_description'] = '';
1230 $campaign_settings_v2['settings']['general']['form_css_class'] = '';
1231 $campaign_settings_v2['settings']['general']['goal'] = '0' === $campaign_goal ? '' : Charitable_Currency::get_instance()->sanitize_monetary_amount( (string) ( $campaign_goal ) );
1232 $campaign_settings_v2['settings']['general']['end_date'] = Charitable_Campaign::sanitize_campaign_end_date( $campaign_end_date );
1233 $campaign_settings_v2['settings']['general']['campaign_creator_id'] = get_current_user_id();
1234
1235 $campaign_settings_v2['settings']['donation-options'] = $this->get_donation_options();
1236
1237 $campaign_settings_v2['campaign_id'] = $campaign_id;
1238
1239 $campaign_settings_v2['tab_order'] = array(
1240 0 => 0,
1241 1 => 1,
1242 2 => 2,
1243 );
1244
1245 // Allow addons to update, sync, or override settings before they are saved.
1246 $campaign_settings_v2 = apply_filters( 'charitable_campaign_builder_save_campaign_settings', $campaign_settings_v2, $campaign_id );
1247
1248 // Save the campaign settings.
1249 $updated = update_post_meta( $campaign_id, 'campaign_settings_v2', $campaign_settings_v2 );
1250
1251 if ( ! $updated ) {
1252
1253 wp_send_json_error(
1254 array(
1255 'message' => 'Error saving campaign settings.',
1256 'error' => 'error-saving-campaign-settings',
1257 )
1258 );
1259 } else {
1260
1261 // Create a (temp) option that marks the creation of the campaign during the setup process so we can direct the user back to the campaign.
1262
1263 update_option( 'charitable_setup_campaign_created', $campaign_id, false );
1264
1265 if ( class_exists( 'Charitable_Checklist' ) ) {
1266 $checklist_class = Charitable_Checklist::get_instance();
1267 $checklist_class->mark_step_completed( 'first-campaign' );
1268 }
1269
1270 wp_send_json_success(
1271 array(
1272 'message' => 'Campaign created.',
1273 'status' => 'campaign-created',
1274 )
1275 );
1276 }
1277
1278 exit;
1279 }
1280
1281 /**
1282 * Convert layout structure to the new format.
1283 *
1284 * @since 1.8.4
1285 * @param array $original_layout The original layout.
1286 *
1287 * @return array
1288 */
1289 public function transform_layout_structure( $original_layout ) {
1290 $new_layout = [
1291 'rows' => [],
1292 ];
1293
1294 $field_counter = 0;
1295 $field_type_counter = 0;
1296 $section_counter = 0;
1297 $columns_counter = 0;
1298 $alt_index_starter = 0;
1299
1300 foreach ( $original_layout as $row_index => $row ) {
1301 if ( ! is_numeric( $row_index ) ) {
1302 continue;
1303 }
1304
1305 $new_row = [
1306 'type' => $row['type'],
1307 'css_class' => $row['css_class'] ?? 'no-css',
1308 'columns' => [],
1309 'fields' => [],
1310 ];
1311
1312 $alt_number = 0;
1313
1314 // Process columns.
1315 foreach ( $row['columns'] as $column_index => $column ) {
1316 $new_column = [
1317 'sections' => [],
1318 ];
1319
1320 foreach ( $column as $section ) {
1321 if ( $section['type'] === 'fields' ) {
1322 $new_section = [
1323 'type' => 'fields',
1324 'fields' => [],
1325 ];
1326
1327 $field_indices = [];
1328 foreach ( $section['fields'] as $field ) {
1329 // Add to row's fields array.
1330 $new_row['fields'][ $field_type_counter ] = $field['type'];
1331 // Store the index for this field.
1332 $field_indices[] = $field_counter;
1333 ++$field_counter;
1334 ++$field_type_counter;
1335 }
1336
1337 $alt_index_starter = count( $new_row['fields'] );
1338
1339 $new_section['fields'] = $field_indices;
1340 $new_column['sections'][ $section_counter ] = $new_section;
1341
1342 ++$section_counter;
1343
1344 } elseif ( $section['type'] === 'tabs' ) {
1345 $new_section = [
1346 'type' => 'tabs',
1347 'tabs' => [],
1348 ];
1349
1350 foreach ( $section['tabs'] as $tab ) {
1351 $new_tab = [
1352 'title' => $tab['title'],
1353 'type' => 'fields',
1354 'slug' => $tab['slug'] ?? '',
1355 'fields' => [],
1356 ];
1357
1358 $field_indices = [];
1359 if ( ! empty( $tab['fields'] ) ) {
1360 foreach ( $tab['fields'] as $field ) {
1361 // Add to row's fields array.
1362 $new_row['fields'][ $field_type_counter ] = $field['type'];
1363 // Store the index for this field.
1364 $field_indices[] = $field_counter;
1365 ++$field_counter;
1366 ++$field_type_counter;
1367 }
1368 }
1369
1370 $new_tab['fields'] = $field_indices;
1371 $new_section['tabs'][] = $new_tab;
1372 }
1373
1374 $new_column['sections'][ $section_counter ] = $new_section;
1375
1376 ++$section_counter;
1377 }
1378 }
1379
1380 $new_row['columns'][ $columns_counter ] = $new_column;
1381 ++$columns_counter;
1382 }
1383
1384 $new_layout['rows'][] = $new_row;
1385 }
1386
1387 return $new_layout;
1388 }
1389
1390 /**
1391 * Get the tabs from the template for the campaign settings.
1392 *
1393 * @param array $template_settings The template settings.
1394 *
1395 * @since 1.8.4
1396 * @version 1.8.9.7
1397 *
1398 * @return array
1399 */
1400 public function get_tabs_from_template_for_campaign_settings( $template_settings ) {
1401
1402 $tabs = [];
1403
1404 foreach ( $template_settings['layout'] as $row ) {
1405 foreach ( $row as $column ) {
1406 if ( ! is_array( $column ) ) {
1407 continue;
1408 }
1409 foreach ( $column as $section ) {
1410 if ( ! is_array( $section ) ) {
1411 continue;
1412 }
1413 foreach ( $section as $mini_sections ) {
1414 if ( ! is_array( $mini_sections ) || ! isset( $mini_sections['tabs'] ) ) {
1415 continue;
1416 }
1417 foreach ( $mini_sections['tabs'] as $found_tabs ) {
1418 $tabs[] = $found_tabs;
1419 }
1420 }
1421 }
1422 }
1423 }
1424
1425 $returned_tabs = [];
1426 foreach ( $tabs as $index => $tab ) {
1427 $returned_tabs[] = array(
1428 'title' => isset( $tab['title'] ) ? $tab['title'] : '',
1429 );
1430 }
1431
1432 return $returned_tabs;
1433 }
1434
1435 /**
1436 * Map the "fields" option in the root of the array from the template to the campaign settings.
1437 *
1438 * @since 1.8.4
1439 * @param array $input The input (campaign array).
1440 *
1441 * @return array
1442 */
1443 public function campaign_map_fields( $input ) {
1444 $output = array( 'fields' => array() );
1445
1446 // Check if 'layout' exists and is an array.
1447 if ( isset( $input['layout'] ) && is_array( $input['layout'] ) ) {
1448 foreach ( $input['layout'] as $layout_item ) {
1449 // Check if 'columns' exists and is an array.
1450 if ( isset( $layout_item['columns'] ) && is_array( $layout_item['columns'] ) ) {
1451 foreach ( $layout_item['columns'] as $column ) {
1452 if ( is_array( $column ) ) {
1453 foreach ( $column as $column_item ) {
1454 // Check if 'fields' exists and is an array.
1455 if ( isset( $column_item['fields'] ) && is_array( $column_item['fields'] ) ) {
1456 foreach ( $column_item['fields'] as $field ) {
1457 // Process each field.
1458 $mapped_field = $this->process_field( $field );
1459 $output['fields'][] = $mapped_field;
1460 }
1461 } elseif ( isset( $column_item['tabs'] ) && is_array( $column_item['tabs'] ) ) { // Check if 'tabs' exists (e.g., in the second layout item).
1462 foreach ( $column_item['tabs'] as $tab ) {
1463 if ( isset( $tab['fields'] ) && is_array( $tab['fields'] ) ) {
1464 foreach ( $tab['fields'] as $field ) {
1465 $mapped_field = $this->process_field( $field );
1466 $output['fields'][] = $mapped_field;
1467 }
1468 }
1469 }
1470 }
1471 }
1472 }
1473 }
1474 }
1475 }
1476 }
1477
1478 return $output['fields'];
1479 }
1480
1481 /**
1482 * Process the field from the campaign_map_fields function.
1483 *
1484 * @since 1.8.4
1485 * @param array $field The field.
1486 *
1487 * @return array
1488 */
1489 public function process_field( $field ) {
1490 $mapped_field = array();
1491
1492 // Get the field type.
1493 $type = isset( $field['type'] ) ? $field['type'] : '';
1494
1495 // Map fields based on the type.
1496 switch ( $type ) {
1497 case 'photo':
1498 $mapped_field['file'] = isset( $field['file'] ) ? $field['file'] : '';
1499 $mapped_field['alt_text'] = isset( $field['alt_text'] ) ? $field['alt_text'] : '';
1500 $mapped_field['default'] = isset( $field['default'] ) ? $field['default'] : '';
1501 $mapped_field['width_percentage'] = isset( $field['width_percentage'] ) ? $field['width_percentage'] : 100;
1502 $mapped_field['align'] = isset( $field['align'] ) ? $field['align'] : 'center';
1503 $mapped_field['css_class'] = isset( $field['css_class'] ) ? $field['css_class'] : '';
1504 break;
1505
1506 case 'campaign-description':
1507 case 'text':
1508 $mapped_field['headline'] = isset( $field['headline'] ) ? $field['headline'] : '';
1509 $mapped_field['content'] = isset( $field['content'] ) ? $field['content'] : '';
1510 $mapped_field['width_percentage'] = isset( $field['width_percentage'] ) ? $field['width_percentage'] : 100;
1511 $mapped_field['align'] = isset( $field['align'] ) ? $field['align'] : 'left';
1512 $mapped_field['css_class'] = isset( $field['css_class'] ) ? $field['css_class'] : '';
1513 break;
1514
1515 case 'progress-bar':
1516 case 'campaign-summary':
1517 $mapped_field['headline'] = isset( $field['headline'] ) ? $field['headline'] : '';
1518 $mapped_field['show_hide'] = array();
1519 if ( isset( $field['show_hide'] ) ) {
1520 foreach ( $field['show_hide'] as $key => $value ) {
1521 if ( $value ) {
1522 $mapped_field['show_hide'][ $key ] = $key;
1523 }
1524 }
1525 }
1526 $mapped_field['meta_position'] = isset( $field['meta_position'] ) ? $field['meta_position'] : '';
1527 $mapped_field['label_donate'] = isset( $field['label_donate'] ) ? $field['label_donate'] : '';
1528 $mapped_field['label_goal'] = isset( $field['label_goal'] ) ? $field['label_goal'] : '';
1529 $mapped_field['width_percentage'] = isset( $field['width_percentage'] ) ? $field['width_percentage'] : 100;
1530 $mapped_field['align'] = isset( $field['align'] ) ? $field['align'] : 'left';
1531 $mapped_field['css_class'] = isset( $field['css_class'] ) ? $field['css_class'] : '';
1532 break;
1533
1534 case 'donate-button':
1535 $mapped_field['button_label'] = isset( $field['button_label'] ) ? $field['button_label'] : '';
1536 $mapped_field['width_percentage'] = isset( $field['width_percentage'] ) ? $field['width_percentage'] : 100;
1537 $mapped_field['align'] = isset( $field['align'] ) ? $field['align'] : 'left';
1538 $mapped_field['css_class'] = isset( $field['css_class'] ) ? $field['css_class'] : '';
1539 break;
1540
1541 default:
1542 // If the type is unrecognized, you can decide how to handle it.
1543 $mapped_field = $field;
1544 break;
1545 }
1546
1547 return $mapped_field;
1548 }
1549
1550 /**
1551 * Setup default donation options ready for the campaign.
1552 *
1553 * @since 1.8.4
1554 *
1555 * @return array
1556 */
1557 public function get_donation_options() {
1558
1559 return [
1560
1561 'donation_amounts' => [
1562 1 => [
1563 'amount' => 5,
1564 'description' => 'This is a small donation.',
1565 ],
1566 2 => [
1567 'amount' => 10,
1568 'description' => 'This is a slightly larger donation.',
1569 ],
1570 3 => [
1571 'amount' => 15,
1572 'description' => 'This is a larger (and default) donation.',
1573 ],
1574 4 => [
1575 'amount' => 20,
1576 'description' => 'This is a large donation.',
1577 ],
1578 ],
1579 'suggested_donations_default' => 3,
1580 'minimum_donation_amount' => null,
1581 'allow_custom_donations' => 1,
1582
1583 ];
1584 }
1585
1586
1587 /**
1588 * Clean up and mark the setup as complete.
1589 *
1590 * @since 1.8.4
1591 * @version 1.8.5 - Added charitable_activate_pro option.
1592 *
1593 * @return void
1594 */
1595 public function ajax_process_complete() {
1596
1597 // security check.
1598 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'charitable-setup' ) ) { // phpcs:ignore
1599 wp_send_json_error(
1600 array(
1601 'message' => 'Invalid nonce.',
1602 'error' => 'invalid-nonce',
1603 )
1604 );
1605 }
1606
1607 // CRITICAL SAFETY CHECK: Only set Pro activation if Pro plugin actually exists
1608 $pro_plugin_path = WP_PLUGIN_DIR . '/charitable-pro/charitable-pro.php';
1609 $should_activate_pro = file_exists( $pro_plugin_path );
1610
1611 // remove the transient, set the option.
1612 delete_transient( 'charitable_ss_onboarding' );
1613 update_option( 'charitable_ss_complete', true );
1614
1615 // Only set Pro activation if Pro is actually available
1616 if ( $should_activate_pro ) {
1617 update_option( 'charitable_activate_pro', true );
1618 } else {
1619 // Log this for debugging
1620 if ( charitable_is_debug() ) {
1621 error_log( 'Charitable: Pro plugin not found during setup completion - skipping Pro activation' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1622 }
1623 }
1624
1625 wp_send_json_success(
1626 array(
1627 'message' => 'Setup completed.',
1628 'status' => 'setup-completed',
1629 )
1630 );
1631 }
1632
1633 /**
1634 * Retrieve campaign.
1635 *
1636 * @since 1.8.4
1637 *
1638 * @return mixed
1639 */
1640 public function retrieve_campaign() {
1641
1642 $campaign = get_option( 'charitable_setup_campaign', array() );
1643
1644 if ( ! is_array( $campaign ) ) {
1645 return array();
1646 }
1647
1648 $campaign = array_map( 'sanitize_text_field', $campaign );
1649 $campaign = array_map( 'trim', $campaign );
1650
1651 if ( empty( $campaign ) ) {
1652 return array();
1653 }
1654
1655 // check and see if we have the vital fields (a campaign name and a template).
1656 if ( ! key_exists( 'title', $campaign ) || ! key_exists( 'template', $campaign ) ) {
1657 return false;
1658 }
1659 if ( empty( $campaign['title'] ) || empty( $campaign['template'] ) ) {
1660 return false;
1661 }
1662
1663 return $campaign;
1664 }
1665
1666 /**
1667 * Retrieve payment methods.
1668 *
1669 * @since 1.8.4
1670 *
1671 * @return mixed
1672 */
1673 public function retrieve_payment_methods() {
1674
1675 $payment_methods = get_option( 'charitable_setup_payment_methods', array() );
1676
1677 if ( ! is_array( $payment_methods ) ) {
1678 return array();
1679 }
1680
1681 $payment_methods = array_map( 'sanitize_text_field', $payment_methods );
1682
1683 if ( empty( $payment_methods ) ) {
1684 return array();
1685 }
1686
1687 return $payment_methods;
1688 }
1689
1690 /**
1691 * Helper function to determine if a plugin is installed.
1692 *
1693 * @since 1.8.4
1694 * @param string $plugin_slug The plugin slug.
1695 *
1696 * @return mixed
1697 */
1698 public function is_plugin_installed( $plugin_slug ) {
1699
1700 // Get the full path to the plugins directory.
1701 $plugin_dir = WP_PLUGIN_DIR . '/' . $plugin_slug;
1702
1703 // Check if the directory exists for the given plugin slug.
1704 return is_dir( $plugin_dir );
1705 }
1706
1707 /**
1708 * Process meta.
1709 *
1710 * @since 1.8.4
1711 * @version 1.8.4.2 - Upon successful tracking opt-in, send initial usage information.
1712 *
1713 * @param array $meta The meta to process.
1714 * @return bool
1715 */
1716 public function process_meta( $meta = array() ) {
1717
1718 if ( ! $meta ) {
1719 return false;
1720 }
1721
1722 $meta = array_map( 'sanitize_text_field', $meta );
1723
1724 // Define vars to determine if we can/should check off checklist items depending on what is updated from onboarding.
1725 $checklist_general_settings_completed = false;
1726 $checklist_email_settings_completed = false;
1727
1728 // step 2, country, currency, decimal count.
1729
1730 if ( key_exists( 'country', $meta ) ) {
1731 $checklist_general_settings_completed = true;
1732 $this->set_charitable_option( 'country', $meta['country'] );
1733 }
1734
1735 if ( key_exists( 'currency', $meta ) ) {
1736 $checklist_general_settings_completed = true;
1737 $this->set_charitable_option( 'currency', $meta['currency'] );
1738 }
1739
1740 if ( key_exists( 'decimal_count', $meta ) ) {
1741 $checklist_general_settings_completed = true;
1742 $this->set_charitable_option( 'decimal_count', $meta['decimal'] );
1743 }
1744
1745 // step 2, email settings.
1746 if ( key_exists( 'email_donor_notification', $meta ) ) {
1747 $checklist_email_settings_completed = true;
1748 $this->set_charitable_email_option( 'email_donor_notification' );
1749 }
1750 if ( key_exists( 'email_admin_notification', $meta ) ) {
1751 $checklist_email_settings_completed = true;
1752 $this->set_charitable_email_option( 'email_admin_notification' );
1753 }
1754
1755 // If the general settings are completed, we can check off the checklist item.
1756 if ( $checklist_general_settings_completed ) {
1757 $checklist_class = Charitable_Checklist::get_instance();
1758 $checklist_class->mark_step_completed( 'general-settings' );
1759 }
1760 if ( $checklist_email_settings_completed ) {
1761 $checklist_class = Charitable_Checklist::get_instance();
1762 $checklist_class->mark_step_completed( 'email-settings' );
1763 }
1764 // misc.
1765 if ( key_exists( 'tracking', $meta ) ) {
1766 if ( 'yes' === $meta['tracking'] ) {
1767 if ( charitable_is_debug() ) {
1768 error_log( 'tracking is enabled' ); // phpcs:ignore
1769 }
1770 charitable_update_usage_tracking_setting( true );
1771 // Send initial usage information just once (after that scheduled), now that we have permission.
1772 if ( class_exists( 'Charitable_Tracking' ) ) {
1773 Charitable_Tracking::get_instance()->send_checkins( false, true );
1774 }
1775 }
1776 }
1777 if ( key_exists( 'email_signup', $meta ) ) {
1778 // if the value is a valid santitized email address...
1779 if ( is_email( $meta['email_signup'] ) ) {
1780 if ( charitable_is_debug() ) {
1781 error_log( 'email signup is valid' ); // phpcs:ignore
1782 }
1783 update_option( 'charitable_email_signup', $meta['email_signup'] );
1784 }
1785 }
1786
1787 return true;
1788 }
1789
1790 /**
1791 * Process plugins.
1792 *
1793 * @since 1.8.4
1794 *
1795 * @param array $plugins The plugins to process.
1796 * @return void
1797 */
1798 public function process_plugins( $plugins = array() ) {
1799
1800 if ( ! $plugins ) {
1801 return;
1802 }
1803
1804 $plugins = array_map( 'sanitize_text_field', $plugins );
1805
1806 return $plugins;
1807 }
1808
1809 /**
1810 * Process meta.
1811 *
1812 * @since 1.8.4
1813 *
1814 * @param array $payment_methods The methods to process.
1815 * @return bool
1816 */
1817 public function process_payment_methods( $payment_methods = array() ) {
1818
1819 if ( ! $payment_methods || ! class_exists( 'Charitable_Gateways' ) ) {
1820 return false;
1821 }
1822
1823 $ret = Charitable_Gateways::get_instance()->check_setup_payment_methods();
1824
1825 return $ret;
1826 }
1827
1828 /**
1829 * Update a Charitable option.
1830 *
1831 * @since 1.8.4
1832 *
1833 * @param string $setting The setting key.
1834 * @param mixed $value The setting value.
1835 * @return void
1836 */
1837 private function set_charitable_option( $setting = '', $value = false ) {
1838 $settings = get_option( 'charitable_settings', [] );
1839
1840 $settings[ $setting ] = $value;
1841
1842 if ( defined( 'CHARITABLE_DEBUG_SETUP' ) ) {
1843 if ( charitable_is_debug() ) {
1844 error_log( 'Charitable Setup: Updated ' . $setting . ' to ' . $value ); // phpcs:ignore
1845 }
1846 }
1847
1848 update_option( 'charitable_settings', $settings );
1849 }
1850
1851 /**
1852 * Process email setting.
1853 *
1854 * @since 1.8.4
1855 *
1856 * @param string $email_setting The email setting to process.
1857 * @return void
1858 */
1859 private function set_charitable_email_option( $email_setting = '' ) {
1860
1861 $settings = get_option( 'charitable_settings', [] );
1862 $enabled_emails = ! empty( $settings['enabled_emails'] ) ? $settings['enabled_emails'] : [];
1863
1864 switch ( $email_setting ) {
1865 case 'email_donor_notification':
1866 if ( ! key_exists( 'donation_receipt', $enabled_emails ) ) {
1867 $enabled_emails['donation_receipt'] = 'Charitable_Email_Donation_Receipt';
1868 if ( defined( 'CHARITABLE_DEBUG_SETUP' ) ) {
1869 if ( charitable_is_debug() ) {
1870 error_log( 'Charitable Setup: Added donation_receipt to enabled_emails' ); // phpcs:ignore
1871 }
1872 }
1873 }
1874 break;
1875 case 'email_admin_notification':
1876 if ( ! key_exists( 'new_donation', $enabled_emails ) ) {
1877 $enabled_emails['new_donation'] = 'Charitable_Email_New_Donation';
1878 if ( defined( 'CHARITABLE_DEBUG_SETUP' ) ) {
1879 if ( charitable_is_debug() ) {
1880 error_log( 'Charitable Setup: Added new_donation to enabled_emails' ); // phpcs:ignore
1881 }
1882 }
1883 }
1884 break;
1885 }
1886
1887 $settings['enabled_emails'] = $enabled_emails;
1888
1889 update_option( 'charitable_settings', $settings );
1890 }
1891
1892 /**
1893 * Get localized strings.
1894 *
1895 * @since 1.8.4
1896 *
1897 * @return array
1898 */
1899 private function get_localized_strings() {
1900
1901 // $currency_helper = charitable_get_currency_helper();
1902
1903 $stripe_connect_url = '';
1904
1905 $payment_methods = $this->retrieve_payment_methods();
1906
1907 $stripe_returned = isset( $_GET['setup'] ) && 'stripe-connect' === $_GET['setup'] ? true : false; // phpcs:ignore
1908
1909 if ( in_array( 'stripe', $payment_methods, true ) && class_exists( 'Charitable_Gateway_Stripe_AM' ) ) {
1910 $stripe_gateway = new Charitable_Gateway_Stripe_AM();
1911 $redirect_url = $this->stripe_redirect_url;
1912 $stripe_connect_url = ! $stripe_gateway->maybe_stripe_connected() ? $stripe_gateway->get_stripe_connect_url( $redirect_url ) : '';
1913 }
1914
1915 $strings = array(
1916 'version' => '1.8.4',
1917 'setup_step' => $this->get_current_setup_step(),
1918 'ajax_url' => admin_url( 'admin-ajax.php' ),
1919 'headlines' => array(
1920 'start' => esc_html__( 'Starting...', 'charitable' ),
1921 'meta' => esc_html__( 'Setting Up General Settings...', 'charitable' ),
1922 'plugins' => esc_html__( 'Updating Plugins...', 'charitable' ),
1923 'plugins_installed' => esc_html__( 'Installing Plugins...', 'charitable' ),
1924 'plugins_activated' => esc_html__( 'Activating Plugins...', 'charitable' ),
1925 'activate_license' => esc_html__( 'Setting Up Features...', 'charitable' ),
1926 'features' => esc_html__( 'Setting Up Features...', 'charitable' ),
1927 'campaign' => esc_html__( 'Setting Up Your First Campaign...', 'charitable' ),
1928 'payment_methods' => esc_html__( 'Setting Up Payment Methods...', 'charitable' ),
1929 'almost_complete' => esc_html__( 'Almost done!', 'charitable' ),
1930 'complete' => esc_html__( 'Congratulations!', 'charitable' ),
1931 ),
1932 'text' => array(
1933 'start' => '',
1934 'meta' => esc_html__( 'Updating country, currency, and related settings for you.', 'charitable' ),
1935 'plugins' => esc_html__( 'Checking plugins...', 'charitable' ),
1936 'plugins_installed' => '',
1937 'plugins_activated' => '',
1938 'activate_license' => esc_html__( 'Checking license and site information...', 'charitable' ),
1939 'features' => esc_html__( 'Checking license and site information...', 'charitable' ),
1940 'campaign' => esc_html__( 'Creating draft...', 'charitable' ),
1941 'payment_methods' => esc_html__( 'Checking payment methods...', 'charitable' ),
1942 'almost_complete' => esc_html__( 'Finishing things...', 'charitable' ),
1943 'almost_complete_stripe' => esc_html__( 'Stripe offers a seamless and secure payment experience for both you and your donors.', 'charitable' ),
1944 'complete' => esc_html__( 'Your Charitable install has been setup and is ready for you.', 'charitable' ),
1945 ),
1946 'nonce' => wp_create_nonce( 'charitable-admin' ),
1947 'setup_nonce' => wp_create_nonce( 'charitable-setup' ),
1948 'plugins' => array(
1949 'install' => $this->plugins_to_install(),
1950 'activate' => $this->plugins_to_activate(),
1951 ),
1952 'plugins_meta' => $this->plugin_meta,
1953 'features' => $this->features_to_install(),
1954 'key' => $this->get_license_key(),
1955 'key_nonce' => wp_create_nonce( 'charitable_settings-options' ),
1956 'features_meta' => $this->feature_meta,
1957 /* translators: %1$s: List of requested features */
1958 'features_message' => esc_html__( 'In order to install and activate some of the features you requested %1$s you need to activate your PRO license after setup is complete.', 'charitable' ),
1959 'features_message_activate' => esc_html__( 'Activating license and installing addons...', 'charitable' ),
1960 'payment_methods' => $payment_methods,
1961 'stripe_connect_url' => $stripe_connect_url,
1962 'campaign_skip' => $this->maybe_skip_campaign_setup(),
1963 'checklist_url' => admin_url( 'admin.php?page=charitable-setup-checklist' ),
1964 'stripe_skip_html' => '<a href="' . admin_url( 'admin.php?page=charitable-setup-checklist' ) . '">' . esc_html__( 'Skip setting up Stripe', 'charitable' ) . '</a>.',
1965 'stripe_returned' => $stripe_returned,
1966 'checklist_completed' => get_option( 'charitable_ss_complete', false ) ? true : false,
1967 'test_mode' => ! empty( $_GET['test_mode'] ), // phpcs:ignore
1968 );
1969
1970 $strings = apply_filters( 'charitable_setup_strings', $strings );
1971
1972 return (array) $strings;
1973 }
1974
1975 /**
1976 * Get the requested feature string for the install message about license activation.
1977 *
1978 * @since 1.8.4
1979 *
1980 * @return string
1981 */
1982 public function get_requested_feature_string() {
1983
1984 $features = $this->features_to_install();
1985
1986 if ( empty( $features ) ) {
1987 return '';
1988 }
1989
1990 // create an array of names from feature_meta, with strong tags.
1991 $features = array_map(
1992 function ( $feature ) {
1993 return '<strong>' . $this->feature_meta[ $feature ]['name'] . '</strong>';
1994 },
1995 $features
1996 );
1997
1998 // make string with a comma and an 'and' before the last feature.
1999 $features = implode( ', ', $features );
2000
2001 return $features;
2002 }
2003
2004 /**
2005 * Returns and/or create the single instance of this class.
2006 *
2007 * @since 1.8.4
2008 *
2009 * @return Charitable_Setup
2010 */
2011 public static function get_instance() {
2012 if ( is_null( self::$instance ) ) {
2013 self::$instance = new self();
2014 }
2015
2016 return self::$instance;
2017 }
2018 }
2019
2020 endif;
2021