PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.0.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.0.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 / campaigns-api.php

campaigns-api.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.0.0, at inc/api/campaigns-api.php

968 lines 26.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Campaigns REST API endpoints.
4 *
5 * @package SureDonation
6 */
7
8 namespace SureDonation\Inc\API;
9
10 use SureDonation\Inc\Helper;
11 use WP_Error;
12 use WP_REST_Request;
13 use WP_REST_Response;
14 use WP_REST_Server;
15
16 // Exit if accessed directly.
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 /**
22 * Campaigns API class.
23 *
24 * @since 0.0.1
25 */
26 class Campaigns_API {
27 /**
28 * Get campaign endpoints.
29 *
30 * @return array<string, mixed>
31 * @since 0.0.1
32 */
33 public function get_endpoints() {
34 return [
35 // Get campaigns list & create campaign.
36 '/campaigns' => [
37 [
38 'methods' => WP_REST_Server::READABLE,
39 'callback' => [ $this, 'get_campaigns' ],
40 'permission_callback' => [ $this, 'check_permissions' ],
41 ],
42 [
43 'methods' => WP_REST_Server::CREATABLE,
44 'callback' => [ $this, 'create_campaign' ],
45 'permission_callback' => [ $this, 'check_permissions' ],
46 'args' => [
47 'title' => [
48 'required' => true,
49 'sanitize_callback' => 'sanitize_text_field',
50 ],
51 'description' => [
52 'sanitize_callback' => 'wp_kses_post',
53 ],
54 'goal_type' => [
55 'default' => 'raised_amount',
56 'enum' => [ 'raised_amount', 'donation_count' ],
57 'sanitize_callback' => 'sanitize_text_field',
58 ],
59 'goal_amount' => [
60 'sanitize_callback' => static function ( $value ) {
61 return '' !== $value ? floatval( $value ) : '';
62 },
63 'validate_callback' => static function ( $value ) {
64 if ( '' === $value ) {
65 return true;
66 }
67 $amount = floatval( $value );
68 return $amount >= 0 && $amount <= 999999999.99;
69 },
70 ],
71 'campaign_status' => [
72 'default' => 'active',
73 'enum' => [ 'active', 'paused', 'completed' ],
74 ],
75 'featured_image' => [
76 'sanitize_callback' => 'absint',
77 ],
78 'email_settings' => [
79 'type' => 'object',
80 'sanitize_callback' => [ $this, 'sanitize_email_settings' ],
81 ],
82 'require_terms' => [
83 'type' => 'boolean',
84 'sanitize_callback' => 'rest_sanitize_boolean',
85 ],
86 'terms_text' => [
87 'sanitize_callback' => 'sanitize_text_field',
88 ],
89 ],
90 ],
91 ],
92
93 // Update campaign.
94 '/campaigns/(?P<id>\d+)' => [
95 [
96 'methods' => WP_REST_Server::READABLE,
97 'callback' => [ $this, 'get_campaign' ],
98 'permission_callback' => [ $this, 'check_permissions' ],
99 'args' => [
100 'id' => [
101 'required' => true,
102 'validate_callback' => static function ( $param ) {
103 return is_numeric( $param );
104 },
105 ],
106 ],
107 ],
108 [
109 'methods' => WP_REST_Server::EDITABLE,
110 'callback' => [ $this, 'update_campaign' ],
111 'permission_callback' => [ $this, 'check_permissions' ],
112 'args' => [
113 'id' => [
114 'required' => true,
115 'validate_callback' => static function ( $param ) {
116 return is_numeric( $param );
117 },
118 ],
119 'title' => [
120 'sanitize_callback' => 'sanitize_text_field',
121 ],
122 'description' => [
123 'sanitize_callback' => 'wp_kses_post',
124 ],
125 'goal_type' => [
126 'enum' => [ 'raised_amount', 'donation_count' ],
127 'sanitize_callback' => 'sanitize_text_field',
128 ],
129 'goal_amount' => [
130 'sanitize_callback' => static function ( $value ) {
131 return '' !== $value ? floatval( $value ) : '';
132 },
133 'validate_callback' => static function ( $value ) {
134 if ( '' === $value ) {
135 return true;
136 }
137 $amount = floatval( $value );
138 return $amount >= 0 && $amount <= 999999999.99;
139 },
140 ],
141 'campaign_status' => [
142 'enum' => [ 'active', 'paused', 'completed' ],
143 ],
144 'featured_image' => [
145 'sanitize_callback' => 'absint',
146 ],
147 'email_settings' => [
148 'type' => 'object',
149 'sanitize_callback' => [ $this, 'sanitize_email_settings' ],
150 ],
151 'require_terms' => [
152 'type' => 'boolean',
153 'sanitize_callback' => 'rest_sanitize_boolean',
154 ],
155 'terms_text' => [
156 'sanitize_callback' => 'sanitize_text_field',
157 ],
158 ],
159 ],
160 [
161 'methods' => WP_REST_Server::DELETABLE,
162 'callback' => [ $this, 'delete_campaign' ],
163 'permission_callback' => [ $this, 'check_permissions' ],
164 'args' => [
165 'id' => [
166 'required' => true,
167 'validate_callback' => static function ( $param ) {
168 return is_numeric( $param );
169 },
170 ],
171 ],
172 ],
173 ],
174
175 // Update campaign status.
176 '/campaigns/(?P<id>\d+)/status' => [
177 'methods' => WP_REST_Server::EDITABLE,
178 'callback' => [ $this, 'update_campaign_status' ],
179 'permission_callback' => [ $this, 'check_permissions' ],
180 'args' => [
181 'id' => [
182 'required' => true,
183 'validate_callback' => static function ( $param ) {
184 return is_numeric( $param );
185 },
186 ],
187 'status' => [
188 'required' => true,
189 'enum' => [ 'publish', 'draft', 'trash' ],
190 ],
191 ],
192 ],
193
194 // Duplicate campaign.
195 '/campaigns/(?P<id>\d+)/duplicate' => [
196 'methods' => WP_REST_Server::CREATABLE,
197 'callback' => [ $this, 'duplicate_campaign' ],
198 'permission_callback' => [ $this, 'check_permissions' ],
199 'args' => [
200 'id' => [
201 'required' => true,
202 'validate_callback' => static function ( $param ) {
203 return is_numeric( $param );
204 },
205 ],
206 ],
207 ],
208
209 // Bulk actions.
210 '/campaigns/bulk' => [
211 'methods' => WP_REST_Server::EDITABLE,
212 'callback' => [ $this, 'bulk_action' ],
213 'permission_callback' => [ $this, 'check_permissions' ],
214 'args' => [
215 'action' => [
216 'required' => true,
217 'enum' => [ 'delete', 'publish', 'draft', 'trash' ],
218 ],
219 'ids' => [
220 'required' => true,
221 'validate_callback' => static function ( $param ) {
222 return is_array( $param ) && ! empty( $param );
223 },
224 ],
225 ],
226 ],
227
228 // Get form locations for a campaign.
229 '/campaigns/(?P<id>\d+)/form-locations' => [
230 'methods' => WP_REST_Server::READABLE,
231 'callback' => [ $this, 'get_form_locations' ],
232 'permission_callback' => [ $this, 'check_permissions' ],
233 'args' => [
234 'id' => [
235 'required' => true,
236 'validate_callback' => static function ( $param ) {
237 return is_numeric( $param );
238 },
239 ],
240 ],
241 ],
242
243 // Create (seed) the campaign page layout.
244 '/campaigns/(?P<id>\d+)/page' => [
245 'methods' => WP_REST_Server::CREATABLE,
246 'callback' => [ $this, 'create_campaign_page' ],
247 'permission_callback' => [ $this, 'check_permissions' ],
248 'args' => [
249 'id' => [
250 'required' => true,
251 'validate_callback' => static function ( $param ) {
252 return is_numeric( $param );
253 },
254 ],
255 ],
256 ],
257 ];
258 }
259
260 /**
261 * Get a single campaign by ID.
262 *
263 * @param WP_REST_Request $request Request object.
264 * @return WP_REST_Response|WP_Error Response object.
265 * @since 0.0.1
266 */
267 public function get_campaign( $request ) {
268 $campaign_id = absint( $request->get_param( 'id' ) );
269
270 // Get the campaign post.
271 $post = get_post( $campaign_id );
272
273 // Check if campaign exists and is of correct type.
274 if ( ! $post || SUREDONATION_POST_TYPE !== $post->post_type ) {
275 return new WP_Error(
276 'campaign_not_found',
277 __( 'Campaign not found.', 'suredonation' ),
278 [ 'status' => 404 ]
279 );
280 }
281
282 // Format and return the campaign data.
283 $campaign = $this->format_campaign( $post );
284
285 return new WP_REST_Response(
286 [
287 'success' => true,
288 'campaign' => $campaign,
289 ],
290 200
291 );
292 }
293
294 /**
295 * Create (seed) the default campaign page layout for a campaign.
296 *
297 * Idempotent: seeding only runs when the campaign has no page content yet, so
298 * an existing, user-edited page is never overwritten. Returns the campaign in
299 * its post-seed state so the admin can flip the button to "View Campaign".
300 *
301 * @param WP_REST_Request $request Request object.
302 * @return WP_REST_Response|WP_Error Response object.
303 * @since 1.0.0
304 */
305 public function create_campaign_page( $request ) {
306 $campaign_id = absint( $request->get_param( 'id' ) );
307 $post = get_post( $campaign_id );
308
309 if ( ! $post || SUREDONATION_POST_TYPE !== $post->post_type ) {
310 return new WP_Error(
311 'campaign_not_found',
312 __( 'Campaign not found.', 'suredonation' ),
313 [ 'status' => 404 ]
314 );
315 }
316
317 \SureDonation\Inc\Campaigns\Campaign_Page::seed_if_empty( $campaign_id );
318
319 // Refresh the post so the response reflects the seeded content.
320 $post = get_post( $campaign_id );
321
322 return new WP_REST_Response(
323 [
324 'success' => true,
325 'campaign' => $this->format_campaign( $post ),
326 ],
327 200
328 );
329 }
330
331 /**
332 * Get campaigns list with filters, sorting, and pagination.
333 *
334 * @param WP_REST_Request $request Request object.
335 * @return WP_REST_Response|WP_Error Response object.
336 * @since 0.0.1
337 */
338 public function get_campaigns( $request ) {
339 $page = $request->get_param( 'page' ) ?? 1;
340 $per_page = (int) ( $request->get_param( 'per_page' ) ?? 20 );
341 $per_page = -1 === $per_page ? -1 : absint( $per_page );
342 if ( 0 === $per_page ) {
343 $per_page = 20;
344 }
345 $search = $request->get_param( 'search' ) ?? '';
346 $status = $request->get_param( 'status' ) ?? 'all';
347 $sort_by = $request->get_param( 'sort_by' ) ?? 'date';
348 $order = $request->get_param( 'order' ) ?? 'desc';
349
350 // Build query args.
351 $args = [
352 'post_type' => SUREDONATION_POST_TYPE,
353 'posts_per_page' => $per_page,
354 'paged' => -1 === $per_page ? 1 : absint( $page ),
355 'orderby' => $this->get_orderby_field( $sort_by ),
356 'order' => strtoupper( $order ),
357 ];
358
359 // Add status filter.
360 if ( 'all' !== $status ) {
361 $args['post_status'] = $status;
362 } else {
363 $args['post_status'] = [ 'publish', 'draft' ];
364 }
365
366 // Add search.
367 if ( ! empty( $search ) ) {
368 $args['s'] = sanitize_text_field( $search );
369 }
370
371 // Execute query.
372 $query = new \WP_Query( $args );
373
374 // Format campaigns data.
375 $campaigns = [];
376 if ( $query->have_posts() ) {
377 foreach ( $query->posts as $post ) {
378 $post_obj = $post instanceof \WP_Post ? $post : get_post( $post );
379 if ( $post_obj instanceof \WP_Post ) {
380 $campaigns[] = $this->format_campaign( $post_obj );
381 }
382 }
383 }
384
385 // Prepare response.
386 return new WP_REST_Response(
387 [
388 'campaigns' => $campaigns,
389 'pagination' => [
390 'total' => (int) $query->found_posts,
391 'total_pages' => (int) $query->max_num_pages,
392 'per_page' => (int) $per_page,
393 'current' => (int) $page,
394 ],
395 ]
396 );
397 }
398
399 /**
400 * Create a new campaign.
401 *
402 * @param WP_REST_Request $request Request object.
403 * @return WP_REST_Response|WP_Error Response object.
404 * @since 0.0.1
405 */
406 public function create_campaign( $request ) {
407 $title = $request->get_param( 'title' );
408 $description = $request->get_param( 'description' );
409 $goal_type = $request->get_param( 'goal_type' ) ?? 'raised_amount';
410 $goal_amount = $request->get_param( 'goal_amount' );
411 $campaign_status = $request->get_param( 'campaign_status' ) ?? 'active';
412 $featured_image = $request->get_param( 'featured_image' );
413
414 // Create the campaign post. The description is stored as the excerpt so
415 // post_content stays reserved for the campaign page layout.
416 $post_id = wp_insert_post(
417 [
418 'post_type' => SUREDONATION_POST_TYPE,
419 'post_title' => $title,
420 'post_excerpt' => $description,
421 'post_status' => 'publish',
422 'post_author' => get_current_user_id(),
423 ],
424 true
425 );
426
427 if ( is_wp_error( $post_id ) ) {
428 return new WP_Error(
429 'create_failed',
430 __( 'Failed to create campaign.', 'suredonation' ),
431 [ 'status' => 500 ]
432 );
433 }
434
435 // Save campaign meta as single consolidated meta key.
436 $meta_values = [
437 'goal_type' => $goal_type,
438 'goal_amount' => isset( $goal_amount ) && '' !== $goal_amount ? floatval( $goal_amount ) : 0,
439 'campaign_status' => $campaign_status,
440 ];
441
442 $email_settings = $request->get_param( 'email_settings' );
443 if ( ! empty( $email_settings ) ) {
444 $meta_values['email_settings'] = $email_settings;
445 }
446
447 $require_terms = $request->get_param( 'require_terms' );
448 if ( ! is_null( $require_terms ) ) {
449 $meta_values['require_terms'] = (bool) $require_terms;
450 }
451
452 $terms_text = $request->get_param( 'terms_text' );
453 if ( ! is_null( $terms_text ) ) {
454 $meta_values['terms_text'] = $terms_text;
455 }
456
457 Helper::update_campaign_meta( $post_id, $meta_values );
458
459 // Set featured image if provided.
460 if ( ! empty( $featured_image ) ) {
461 set_post_thumbnail( $post_id, $featured_image );
462 }
463
464 return new WP_REST_Response(
465 [
466 'success' => true,
467 'message' => __( 'Campaign created successfully.', 'suredonation' ),
468 'campaign' => $this->format_campaign( get_post( $post_id ) ),
469 ],
470 201
471 );
472 }
473
474 /**
475 * Update an existing campaign.
476 *
477 * @param WP_REST_Request $request Request object.
478 * @return WP_REST_Response|WP_Error Response object.
479 * @since 0.0.1
480 */
481 public function update_campaign( $request ) {
482 $campaign_id = $request->get_param( 'id' );
483
484 // Check if campaign exists.
485 $campaign = get_post( $campaign_id );
486 if ( ! $campaign || SUREDONATION_POST_TYPE !== $campaign->post_type ) {
487 return new WP_Error(
488 'campaign_not_found',
489 __( 'Campaign not found.', 'suredonation' ),
490 [ 'status' => 404 ]
491 );
492 }
493
494 // Prepare post data.
495 $post_data = [
496 'ID' => $campaign_id,
497 ];
498
499 $title = $request->get_param( 'title' );
500 $description = $request->get_param( 'description' );
501
502 if ( ! empty( $title ) ) {
503 $post_data['post_title'] = $title;
504 }
505
506 // Use is_null (not empty) so an empty string can clear the description;
507 // it feeds the seeded campaign page paragraph.
508 if ( ! is_null( $description ) ) {
509 $post_data['post_excerpt'] = $description;
510 }
511
512 // Update post if there are changes.
513 if ( count( $post_data ) > 1 ) {
514 $result = wp_update_post( $post_data, true );
515 if ( is_wp_error( $result ) ) {
516 return new WP_Error(
517 'update_failed',
518 __( 'Failed to update campaign.', 'suredonation' ),
519 [ 'status' => 500 ]
520 );
521 }
522 }
523
524 // Update campaign meta (consolidated single key).
525 $meta_values = [];
526 $featured_image = $request->get_param( 'featured_image' );
527
528 $goal_type = $request->get_param( 'goal_type' );
529 if ( ! is_null( $goal_type ) ) {
530 $meta_values['goal_type'] = $goal_type;
531 }
532
533 $goal_amount = $request->get_param( 'goal_amount' );
534 if ( ! is_null( $goal_amount ) ) {
535 $meta_values['goal_amount'] = '' !== $goal_amount ? floatval( $goal_amount ) : 0;
536 }
537
538 $campaign_status = $request->get_param( 'campaign_status' );
539 if ( ! empty( $campaign_status ) ) {
540 $meta_values['campaign_status'] = $campaign_status;
541 }
542
543 $email_settings = $request->get_param( 'email_settings' );
544 if ( ! is_null( $email_settings ) ) {
545 $meta_values['email_settings'] = $email_settings;
546 }
547
548 $require_terms = $request->get_param( 'require_terms' );
549 if ( ! is_null( $require_terms ) ) {
550 $meta_values['require_terms'] = (bool) $require_terms;
551 }
552
553 $terms_text = $request->get_param( 'terms_text' );
554 if ( ! is_null( $terms_text ) ) {
555 $meta_values['terms_text'] = $terms_text;
556 }
557
558 if ( ! empty( $meta_values ) ) {
559 Helper::update_campaign_meta( $campaign_id, $meta_values );
560 }
561
562 // Update featured image if provided.
563 if ( ! is_null( $featured_image ) ) {
564 if ( empty( $featured_image ) ) {
565 delete_post_thumbnail( $campaign_id );
566 } else {
567 set_post_thumbnail( $campaign_id, $featured_image );
568 }
569 }
570
571 return new WP_REST_Response(
572 [
573 'success' => true,
574 'message' => __( 'Campaign updated successfully.', 'suredonation' ),
575 'campaign' => $this->format_campaign( get_post( $campaign_id ) ),
576 ],
577 200
578 );
579 }
580
581 /**
582 * Update campaign status.
583 *
584 * @param WP_REST_Request $request Request object.
585 * @return WP_REST_Response|WP_Error Response object.
586 * @since 0.0.1
587 */
588 public function update_campaign_status( $request ) {
589 $campaign_id = absint( $request->get_param( 'id' ) );
590 $status = $request->get_param( 'status' );
591
592 // Verify the post exists and belongs to our post type.
593 $campaign = get_post( $campaign_id );
594 if ( ! $campaign || SUREDONATION_POST_TYPE !== $campaign->post_type ) {
595 return new WP_Error(
596 'campaign_not_found',
597 __( 'Campaign not found.', 'suredonation' ),
598 [ 'status' => 404 ]
599 );
600 }
601
602 $result = wp_update_post(
603 [
604 'ID' => $campaign_id,
605 'post_status' => $status,
606 ],
607 true
608 );
609
610 if ( is_wp_error( $result ) ) {
611 return new WP_Error(
612 'update_failed',
613 __( 'Failed to update campaign status.', 'suredonation' ),
614 [ 'status' => 500 ]
615 );
616 }
617
618 return new WP_REST_Response(
619 [
620 'success' => true,
621 'message' => __( 'Campaign status updated successfully.', 'suredonation' ),
622 ],
623 200
624 );
625 }
626
627 /**
628 * Duplicate campaign.
629 *
630 * @param WP_REST_Request $request Request object.
631 * @return WP_REST_Response|WP_Error Response object.
632 * @since 0.0.1
633 */
634 public function duplicate_campaign( $request ) {
635 $campaign_id = $request->get_param( 'id' );
636
637 // Get original campaign.
638 $original = get_post( $campaign_id );
639 if ( ! $original || SUREDONATION_POST_TYPE !== $original->post_type ) {
640 return new WP_Error(
641 'campaign_not_found',
642 __( 'Campaign not found.', 'suredonation' ),
643 [ 'status' => 404 ]
644 );
645 }
646
647 // Create duplicate. The page (post_content) is intentionally NOT copied:
648 // its blocks carry the original campaign's id, so a fresh page is seeded
649 // for the duplicate on first publish (or via the Create Campaign Page CTA),
650 // guaranteeing the correct id and its own default form. The description
651 // (post_excerpt) is carried over.
652 $duplicate_id = wp_insert_post(
653 [
654 'post_type' => $original->post_type,
655 'post_title' => $original->post_title . ' (Copy)',
656 'post_excerpt' => $original->post_excerpt,
657 'post_status' => 'draft',
658 'post_author' => get_current_user_id(),
659 ],
660 true
661 );
662
663 if ( is_wp_error( $duplicate_id ) ) {
664 return new WP_Error(
665 'duplicate_failed',
666 __( 'Failed to duplicate campaign.', 'suredonation' ),
667 [ 'status' => 500 ]
668 );
669 }
670
671 // Copy consolidated campaign meta.
672 $meta = get_post_meta( $campaign_id, Helper::SUREDONATION_CAMPAIGN_META_KEY, true );
673 if ( ! empty( $meta ) ) {
674 update_post_meta( $duplicate_id, Helper::SUREDONATION_CAMPAIGN_META_KEY, $meta );
675 }
676
677 return new WP_REST_Response(
678 [
679 'success' => true,
680 'message' => __( 'Campaign duplicated successfully.', 'suredonation' ),
681 'campaign' => $this->format_campaign( get_post( $duplicate_id ) ),
682 ],
683 201
684 );
685 }
686
687 /**
688 * Delete campaign.
689 *
690 * @param WP_REST_Request $request Request object.
691 * @return WP_REST_Response|WP_Error Response object.
692 * @since 0.0.1
693 */
694 public function delete_campaign( $request ) {
695 $campaign_id = $request->get_param( 'id' );
696
697 $campaign = get_post( $campaign_id );
698
699 if ( ! $campaign || SUREDONATION_POST_TYPE !== $campaign->post_type ) {
700 return new WP_Error(
701 'invalid_campaign',
702 __( 'Invalid campaign ID.', 'suredonation' ),
703 [ 'status' => 404 ]
704 );
705 }
706
707 $result = wp_delete_post( $campaign_id, true );
708
709 if ( ! $result ) {
710 return new WP_Error(
711 'delete_failed',
712 __( 'Failed to delete campaign.', 'suredonation' ),
713 [ 'status' => 500 ]
714 );
715 }
716
717 return new WP_REST_Response(
718 [
719 'success' => true,
720 'message' => __( 'Campaign deleted successfully.', 'suredonation' ),
721 ],
722 200
723 );
724 }
725
726 /**
727 * Bulk action on campaigns.
728 *
729 * @param WP_REST_Request $request Request object.
730 * @return WP_REST_Response|WP_Error Response object.
731 * @since 0.0.1
732 */
733 public function bulk_action( $request ) {
734 $action = $request->get_param( 'action' );
735 $ids = $request->get_param( 'ids' );
736
737 $success_count = 0;
738 $error_count = 0;
739
740 foreach ( $ids as $id ) {
741 $id = absint( $id );
742
743 // Verify each post belongs to our post type before operating on it.
744 $post = get_post( $id );
745 if ( ! $post || SUREDONATION_POST_TYPE !== $post->post_type ) {
746 ++$error_count;
747 continue;
748 }
749
750 if ( 'delete' === $action ) {
751 $result = wp_delete_post( $id, true );
752 if ( $result ) {
753 ++$success_count;
754 } else {
755 ++$error_count;
756 }
757 } else {
758 $result = wp_update_post(
759 [
760 'ID' => $id,
761 'post_status' => $action,
762 ],
763 true
764 );
765 if ( ! is_wp_error( $result ) ) {
766 ++$success_count;
767 } else {
768 ++$error_count;
769 }
770 }
771 }
772
773 return new WP_REST_Response(
774 [
775 'success' => true,
776 'message' => sprintf(
777 // translators: %1$d: success count, %2$d: error count.
778 __( 'Bulk action completed. Success: %1$d, Failed: %2$d', 'suredonation' ),
779 $success_count,
780 $error_count
781 ),
782 'success_count' => $success_count,
783 'error_count' => $error_count,
784 ],
785 200
786 );
787 }
788
789 /**
790 * Sanitize email settings.
791 *
792 * @param array<string, mixed> $settings Email settings to sanitize.
793 * @return array<string, mixed> Sanitized email settings.
794 * @since 0.0.1
795 */
796 public function sanitize_email_settings( $settings ) {
797 if ( empty( $settings ) || ! is_array( $settings ) ) {
798 return [];
799 }
800
801 $sanitized = [];
802
803 // Sanitize boolean.
804 $sanitized['enabled'] = ! empty( $settings['enabled'] );
805
806 // Sanitize text fields.
807 if ( isset( $settings['subject'] ) ) {
808 $sanitized['subject'] = sanitize_text_field( Helper::get_string_value( $settings['subject'] ) );
809 }
810
811 if ( isset( $settings['from_name'] ) ) {
812 $sanitized['from_name'] = sanitize_text_field( Helper::get_string_value( $settings['from_name'] ) );
813 }
814
815 // Sanitize email fields.
816 if ( isset( $settings['from_email'] ) ) {
817 $sanitized['from_email'] = sanitize_email( Helper::get_string_value( $settings['from_email'] ) );
818 }
819
820 if ( isset( $settings['reply_to'] ) ) {
821 $sanitized['reply_to'] = sanitize_email( Helper::get_string_value( $settings['reply_to'] ) );
822 }
823
824 // Sanitize email body (allow HTML).
825 if ( isset( $settings['email_body'] ) ) {
826 $sanitized['email_body'] = wp_kses_post( Helper::get_string_value( $settings['email_body'] ) );
827 }
828
829 return $sanitized;
830 }
831
832 /**
833 * Get pages/posts where donation form block is used for this campaign.
834 *
835 * @param WP_REST_Request $request Request object.
836 * @return WP_REST_Response|WP_Error Response object.
837 * @since 0.0.1
838 */
839 public function get_form_locations( $request ) {
840 $campaign_id = absint( $request->get_param( 'id' ) );
841
842 // Check if campaign exists.
843 $campaign = get_post( $campaign_id );
844 if ( ! $campaign || SUREDONATION_POST_TYPE !== $campaign->post_type ) {
845 return new WP_Error(
846 'campaign_not_found',
847 __( 'Campaign not found.', 'suredonation' ),
848 [ 'status' => 404 ]
849 );
850 }
851
852 global $wpdb;
853
854 // Search for posts containing the donation form block for this campaign.
855 // The block stores campaignId in its attributes like: "campaignId":123.
856 $search_pattern = '%"campaignId":' . $campaign_id . '%';
857
858 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
859 $posts = $wpdb->get_results(
860 $wpdb->prepare(
861 "SELECT ID, post_title, post_type, post_status, post_modified
862 FROM %i
863 WHERE post_content LIKE %s
864 AND post_status IN ('publish', 'draft', 'pending', 'private')
865 AND post_type IN ('page', 'post', 'suredonation_cmpgn')
866 ORDER BY post_modified DESC",
867 $wpdb->posts,
868 $search_pattern
869 )
870 );
871
872 $locations = [];
873 if ( $posts ) {
874 foreach ( $posts as $post ) {
875 $edit_url = admin_url( 'post.php?post=' . $post->ID . '&action=edit' );
876 $view_url = get_permalink( $post->ID );
877
878 $locations[] = [
879 'id' => (int) $post->ID,
880 'title' => $post->post_title,
881 'type' => $post->post_type,
882 'status' => $post->post_status,
883 'modified_at' => $post->post_modified,
884 'edit_url' => $edit_url,
885 'view_url' => 'publish' === $post->post_status ? $view_url : null,
886 ];
887 }
888 }
889
890 return new WP_REST_Response(
891 [
892 'success' => true,
893 'locations' => $locations,
894 ],
895 200
896 );
897 }
898
899 /**
900 * Check if user has permission to manage campaigns.
901 *
902 * @return bool True if user has permission.
903 * @since 0.0.1
904 */
905 public function check_permissions() {
906 return current_user_can( 'manage_options' );
907 }
908
909 /**
910 * Format campaign data for API response.
911 *
912 * @param \WP_Post|null $post Post object.
913 * @return array<string,mixed> Formatted campaign data.
914 * @since 0.0.1
915 */
916 private function format_campaign( $post ) {
917 if ( ! $post instanceof \WP_Post ) {
918 return [];
919 }
920 // Get real-time stats from donations database table.
921 $stats = \SureDonation\Inc\Campaigns\Campaign_Stats::get_stats( $post->ID );
922 $meta = Helper::get_campaign_meta( $post->ID );
923 $thumbnail_url = get_the_post_thumbnail_url( $post->ID, 'medium' );
924
925 return [
926 'id' => $post->ID,
927 'title' => $post->post_title,
928 'description' => $post->post_excerpt,
929 'status' => $stats['campaign_status'],
930 'goal_type' => $meta['goal_type'],
931 'goal' => $stats['goal_amount'],
932 'raised' => $stats['total_raised'],
933 'donors' => $stats['donor_count'],
934 'donations' => $stats['donation_count'],
935 'donation_count' => $stats['donation_count'],
936 'progress' => $stats['progress_percentage'],
937 'email_settings' => $meta['email_settings'],
938 'require_terms' => $meta['require_terms'],
939 'terms_text' => $meta['terms_text'],
940 'created_at' => $post->post_date,
941 'modified_at' => $post->post_modified,
942 'author' => get_the_author_meta( 'display_name', (int) $post->post_author ),
943 'has_page' => \SureDonation\Inc\Campaigns\Campaign_Page::has_page( $post->ID ),
944 'permalink' => 'publish' === $post->post_status ? get_permalink( $post->ID ) : null,
945 'edit_link' => admin_url( 'post.php?post=' . $post->ID . '&action=edit' ),
946 'featured_image' => (int) get_post_thumbnail_id( $post->ID ),
947 'featured_image_url' => $thumbnail_url ? $thumbnail_url : '',
948 ];
949 }
950
951 /**
952 * Get orderby field based on sort parameter.
953 *
954 * @param string $sort_by Sort parameter.
955 * @return string WordPress orderby field.
956 * @since 0.0.1
957 */
958 private function get_orderby_field( $sort_by ) {
959 $orderby_map = [
960 'date' => 'date',
961 'title' => 'title',
962 'status' => 'post_status',
963 ];
964
965 return $orderby_map[ $sort_by ] ?? 'date';
966 }
967 }
968