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

981 lines 27.3 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 ( 'paused' === $status ) {
361 // "Paused" is a campaign business status stored inside the campaign
362 // meta JSON (not a WP post status), so match published campaigns whose
363 // meta marks them paused.
364 $args['post_status'] = 'publish';
365 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Admin-only campaign list filter.
366 $args['meta_query'] = [
367 [
368 'key' => Helper::SUREDONATION_CAMPAIGN_META_KEY,
369 'value' => '"campaign_status":"paused"',
370 'compare' => 'LIKE',
371 ],
372 ];
373 } elseif ( 'all' !== $status ) {
374 $args['post_status'] = $status;
375 } else {
376 $args['post_status'] = [ 'publish', 'draft' ];
377 }
378
379 // Add search.
380 if ( ! empty( $search ) ) {
381 $args['s'] = sanitize_text_field( $search );
382 }
383
384 // Execute query.
385 $query = new \WP_Query( $args );
386
387 // Format campaigns data.
388 $campaigns = [];
389 if ( $query->have_posts() ) {
390 foreach ( $query->posts as $post ) {
391 $post_obj = $post instanceof \WP_Post ? $post : get_post( $post );
392 if ( $post_obj instanceof \WP_Post ) {
393 $campaigns[] = $this->format_campaign( $post_obj );
394 }
395 }
396 }
397
398 // Prepare response.
399 return new WP_REST_Response(
400 [
401 'campaigns' => $campaigns,
402 'pagination' => [
403 'total' => (int) $query->found_posts,
404 'total_pages' => (int) $query->max_num_pages,
405 'per_page' => (int) $per_page,
406 'current' => (int) $page,
407 ],
408 ]
409 );
410 }
411
412 /**
413 * Create a new campaign.
414 *
415 * @param WP_REST_Request $request Request object.
416 * @return WP_REST_Response|WP_Error Response object.
417 * @since 0.0.1
418 */
419 public function create_campaign( $request ) {
420 $title = $request->get_param( 'title' );
421 $description = $request->get_param( 'description' );
422 $goal_type = $request->get_param( 'goal_type' ) ?? 'raised_amount';
423 $goal_amount = $request->get_param( 'goal_amount' );
424 $campaign_status = $request->get_param( 'campaign_status' ) ?? 'active';
425 $featured_image = $request->get_param( 'featured_image' );
426
427 // Create the campaign post. The description is stored as the excerpt so
428 // post_content stays reserved for the campaign page layout.
429 $post_id = wp_insert_post(
430 [
431 'post_type' => SUREDONATION_POST_TYPE,
432 'post_title' => $title,
433 'post_excerpt' => $description,
434 'post_status' => 'publish',
435 'post_author' => get_current_user_id(),
436 ],
437 true
438 );
439
440 if ( is_wp_error( $post_id ) ) {
441 return new WP_Error(
442 'create_failed',
443 __( 'Failed to create campaign.', 'suredonation' ),
444 [ 'status' => 500 ]
445 );
446 }
447
448 // Save campaign meta as single consolidated meta key.
449 $meta_values = [
450 'goal_type' => $goal_type,
451 'goal_amount' => isset( $goal_amount ) && '' !== $goal_amount ? floatval( $goal_amount ) : 0,
452 'campaign_status' => $campaign_status,
453 ];
454
455 $email_settings = $request->get_param( 'email_settings' );
456 if ( ! empty( $email_settings ) ) {
457 $meta_values['email_settings'] = $email_settings;
458 }
459
460 $require_terms = $request->get_param( 'require_terms' );
461 if ( ! is_null( $require_terms ) ) {
462 $meta_values['require_terms'] = (bool) $require_terms;
463 }
464
465 $terms_text = $request->get_param( 'terms_text' );
466 if ( ! is_null( $terms_text ) ) {
467 $meta_values['terms_text'] = $terms_text;
468 }
469
470 Helper::update_campaign_meta( $post_id, $meta_values );
471
472 // Set featured image if provided.
473 if ( ! empty( $featured_image ) ) {
474 set_post_thumbnail( $post_id, $featured_image );
475 }
476
477 return new WP_REST_Response(
478 [
479 'success' => true,
480 'message' => __( 'Campaign created successfully.', 'suredonation' ),
481 'campaign' => $this->format_campaign( get_post( $post_id ) ),
482 ],
483 201
484 );
485 }
486
487 /**
488 * Update an existing campaign.
489 *
490 * @param WP_REST_Request $request Request object.
491 * @return WP_REST_Response|WP_Error Response object.
492 * @since 0.0.1
493 */
494 public function update_campaign( $request ) {
495 $campaign_id = $request->get_param( 'id' );
496
497 // Check if campaign exists.
498 $campaign = get_post( $campaign_id );
499 if ( ! $campaign || SUREDONATION_POST_TYPE !== $campaign->post_type ) {
500 return new WP_Error(
501 'campaign_not_found',
502 __( 'Campaign not found.', 'suredonation' ),
503 [ 'status' => 404 ]
504 );
505 }
506
507 // Prepare post data.
508 $post_data = [
509 'ID' => $campaign_id,
510 ];
511
512 $title = $request->get_param( 'title' );
513 $description = $request->get_param( 'description' );
514
515 if ( ! empty( $title ) ) {
516 $post_data['post_title'] = $title;
517 }
518
519 // Use is_null (not empty) so an empty string can clear the description;
520 // it feeds the seeded campaign page paragraph.
521 if ( ! is_null( $description ) ) {
522 $post_data['post_excerpt'] = $description;
523 }
524
525 // Update post if there are changes.
526 if ( count( $post_data ) > 1 ) {
527 $result = wp_update_post( $post_data, true );
528 if ( is_wp_error( $result ) ) {
529 return new WP_Error(
530 'update_failed',
531 __( 'Failed to update campaign.', 'suredonation' ),
532 [ 'status' => 500 ]
533 );
534 }
535 }
536
537 // Update campaign meta (consolidated single key).
538 $meta_values = [];
539 $featured_image = $request->get_param( 'featured_image' );
540
541 $goal_type = $request->get_param( 'goal_type' );
542 if ( ! is_null( $goal_type ) ) {
543 $meta_values['goal_type'] = $goal_type;
544 }
545
546 $goal_amount = $request->get_param( 'goal_amount' );
547 if ( ! is_null( $goal_amount ) ) {
548 $meta_values['goal_amount'] = '' !== $goal_amount ? floatval( $goal_amount ) : 0;
549 }
550
551 $campaign_status = $request->get_param( 'campaign_status' );
552 if ( ! empty( $campaign_status ) ) {
553 $meta_values['campaign_status'] = $campaign_status;
554 }
555
556 $email_settings = $request->get_param( 'email_settings' );
557 if ( ! is_null( $email_settings ) ) {
558 $meta_values['email_settings'] = $email_settings;
559 }
560
561 $require_terms = $request->get_param( 'require_terms' );
562 if ( ! is_null( $require_terms ) ) {
563 $meta_values['require_terms'] = (bool) $require_terms;
564 }
565
566 $terms_text = $request->get_param( 'terms_text' );
567 if ( ! is_null( $terms_text ) ) {
568 $meta_values['terms_text'] = $terms_text;
569 }
570
571 if ( ! empty( $meta_values ) ) {
572 Helper::update_campaign_meta( $campaign_id, $meta_values );
573 }
574
575 // Update featured image if provided.
576 if ( ! is_null( $featured_image ) ) {
577 if ( empty( $featured_image ) ) {
578 delete_post_thumbnail( $campaign_id );
579 } else {
580 set_post_thumbnail( $campaign_id, $featured_image );
581 }
582 }
583
584 return new WP_REST_Response(
585 [
586 'success' => true,
587 'message' => __( 'Campaign updated successfully.', 'suredonation' ),
588 'campaign' => $this->format_campaign( get_post( $campaign_id ) ),
589 ],
590 200
591 );
592 }
593
594 /**
595 * Update campaign status.
596 *
597 * @param WP_REST_Request $request Request object.
598 * @return WP_REST_Response|WP_Error Response object.
599 * @since 0.0.1
600 */
601 public function update_campaign_status( $request ) {
602 $campaign_id = absint( $request->get_param( 'id' ) );
603 $status = $request->get_param( 'status' );
604
605 // Verify the post exists and belongs to our post type.
606 $campaign = get_post( $campaign_id );
607 if ( ! $campaign || SUREDONATION_POST_TYPE !== $campaign->post_type ) {
608 return new WP_Error(
609 'campaign_not_found',
610 __( 'Campaign not found.', 'suredonation' ),
611 [ 'status' => 404 ]
612 );
613 }
614
615 $result = wp_update_post(
616 [
617 'ID' => $campaign_id,
618 'post_status' => $status,
619 ],
620 true
621 );
622
623 if ( is_wp_error( $result ) ) {
624 return new WP_Error(
625 'update_failed',
626 __( 'Failed to update campaign status.', 'suredonation' ),
627 [ 'status' => 500 ]
628 );
629 }
630
631 return new WP_REST_Response(
632 [
633 'success' => true,
634 'message' => __( 'Campaign status updated successfully.', 'suredonation' ),
635 ],
636 200
637 );
638 }
639
640 /**
641 * Duplicate campaign.
642 *
643 * @param WP_REST_Request $request Request object.
644 * @return WP_REST_Response|WP_Error Response object.
645 * @since 0.0.1
646 */
647 public function duplicate_campaign( $request ) {
648 $campaign_id = $request->get_param( 'id' );
649
650 // Get original campaign.
651 $original = get_post( $campaign_id );
652 if ( ! $original || SUREDONATION_POST_TYPE !== $original->post_type ) {
653 return new WP_Error(
654 'campaign_not_found',
655 __( 'Campaign not found.', 'suredonation' ),
656 [ 'status' => 404 ]
657 );
658 }
659
660 // Create duplicate. The page (post_content) is intentionally NOT copied:
661 // its blocks carry the original campaign's id, so a fresh page is seeded
662 // for the duplicate on first publish (or via the Create Campaign Page CTA),
663 // guaranteeing the correct id and its own default form. The description
664 // (post_excerpt) is carried over.
665 $duplicate_id = wp_insert_post(
666 [
667 'post_type' => $original->post_type,
668 'post_title' => $original->post_title . ' (Copy)',
669 'post_excerpt' => $original->post_excerpt,
670 'post_status' => 'draft',
671 'post_author' => get_current_user_id(),
672 ],
673 true
674 );
675
676 if ( is_wp_error( $duplicate_id ) ) {
677 return new WP_Error(
678 'duplicate_failed',
679 __( 'Failed to duplicate campaign.', 'suredonation' ),
680 [ 'status' => 500 ]
681 );
682 }
683
684 // Copy consolidated campaign meta.
685 $meta = get_post_meta( $campaign_id, Helper::SUREDONATION_CAMPAIGN_META_KEY, true );
686 if ( ! empty( $meta ) ) {
687 update_post_meta( $duplicate_id, Helper::SUREDONATION_CAMPAIGN_META_KEY, $meta );
688 }
689
690 return new WP_REST_Response(
691 [
692 'success' => true,
693 'message' => __( 'Campaign duplicated successfully.', 'suredonation' ),
694 'campaign' => $this->format_campaign( get_post( $duplicate_id ) ),
695 ],
696 201
697 );
698 }
699
700 /**
701 * Delete campaign.
702 *
703 * @param WP_REST_Request $request Request object.
704 * @return WP_REST_Response|WP_Error Response object.
705 * @since 0.0.1
706 */
707 public function delete_campaign( $request ) {
708 $campaign_id = $request->get_param( 'id' );
709
710 $campaign = get_post( $campaign_id );
711
712 if ( ! $campaign || SUREDONATION_POST_TYPE !== $campaign->post_type ) {
713 return new WP_Error(
714 'invalid_campaign',
715 __( 'Invalid campaign ID.', 'suredonation' ),
716 [ 'status' => 404 ]
717 );
718 }
719
720 $result = wp_delete_post( $campaign_id, true );
721
722 if ( ! $result ) {
723 return new WP_Error(
724 'delete_failed',
725 __( 'Failed to delete campaign.', 'suredonation' ),
726 [ 'status' => 500 ]
727 );
728 }
729
730 return new WP_REST_Response(
731 [
732 'success' => true,
733 'message' => __( 'Campaign deleted successfully.', 'suredonation' ),
734 ],
735 200
736 );
737 }
738
739 /**
740 * Bulk action on campaigns.
741 *
742 * @param WP_REST_Request $request Request object.
743 * @return WP_REST_Response|WP_Error Response object.
744 * @since 0.0.1
745 */
746 public function bulk_action( $request ) {
747 $action = $request->get_param( 'action' );
748 $ids = $request->get_param( 'ids' );
749
750 $success_count = 0;
751 $error_count = 0;
752
753 foreach ( $ids as $id ) {
754 $id = absint( $id );
755
756 // Verify each post belongs to our post type before operating on it.
757 $post = get_post( $id );
758 if ( ! $post || SUREDONATION_POST_TYPE !== $post->post_type ) {
759 ++$error_count;
760 continue;
761 }
762
763 if ( 'delete' === $action ) {
764 $result = wp_delete_post( $id, true );
765 if ( $result ) {
766 ++$success_count;
767 } else {
768 ++$error_count;
769 }
770 } else {
771 $result = wp_update_post(
772 [
773 'ID' => $id,
774 'post_status' => $action,
775 ],
776 true
777 );
778 if ( ! is_wp_error( $result ) ) {
779 ++$success_count;
780 } else {
781 ++$error_count;
782 }
783 }
784 }
785
786 return new WP_REST_Response(
787 [
788 'success' => true,
789 'message' => sprintf(
790 // translators: %1$d: success count, %2$d: error count.
791 __( 'Bulk action completed. Success: %1$d, Failed: %2$d', 'suredonation' ),
792 $success_count,
793 $error_count
794 ),
795 'success_count' => $success_count,
796 'error_count' => $error_count,
797 ],
798 200
799 );
800 }
801
802 /**
803 * Sanitize email settings.
804 *
805 * @param array<string, mixed> $settings Email settings to sanitize.
806 * @return array<string, mixed> Sanitized email settings.
807 * @since 0.0.1
808 */
809 public function sanitize_email_settings( $settings ) {
810 if ( empty( $settings ) || ! is_array( $settings ) ) {
811 return [];
812 }
813
814 $sanitized = [];
815
816 // Sanitize boolean.
817 $sanitized['enabled'] = ! empty( $settings['enabled'] );
818
819 // Sanitize text fields.
820 if ( isset( $settings['subject'] ) ) {
821 $sanitized['subject'] = sanitize_text_field( Helper::get_string_value( $settings['subject'] ) );
822 }
823
824 if ( isset( $settings['from_name'] ) ) {
825 $sanitized['from_name'] = sanitize_text_field( Helper::get_string_value( $settings['from_name'] ) );
826 }
827
828 // Sanitize email fields.
829 if ( isset( $settings['from_email'] ) ) {
830 $sanitized['from_email'] = sanitize_email( Helper::get_string_value( $settings['from_email'] ) );
831 }
832
833 if ( isset( $settings['reply_to'] ) ) {
834 $sanitized['reply_to'] = sanitize_email( Helper::get_string_value( $settings['reply_to'] ) );
835 }
836
837 // Sanitize email body (allow HTML).
838 if ( isset( $settings['email_body'] ) ) {
839 $sanitized['email_body'] = wp_kses_post( Helper::get_string_value( $settings['email_body'] ) );
840 }
841
842 return $sanitized;
843 }
844
845 /**
846 * Get pages/posts where donation form block is used for this campaign.
847 *
848 * @param WP_REST_Request $request Request object.
849 * @return WP_REST_Response|WP_Error Response object.
850 * @since 0.0.1
851 */
852 public function get_form_locations( $request ) {
853 $campaign_id = absint( $request->get_param( 'id' ) );
854
855 // Check if campaign exists.
856 $campaign = get_post( $campaign_id );
857 if ( ! $campaign || SUREDONATION_POST_TYPE !== $campaign->post_type ) {
858 return new WP_Error(
859 'campaign_not_found',
860 __( 'Campaign not found.', 'suredonation' ),
861 [ 'status' => 404 ]
862 );
863 }
864
865 global $wpdb;
866
867 // Search for posts containing the donation form block for this campaign.
868 // The block stores campaignId in its attributes like: "campaignId":123.
869 $search_pattern = '%"campaignId":' . $campaign_id . '%';
870
871 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
872 $posts = $wpdb->get_results(
873 $wpdb->prepare(
874 "SELECT ID, post_title, post_type, post_status, post_modified
875 FROM %i
876 WHERE post_content LIKE %s
877 AND post_status IN ('publish', 'draft', 'pending', 'private')
878 AND post_type IN ('page', 'post', 'suredonation_cmpgn')
879 ORDER BY post_modified DESC",
880 $wpdb->posts,
881 $search_pattern
882 )
883 );
884
885 $locations = [];
886 if ( $posts ) {
887 foreach ( $posts as $post ) {
888 $edit_url = admin_url( 'post.php?post=' . $post->ID . '&action=edit' );
889 $view_url = get_permalink( $post->ID );
890
891 $locations[] = [
892 'id' => (int) $post->ID,
893 'title' => $post->post_title,
894 'type' => $post->post_type,
895 'status' => $post->post_status,
896 'modified_at' => $post->post_modified,
897 'edit_url' => $edit_url,
898 'view_url' => 'publish' === $post->post_status ? $view_url : null,
899 ];
900 }
901 }
902
903 return new WP_REST_Response(
904 [
905 'success' => true,
906 'locations' => $locations,
907 ],
908 200
909 );
910 }
911
912 /**
913 * Check if user has permission to manage campaigns.
914 *
915 * @return bool True if user has permission.
916 * @since 0.0.1
917 */
918 public function check_permissions() {
919 return current_user_can( 'manage_options' );
920 }
921
922 /**
923 * Format campaign data for API response.
924 *
925 * @param \WP_Post|null $post Post object.
926 * @return array<string,mixed> Formatted campaign data.
927 * @since 0.0.1
928 */
929 private function format_campaign( $post ) {
930 if ( ! $post instanceof \WP_Post ) {
931 return [];
932 }
933 // Get real-time stats from donations database table.
934 $stats = \SureDonation\Inc\Campaigns\Campaign_Stats::get_stats( $post->ID );
935 $meta = Helper::get_campaign_meta( $post->ID );
936 $thumbnail_url = get_the_post_thumbnail_url( $post->ID, 'medium' );
937
938 return [
939 'id' => $post->ID,
940 'title' => $post->post_title,
941 'description' => $post->post_excerpt,
942 'status' => $stats['campaign_status'],
943 'goal_type' => $meta['goal_type'],
944 'goal' => $stats['goal_amount'],
945 'raised' => $stats['total_raised'],
946 'donors' => $stats['donor_count'],
947 'donations' => $stats['donation_count'],
948 'donation_count' => $stats['donation_count'],
949 'progress' => $stats['progress_percentage'],
950 'email_settings' => $meta['email_settings'],
951 'require_terms' => $meta['require_terms'],
952 'terms_text' => $meta['terms_text'],
953 'created_at' => $post->post_date,
954 'modified_at' => $post->post_modified,
955 'author' => get_the_author_meta( 'display_name', (int) $post->post_author ),
956 'has_page' => \SureDonation\Inc\Campaigns\Campaign_Page::has_page( $post->ID ),
957 'permalink' => 'publish' === $post->post_status ? get_permalink( $post->ID ) : null,
958 'edit_link' => admin_url( 'post.php?post=' . $post->ID . '&action=edit' ),
959 'featured_image' => (int) get_post_thumbnail_id( $post->ID ),
960 'featured_image_url' => $thumbnail_url ? $thumbnail_url : '',
961 ];
962 }
963
964 /**
965 * Get orderby field based on sort parameter.
966 *
967 * @param string $sort_by Sort parameter.
968 * @return string WordPress orderby field.
969 * @since 0.0.1
970 */
971 private function get_orderby_field( $sort_by ) {
972 $orderby_map = [
973 'date' => 'date',
974 'title' => 'title',
975 'status' => 'post_status',
976 ];
977
978 return $orderby_map[ $sort_by ] ?? 'date';
979 }
980 }
981