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 / import-items / class-charitable-givewp-importer.php

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

649 lines 17.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * GiveWP Migration Tool for Charitable Lite.
4 *
5 * Imports campaigns and donations directly from GiveWP database tables.
6 *
7 * @package Charitable/Classes/Charitable_GiveWP_Importer
8 * @author David Bisset
9 * @copyright Copyright (c) 2023, WP Charitable LLC
10 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
11 * @since 1.8.10
12 * @version 1.8.10
13 */
14
15 // Exit if accessed directly.
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 if ( ! class_exists( 'Charitable_GiveWP_Importer' ) ) :
21
22 /**
23 * Charitable_GiveWP_Importer
24 *
25 * Handles direct database migration from GiveWP to Charitable.
26 * Lite version supports campaigns and donations only.
27 *
28 * @since 1.8.10
29 */
30 class Charitable_GiveWP_Importer {
31
32 /**
33 * The single instance of this class.
34 *
35 * @var Charitable_GiveWP_Importer|null
36 */
37 private static $instance = null;
38
39 /**
40 * Batch size for processing items.
41 *
42 * @var int
43 */
44 private $batch_size = 25;
45
46 /**
47 * Create object instance.
48 *
49 * @since 1.8.10
50 */
51 private function __construct() {
52 }
53
54 /**
55 * Returns and/or create the single instance of this class.
56 *
57 * @since 1.8.10
58 *
59 * @return Charitable_GiveWP_Importer
60 */
61 public static function get_instance() {
62 if ( is_null( self::$instance ) ) {
63 self::$instance = new self();
64 }
65
66 return self::$instance;
67 }
68
69 /**
70 * Enqueue scripts for the GiveWP migration tool.
71 *
72 * @since 1.8.10
73 *
74 * @return void
75 */
76 public function enqueue_scripts() {
77 if ( ! charitable_is_tools_view( 'import' ) || ! charitable_is_tools_view( 'import__givewp' ) ) {
78 return;
79 }
80
81 wp_enqueue_script(
82 'charitable-givewp-migration',
83 charitable()->get_path( 'assets', false ) . 'js/admin/charitable-givewp-migration.js',
84 array( 'jquery' ),
85 charitable()->get_version(),
86 true
87 );
88
89 wp_localize_script(
90 'charitable-givewp-migration',
91 'charitable_givewp_migration',
92 array(
93 'ajax_url' => admin_url( 'admin-ajax.php' ),
94 'nonce' => wp_create_nonce( 'charitable_givewp_migration' ),
95 'i18n' => array(
96 'starting' => __( 'Starting migration...', 'charitable' ),
97 'importing' => __( 'Importing %1$s... (%2$d/%3$d)', 'charitable' ),
98 'complete' => __( 'Migration complete!', 'charitable' ),
99 'dry_complete' => __( 'Dry run complete! No data was written to the database.', 'charitable' ),
100 'error' => __( 'An error occurred during migration.', 'charitable' ),
101 'confirm' => __( 'Are you sure you want to start the migration? This will import GiveWP data into Charitable.', 'charitable' ),
102 'confirm_dry' => __( 'Start a dry run? This will simulate the migration without writing any data.', 'charitable' ),
103 'campaigns' => __( 'campaigns', 'charitable' ),
104 'donations' => __( 'donations', 'charitable' ),
105 ),
106 )
107 );
108 }
109
110 /**
111 * Check if GiveWP is active.
112 *
113 * @since 1.8.10
114 *
115 * @return bool
116 */
117 public function is_givewp_active() {
118 return defined( 'GIVE_VERSION' );
119 }
120
121 /**
122 * Get counts of GiveWP items available for import.
123 *
124 * @since 1.8.10
125 *
126 * @return array
127 */
128 public function get_counts() {
129 global $wpdb;
130
131 $counts = array(
132 'campaigns' => 0,
133 'donations' => 0,
134 );
135
136 if ( ! $this->is_givewp_active() ) {
137 return $counts;
138 }
139
140 // Count GiveWP forms (campaigns).
141 $counts['campaigns'] = (int) $wpdb->get_var(
142 $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status IN ('publish', 'draft', 'private')", 'give_forms' )
143 );
144
145 // Count GiveWP payments (donations).
146 $counts['donations'] = (int) $wpdb->get_var(
147 $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status != %s", 'give_payment', 'trash' )
148 );
149
150 return $counts;
151 }
152
153 /**
154 * Initialize an import session.
155 *
156 * @since 1.8.10
157 *
158 * @param array $options Import options.
159 *
160 * @return string|false Import ID or false on failure.
161 */
162 public function init_import( $options = array() ) {
163 $import_id = wp_generate_uuid4();
164
165 $defaults = array(
166 'campaigns' => true,
167 'donations' => true,
168 'dry_run' => false,
169 );
170
171 $options = wp_parse_args( $options, $defaults );
172
173 // Build phases based on options.
174 $phases = array();
175 if ( $options['campaigns'] ) {
176 $phases[] = 'campaigns';
177 }
178 if ( $options['donations'] ) {
179 $phases[] = 'donations';
180 }
181
182 if ( empty( $phases ) ) {
183 return false;
184 }
185
186 $progress = array(
187 'import_id' => $import_id,
188 'options' => $options,
189 'phases' => $phases,
190 'current_phase' => 0,
191 'offset' => 0,
192 'status' => 'running',
193 'campaign_map' => array(), // Maps GiveWP form IDs to Charitable campaign IDs.
194 'results' => array(
195 'campaigns' => array(
196 'imported' => 0,
197 'skipped' => 0,
198 'errors' => 0,
199 ),
200 'donations' => array(
201 'imported' => 0,
202 'skipped' => 0,
203 'errors' => 0,
204 ),
205 ),
206 );
207
208 set_transient( 'charitable_givewp_import_' . $import_id, $progress, HOUR_IN_SECONDS );
209
210 return $import_id;
211 }
212
213 /**
214 * Run the next batch of the import.
215 *
216 * @since 1.8.10
217 *
218 * @param string $import_id The import session ID.
219 *
220 * @return array|false Progress data or false on failure.
221 */
222 public function run_batch( $import_id ) {
223 $progress = get_transient( 'charitable_givewp_import_' . $import_id );
224
225 if ( ! $progress || 'running' !== $progress['status'] ) {
226 return false;
227 }
228
229 $current_phase_index = $progress['current_phase'];
230
231 if ( $current_phase_index >= count( $progress['phases'] ) ) {
232 $progress['status'] = 'complete';
233 set_transient( 'charitable_givewp_import_' . $import_id, $progress, HOUR_IN_SECONDS );
234 return $progress;
235 }
236
237 $phase = $progress['phases'][ $current_phase_index ];
238 $offset = $progress['offset'];
239
240 $dry_run = ! empty( $progress['options']['dry_run'] );
241
242 switch ( $phase ) {
243 case 'campaigns':
244 $processed = $this->import_campaigns_batch( $progress, $offset, $dry_run );
245 break;
246
247 case 'donations':
248 $processed = $this->import_donations_batch( $progress, $offset, $dry_run );
249 break;
250
251 default:
252 $processed = 0;
253 break;
254 }
255
256 if ( $processed < $this->batch_size ) {
257 // Phase complete, move to next.
258 $progress['current_phase'] = $current_phase_index + 1;
259 $progress['offset'] = 0;
260 } else {
261 $progress['offset'] = $offset + $processed;
262 }
263
264 // Check if all phases are done.
265 if ( $progress['current_phase'] >= count( $progress['phases'] ) ) {
266 $progress['status'] = 'complete';
267 }
268
269 set_transient( 'charitable_givewp_import_' . $import_id, $progress, HOUR_IN_SECONDS );
270
271 return $progress;
272 }
273
274 /**
275 * Import a batch of campaigns from GiveWP forms.
276 *
277 * @since 1.8.10
278 *
279 * @param array &$progress The progress data (passed by reference).
280 * @param int $offset The offset for the query.
281 * @param bool $dry_run Whether this is a dry run.
282 *
283 * @return int Number of items processed in this batch.
284 */
285 private function import_campaigns_batch( &$progress, $offset, $dry_run = false ) {
286 global $wpdb;
287
288 $forms = $wpdb->get_results(
289 $wpdb->prepare(
290 "SELECT * FROM {$wpdb->posts}
291 WHERE post_type = 'give_forms'
292 AND post_status IN ('publish', 'draft', 'private')
293 ORDER BY ID ASC
294 LIMIT %d OFFSET %d",
295 $this->batch_size,
296 $offset
297 )
298 );
299
300 if ( empty( $forms ) ) {
301 return 0;
302 }
303
304 foreach ( $forms as $form ) {
305 // Check for duplicate (already imported).
306 $existing = $wpdb->get_var(
307 $wpdb->prepare(
308 "SELECT post_id FROM {$wpdb->postmeta}
309 WHERE meta_key = '_givewp_form_id' AND meta_value = %s
310 LIMIT 1",
311 $form->ID
312 )
313 );
314
315 if ( $existing ) {
316 ++$progress['results']['campaigns']['skipped'];
317 // Still map it for donation imports.
318 $progress['campaign_map'][ $form->ID ] = $existing;
319 continue;
320 }
321
322 if ( $dry_run ) {
323 ++$progress['results']['campaigns']['imported'];
324 continue;
325 }
326
327 // Get the GiveWP form goal.
328 $goal = get_post_meta( $form->ID, '_give_set_goal', true );
329
330 $campaign_data = array(
331 'post_title' => $form->post_title,
332 'post_content' => $form->post_content,
333 'post_status' => 'draft',
334 'post_type' => 'campaign',
335 'post_author' => $form->post_author,
336 'post_date' => $form->post_date,
337 'post_date_gmt' => $form->post_date_gmt,
338 );
339
340 $campaign_id = wp_insert_post( $campaign_data );
341
342 if ( is_wp_error( $campaign_id ) ) {
343 ++$progress['results']['campaigns']['errors'];
344 continue;
345 }
346
347 // Set goal if available.
348 if ( ! empty( $goal ) && is_numeric( $goal ) ) {
349 update_post_meta( $campaign_id, '_campaign_goal', sanitize_text_field( $goal ) );
350 }
351
352 // Store reference to original GiveWP form.
353 update_post_meta( $campaign_id, '_givewp_form_id', $form->ID );
354
355 // Map for donation import.
356 $progress['campaign_map'][ $form->ID ] = $campaign_id;
357
358 ++$progress['results']['campaigns']['imported'];
359 }
360
361 return count( $forms );
362 }
363
364 /**
365 * Import a batch of donations from GiveWP payments.
366 *
367 * @since 1.8.10
368 *
369 * @param array &$progress The progress data (passed by reference).
370 * @param int $offset The offset for the query.
371 * @param bool $dry_run Whether this is a dry run.
372 *
373 * @return int Number of items processed in this batch.
374 */
375 private function import_donations_batch( &$progress, $offset, $dry_run = false ) {
376 global $wpdb;
377
378 $payments = $wpdb->get_results(
379 $wpdb->prepare(
380 "SELECT * FROM {$wpdb->posts}
381 WHERE post_type = 'give_payment'
382 AND post_status != 'trash'
383 ORDER BY ID ASC
384 LIMIT %d OFFSET %d",
385 $this->batch_size,
386 $offset
387 )
388 );
389
390 if ( empty( $payments ) ) {
391 return 0;
392 }
393
394 // Block emails during bulk import.
395 if ( ! $dry_run ) {
396 $this->start_blocking_emails();
397 }
398
399 foreach ( $payments as $payment ) {
400 // Check for duplicate.
401 $existing = $wpdb->get_var(
402 $wpdb->prepare(
403 "SELECT post_id FROM {$wpdb->postmeta}
404 WHERE meta_key = '_givewp_donation_id' AND meta_value = %s
405 LIMIT 1",
406 $payment->ID
407 )
408 );
409
410 if ( $existing ) {
411 ++$progress['results']['donations']['skipped'];
412 continue;
413 }
414
415 if ( $dry_run ) {
416 ++$progress['results']['donations']['imported'];
417 continue;
418 }
419
420 // Get GiveWP donation meta.
421 $amount = get_post_meta( $payment->ID, '_give_payment_total', true );
422 $form_id = get_post_meta( $payment->ID, '_give_payment_form_id', true );
423 $email = get_post_meta( $payment->ID, '_give_payment_donor_email', true );
424 $first_name = get_post_meta( $payment->ID, '_give_donor_billing_first_name', true );
425 $last_name = get_post_meta( $payment->ID, '_give_donor_billing_last_name', true );
426 $gateway = get_post_meta( $payment->ID, '_give_payment_gateway', true );
427
428 if ( empty( $email ) || empty( $amount ) ) {
429 ++$progress['results']['donations']['errors'];
430 continue;
431 }
432
433 // Resolve campaign ID.
434 $campaign_id = 0;
435 if ( ! empty( $form_id ) && isset( $progress['campaign_map'][ $form_id ] ) ) {
436 $campaign_id = $progress['campaign_map'][ $form_id ];
437 }
438
439 // Map GiveWP status to Charitable status.
440 $status = $this->map_payment_status( $payment->post_status );
441
442 $donation_args = array(
443 'user' => array(
444 'email' => sanitize_email( $email ),
445 'first_name' => sanitize_text_field( $first_name ),
446 'last_name' => sanitize_text_field( $last_name ),
447 ),
448 'campaigns' => array(
449 array(
450 'campaign_id' => $campaign_id,
451 'amount' => floatval( $amount ),
452 ),
453 ),
454 'gateway' => sanitize_text_field( $gateway ),
455 'status' => $status,
456 'date' => $payment->post_date,
457 'log_note' => __( 'Imported from GiveWP via Migration Tool.', 'charitable' ),
458 );
459
460 $donation_id = charitable_create_donation( $donation_args );
461
462 if ( $donation_id ) {
463 update_post_meta( $donation_id, '_givewp_donation_id', $payment->ID );
464
465 // Copy address meta if available.
466 $address_fields = array(
467 '_give_donor_billing_address1' => 'address',
468 '_give_donor_billing_address2' => 'address_2',
469 '_give_donor_billing_city' => 'city',
470 '_give_donor_billing_state' => 'state',
471 '_give_donor_billing_zip' => 'postcode',
472 '_give_donor_billing_country' => 'country',
473 );
474
475 foreach ( $address_fields as $give_key => $charitable_key ) {
476 $value = get_post_meta( $payment->ID, $give_key, true );
477 if ( ! empty( $value ) ) {
478 update_post_meta( $donation_id, 'donor_' . $charitable_key, sanitize_text_field( $value ) );
479 }
480 }
481
482 ++$progress['results']['donations']['imported'];
483 } else {
484 ++$progress['results']['donations']['errors'];
485 }
486 }
487
488 if ( ! $dry_run ) {
489 $this->stop_blocking_emails();
490 }
491
492 return count( $payments );
493 }
494
495 /**
496 * Map a GiveWP payment post_status to a Charitable donation status.
497 *
498 * @since 1.8.10
499 *
500 * @param string $givewp_status The GiveWP payment post_status.
501 *
502 * @return string The Charitable donation status.
503 */
504 private function map_payment_status( $givewp_status ) {
505 $map = array(
506 'give_subscription' => 'charitable-completed',
507 'publish' => 'charitable-completed',
508 'give_pending' => 'charitable-pending',
509 'pending' => 'charitable-pending',
510 'give_processing' => 'charitable-pending',
511 'give_complete' => 'charitable-completed',
512 'give_cancelled' => 'charitable-cancelled',
513 'give_abandoned' => 'charitable-cancelled',
514 'give_failed' => 'charitable-failed',
515 'give_revoked' => 'charitable-cancelled',
516 'give_preapproval' => 'charitable-pending',
517 'refunded' => 'charitable-refunded',
518 'give_refunded' => 'charitable-refunded',
519 );
520
521 return isset( $map[ $givewp_status ] ) ? $map[ $givewp_status ] : 'charitable-pending';
522 }
523
524 /**
525 * Block Charitable email notifications during bulk import.
526 *
527 * @since 1.8.10
528 *
529 * @return void
530 */
531 private function start_blocking_emails() {
532 add_filter( 'charitable_send_email', '__return_false', 999 );
533 }
534
535 /**
536 * Unblock Charitable email notifications after bulk import.
537 *
538 * @since 1.8.10
539 *
540 * @return void
541 */
542 private function stop_blocking_emails() {
543 remove_filter( 'charitable_send_email', '__return_false', 999 );
544 }
545
546 /**
547 * Get the import summary from a completed import.
548 *
549 * @since 1.8.10
550 *
551 * @param string $import_id The import session ID.
552 *
553 * @return array|false The results array or false.
554 */
555 public function get_import_summary( $import_id ) {
556 $progress = get_transient( 'charitable_givewp_import_' . $import_id );
557
558 if ( ! $progress ) {
559 return false;
560 }
561
562 return $progress['results'];
563 }
564
565 /**
566 * Handle the AJAX request to start a migration.
567 *
568 * @since 1.8.10
569 *
570 * @return void
571 */
572 public function ajax_import_start() {
573 check_ajax_referer( 'charitable_givewp_migration', 'nonce' );
574
575 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
576 wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'charitable' ) ) );
577 }
578
579 if ( ! $this->is_givewp_active() ) {
580 wp_send_json_error( array( 'message' => __( 'GiveWP is not active.', 'charitable' ) ) );
581 }
582
583 $options = array(
584 'campaigns' => ! empty( $_POST['campaigns'] ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
585 'donations' => ! empty( $_POST['donations'] ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
586 'dry_run' => ! empty( $_POST['dry_run'] ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
587 );
588
589 $import_id = $this->init_import( $options );
590
591 if ( ! $import_id ) {
592 wp_send_json_error( array( 'message' => __( 'No import types selected.', 'charitable' ) ) );
593 }
594
595 $counts = $this->get_counts();
596
597 wp_send_json_success(
598 array(
599 'import_id' => $import_id,
600 'counts' => $counts,
601 'options' => $options,
602 )
603 );
604 }
605
606 /**
607 * Handle the AJAX request to process a migration batch.
608 *
609 * @since 1.8.10
610 *
611 * @return void
612 */
613 public function ajax_import_batch() {
614 check_ajax_referer( 'charitable_givewp_migration', 'nonce' );
615
616 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
617 wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'charitable' ) ) );
618 }
619
620 $import_id = isset( $_POST['import_id'] ) ? sanitize_text_field( wp_unslash( $_POST['import_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
621
622 if ( empty( $import_id ) ) {
623 wp_send_json_error( array( 'message' => __( 'Invalid import session.', 'charitable' ) ) );
624 }
625
626 $progress = $this->run_batch( $import_id );
627
628 if ( false === $progress ) {
629 wp_send_json_error( array( 'message' => __( 'Import session not found or already completed.', 'charitable' ) ) );
630 }
631
632 $current_phase = '';
633 if ( isset( $progress['phases'][ $progress['current_phase'] ] ) ) {
634 $current_phase = $progress['phases'][ $progress['current_phase'] ];
635 }
636
637 wp_send_json_success(
638 array(
639 'status' => $progress['status'],
640 'current_phase' => $current_phase,
641 'results' => $progress['results'],
642 'dry_run' => ! empty( $progress['options']['dry_run'] ),
643 )
644 );
645 }
646 }
647
648 endif;
649