PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
1.6.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
← All changes | inc/api/campaigns-api.php +131 -17 1.0.0 → 1.6.1 View file →
@@ -7,8 +7,10 @@
7 7
8 8 namespace SureDonation\Inc\API;
9 9
10 10 use SureDonation\Inc\Helper;
11 +use SureDonation\Inc\Campaign_Templates\Campaign_Templates;
12 +use SureDonation\Inc\Campaigns\Campaign_Cpt;
11 13 use WP_Error;
12 14 use WP_REST_Request;
13 15 use WP_REST_Response;
14 16 use WP_REST_Server;
@@ -85,12 +87,39 @@
85 87 ],
86 88 'terms_text' => [
87 89 'sanitize_callback' => 'sanitize_text_field',
88 90 ],
91 + 'template_id' => [
92 + 'sanitize_callback' => 'sanitize_key',
93 + 'validate_callback' => static function ( $value ) {
94 + // Empty ⇒ scratch. Otherwise must resolve to a known template.
95 + return '' === $value || Campaign_Templates::get_instance()->exists( $value );
96 + },
97 + ],
89 98 ],
90 99 ],
91 100 ],
92 101
102 + // List campaign templates for the picker.
103 + '/campaign-templates' => [
104 + [
105 + 'methods' => WP_REST_Server::READABLE,
106 + 'callback' => [ $this, 'get_campaign_templates' ],
107 + 'permission_callback' => [ $this, 'check_permissions' ],
108 + ],
109 + ],
110 +
111 + // Analytics ping fired when the template picker is opened. Separate
112 + // from the listing route above because that one is fetched on every
113 + // campaigns-page load, not on open, so it cannot stand in for intent.
114 + '/campaign-templates/track-picker' => [
115 + [
116 + 'methods' => WP_REST_Server::EDITABLE,
117 + 'callback' => [ $this, 'track_template_picker' ],
118 + 'permission_callback' => [ $this, 'check_permissions' ],
119 + ],
120 + ],
121 +
93 122 // Update campaign.
94 123 '/campaigns/(?P<id>\d+)' => [
95 124 [
96 125 'methods' => WP_REST_Server::READABLE,
@@ -356,9 +385,22 @@
356 385 'order' => strtoupper( $order ),
357 386 ];
358 387
359 388 // Add status filter.
360 - if ( 'all' !== $status ) {
389 + if ( 'paused' === $status ) {
390 + // "Paused" is a campaign business status stored inside the campaign
391 + // meta JSON (not a WP post status), so match published campaigns whose
392 + // meta marks them paused.
393 + $args['post_status'] = 'publish';
394 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Admin-only campaign list filter.
395 + $args['meta_query'] = [
396 + [
397 + 'key' => Helper::SUREDONATION_CAMPAIGN_META_KEY,
398 + 'value' => '"campaign_status":"paused"',
399 + 'compare' => 'LIKE',
400 + ],
401 + ];
402 + } elseif ( 'all' !== $status ) {
361 403 $args['post_status'] = $status;
362 404 } else {
363 405 $args['post_status'] = [ 'publish', 'draft' ];
364 406 }
@@ -396,8 +438,48 @@
396 438 );
397 439 }
398 440
399 441 /**
442 + * List the available campaign templates for the picker.
443 + *
444 + * @param WP_REST_Request $request Request object.
445 + * @return WP_REST_Response Response object.
446 + * @since 1.5.0
447 + */
448 + public function get_campaign_templates( $request ) {
449 + unset( $request );
450 +
451 + return new WP_REST_Response(
452 + [
453 + 'templates' => Campaign_Templates::get_instance()->get_all(),
454 + ]
455 + );
456 + }
457 +
458 + /**
459 + * Record that the campaign template picker was opened.
460 + *
461 + * The action fires on every call; the analytics listener dedups, so this only
462 + * ever records once per site. Cheap enough to call on each open.
463 + *
464 + * @param WP_REST_Request $request Request object.
465 + * @return WP_REST_Response Response object.
466 + * @since 1.5.0
467 + */
468 + public function track_template_picker( $request ) {
469 + unset( $request );
470 +
471 + /**
472 + * Fires when a user opens the campaign template picker.
473 + *
474 + * @since 1.5.0
475 + */
476 + do_action( 'suredonation_campaign_template_picker_opened' );
477 +
478 + return new WP_REST_Response( [ 'success' => true ] );
479 + }
480 +
481 + /**
400 482 * Create a new campaign.
401 483 *
402 484 * @param WP_REST_Request $request Request object.
403 485 * @return WP_REST_Response|WP_Error Response object.
@@ -403,28 +485,40 @@
403 485 * @return WP_REST_Response|WP_Error Response object.
404 486 * @since 0.0.1
405 487 */
406 488 public function create_campaign( $request ) {
407 - $title = $request->get_param( 'title' );
408 - $description = $request->get_param( 'description' );
489 + $title = $request->get_param( 'title' );
490 + // Normalize to string so an omitted description does not become a null
491 + // post_excerpt (the column is NOT NULL and would fail the insert).
492 + $description = Helper::get_string_value( $request->get_param( 'description' ) );
409 493 $goal_type = $request->get_param( 'goal_type' ) ?? 'raised_amount';
410 494 $goal_amount = $request->get_param( 'goal_amount' );
411 495 $campaign_status = $request->get_param( 'campaign_status' ) ?? 'active';
412 496 $featured_image = $request->get_param( 'featured_image' );
497 + $template_id = (string) $request->get_param( 'template_id' );
413 498
414 - // Create the campaign post. The description is stored as the excerpt so
499 + // Build the insert args. The description is stored as the excerpt so
415 500 // 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 - );
501 + $insert_args = [
502 + 'post_type' => SUREDONATION_POST_TYPE,
503 + 'post_title' => $title,
504 + 'post_excerpt' => $description,
505 + 'post_status' => 'publish',
506 + 'post_author' => get_current_user_id(),
507 + ];
426 508
509 + // Record the chosen template via meta_input so it is set BEFORE the
510 + // save_post seeding hooks fire (they run during wp_insert_post). A
511 + // post-insert update_post_meta would be too late.
512 + if ( '' !== $template_id ) {
513 + $insert_args['meta_input'] = [
514 + Campaign_Cpt::META_TEMPLATE_ID => $template_id,
515 + ];
516 + }
517 +
518 + // Create the campaign post.
519 + $post_id = wp_insert_post( $insert_args, true );
520 +
427 521 if ( is_wp_error( $post_id ) ) {
428 522 return new WP_Error(
429 523 'create_failed',
430 524 __( 'Failed to create campaign.', 'suredonation' ),
@@ -431,8 +525,22 @@
431 525 [ 'status' => 500 ]
432 526 );
433 527 }
434 528
529 + if ( '' !== $template_id ) {
530 + /**
531 + * Fires after a campaign is created from a template.
532 + *
533 + * The id has already been validated against the template registry by
534 + * the route's validate_callback, so listeners receive a known id.
535 + *
536 + * @param string $template_id Template the campaign was created from.
537 + * @param int $post_id The new campaign's post ID.
538 + * @since 1.5.0
539 + */
540 + do_action( 'suredonation_campaign_created_from_template', $template_id, $post_id );
541 + }
542 +
435 543 // Save campaign meta as single consolidated meta key.
436 544 $meta_values = [
437 545 'goal_type' => $goal_type,
438 546 'goal_amount' => isset( $goal_amount ) && '' !== $goal_amount ? floatval( $goal_amount ) : 0,
@@ -852,9 +960,14 @@
852 960 global $wpdb;
853 961
854 962 // Search for posts containing the donation form block for this campaign.
855 963 // The block stores campaignId in its attributes like: "campaignId":123.
856 - $search_pattern = '%"campaignId":' . $campaign_id . '%';
964 + // Anchor on the value's terminator: without it campaign 12 also matches
965 + // "campaignId":123. Block attributes are JSON, so the id is followed by
966 + // a comma or the closing brace.
967 + $escaped_id = $wpdb->esc_like( '"campaignId":' . $campaign_id );
968 + $search_pattern = '%' . $escaped_id . ',%';
969 + $alt_pattern = '%' . $escaped_id . '}%';
857 970
858 971 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
859 972 $posts = $wpdb->get_results(
860 973 $wpdb->prepare(
@@ -859,14 +972,15 @@
859 972 $posts = $wpdb->get_results(
860 973 $wpdb->prepare(
861 974 "SELECT ID, post_title, post_type, post_status, post_modified
862 975 FROM %i
863 - WHERE post_content LIKE %s
976 + WHERE ( post_content LIKE %s OR post_content LIKE %s )
864 977 AND post_status IN ('publish', 'draft', 'pending', 'private')
865 978 AND post_type IN ('page', 'post', 'suredonation_cmpgn')
866 979 ORDER BY post_modified DESC",
867 980 $wpdb->posts,
868 - $search_pattern
981 + $search_pattern,
982 + $alt_pattern
869 983 )
870 984 );
871 985
872 986 $locations = [];