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-import-items.php

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

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