PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13 1.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 1.7.0.9 All 211 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.13, at includes/admin/onboarding/class-charitable-setup.php

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