PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.3.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.3.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / api / import-export-api.php

import-export-api.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.3.0, at inc/api/import-export-api.php

665 lines 23.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Import & Export REST API endpoints.
4 *
5 * SureDonation own-data import/export (donations, donors, campaigns, settings).
6 * Distinct from the GiveWP migration API — this reads/writes SureDonation's own
7 * export format. Route handlers are filled in by later tasks; this scaffold
8 * registers the routes and enforces the capability + nonce baseline.
9 *
10 * @package SureDonation
11 * @since 1.3.0
12 */
13
14 namespace SureDonation\Inc\API;
15
16 use SureDonation\Inc\Campaigns\Campaign_Cpt;
17 use SureDonation\Inc\Database\Tables\Donations;
18 use SureDonation\Inc\Helper;
19 use SureDonation\Inc\Import_Export\Config_IO;
20 use SureDonation\Inc\Import_Export\Csv_Exporter;
21 use SureDonation\Inc\Import_Export\Import\Column_Map;
22 use SureDonation\Inc\Import_Export\Import\Csv_File;
23 use SureDonation\Inc\Import_Export\Import\Import_Runner;
24 use WP_Error;
25 use WP_REST_Response;
26 use WP_REST_Server;
27
28 // Exit if accessed directly.
29 if ( ! defined( 'ABSPATH' ) ) {
30 exit;
31 }
32
33 /**
34 * Import & Export API class.
35 *
36 * @since 1.3.0
37 */
38 class Import_Export_API {
39
40 /**
41 * Get Import & Export endpoints.
42 *
43 * Exports are READABLE (filters passed as query args, matching the existing
44 * `/donors/export` route); imports are EDITABLE (they upload and write).
45 *
46 * @return array<string, mixed>
47 * @since 1.3.0
48 */
49 public function get_endpoints() {
50 return [
51 // --- Export (read) ---
52 '/export/donations' => [
53 'methods' => WP_REST_Server::READABLE,
54 'callback' => [ $this, 'export_donations' ],
55 'permission_callback' => [ $this, 'check_permissions' ],
56 ],
57 '/export/donors' => [
58 'methods' => WP_REST_Server::READABLE,
59 'callback' => [ $this, 'export_donors' ],
60 'permission_callback' => [ $this, 'check_permissions' ],
61 ],
62 '/export/campaigns' => [
63 'methods' => WP_REST_Server::READABLE,
64 'callback' => [ $this, 'export_campaigns' ],
65 'permission_callback' => [ $this, 'check_permissions' ],
66 ],
67 '/export/settings' => [
68 'methods' => WP_REST_Server::READABLE,
69 'callback' => [ $this, 'export_settings' ],
70 'permission_callback' => [ $this, 'check_permissions' ],
71 ],
72
73 // --- Import (write) ---
74 '/import/analyze' => [
75 'methods' => WP_REST_Server::CREATABLE,
76 'callback' => [ $this, 'import_analyze' ],
77 'permission_callback' => [ $this, 'check_permissions' ],
78 ],
79 '/import/start' => [
80 'methods' => WP_REST_Server::CREATABLE,
81 'callback' => [ $this, 'import_start' ],
82 'permission_callback' => [ $this, 'check_permissions' ],
83 ],
84 '/import/batch' => [
85 'methods' => WP_REST_Server::CREATABLE,
86 'callback' => [ $this, 'import_batch' ],
87 'permission_callback' => [ $this, 'check_permissions' ],
88 ],
89 '/import/status/(?P<id>[A-Za-z0-9\-]+)' => [
90 'methods' => WP_REST_Server::READABLE,
91 'callback' => [ $this, 'import_status' ],
92 'permission_callback' => [ $this, 'check_permissions' ],
93 ],
94 '/import/campaigns' => [
95 'methods' => WP_REST_Server::CREATABLE,
96 'callback' => [ $this, 'import_campaigns' ],
97 'permission_callback' => [ $this, 'check_permissions' ],
98 ],
99 '/import/settings' => [
100 'methods' => WP_REST_Server::CREATABLE,
101 'callback' => [ $this, 'import_settings' ],
102 'permission_callback' => [ $this, 'check_permissions' ],
103 ],
104 ];
105 }
106
107 /**
108 * Capability + nonce gate for every route.
109 *
110 * Requires `manage_options`. For write methods, also verifies the
111 * `X-WP-Nonce` header (or `_wpnonce` param) against the `wp_rest` action.
112 * Mirrors the check used by the other SureDonation REST controllers.
113 *
114 * @param \WP_REST_Request|null $request Request object.
115 * @return bool|WP_Error True when allowed, WP_Error otherwise.
116 * @since 1.3.0
117 */
118 public function check_permissions( $request = null ) {
119 if ( ! current_user_can( 'manage_options' ) ) {
120 return new WP_Error(
121 'rest_forbidden',
122 __( 'You are not allowed to import or export data.', 'suredonation' ),
123 [ 'status' => rest_authorization_required_code() ]
124 );
125 }
126
127 if ( $request instanceof \WP_REST_Request ) {
128 $method = strtoupper( $request->get_method() );
129 if ( in_array( $method, [ 'POST', 'PUT', 'PATCH', 'DELETE' ], true ) ) {
130 $nonce = $request->get_header( 'X-WP-Nonce' );
131 if ( empty( $nonce ) ) {
132 $nonce_param = $request->get_param( '_wpnonce' );
133 $nonce = is_string( $nonce_param ) ? $nonce_param : '';
134 }
135 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
136 return new WP_Error(
137 'rest_forbidden',
138 __( 'Invalid or missing nonce.', 'suredonation' ),
139 [ 'status' => 403 ]
140 );
141 }
142 }
143 }
144
145 return true;
146 }
147
148 /**
149 * Export one-time donations to CSV.
150 *
151 * Reads optional filters (payment status, campaign, payment mode, gateway,
152 * and a created_at date range), fetches the matching one-time donations, and
153 * returns the CSV as a string for the client to download. Recurring and
154 * renewal rows are excluded — the free export is one-time only. Custom form
155 * field values in donation_data['fields'] are appended as trailing columns.
156 *
157 * @param \WP_REST_Request $request Request object.
158 * @return WP_REST_Response Response with { success, csv, filename, truncated, total_count, exported }.
159 * @since 1.3.0
160 */
161 public function export_donations( $request ) {
162 $filters = [
163 'status' => sanitize_text_field( (string) $request->get_param( 'status' ) ),
164 'campaign_id' => absint( $request->get_param( 'campaign' ) ),
165 'payment_mode' => sanitize_text_field( (string) $request->get_param( 'mode' ) ),
166 'gateway' => sanitize_text_field( (string) $request->get_param( 'gateway' ) ),
167 'after' => sanitize_text_field( (string) $request->get_param( 'after' ) ),
168 'before' => sanitize_text_field( (string) $request->get_param( 'before' ) ),
169 ];
170
171 // Cap the payload at 10k rows (matches the donor export) and signal
172 // truncation so an admin knows whether the file is complete.
173 $export_cap = 10000;
174 $total_count = Donations::count_for_export( $filters );
175 $truncated = $total_count > $export_cap;
176 $donations = Donations::get_for_export( $filters, $export_cap, 0 );
177
178 // First pass: collect the union of custom-field labels so every row
179 // shares one consistent set of trailing columns.
180 $field_labels = [];
181 foreach ( $donations as $donation ) {
182 foreach ( $this->get_donation_custom_fields( $donation ) as $label => $value ) {
183 if ( ! in_array( $label, $field_labels, true ) ) {
184 $field_labels[] = $label;
185 }
186 }
187 }
188
189 $rows = [];
190 $rows[] = array_merge(
191 Column_Map::standard_donation_export_labels(),
192 $field_labels
193 );
194
195 $title_cache = [];
196 foreach ( $donations as $donation ) {
197 $campaign_id = absint( Helper::get_string_value( $donation['campaign_id'] ?? 0 ) );
198 $form_id = absint( Helper::get_string_value( $donation['form_id'] ?? 0 ) );
199
200 $row = [
201 $donation['id'] ?? '',
202 $campaign_id ? $campaign_id : '',
203 $campaign_id ? $this->resolve_title( $campaign_id, $title_cache ) : '',
204 $form_id ? $form_id : '',
205 $form_id ? $this->resolve_title( $form_id, $title_cache ) : '',
206 $donation['donor_id'] ?? '',
207 $donation['donor_name'] ?? '',
208 $donation['donor_email'] ?? '',
209 $donation['donor_phone'] ?? '',
210 $donation['amount'] ?? '',
211 $donation['fees_covered'] ?? '',
212 $donation['refunded_amount'] ?? '',
213 $donation['currency'] ?? '',
214 $donation['gateway'] ?? '',
215 $donation['payment_status'] ?? '',
216 $donation['payment_mode'] ?? '',
217 $donation['transaction_id'] ?? '',
218 $donation['donation_type'] ?? '',
219 $donation['subscription_id'] ?? '',
220 $donation['subscription_status'] ?? '',
221 ! empty( $donation['parent_subscription_id'] ) ? $donation['parent_subscription_id'] : '',
222 ! empty( $donation['is_anonymous'] ) ? __( 'Yes', 'suredonation' ) : __( 'No', 'suredonation' ),
223 $donation['donor_comment'] ?? '',
224 $donation['ip_address'] ?? '',
225 $donation['created_at'] ?? '',
226 $donation['import_source'] ?? '',
227 ! empty( $donation['import_source_id'] ) ? $donation['import_source_id'] : '',
228 ];
229
230 $field_values = $this->get_donation_custom_fields( $donation );
231 foreach ( $field_labels as $label ) {
232 $row[] = $field_values[ $label ] ?? '';
233 }
234
235 $rows[] = $row;
236 }
237
238 return new WP_REST_Response(
239 [
240 'success' => true,
241 'csv' => Csv_Exporter::build( $rows ),
242 'filename' => 'suredonation-donations-export-' . gmdate( 'Y-m-d' ) . '.csv',
243 'truncated' => $truncated,
244 'total_count' => $total_count,
245 'exported' => count( $donations ),
246 ],
247 200
248 );
249 }
250
251 /**
252 * Extract a donation's submitted custom form-field values as a label => value map.
253 *
254 * @param array<string, mixed> $donation Decoded donation row.
255 * @return array<string, string> Custom-field values keyed by label.
256 * @since 1.3.0
257 */
258 private function get_donation_custom_fields( $donation ) {
259 $out = [];
260 $data = ( isset( $donation['donation_data'] ) && is_array( $donation['donation_data'] ) )
261 ? $donation['donation_data']
262 : [];
263 $fields = ( isset( $data['fields'] ) && is_array( $data['fields'] ) )
264 ? $data['fields']
265 : [];
266
267 foreach ( $fields as $field ) {
268 if ( ! is_array( $field ) || empty( $field['label'] ) ) {
269 continue;
270 }
271 $label = Helper::get_string_value( $field['label'] );
272 if ( '' !== $label ) {
273 $out[ $label ] = Helper::get_string_value( $field['value'] ?? '' );
274 }
275 }
276
277 return $out;
278 }
279
280 /**
281 * Resolve and cache a post title (campaign or form) for the export.
282 *
283 * @param int $post_id Post ID.
284 * @param array<string, string> $cache Title cache, keyed by post id, by reference.
285 * @return string Post title.
286 * @since 1.3.0
287 */
288 private function resolve_title( $post_id, &$cache ) {
289 $key = (string) $post_id;
290 if ( ! isset( $cache[ $key ] ) ) {
291 $cache[ $key ] = (string) get_the_title( $post_id );
292 }
293 return $cache[ $key ];
294 }
295
296 /**
297 * Export donors to CSV.
298 *
299 * Delegates to the existing donor export (Donors_API::export_donors_csv) so
300 * the CSV columns and query stay in one place — the Import & Export tab just
301 * exposes it under the unified /export namespace. Honors the same `search`
302 * and `campaign` query args.
303 *
304 * @param \WP_REST_Request $request Request object.
305 * @return WP_REST_Response|WP_Error CSV payload or error.
306 * @since 1.3.0
307 */
308 public function export_donors( $request ) {
309 return ( new Donors_API() )->export_donors_csv( $request );
310 }
311
312 /**
313 * Export campaigns (with their linked forms) as JSON.
314 *
315 * Optional `ids` query arg (comma-separated) limits the export to specific
316 * campaigns; omit it to export all. The response carries the JSON object the
317 * client downloads as a .json file.
318 *
319 * @param \WP_REST_Request $request Request object.
320 * @return WP_REST_Response Response with { success, data, filename, count }.
321 * @since 1.3.0
322 */
323 public function export_campaigns( $request ) {
324 $ids_param = $request->get_param( 'ids' );
325 $ids = [];
326 if ( is_string( $ids_param ) && '' !== $ids_param ) {
327 $ids = array_filter( array_map( 'absint', explode( ',', $ids_param ) ) );
328 } elseif ( is_array( $ids_param ) ) {
329 $ids = array_filter( array_map( 'absint', $ids_param ) );
330 }
331
332 $campaigns = Config_IO::export_campaigns( $ids );
333
334 return new WP_REST_Response(
335 [
336 'success' => true,
337 'data' => [
338 'type' => 'suredonation-campaigns',
339 'version' => defined( 'SUREDONATION_VER' ) ? SUREDONATION_VER : '',
340 'campaigns' => $campaigns,
341 ],
342 'filename' => 'suredonation-campaigns-export-' . gmdate( 'Y-m-d' ) . '.json',
343 'count' => count( $campaigns ),
344 ],
345 200
346 );
347 }
348
349 /**
350 * Export SureDonation settings as JSON (credentials excluded).
351 *
352 * @return WP_REST_Response Response with { success, data, filename }.
353 * @since 1.3.0
354 */
355 public function export_settings() {
356 return new WP_REST_Response(
357 [
358 'success' => true,
359 'data' => [
360 'type' => 'suredonation-settings',
361 'version' => defined( 'SUREDONATION_VER' ) ? SUREDONATION_VER : '',
362 'settings' => Config_IO::export_settings(),
363 ],
364 'filename' => 'suredonation-settings-export-' . gmdate( 'Y-m-d' ) . '.json',
365 ],
366 200
367 );
368 }
369
370 /**
371 * Analyze an uploaded CSV: store it, read the header + a sample, auto-map
372 * columns to fields, and detect the entity (donations vs donors).
373 *
374 * Returns a token the import start/batch calls reference so the same stored
375 * file is reused without re-uploading.
376 *
377 * @param \WP_REST_Request $request Request object (multipart with a `file`).
378 * @return WP_REST_Response|WP_Error Analysis payload or error.
379 * @since 1.3.0
380 */
381 public function import_analyze( $request ) {
382 $files = $request->get_file_params();
383 $file = isset( $files['file'] ) && is_array( $files['file'] ) ? $files['file'] : null;
384
385 if ( null === $file || empty( $file['tmp_name'] ) || ! empty( $file['error'] ) ) {
386 return new WP_Error( 'suredonation_no_file', __( 'No file was uploaded.', 'suredonation' ), [ 'status' => 400 ] );
387 }
388
389 $name = sanitize_file_name( (string) ( $file['name'] ?? '' ) );
390 if ( ! preg_match( '/\.csv$/i', $name ) ) {
391 return new WP_Error( 'suredonation_invalid_file', __( 'Please upload a .csv file.', 'suredonation' ), [ 'status' => 400 ] );
392 }
393
394 $size = isset( $file['size'] ) ? (int) $file['size'] : 0;
395 if ( $size <= 0 || $size > 20 * MB_IN_BYTES ) {
396 return new WP_Error( 'suredonation_invalid_size', __( 'The file is empty or larger than 20 MB.', 'suredonation' ), [ 'status' => 400 ] );
397 }
398
399 $token = Csv_File::store( (string) $file['tmp_name'] );
400 if ( false === $token ) {
401 return new WP_Error( 'suredonation_store_failed', __( 'Could not process the uploaded file.', 'suredonation' ), [ 'status' => 500 ] );
402 }
403
404 $headers = Csv_File::read_header( $token );
405 if ( empty( $headers ) ) {
406 Csv_File::delete( $token );
407 return new WP_Error( 'suredonation_empty_file', __( 'The file has no header row.', 'suredonation' ), [ 'status' => 400 ] );
408 }
409
410 $entity = sanitize_text_field( (string) $request->get_param( 'entity' ) );
411 if ( ! in_array( $entity, [ 'donations', 'donors' ], true ) ) {
412 $entity = Column_Map::detect_entity( $headers );
413 }
414
415 $mapping = Column_Map::auto_map( $headers, $entity );
416
417 $fields = [];
418 foreach ( Column_Map::fields_for( $entity ) as $key => $def ) {
419 $fields[] = [
420 'field' => $key,
421 'label' => $def['label'],
422 'required' => ! empty( $def['required'] ),
423 ];
424 }
425
426 return new WP_REST_Response(
427 [
428 'success' => true,
429 'token' => $token,
430 'entity' => $entity,
431 'headers' => $headers,
432 'mapping' => $mapping,
433 'fields' => $fields,
434 'sample' => Csv_File::read_sample( $token, 5 ),
435 'total_rows' => Csv_File::count_rows( $token ),
436 ],
437 200
438 );
439 }
440
441 /**
442 * Start a CSV import session from a previously analyzed file.
443 *
444 * @param \WP_REST_Request $request Request with token, entity, mapping, options.
445 * @return WP_REST_Response|WP_Error Session info or error.
446 * @since 1.3.0
447 */
448 public function import_start( $request ) {
449 $token = sanitize_text_field( (string) $request->get_param( 'token' ) );
450 if ( false === Csv_File::path_for( $token ) ) {
451 return new WP_Error( 'suredonation_invalid_token', __( 'The uploaded file could not be found. Please upload it again.', 'suredonation' ), [ 'status' => 400 ] );
452 }
453
454 $entity = sanitize_text_field( (string) $request->get_param( 'entity' ) );
455 if ( ! in_array( $entity, [ 'donations', 'donors' ], true ) ) {
456 return new WP_Error( 'suredonation_invalid_entity', __( 'Invalid import type.', 'suredonation' ), [ 'status' => 400 ] );
457 }
458
459 // Columns are mapped server-side from the file's header row. A
460 // SureDonation export auto-maps in full, so there is no manual mapping
461 // step; a file whose required columns don't resolve is rejected.
462 $headers = Csv_File::read_header( $token );
463 $mapping = Column_Map::auto_map( $headers, $entity );
464
465 $missing = Column_Map::missing_required( $mapping, $entity );
466 if ( ! empty( $missing ) ) {
467 return new WP_Error(
468 'suredonation_unrecognized_csv',
469 sprintf(
470 /* translators: %s: comma-separated required column labels. */
471 __( 'This does not look like a SureDonation export. Required columns were not found: %s. Please upload the CSV exported by SureDonation.', 'suredonation' ),
472 implode( ', ', $missing )
473 ),
474 [ 'status' => 400 ]
475 );
476 }
477
478 $options = [
479 'headers' => $headers,
480 'dry_run' => (bool) $request->get_param( 'dry_run' ),
481 ];
482
483 // Donations are imported into an explicitly chosen campaign.
484 if ( 'donations' === $entity ) {
485 $campaign_id = absint( $request->get_param( 'campaign_id' ) );
486 $campaign = $campaign_id ? get_post( $campaign_id ) : null;
487 if ( ! $campaign instanceof \WP_Post || Campaign_Cpt::POST_TYPE !== $campaign->post_type ) {
488 return new WP_Error( 'suredonation_invalid_campaign', __( 'Please choose a campaign to import the donations into.', 'suredonation' ), [ 'status' => 400 ] );
489 }
490 $options['campaign_id'] = $campaign_id;
491 }
492
493 $total_rows = Csv_File::count_rows( $token );
494 $progress = Import_Runner::create( $entity, $token, $mapping, $options, $total_rows );
495
496 return new WP_REST_Response(
497 [
498 'success' => true,
499 'import_id' => $progress['import_id'],
500 'total_rows' => $total_rows,
501 'batch_size' => Import_Runner::BATCH_SIZE,
502 ],
503 200
504 );
505 }
506
507 /**
508 * Process the next batch of an import session.
509 *
510 * @param \WP_REST_Request $request Request with import_id.
511 * @return WP_REST_Response|WP_Error Progress or error.
512 * @since 1.3.0
513 */
514 public function import_batch( $request ) {
515 $import_id = sanitize_text_field( (string) $request->get_param( 'import_id' ) );
516 $session = Import_Runner::get( $import_id );
517 if ( false === $session ) {
518 return new WP_Error( 'suredonation_invalid_import', __( 'Import session not found or already finished.', 'suredonation' ), [ 'status' => 400 ] );
519 }
520 if ( ! $this->user_owns_session( $session ) ) {
521 return new WP_Error( 'rest_forbidden', __( 'You cannot modify an import started by another user.', 'suredonation' ), [ 'status' => rest_authorization_required_code() ] );
522 }
523 $progress = Import_Runner::run_batch( $import_id );
524 if ( false === $progress ) {
525 return new WP_Error( 'suredonation_invalid_import', __( 'Import session not found or already finished.', 'suredonation' ), [ 'status' => 400 ] );
526 }
527 return new WP_REST_Response( $this->import_progress_response( $progress ), 200 );
528 }
529
530 /**
531 * Whether the current user may act on an import session.
532 *
533 * All callers already hold `manage_options`; this additionally scopes a
534 * session to the admin who started it. Sessions with no recorded owner
535 * fall back to the capability gate.
536 *
537 * @param array<string, mixed> $progress Session payload.
538 * @return bool
539 * @since 1.3.0
540 */
541 private function user_owns_session( $progress ) {
542 $owner = Helper::get_integer_value( $progress['started_by'] ?? 0 );
543 return 0 === $owner || get_current_user_id() === $owner;
544 }
545
546 /**
547 * Read an import session's current status.
548 *
549 * @param \WP_REST_Request $request Request with the id path param.
550 * @return WP_REST_Response|WP_Error Progress or error.
551 * @since 1.3.0
552 */
553 public function import_status( $request ) {
554 $import_id = sanitize_text_field( (string) $request->get_param( 'id' ) );
555 $progress = Import_Runner::get( $import_id );
556 if ( false === $progress ) {
557 return new WP_Error( 'suredonation_invalid_import', __( 'Import session not found.', 'suredonation' ), [ 'status' => 404 ] );
558 }
559 if ( ! $this->user_owns_session( $progress ) ) {
560 return new WP_Error( 'rest_forbidden', __( 'You cannot view an import started by another user.', 'suredonation' ), [ 'status' => rest_authorization_required_code() ] );
561 }
562 return new WP_REST_Response( $this->import_progress_response( $progress ), 200 );
563 }
564
565 /**
566 * Shape an import session's progress for the client.
567 *
568 * @param array<string, mixed> $progress Session payload.
569 * @return array<string, mixed> Trimmed progress view.
570 * @since 1.3.0
571 */
572 private function import_progress_response( $progress ) {
573 $entity = is_string( $progress['entity'] ?? null ) ? $progress['entity'] : '';
574 $results = is_array( $progress['results'] ?? null ) ? $progress['results'] : [];
575 $result = is_array( $results[ $entity ] ?? null ) ? $results[ $entity ] : [];
576
577 $processed = Helper::get_integer_value( $result['imported'] ?? 0 ) + Helper::get_integer_value( $result['skipped'] ?? 0 ) + Helper::get_integer_value( $result['errors'] ?? 0 );
578 $total = Helper::get_integer_value( $progress['total_rows'] ?? 0 );
579 $status = is_string( $progress['status'] ?? null ) ? $progress['status'] : '';
580
581 $percentage = $total > 0 ? min( 100, (int) round( $processed / $total * 100 ) ) : 100;
582 if ( 'complete' === $status ) {
583 $percentage = 100;
584 }
585
586 return [
587 'success' => true,
588 'import_id' => is_string( $progress['import_id'] ?? null ) ? $progress['import_id'] : '',
589 'status' => $status,
590 'entity' => $entity,
591 'total_rows' => $total,
592 'processed' => $processed,
593 'percentage' => $percentage,
594 'results' => $result,
595 'error' => is_string( $progress['error'] ?? null ) ? $progress['error'] : '',
596 ];
597 }
598
599 /**
600 * Import campaigns (with their forms) from an exported JSON payload.
601 *
602 * @param \WP_REST_Request $request Request with a `data` object ({ campaigns: [...] }).
603 * @return WP_REST_Response|WP_Error Result or error.
604 * @since 1.3.0
605 */
606 public function import_campaigns( $request ) {
607 $data = $request->get_param( 'data' );
608 $campaigns = ( is_array( $data ) && is_array( $data['campaigns'] ?? null ) )
609 ? $data['campaigns']
610 : [];
611
612 if ( empty( $campaigns ) ) {
613 return new WP_Error( 'suredonation_no_campaigns', __( 'No campaigns were found in the file.', 'suredonation' ), [ 'status' => 400 ] );
614 }
615
616 $result = Config_IO::import_campaigns( $campaigns );
617
618 return new WP_REST_Response(
619 [
620 'success' => true,
621 'message' => sprintf(
622 /* translators: 1: campaign count, 2: form count. */
623 __( 'Imported %1$d campaigns and %2$d forms.', 'suredonation' ),
624 $result['campaigns'],
625 $result['forms']
626 ),
627 'campaigns' => $result['campaigns'],
628 'forms' => $result['forms'],
629 ],
630 200
631 );
632 }
633
634 /**
635 * Import settings from an exported JSON payload (Merge/Replace, secret-safe).
636 *
637 * @param \WP_REST_Request $request Request with a `data` object ({ settings: {...} }) and `mode`.
638 * @return WP_REST_Response|WP_Error Result or error.
639 * @since 1.3.0
640 */
641 public function import_settings( $request ) {
642 $data = $request->get_param( 'data' );
643 $settings = ( is_array( $data ) && is_array( $data['settings'] ?? null ) )
644 ? $data['settings']
645 : [];
646 $mode = 'replace' === $request->get_param( 'mode' ) ? 'replace' : 'merge';
647
648 if ( empty( $settings ) ) {
649 return new WP_Error( 'suredonation_no_settings', __( 'No settings were found in the file.', 'suredonation' ), [ 'status' => 400 ] );
650 }
651
652 $result = Config_IO::import_settings( $settings, $mode );
653
654 return new WP_REST_Response(
655 [
656 'success' => true,
657 'message' => __( 'Settings imported.', 'suredonation' ),
658 'notice' => __( 'Payment credentials were not imported. Reconnect your gateways under Settings → Payment Methods.', 'suredonation' ),
659 'applied' => $result['applied'],
660 ],
661 200
662 );
663 }
664 }
665