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

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

925 lines 32.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * This class is responsible for adding the Charitable admin pages.
4 *
5 * @package Charitable/Classes/Charitable_Import_Items
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.7.0.7
10 * @version 1.8.9.1
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Import_Items' ) ) :
19
20 /**
21 * Charitable_Import_Items
22 *
23 * @since 1.7.0.7
24 */
25 final class Charitable_Import_Items {
26
27 /**
28 * The single instance of this class.
29 *
30 * @var Charitable_Import_Items|null
31 */
32 private static $instance = null;
33
34 /**
35 * Holds any plugin error messages.
36 *
37 * @since 1.0.0
38 *
39 * @var array
40 */
41 public $errors = array();
42
43 /**
44 * Create class object.
45 *
46 * @since 1.7.0.7
47 */
48 private function __construct() {
49 }
50
51 /**
52 * Returns and/or create the single instance of this class.
53 *
54 * @since 1.7.0.7
55 *
56 * @return Charitable_Import_Items
57 */
58 public static function get_instance() {
59 if ( is_null( self::$instance ) ) {
60 self::$instance = new self();
61 }
62
63 return self::$instance;
64 }
65
66 /**
67 * This forces the charitable menu to be open for category and tag pages.
68 *
69 * @since 1.7.0.7
70 *
71 * @return string
72 */
73 public function admin_accept_import_campaign_request() {
74
75 global $wpdb;
76
77 if ( ! is_admin() || empty( $_POST ) ) {
78 return;
79 }
80
81 if ( ! isset( $_POST['charitable_nonce'] ) || ! wp_verify_nonce( $_POST['charitable_nonce'], 'import_campaign' ) ) { // phpcs:ignore
82 return;
83 }
84
85 if ( ! $this->has_json_extension() ) {
86 $this->errors[] = __( 'Sorry, but Charitable import files must be in <code>.json</code> format.', 'charitable' );
87 $redirect_link = admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=charitable' );
88 wp_safe_redirect( $redirect_link );
89 exit;
90 }
91
92 // Retrieve the JSON contents of the file. If that fails, return an error.
93 $contents = $this->get_file_contents();
94 if ( ! $contents ) {
95 $this->errors[] = __( 'Sorry, but there was an error retrieving the contents of the campaign export file. Please try again.', 'charitable' );
96 $redirect_link = admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=charitable' );
97 wp_safe_redirect( $redirect_link );
98 exit;
99 }
100
101 // Decode the settings and start processing.
102 $data = json_decode( $contents, true );
103
104 // get the old post ID as we assign the new one, as we will want to document this as metadata.
105 $old_post_id = $data['post']['ID'];
106
107 // since we don't import authors/campaign creators, determine if the email address of the campaign creater exists in this site.
108 $old_post_author_email = $data['campaign_creator']['user_email'];
109 $campaign_creator = get_user_by( 'email', $old_post_author_email );
110 if ( $campaign_creator ) {
111 $campaign_creator_id = $campaign_creator->ID;
112 } else {
113 // assign it to the user who is logged in?
114 $campaign_creator_id = get_current_user_id();
115 }
116
117 $campaign_data = array(
118 'post_title' => $data['post']['post_title'],
119 'post_content' => $data['post']['post_content'],
120 'post_excerpt' => $data['post']['post_excerpt'],
121 'post_date' => $data['post']['post_date'],
122 'post_date_gmt' => $data['post']['post_date_gmt'],
123 'comment_status' => $data['post']['comment_status'],
124 'ping_status' => $data['post']['ping_status'],
125 'post_name' => $data['post']['post_name'],
126 'post_modified' => $data['post']['post_modified'],
127 'post_modified_gmt' => $data['post']['post_modified_gmt'],
128 'post_content_filtered' => $data['post']['post_content_filtered'],
129 'guid' => $data['post']['guid'],
130 'post_mime_type' => $data['post']['post_mime_type'],
131 'comment_count' => $data['post']['comment_count'],
132 'post_status' => 'draft',
133 'post_type' => 'campaign',
134 'post_author' => $campaign_creator_id,
135 );
136 $campaign_id = wp_insert_post( $campaign_data );
137
138 foreach ( $data['meta'] as $meta_import_id => $meta_to_import ) {
139 if ( is_serialized( $meta_to_import['meta_value'] ) ) {
140 $data_to_import = unserialize( $meta_to_import['meta_value'] );
141 } else {
142 $data_to_import = $meta_to_import['meta_value'];
143 }
144 add_post_meta( $campaign_id, $meta_to_import['meta_key'], $data_to_import );
145 }
146
147 add_post_meta( $campaign_id, '_campaign_imported_campaign_key', intval( $old_post_id ) );
148
149 // categories and tags.
150 if ( isset( $data['campaign_category'] ) && ! empty( $data['campaign_category'] ) ) {
151 foreach ( $data['campaign_category'] as $term_to_import ) {
152 $term = term_exists( $term_to_import['slug'], 'campaign_category' );
153 if ( ! $term ) { // create category.
154 $term_id = wp_insert_term(
155 $term_to_import['name'], // the term.
156 'campaign_category', // the taxonomy.
157 array(
158 'description' => $term_to_import['description'],
159 'slug' => $term_to_import['slug'],
160 )
161 );
162 } else {
163 $term_id = intval( $term['term_id'] );
164 }
165 wp_set_post_terms( $campaign_id, array( $term_id ), 'campaign_category', true );
166
167 }
168 }
169 if ( isset( $data['campaign_tag'] ) && ! empty( $data['campaign_tag'] ) ) {
170 foreach ( $data['campaign_tag'] as $term_to_import ) {
171 $term = term_exists( $term_to_import['slug'], 'campaign_tag' );
172 if ( ! $term ) { // create category.
173 $term_id = wp_insert_term(
174 $term_to_import['name'], // the term.
175 'campaign_tag', // the taxonomy.
176 array(
177 'description' => $term_to_import['description'],
178 'slug' => $term_to_import['slug'],
179 )
180 );
181 } else {
182 $term_id = intval( $term['term_id'] );
183 }
184 wp_set_post_terms( $campaign_id, array( $term_id ), 'campaign_tag', true );
185
186 }
187 }
188
189 // get featured image.
190 if ( isset( $data['thumbnail'] ) && ! empty( $data['thumbnail'] ) ) {
191
192 // Prepare variables.
193 $src = esc_url( $data['thumbnail'] );
194 $stream = wp_remote_get( $src, array( 'timeout' => 60 ) );
195 $type = wp_remote_retrieve_header( $stream, 'content-type' );
196 $filename = basename( $src );
197 $fileinfo = pathinfo( $filename );
198
199 // If the filename doesn't have an extension on it, determine the filename to use to save this image to the Media Library
200 // This fixes importing URLs with no file extension e.g. http://placehold.it/300x300 (which is a PNG).
201 if ( ! isset( $fileinfo['extension'] ) || empty( $fileinfo['extension'] ) ) {
202 switch ( $type ) {
203 case 'image/jpeg':
204 $filename = $filename . '.jpeg';
205 break;
206 case 'image/jpg':
207 $filename = $filename . '.jpg';
208 break;
209 case 'image/gif':
210 $filename = $filename . '.gif';
211 break;
212 case 'image/png':
213 $filename = $filename . '.png';
214 break;
215 case 'image/webp':
216 $filename = $filename . '.webp';
217 break;
218 }
219 }
220
221 // If we cannot get the image or determine the type, bail.
222 if ( is_wp_error( $stream ) ) {
223
224 if ( charitable_is_debug() ) {
225 error_log( 'admin_accept_import_campaign_request:: ' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
226 error_log( print_r( $stream, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
227 }
228
229 } elseif ( ! $type || strpos( $type, 'text/html' ) !== false ) {
230
231 if ( charitable_is_debug() ) {
232 error_log( 'admin_accept_import_campaign_request type: ' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
233 error_log( print_r( $type, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
234 }
235
236 } else {
237 // It is an image. Stream the image.
238 $mirror = wp_upload_bits( $filename, null, wp_remote_retrieve_body( $stream ) );
239
240 // If there is an error, bail.
241 if ( ! empty( $mirror['error'] ) ) {
242
243 if ( charitable_is_debug() ) {
244 error_log( 'admin_accept_import_campaign_request:: ' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
245 error_log( print_r( $stream, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
246 }
247
248 } else {
249 // Check if the $item has title, caption, alt specified
250 // If so, store those values against the attachment so they're included in the Gallery
251 // If not, fallback to the defaults.
252 $attachment = array(
253 'post_title' => ( ( isset( $data['post']['post_title'] ) && ! empty( $data['post']['post_title'] ) ) ? $data['post']['post_title'] : urldecode( $filename ) ), // Title.
254 'post_mime_type' => $type,
255 'post_excerpt' => ( ( isset( $data['post']['post_excerpt'] ) && ! empty( $data['post']['post_excerpt'] ) ) ? $data['post']['post_excerpt'] : '' ), // Caption.
256 );
257 $attach_id = wp_insert_attachment( $attachment, $mirror['file'], $campaign_id );
258 if ( ( isset( $item['alt'] ) && ! empty( $item['alt'] ) ) ) {
259 update_post_meta( $attach_id, '_wp_attachment_image_alt', $item['alt'] );
260 }
261
262 // Generate and update attachment metadata.
263 if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
264 require ABSPATH . 'wp-admin/includes/image.php';
265 }
266
267 // Generate and update attachment metadata.
268 $attach_data = wp_generate_attachment_metadata( $attach_id, $mirror['file'] );
269 wp_update_attachment_metadata( $attach_id, $attach_data );
270
271 // And finally assign featured image to post.
272 set_post_thumbnail( $campaign_id, $attach_id );
273
274 }
275 }
276
277 }
278
279 $this->add_update_message( __( 'Campaign imported.', 'charitable' ) . ' <a href="' . admin_url( 'post.php?post=' . $campaign_id . '&action=edit' ) . '">View campaign</a>.', 'success' );
280
281 $redirect_link = admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=charitable' );
282 wp_safe_redirect( $redirect_link );
283 exit;
284 }
285
286 /**
287 * This forces the charitable menu to be open for category and tag pages.
288 *
289 * @since 1.7.0.7
290 *
291 * @return string
292 */
293 public function admin_accept_import_donations_request() {
294
295 global $wpdb;
296
297 if ( ! is_admin() || empty( $_POST ) ) {
298 return;
299 }
300
301 if ( ! isset( $_POST['charitable_nonce'] ) || ! wp_verify_nonce( $_POST['charitable_nonce'], 'import_donations' ) ) { // phpcs:ignore
302 return;
303 }
304
305 if ( ! $this->has_json_extension( 'import_donations' ) ) {
306 $this->add_update_message( __( 'Sorry, but Charitable import files must be in <code>.json</code> format.', 'charitable' ), 'error' );
307 $redirect_link = admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=charitable' );
308 wp_safe_redirect( $redirect_link );
309 exit;
310 }
311
312 // Retrieve the JSON contents of the file. If that fails, return an error.
313 $contents = $this->get_file_contents( 'import_donations' );
314 if ( ! $contents ) {
315 $this->add_update_message( __( 'Sorry, but there was an error retrieving the contents of the donation export file. Please try again.', 'charitable' ), 'error' );
316 $redirect_link = admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=charitable' );
317 wp_safe_redirect( $redirect_link );
318 exit;
319 }
320
321 // Decode the settings and start processing.
322 $data = json_decode( $contents, true );
323
324 // Get the campaign we are importing these donations into, otherwise bail.
325 $campaign_id = isset( $_POST['charitable_settings']['tools_campaign'] ) ? intval( $_POST['charitable_settings']['tools_campaign'] ) : false;
326
327 if ( false === $campaign_id || ! isset( $data['campaigns'] ) || count( $data['campaigns'] ) === 0 ) {
328 $this->add_update_message( __( 'Sorry, but there was an error attempting to determine the campaign ID for this import. Please try again.', 'charitable' ), 'error' );
329 $redirect_link = admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=charitable' );
330 wp_safe_redirect( $redirect_link );
331 exit;
332 }
333
334 $campaign_name = get_the_title( $campaign_id ) ? get_the_title( $campaign_id ) : 'Unknown Campaign';
335 $donations_added = 0;
336
337 foreach ( $data['campaigns'] as $old_campaign_id => $campaign_donations_info ) {
338 foreach ( $campaign_donations_info['donation_posts'] as $old_donation_id => $donation_data ) {
339
340 $donor_data = false;
341 $found_key = false;
342 $donation_log = array();
343
344 if ( ! isset( $data['campaigns'][ $old_campaign_id ]['charitable_campaign_donations'] ) ) {
345 continue;
346 }
347
348 // confirm the existance of the extra table data, otherwise adding this donation is meaningless.
349 foreach ( $campaign_donations_info['charitable_campaign_donations'] as $key => $charitable_campaign_donation ) {
350 if ( intval( $charitable_campaign_donation['donation_id'] ) === $old_donation_id ) {
351 $found_key = $key;
352 }
353 }
354
355 if ( ! $found_key ) {
356 continue;
357 }
358
359 if ( ! isset( $donation_data['post'] ) ) {
360 if ( charitable_is_debug() ) {
361 error_log( 'import post fail donation data: ' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
362 error_log( print_r( $donation_data, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
363 }
364 return;
365 }
366
367 // create the donation post.
368 $donation_post_args = array(
369 'post_title' => $donation_data['post']['post_title'],
370 'post_content' => $donation_data['post']['post_content'],
371 'post_excerpt' => $donation_data['post']['post_excerpt'],
372 'post_date' => $donation_data['post']['post_date'],
373 'post_date_gmt' => $donation_data['post']['post_date_gmt'],
374 'comment_status' => $donation_data['post']['comment_status'],
375 'ping_status' => $donation_data['post']['ping_status'],
376 'post_name' => $donation_data['post']['post_name'],
377 'post_modified' => $donation_data['post']['post_modified'],
378 'post_modified_gmt' => $donation_data['post']['post_modified_gmt'],
379 'post_content_filtered' => $donation_data['post']['post_content_filtered'],
380 'guid' => $donation_data['post']['guid'],
381 'post_mime_type' => $donation_data['post']['post_mime_type'],
382 'comment_count' => $donation_data['post']['comment_count'],
383 'post_status' => $donation_data['post']['post_status'], // 'charitable-pending',
384 'post_type' => 'donation',
385 'post_author' => $this->get_creator_id( $donation_data['author'] ), // $campaign_creator_id,
386 );
387 $donation_id = wp_insert_post( $donation_post_args );
388
389 // import meta.
390 foreach ( $donation_data['meta'] as $meta_import_id => $meta_to_import ) {
391 if ( is_serialized( $meta_to_import['meta_value'] ) ) {
392 $data_to_import = unserialize( $meta_to_import['meta_value'] ); // phpcs:ignore
393 } else {
394 $data_to_import = $meta_to_import['meta_value'];
395 }
396 if ( $meta_to_import['meta_key'] === 'donor' ) {
397 $donor_data = unserialize( $meta_to_import['meta_value'] ); // phpcs:ignore
398 }
399 if ( $meta_to_import['meta_key'] === '_donation_log' ) {
400 $donation_log = unserialize( $meta_to_import['meta_value'] ); // phpcs:ignore
401 }
402
403 add_post_meta( $donation_id, $meta_to_import['meta_key'], $data_to_import );
404 }
405
406 add_post_meta( $donation_id, '_campaign_imported_donation_id', intval( $old_campaign_id ) );
407
408 // determine donor id from the donor data (email is key).
409 $donor_id = $this->get_donor_id( $donor_data ); // $campaign_creator_id,
410
411 // update custom table.
412 $table = $wpdb->prefix . 'charitable_campaign_donations';
413 $args = array(
414 'donation_id' => $donation_id,
415 'donor_id' => $donor_id,
416 'campaign_id' => $campaign_id,
417 'campaign_name' => $campaign_name,
418 'amount' => $campaign_donations_info['charitable_campaign_donations'][ $found_key ]['amount'], // ... and so on
419 );
420
421 $wpdb->insert( $table, $args ); // phpcs:ignore
422
423 // update log.
424 $donation_log[] = array(
425 'time' => time(),
426 'message' => 'Donation Imported.',
427 );
428 update_post_meta( $donation_id, '_donation_log', $donation_log );
429
430 ++$donations_added;
431
432 }
433 }
434
435 /* translators: %d is the number of donations imported. */
436 $this->add_update_message( sprintf( __( '%d donations imported.', 'charitable' ), $donations_added ), 'success' );
437
438 $redirect_link = admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=charitable&status=success+' . $donations_added );
439 wp_safe_redirect( $redirect_link );
440 exit;
441 }
442
443 /**
444 * Determines if a gallery import file has a proper file extension.
445 *
446 * @since 1.0.0
447 *
448 * @param string $import_what The type of import file.
449 *
450 * @return bool True if the imported gallery file has a proper file extension, false otherwise.
451 */
452 public function has_json_extension( $import_what = 'import_campaign' ) {
453
454 $file_array = isset( $_FILES['charitable_settings']['name']['import'][ $import_what ] ) ? explode( '.', $_FILES['charitable_settings']['name']['import'][ $import_what ] ) : null; // phpcs:ignore
455 $extension = end( $file_array );
456 return 'json' === $extension;
457 }
458
459 /**
460 * Retrieve the contents of the imported gallery file.
461 *
462 * @since 1.0.0
463 *
464 * @param string $import_what The type of import file.
465 *
466 * @return string|bool JSON contents string if successful, false otherwise.
467 */
468 public function get_file_contents( $import_what = 'import_campaign' ) {
469 $file = isset( $_FILES['charitable_settings']['tmp_name']['import'][ $import_what ] ) // phpcs:ignore
470 ? wp_unslash( $_FILES['charitable_settings']['tmp_name']['import'][ $import_what ] ) // phpcs:ignore
471 : false; // phpcs:ignore
472
473 if ( ! $file || ! file_exists( $file ) ) {
474 return false;
475 }
476
477 $contents = file_get_contents( $file );
478 if ( $contents === false ) {
479 return false;
480 }
481
482 return $contents;
483 }
484
485 /**
486 * Locate the user id of someone using this email
487 *
488 * @since 1.0.0
489 *
490 * @param array $author_data The author data.
491 * @param boolean $zero_allowed Whether a zero value is allowed.
492 *
493 * @return string|bool JSON contents string if successful, false otherwise.
494 */
495 public function get_creator_id( $author_data, $zero_allowed = false ) {
496
497 $email = false;
498
499 // try finding a user by email first.
500 if ( isset( $author_data['email'] ) ) {
501 $email = $author_data['email'];
502 } elseif ( isset( $author_data['user_email'] ) ) {
503 $email = $author_data['user_email'];
504 }
505
506 if ( $email ) {
507 $user = get_user_by( 'email', $email );
508 if ( $user ) {
509 return $user->ID;
510 }
511 }
512
513 return $zero_allowed ? 0 : get_current_user_id();
514 }
515
516 /**
517 * Locate the user id of someone using this email
518 *
519 * @since 1.0.0
520 *
521 * @param array $donor_data The donor data.
522 *
523 * @return string|bool JSON contents string if successful, false otherwise.
524 */
525 public function get_donor_id( $donor_data ) {
526
527 global $wpdb;
528
529 $email = esc_html( $donor_data['email'] );
530
531 // update custom table.
532 $table = $wpdb->prefix . 'charitable_donors';
533
534 // try finding if they still exist first.
535 $donor_id = $wpdb->get_var( "SELECT donor_id FROM $table WHERE email = '$email'" ); // phpcs:ignore
536
537 // if not, create it in the table.
538 if ( $donor_id === null ) {
539
540 $donor_id = $wpdb->insert( // phpcs:ignore
541 $table,
542 array(
543 'donor_id' => $donor_id,
544 'user_id' => $this->get_creator_id( $donor_data, true ),
545 'email' => ! empty( $donor_data['email'] ) ? $donor_data['email'] : '',
546 'first_name' => ! empty( $donor_data['first_name'] ) ? $donor_data['first_name'] : '',
547 'last_name' => ! empty( $donor_data['last_name'] ) ? $donor_data['last_name'] : '',
548 'date_joined' => ! empty( $donor_data['date_joined'] ) ? $donor_data['date_joined'] : '',
549 )
550 );
551
552 }
553
554 return $donor_id;
555 }
556
557 /**
558 * Process a GiveWP CSV donation upload.
559 *
560 * Maps GiveWP CSV columns to Charitable donation creation.
561 *
562 * @since 1.8.10
563 *
564 * @return void
565 */
566 public function admin_accept_import_donations_givewp_request() {
567
568 if ( ! is_admin() || empty( $_POST ) ) {
569 return;
570 }
571
572 if ( ! isset( $_POST['charitable_nonce'] ) || ! wp_verify_nonce( $_POST['charitable_nonce'], 'import_donations_givewp' ) ) { // phpcs:ignore
573 return;
574 }
575
576 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
577 return;
578 }
579
580 $redirect_link = admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=givewp' );
581
582 if ( ! $this->has_correct_extension( 'import__givewp', 'import_donations_givewp_csv', 'csv' ) ) {
583 $this->add_update_message( __( 'Sorry, but the import file must be in CSV format.', 'charitable' ), 'error' );
584 wp_safe_redirect( $redirect_link );
585 exit;
586 }
587
588 $file = $this->get_csv_file_path( 'import__givewp', 'import_donations_givewp_csv' );
589 if ( ! $file ) {
590 $this->add_update_message( __( 'Sorry, there was an error reading the uploaded file. Please try again.', 'charitable' ), 'error' );
591 wp_safe_redirect( $redirect_link );
592 exit;
593 }
594
595 $handle = fopen( $file, 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
596 if ( false === $handle ) {
597 $this->add_update_message( __( 'Sorry, there was an error reading the uploaded file. Please try again.', 'charitable' ), 'error' );
598 wp_safe_redirect( $redirect_link );
599 exit;
600 }
601
602 $headers = fgetcsv( $handle );
603 if ( ! $headers ) {
604 fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
605 $this->add_update_message( __( 'The CSV file appears to be empty or malformed.', 'charitable' ), 'error' );
606 wp_safe_redirect( $redirect_link );
607 exit;
608 }
609
610 // Normalize headers.
611 $headers = array_map( 'trim', $headers );
612
613 $donations_added = 0;
614 $skipped = 0;
615
616 while ( false !== ( $row = fgetcsv( $handle ) ) ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
617 if ( count( $row ) < count( $headers ) ) {
618 ++$skipped;
619 continue;
620 }
621
622 $data = array_combine( $headers, $row );
623
624 $first_name = isset( $data['First Name'] ) ? sanitize_text_field( $data['First Name'] ) : '';
625 $last_name = isset( $data['Last Name'] ) ? sanitize_text_field( $data['Last Name'] ) : '';
626 $email = isset( $data['Email Address'] ) ? sanitize_email( $data['Email Address'] ) : '';
627 $amount = isset( $data['Donation Total'] ) ? floatval( $data['Donation Total'] ) : 0;
628 $date = isset( $data['Donation Date'] ) ? sanitize_text_field( $data['Donation Date'] ) : '';
629 $status = isset( $data['Donation Status'] ) ? sanitize_text_field( $data['Donation Status'] ) : '';
630 $gateway = isset( $data['Payment Gateway'] ) ? sanitize_text_field( $data['Payment Gateway'] ) : '';
631
632 if ( empty( $email ) || $amount <= 0 ) {
633 ++$skipped;
634 continue;
635 }
636
637 $donation_args = array(
638 'user' => array(
639 'email' => $email,
640 'first_name' => $first_name,
641 'last_name' => $last_name,
642 ),
643 'campaigns' => array(
644 array(
645 'campaign_id' => 0,
646 'amount' => $amount,
647 ),
648 ),
649 'gateway' => sanitize_text_field( $gateway ),
650 'status' => $this->map_givewp_status( $status ),
651 'log_note' => __( 'Imported from GiveWP CSV.', 'charitable' ),
652 );
653
654 if ( ! empty( $date ) ) {
655 $donation_args['date'] = $date;
656 }
657
658 $donation_id = charitable_create_donation( $donation_args );
659
660 if ( $donation_id ) {
661 update_post_meta( $donation_id, '_givewp_csv_import', 1 );
662 ++$donations_added;
663 } else {
664 ++$skipped;
665 }
666 }
667
668 fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
669
670 /* translators: %1$d: number of donations imported. %2$d: number of rows skipped. */
671 $this->add_update_message( sprintf( __( '%1$d donations imported from GiveWP CSV. %2$d rows skipped.', 'charitable' ), $donations_added, $skipped ), 'success' );
672
673 wp_safe_redirect( $redirect_link );
674 exit;
675 }
676
677 /**
678 * Process a GiveButter CSV donation upload.
679 *
680 * Maps GiveButter CSV columns to Charitable donation creation.
681 *
682 * @since 1.8.10
683 *
684 * @return void
685 */
686 public function admin_accept_import_donations_givebutter_request() {
687
688 if ( ! is_admin() || empty( $_POST ) ) {
689 return;
690 }
691
692 if ( ! isset( $_POST['charitable_nonce'] ) || ! wp_verify_nonce( $_POST['charitable_nonce'], 'import_donations_givebutter' ) ) { // phpcs:ignore
693 return;
694 }
695
696 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
697 return;
698 }
699
700 $redirect_link = admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=givebutter' );
701
702 if ( ! $this->has_correct_extension( 'import__givebutter', 'import_donations_givebutter_csv', 'csv' ) ) {
703 $this->add_update_message( __( 'Sorry, but the import file must be in CSV format.', 'charitable' ), 'error' );
704 wp_safe_redirect( $redirect_link );
705 exit;
706 }
707
708 $file = $this->get_csv_file_path( 'import__givebutter', 'import_donations_givebutter_csv' );
709 if ( ! $file ) {
710 $this->add_update_message( __( 'Sorry, there was an error reading the uploaded file. Please try again.', 'charitable' ), 'error' );
711 wp_safe_redirect( $redirect_link );
712 exit;
713 }
714
715 $handle = fopen( $file, 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
716 if ( false === $handle ) {
717 $this->add_update_message( __( 'Sorry, there was an error reading the uploaded file. Please try again.', 'charitable' ), 'error' );
718 wp_safe_redirect( $redirect_link );
719 exit;
720 }
721
722 $headers = fgetcsv( $handle );
723 if ( ! $headers ) {
724 fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
725 $this->add_update_message( __( 'The CSV file appears to be empty or malformed.', 'charitable' ), 'error' );
726 wp_safe_redirect( $redirect_link );
727 exit;
728 }
729
730 $headers = array_map( 'trim', $headers );
731
732 $donations_added = 0;
733 $skipped = 0;
734
735 while ( false !== ( $row = fgetcsv( $handle ) ) ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
736 if ( count( $row ) < count( $headers ) ) {
737 ++$skipped;
738 continue;
739 }
740
741 $data = array_combine( $headers, $row );
742
743 $first_name = isset( $data['First Name'] ) ? sanitize_text_field( $data['First Name'] ) : '';
744 $last_name = isset( $data['Last Name'] ) ? sanitize_text_field( $data['Last Name'] ) : '';
745 $email = isset( $data['Email'] ) ? sanitize_email( $data['Email'] ) : '';
746 $amount = isset( $data['Amount'] ) ? floatval( $data['Amount'] ) : 0;
747 $date = isset( $data['Transaction Date (UTC)'] ) ? sanitize_text_field( $data['Transaction Date (UTC)'] ) : '';
748 $status = isset( $data['Status Friendly'] ) ? sanitize_text_field( $data['Status Friendly'] ) : '';
749 $method = isset( $data['Method'] ) ? sanitize_text_field( $data['Method'] ) : '';
750 $message = isset( $data['Public Message'] ) ? sanitize_text_field( $data['Public Message'] ) : '';
751
752 if ( empty( $email ) || $amount <= 0 ) {
753 ++$skipped;
754 continue;
755 }
756
757 $donation_args = array(
758 'user' => array(
759 'email' => $email,
760 'first_name' => $first_name,
761 'last_name' => $last_name,
762 ),
763 'campaigns' => array(
764 array(
765 'campaign_id' => 0,
766 'amount' => $amount,
767 ),
768 ),
769 'gateway' => sanitize_text_field( $method ),
770 'status' => $this->map_givebutter_status( $status ),
771 'log_note' => __( 'Imported from GiveButter CSV.', 'charitable' ),
772 );
773
774 if ( ! empty( $date ) ) {
775 $donation_args['date'] = $date;
776 }
777
778 $donation_id = charitable_create_donation( $donation_args );
779
780 if ( $donation_id ) {
781 update_post_meta( $donation_id, '_givebutter_csv_import', 1 );
782
783 // Add donor comment if there's a public message.
784 if ( ! empty( $message ) ) {
785 wp_insert_comment(
786 array(
787 'comment_post_ID' => $donation_id,
788 'comment_content' => $message,
789 'comment_type' => 'charitable_comment',
790 'comment_approved' => 1,
791 'user_id' => 0,
792 )
793 );
794 }
795
796 ++$donations_added;
797 } else {
798 ++$skipped;
799 }
800 }
801
802 fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
803
804 /* translators: %1$d: number of donations imported. %2$d: number of rows skipped. */
805 $this->add_update_message( sprintf( __( '%1$d donations imported from GiveButter CSV. %2$d rows skipped.', 'charitable' ), $donations_added, $skipped ), 'success' );
806
807 wp_safe_redirect( $redirect_link );
808 exit;
809 }
810
811 /**
812 * Check if the uploaded file has the correct extension.
813 *
814 * Enhanced version supporting sub-tab file field lookups.
815 *
816 * @since 1.8.10
817 *
818 * @param string $section The section key (e.g. 'import__givewp').
819 * @param string $field_name The field name within the section.
820 * @param string $extension The expected file extension (e.g. 'csv').
821 *
822 * @return bool
823 */
824 public function has_correct_extension( $section, $field_name, $extension = 'csv' ) {
825 $file_name = isset( $_FILES['charitable_settings']['name'][ $section ][ $field_name ] ) // phpcs:ignore
826 ? $_FILES['charitable_settings']['name'][ $section ][ $field_name ] // phpcs:ignore
827 : null;
828
829 if ( empty( $file_name ) ) {
830 return false;
831 }
832
833 $file_parts = explode( '.', $file_name );
834 $file_ext = strtolower( end( $file_parts ) );
835
836 return $extension === $file_ext;
837 }
838
839 /**
840 * Get the temporary file path for a CSV upload.
841 *
842 * @since 1.8.10
843 *
844 * @param string $section The section key (e.g. 'import__givewp').
845 * @param string $field_name The field name within the section.
846 *
847 * @return string|false The file path or false.
848 */
849 public function get_csv_file_path( $section, $field_name ) {
850 $file = isset( $_FILES['charitable_settings']['tmp_name'][ $section ][ $field_name ] ) // phpcs:ignore
851 ? wp_unslash( $_FILES['charitable_settings']['tmp_name'][ $section ][ $field_name ] ) // phpcs:ignore
852 : false;
853
854 if ( ! $file || ! file_exists( $file ) ) {
855 return false;
856 }
857
858 return $file;
859 }
860
861 /**
862 * Map a GiveWP donation status to a Charitable status.
863 *
864 * @since 1.8.10
865 *
866 * @param string $givewp_status The GiveWP status string.
867 *
868 * @return string The Charitable donation status.
869 */
870 public function map_givewp_status( $givewp_status ) {
871 $map = array(
872 'Complete' => 'charitable-completed',
873 'Pending' => 'charitable-pending',
874 'Processing' => 'charitable-pending',
875 'Refunded' => 'charitable-refunded',
876 'Failed' => 'charitable-failed',
877 'Cancelled' => 'charitable-cancelled',
878 'Abandoned' => 'charitable-cancelled',
879 'Revoked' => 'charitable-cancelled',
880 );
881
882 return isset( $map[ $givewp_status ] ) ? $map[ $givewp_status ] : 'charitable-pending';
883 }
884
885 /**
886 * Map a GiveButter donation status to a Charitable status.
887 *
888 * @since 1.8.10
889 *
890 * @param string $givebutter_status The GiveButter status string.
891 *
892 * @return string The Charitable donation status.
893 */
894 public function map_givebutter_status( $givebutter_status ) {
895 $map = array(
896 'Succeeded' => 'charitable-completed',
897 'Pending' => 'charitable-pending',
898 'Failed' => 'charitable-failed',
899 'Refunded' => 'charitable-refunded',
900 );
901
902 return isset( $map[ $givebutter_status ] ) ? $map[ $givebutter_status ] : 'charitable-pending';
903 }
904
905 /**
906 * Add an update message.
907 *
908 * @since 1.4.6
909 *
910 * @param string $message The message text.
911 * @param string $type The type of message. Options: 'error', 'success', 'warning', 'info'.
912 * @param boolean $dismissible Whether the message can be dismissed.
913 * @return void
914 */
915 public function add_update_message( $message, $type = 'error', $dismissible = true ) {
916 if ( ! in_array( $type, array( 'error', 'success', 'warning', 'info' ) ) ) {
917 $type = 'error';
918 }
919
920 charitable_get_admin_notices()->add_notice( $message, $type, false, $dismissible );
921 }
922 }
923
924 endif;
925