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 +117 -16 1.3.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,
@@ -409,8 +438,48 @@
409 438 );
410 439 }
411 440
412 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 + /**
413 482 * Create a new campaign.
414 483 *
415 484 * @param WP_REST_Request $request Request object.
416 485 * @return WP_REST_Response|WP_Error Response object.
@@ -416,28 +485,40 @@
416 485 * @return WP_REST_Response|WP_Error Response object.
417 486 * @since 0.0.1
418 487 */
419 488 public function create_campaign( $request ) {
420 - $title = $request->get_param( 'title' );
421 - $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' ) );
422 493 $goal_type = $request->get_param( 'goal_type' ) ?? 'raised_amount';
423 494 $goal_amount = $request->get_param( 'goal_amount' );
424 495 $campaign_status = $request->get_param( 'campaign_status' ) ?? 'active';
425 496 $featured_image = $request->get_param( 'featured_image' );
497 + $template_id = (string) $request->get_param( 'template_id' );
426 498
427 - // 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
428 500 // 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 - );
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 + ];
439 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 +
440 521 if ( is_wp_error( $post_id ) ) {
441 522 return new WP_Error(
442 523 'create_failed',
443 524 __( 'Failed to create campaign.', 'suredonation' ),
@@ -444,8 +525,22 @@
444 525 [ 'status' => 500 ]
445 526 );
446 527 }
447 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 +
448 543 // Save campaign meta as single consolidated meta key.
449 544 $meta_values = [
450 545 'goal_type' => $goal_type,
451 546 'goal_amount' => isset( $goal_amount ) && '' !== $goal_amount ? floatval( $goal_amount ) : 0,
@@ -865,9 +960,14 @@
865 960 global $wpdb;
866 961
867 962 // Search for posts containing the donation form block for this campaign.
868 963 // The block stores campaignId in its attributes like: "campaignId":123.
869 - $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 . '}%';
870 970
871 971 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
872 972 $posts = $wpdb->get_results(
873 973 $wpdb->prepare(
@@ -872,14 +972,15 @@
872 972 $posts = $wpdb->get_results(
873 973 $wpdb->prepare(
874 974 "SELECT ID, post_title, post_type, post_status, post_modified
875 975 FROM %i
876 - WHERE post_content LIKE %s
976 + WHERE ( post_content LIKE %s OR post_content LIKE %s )
877 977 AND post_status IN ('publish', 'draft', 'pending', 'private')
878 978 AND post_type IN ('page', 'post', 'suredonation_cmpgn')
879 979 ORDER BY post_modified DESC",
880 980 $wpdb->posts,
881 - $search_pattern
981 + $search_pattern,
982 + $alt_pattern
882 983 )
883 984 );
884 985
885 986 $locations = [];