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 / post-types / donation-form.php

donation-form.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.0.0, at inc/post-types/donation-form.php

771 lines 23.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Donation Form Custom Post Type.
4 *
5 * @package SureDonation
6 * @since 0.0.1
7 */
8
9 namespace SureDonation\Inc\Post_Types;
10
11 use SureDonation\Inc\Traits\Get_Instance;
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * Donation Form Post Type Class.
20 *
21 * @since 0.0.1
22 */
23 class Donation_Form {
24 use Get_Instance;
25
26 /**
27 * Post type slug.
28 *
29 * @since 0.0.1
30 */
31 public const POST_TYPE = 'suredonation_form';
32
33 /**
34 * Meta key for linked campaign ID.
35 *
36 * @since 0.0.1
37 */
38 public const META_CAMPAIGN_ID = '_suredonation_campaign_id';
39
40 /**
41 * Meta key for the per-form styling settings (JSON blob).
42 *
43 * @var string
44 * @since 1.0.0
45 */
46 public const META_STYLING = '_suredonation_form_styling';
47
48 /**
49 * Constructor.
50 *
51 * @since 0.0.1
52 */
53 public function __construct() {
54 add_action( 'init', [ $this, 'register_post_type' ] );
55 add_action( 'init', [ $this, 'register_meta' ] );
56 add_filter( 'allowed_block_types_all', [ $this, 'restrict_blocks' ], 10, 2 );
57 add_filter( 'render_block_data', [ $this, 'alias_legacy_multi_choice_block' ] );
58 add_filter( 'surerank_excluded_post_types_from_seo_checks', [ $this, 'exclude_from_surerank_seo_checks' ] );
59 add_action( 'load-post-new.php', [ $this, 'set_campaign_on_auto_draft' ] );
60 add_action( 'save_post_' . self::POST_TYPE, [ $this, 'maybe_set_campaign_from_url' ], 10, 2 );
61 add_action( 'save_post_' . self::POST_TYPE, [ $this, 'update_field_slugs' ], 10, 2 );
62 add_action( 'save_post_' . self::POST_TYPE, [ $this, 'store_block_config' ], 10, 2 );
63 }
64
65 /**
66 * Exclude the donation form post type from SureRank's SEO checks.
67 *
68 * Stops SureRank from injecting its SEO meta box / "Optimize" button into
69 * the donation form editor (and its column on the list table), where SEO
70 * is not relevant — mirroring how SureRank excludes sureforms_form.
71 *
72 * @param array<string> $post_types Post types excluded from SEO checks.
73 * @return array<string> Filtered list of excluded post types.
74 * @since 1.0.0
75 */
76 public function exclude_from_surerank_seo_checks( $post_types ) {
77 $post_types = is_array( $post_types ) ? $post_types : [];
78 $post_types[] = self::POST_TYPE;
79
80 return $post_types;
81 }
82
83 /**
84 * Render-time alias: any saved suredonation/multi-choice block (from before the
85 * rename) renders as suredonation/donation-amount. Keeps existing forms working
86 * without a content migration.
87 *
88 * Intentionally registered globally rather than gated on the
89 * suredonation_form post type: forms are embedded on regular pages via
90 * the donation-form block / shortcode, where the queried post is the
91 * page, not the form CPT. The early string compare is cheap, and the
92 * slug is plugin-specific so it only ever matches our own blocks.
93 *
94 * @param array<string, mixed> $parsed_block Parsed block data.
95 * @return array<string, mixed>
96 * @since 1.0.0
97 */
98 public function alias_legacy_multi_choice_block( $parsed_block ) {
99 if ( isset( $parsed_block['blockName'] ) && 'suredonation/multi-choice' === $parsed_block['blockName'] ) {
100 $parsed_block['blockName'] = 'suredonation/donation-amount';
101 }
102 return $parsed_block;
103 }
104
105 /**
106 * Register the donation form post type.
107 *
108 * @return void
109 * @since 0.0.1
110 */
111 public function register_post_type() {
112 $labels = [
113 'name' => _x( 'Donation Forms', 'Post type general name', 'suredonation' ),
114 'singular_name' => _x( 'Donation Form', 'Post type singular name', 'suredonation' ),
115 'menu_name' => _x( 'Donation Forms', 'Admin Menu text', 'suredonation' ),
116 'name_admin_bar' => _x( 'Donation Form', 'Add New on Toolbar', 'suredonation' ),
117 'add_new' => __( 'Add New', 'suredonation' ),
118 'add_new_item' => __( 'Add New Form', 'suredonation' ),
119 'new_item' => __( 'New Form', 'suredonation' ),
120 'edit_item' => __( 'Edit Form', 'suredonation' ),
121 'view_item' => __( 'View Form', 'suredonation' ),
122 'all_items' => __( 'All Forms', 'suredonation' ),
123 'search_items' => __( 'Search Forms', 'suredonation' ),
124 'parent_item_colon' => __( 'Parent Forms:', 'suredonation' ),
125 'not_found' => __( 'No forms found.', 'suredonation' ),
126 'not_found_in_trash' => __( 'No forms found in Trash.', 'suredonation' ),
127 'archives' => _x( 'Form archives', 'The post type archive label used in nav menus.', 'suredonation' ),
128 'insert_into_item' => _x( 'Insert into form', 'Overrides the "Insert into post" phrase.', 'suredonation' ),
129 'uploaded_to_this_item' => _x( 'Uploaded to this form', 'Overrides the "Uploaded to this post" phrase.', 'suredonation' ),
130 'filter_items_list' => _x( 'Filter forms list', 'Screen reader text for the filter links heading.', 'suredonation' ),
131 'items_list_navigation' => _x( 'Forms list navigation', 'Screen reader text for the pagination heading.', 'suredonation' ),
132 'items_list' => _x( 'Forms list', 'Screen reader text for the items list heading.', 'suredonation' ),
133 ];
134
135 $args = [
136 'labels' => $labels,
137 'description' => __( 'Donation forms for SureDonation.', 'suredonation' ),
138 'public' => false,
139 'publicly_queryable' => false,
140 'show_ui' => true,
141 'show_in_menu' => 'suredonation',
142 'query_var' => false,
143 'rewrite' => false,
144 'capability_type' => 'post',
145 'has_archive' => false,
146 'hierarchical' => false,
147 'supports' => [ 'title', 'editor', 'custom-fields' ],
148 'show_in_rest' => true, // Required for Gutenberg.
149 'template' => $this->get_default_template(),
150 'template_lock' => false,
151 ];
152
153 register_post_type( self::POST_TYPE, $args );
154 }
155
156 /**
157 * Register post meta for the donation form.
158 *
159 * @return void
160 * @since 0.0.1
161 */
162 public function register_meta() {
163 register_post_meta(
164 self::POST_TYPE,
165 self::META_CAMPAIGN_ID,
166 [
167 'type' => 'integer',
168 'description' => __( 'The ID of the linked campaign.', 'suredonation' ),
169 'single' => true,
170 'default' => 0,
171 'show_in_rest' => true,
172 'sanitize_callback' => 'absint',
173 'auth_callback' => static function () {
174 return current_user_can( 'manage_options' );
175 },
176 ]
177 );
178
179 register_post_meta(
180 self::POST_TYPE,
181 self::META_STYLING,
182 [
183 'type' => 'string',
184 'description' => __( 'Per-form styling settings (JSON).', 'suredonation' ),
185 'single' => true,
186 'default' => '',
187 'show_in_rest' => true,
188 'sanitize_callback' => [ \SureDonation\Inc\Fields\Form_Styling::class, 'sanitize_json' ],
189 'auth_callback' => static function () {
190 return current_user_can( 'manage_options' );
191 },
192 ]
193 );
194 }
195
196 /**
197 * Restrict allowed blocks in the donation form editor.
198 *
199 * @param bool|array<string> $allowed_block_types Array of allowed block types or true for all.
200 * @param \WP_Block_Editor_Context $context Block editor context.
201 * @return bool|array<string> Array of allowed block types.
202 * @since 0.0.1
203 */
204 public function restrict_blocks( $allowed_block_types, $context ) {
205 if ( ! isset( $context->post ) || self::POST_TYPE !== $context->post->post_type ) {
206 return $allowed_block_types;
207 }
208
209 // SureDonation form blocks.
210 return [
211 'suredonation/input',
212 'suredonation/email',
213 'suredonation/number',
214 'suredonation/donation-amount',
215 'suredonation/anonymous-donation',
216 'suredonation/payment',
217 'suredonation/donate-button',
218 'suredonation/cover-fees',
219 ];
220 }
221
222 /**
223 * Set campaign ID on the auto-draft when creating a new form from a campaign page.
224 *
225 * Hooks into load-post-new.php so the meta is set before the block editor
226 * renders, ensuring the campaign link is stored even if the URL parameter
227 * is lost after the first save/redirect.
228 *
229 * @return void
230 * @since 1.0.0
231 */
232 public function set_campaign_on_auto_draft() {
233 // Only for our post type.
234 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check on admin page load.
235 $post_type = isset( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : '';
236 if ( self::POST_TYPE !== $post_type ) {
237 return;
238 }
239
240 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only; validated below via capability check.
241 $campaign_id = isset( $_GET['campaign_id'] ) ? absint( $_GET['campaign_id'] ) : 0;
242 if ( $campaign_id <= 0 ) {
243 return;
244 }
245
246 // Validate campaign exists and user can access it.
247 $campaign = get_post( $campaign_id );
248 if ( ! $campaign instanceof \WP_Post || SUREDONATION_POST_TYPE !== $campaign->post_type || ! current_user_can( 'edit_post', $campaign_id ) ) {
249 return;
250 }
251
252 // WordPress creates the auto-draft via get_default_post_to_edit() which runs
253 // before our hook. We can get the post ID from the global $post or from the
254 // auto-draft that will be created. Use a filter on wp_insert_post_data to
255 // capture it, or simply hook into wp_insert_post to set meta right after.
256 //
257 // The closure stays attached for the rest of the request, but its condition
258 // (post_type + auto-draft) is narrow enough that subsequent wp_insert_post
259 // calls for other types are no-ops. Only one auto-draft is created per
260 // load-post-new.php request, so a one-shot removal adds complexity without benefit.
261 add_action(
262 'wp_insert_post',
263 static function ( $post_id, $post ) use ( $campaign_id ) {
264 if ( self::POST_TYPE === $post->post_type && 'auto-draft' === $post->post_status ) {
265 update_post_meta( $post_id, self::META_CAMPAIGN_ID, $campaign_id );
266 }
267 },
268 10,
269 2
270 );
271 }
272
273 /**
274 * Set campaign ID from URL parameter when creating a new form.
275 *
276 * This handles the case when a form is created via the "Add Form" button
277 * from the campaign page, which passes campaign_id as a URL parameter.
278 *
279 * SECURITY: This method implements defense-in-depth with multiple checks:
280 * 1. Nonce verification via verify_save_post_nonce() (WordPress REST nonce or classic editor nonce)
281 * 2. Capability check: current_user_can('edit_post', $post_id) for the form
282 * 3. Capability check: current_user_can('edit_post', $campaign_id) for the campaign
283 * 4. Validation: Campaign must exist and be the correct post type
284 *
285 * @param int $post_id Post ID.
286 * @param \WP_Post $post Post object.
287 * @return void
288 * @since 0.0.1
289 */
290 public function maybe_set_campaign_from_url( $post_id, $post ) {
291 unset( $post ); // Unused parameter.
292
293 // Skip autosave.
294 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
295 return;
296 }
297
298 // Skip revisions.
299 if ( wp_is_post_revision( $post_id ) ) {
300 return;
301 }
302
303 // Security check 1: Verify user has permission to edit this form.
304 if ( ! current_user_can( 'edit_post', $post_id ) ) {
305 return;
306 }
307
308 // Security check 2: Verify nonce - handles both block editor (REST API) and classic editor.
309 if ( ! self::verify_save_post_nonce( $post_id ) ) {
310 return;
311 }
312
313 // Only process if campaign_id is not already set.
314 $existing_campaign_id = self::get_form_campaign_id( $post_id );
315 if ( $existing_campaign_id > 0 ) {
316 return;
317 }
318
319 // Get campaign_id from URL parameter (from "Add Form" button on campaign pages).
320 // Security: Nonce verified above via verify_save_post_nonce(). Authorization verified
321 // via capability checks on both form (above) and campaign (below).
322 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified above via verify_save_post_nonce().
323 $campaign_id = isset( $_GET['campaign_id'] ) ? absint( $_GET['campaign_id'] ) : 0;
324
325 if ( $campaign_id > 0 ) {
326 // Security check 3: Verify the campaign exists, is the correct type, and user can edit it.
327 $campaign = get_post( $campaign_id );
328 if ( $campaign instanceof \WP_Post && SUREDONATION_POST_TYPE === $campaign->post_type && current_user_can( 'edit_post', $campaign_id ) ) {
329 self::set_form_campaign_id( $post_id, $campaign_id );
330 }
331 }
332 }
333
334 /**
335 * Store block configuration for server-side validation.
336 *
337 * This method extracts and stores payment block configuration (amount type,
338 * fixed amount, minimum amount, etc.) in post meta. This stored configuration
339 * is used during payment processing to validate that the submitted amount
340 * matches the form's configured values, preventing payment manipulation attacks.
341 *
342 * @param int $post_id Post ID.
343 * @param \WP_Post $post Post object.
344 * @return void
345 * @since 0.0.1
346 */
347 public function store_block_config( $post_id, $post ) {
348 // Skip autosave.
349 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
350 return;
351 }
352
353 // Skip revisions.
354 if ( wp_is_post_revision( $post_id ) ) {
355 return;
356 }
357
358 // Verify user has permission to edit this post.
359 if ( ! current_user_can( 'edit_post', $post_id ) ) {
360 return;
361 }
362
363 // Verify nonce - handles both block editor (REST API) and classic editor.
364 if ( ! self::verify_save_post_nonce( $post_id ) ) {
365 return;
366 }
367
368 // Parse blocks from post content.
369 $blocks = parse_blocks( $post->post_content );
370
371 if ( empty( $blocks ) ) {
372 return;
373 }
374
375 // Store block configuration using Field_Validation class.
376 \SureDonation\Inc\Field_Validation::add_block_config( $blocks, $post_id );
377 }
378
379 /**
380 * Generate unique slugs for SureDonation blocks on form save.
381 *
382 * Parses the form content, generates slugs for blocks that don't have one,
383 * ensures uniqueness, and updates the post content if needed.
384 *
385 * @param int $post_id Post ID.
386 * @param \WP_Post $post Post object.
387 * @return void
388 * @since 0.0.1
389 */
390 public function update_field_slugs( $post_id, $post ) {
391 // Skip autosave.
392 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
393 return;
394 }
395
396 // Skip revisions.
397 if ( wp_is_post_revision( $post_id ) ) {
398 return;
399 }
400
401 // Verify user has permission to edit this post.
402 if ( ! current_user_can( 'edit_post', $post_id ) ) {
403 return;
404 }
405
406 // Verify nonce - handles both block editor (REST API) and classic editor.
407 if ( ! self::verify_save_post_nonce( $post_id ) ) {
408 return;
409 }
410
411 $blocks = parse_blocks( $post->post_content );
412
413 if ( empty( $blocks ) ) {
414 return;
415 }
416
417 // Process blocks to generate slugs.
418 [ $blocks, , $updated ] = \SureDonation\Inc\Helper::process_blocks( $blocks );
419
420 // Only update if blocks were modified.
421 if ( ! $updated ) {
422 return;
423 }
424
425 // Serialize blocks and update post.
426 $post_content = serialize_blocks( $blocks ); // @phpstan-ignore argument.type
427
428 // Remove save action to prevent infinite loop.
429 remove_action( 'save_post_' . self::POST_TYPE, [ $this, 'update_field_slugs' ], 10 );
430
431 wp_update_post(
432 [
433 'ID' => $post_id,
434 'post_content' => $post_content,
435 ]
436 );
437
438 // Re-add save action.
439 add_action( 'save_post_' . self::POST_TYPE, [ $this, 'update_field_slugs' ], 10, 2 );
440 }
441
442 /**
443 * Check if a post is a donation form.
444 *
445 * @param int|\WP_Post $post Post ID or post object.
446 * @return bool
447 * @since 0.0.1
448 */
449 public static function is_donation_form( $post ) {
450 $post = get_post( $post );
451
452 if ( ! $post ) {
453 return false;
454 }
455
456 return self::POST_TYPE === $post->post_type;
457 }
458
459 /**
460 * Get all donation forms.
461 *
462 * @param array<string, mixed> $args Additional WP_Query arguments.
463 * @return array<\WP_Post> Array of donation form posts.
464 * @since 0.0.1
465 */
466 public static function get_forms( $args = [] ) {
467 $defaults = [
468 'post_type' => self::POST_TYPE,
469 'posts_per_page' => -1,
470 'post_status' => 'publish',
471 'orderby' => 'title',
472 'order' => 'ASC',
473 ];
474
475 $query_args = wp_parse_args( $args, $defaults );
476
477 return get_posts( $query_args ); // @phpstan-ignore return.type
478 }
479
480 /**
481 * Get forms linked to a specific campaign.
482 *
483 * @param int $campaign_id Campaign ID.
484 * @return array<\WP_Post> Array of donation form posts.
485 * @since 0.0.1
486 */
487 public static function get_forms_by_campaign( $campaign_id ) {
488 return self::get_forms(
489 [
490 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
491 [
492 'key' => self::META_CAMPAIGN_ID,
493 'value' => $campaign_id,
494 'compare' => '=',
495 'type' => 'NUMERIC',
496 ],
497 ],
498 ]
499 );
500 }
501
502 /**
503 * Get the campaign ID linked to a form.
504 *
505 * @param int $form_id Form ID.
506 * @return int Campaign ID or 0 if not linked.
507 * @since 0.0.1
508 */
509 public static function get_form_campaign_id( $form_id ) {
510 $campaign_id = get_post_meta( $form_id, self::META_CAMPAIGN_ID, true );
511 return is_numeric( $campaign_id ) ? (int) $campaign_id : 0;
512 }
513
514 /**
515 * Set the campaign ID for a form.
516 *
517 * @param int $form_id Form ID.
518 * @param int $campaign_id Campaign ID.
519 * @return bool True on success, false on failure.
520 * @since 0.0.1
521 */
522 public static function set_form_campaign_id( $form_id, $campaign_id ) {
523 return (bool) update_post_meta( $form_id, self::META_CAMPAIGN_ID, absint( $campaign_id ) );
524 }
525
526 /**
527 * Create a default donation form for a campaign.
528 *
529 * Creates a single-page form with all the essential fields matching
530 * the hardcoded template structure.
531 *
532 * @param int $campaign_id Campaign ID to link the form to.
533 * @param string $campaign_name Campaign name for the form title.
534 * @return int|false Form ID on success, false on failure.
535 * @since 0.0.1
536 */
537 public static function create_default_form_for_campaign( $campaign_id, $campaign_name = '' ) {
538 if ( ! $campaign_id ) {
539 return false;
540 }
541
542 // Generate form title.
543 $form_title = $campaign_name
544 ? sprintf(
545 /* translators: %s: campaign name */
546 __( '%s - Donation Form', 'suredonation' ),
547 $campaign_name
548 )
549 : __( 'Donation Form', 'suredonation' );
550
551 // Build the block content.
552 $blocks_content = self::get_default_form_blocks_content();
553
554 // Create the form post.
555 $form_id = wp_insert_post(
556 [
557 'post_title' => $form_title,
558 'post_content' => $blocks_content,
559 'post_status' => 'publish',
560 'post_type' => self::POST_TYPE,
561 'meta_input' => [
562 self::META_CAMPAIGN_ID => $campaign_id,
563 ],
564 ],
565 true
566 );
567
568 if ( is_wp_error( $form_id ) ) {
569 return false;
570 }
571
572 return $form_id;
573 }
574
575 /**
576 * Get the default block template for new forms.
577 *
578 * @return array<int, array<int, mixed>>
579 * @since 0.0.1
580 */
581 private function get_default_template() {
582 return [
583 [
584 'suredonation/input',
585 [
586 'label' => __( 'Full Name', 'suredonation' ),
587 'required' => true,
588 'placeholder' => __( 'Enter your full name', 'suredonation' ),
589 'slug' => 'donor-name',
590 'fieldWidth' => 50,
591 ],
592 ],
593 [
594 'suredonation/email',
595 [
596 'label' => __( 'Email Address', 'suredonation' ),
597 'required' => true,
598 'placeholder' => __( 'Enter your email', 'suredonation' ),
599 'slug' => 'donor-email',
600 'fieldWidth' => 50,
601 ],
602 ],
603 [
604 'suredonation/donation-amount',
605 [
606 'label' => __( 'Select Donation Amount', 'suredonation' ),
607 'required' => true,
608 'choiceType' => 'radio',
609 'layout' => 'horizontal',
610 'slug' => 'donation-amount',
611 'options' => [
612 [
613 'label' => '25',
614 'value' => '25',
615 ],
616 [
617 'label' => '50',
618 'value' => '50',
619 ],
620 [
621 'label' => '100',
622 'value' => '100',
623 ],
624 [
625 'label' => '250',
626 'value' => '250',
627 ],
628 ],
629 ],
630 ],
631 [
632 'suredonation/payment',
633 [
634 'gateway' => 'stripe',
635 'paymentType' => 'one-time',
636 'amountType' => 'variable',
637 'minimumAmount' => 0,
638 'variableAmountField' => 'donation-amount',
639 'customerEmailField' => 'donor-email',
640 'customerNameField' => 'donor-name',
641 ],
642 ],
643 [
644 'suredonation/donate-button',
645 [
646 'buttonText' => __( 'Donate', 'suredonation' ),
647 'slug' => 'donate-button',
648 ],
649 ],
650 ];
651 }
652
653 /**
654 * Get the default form blocks content as serialized block markup.
655 *
656 * Creates a single-page donation form with:
657 * - Multi-choice for preset amounts (radio buttons)
658 * - Input for donor name
659 * - Email for donor email
660 * - Payment block configured for donation-amount variable amount
661 *
662 * @return string Serialized block content.
663 * @since 0.0.1
664 */
665 private static function get_default_form_blocks_content() {
666 $blocks = [];
667
668 // Donor name.
669 $blocks[] = '<!-- wp:suredonation/input ' . wp_json_encode(
670 [
671 'label' => __( 'Full Name', 'suredonation' ),
672 'required' => true,
673 'placeholder' => __( 'Enter your full name', 'suredonation' ),
674 'slug' => 'donor-name',
675 'fieldWidth' => 50,
676 ]
677 ) . ' /-->';
678
679 // Donor email.
680 $blocks[] = '<!-- wp:suredonation/email ' . wp_json_encode(
681 [
682 'label' => __( 'Email Address', 'suredonation' ),
683 'required' => true,
684 'placeholder' => __( 'Enter your email', 'suredonation' ),
685 'slug' => 'donor-email',
686 'fieldWidth' => 50,
687 ]
688 ) . ' /-->';
689
690 // Preset donation amounts using donation-amount (radio buttons).
691 $blocks[] = '<!-- wp:suredonation/donation-amount ' . wp_json_encode(
692 [
693 'label' => __( 'Select Donation Amount', 'suredonation' ),
694 'required' => true,
695 'choiceType' => 'radio',
696 'layout' => 'horizontal',
697 'slug' => 'donation-amount',
698 'options' => [
699 [
700 'label' => '25',
701 'value' => '25',
702 ],
703 [
704 'label' => '50',
705 'value' => '50',
706 ],
707 [
708 'label' => '100',
709 'value' => '100',
710 ],
711 [
712 'label' => '250',
713 'value' => '250',
714 ],
715 ],
716 ]
717 ) . ' /-->';
718
719 // Payment block configured for donation-amount variable amount.
720 $blocks[] = '<!-- wp:suredonation/payment ' . wp_json_encode(
721 [
722 'gateway' => 'stripe',
723 'paymentType' => 'one-time',
724 'amountType' => 'variable',
725 'minimumAmount' => 0,
726 'variableAmountField' => 'donation-amount',
727 'customerEmailField' => 'donor-email',
728 'customerNameField' => 'donor-name',
729 ]
730 ) . ' /-->';
731
732 // Donate button.
733 $blocks[] = '<!-- wp:suredonation/donate-button ' . wp_json_encode(
734 [
735 'buttonText' => __( 'Donate', 'suredonation' ),
736 'slug' => 'donate-button',
737 ]
738 ) . ' /-->';
739
740 return implode( "\n\n", $blocks );
741 }
742
743 /**
744 * Verify nonce for save_post hooks.
745 *
746 * Handles both block editor (REST API) and classic editor nonce verification.
747 * - Block Editor: Verifies the wp_rest nonce via REST_REQUEST constant
748 * - Classic Editor: Verifies _wpnonce with update-post_{$post_id} action
749 *
750 * @param int $post_id Post ID being saved.
751 * @return int|bool 1 if nonce is valid and generated between 0-12 hours (classic editor), 2 if valid and between 12-24 hours (classic editor), true for block editor, false otherwise.
752 * @since 0.0.1
753 */
754 private static function verify_save_post_nonce( $post_id ) {
755 // Block editor saves via REST API - nonce already verified by WordPress REST authentication.
756 // The REST_REQUEST constant is only defined after successful authentication.
757 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
758 return true;
759 }
760
761 // Classic editor - verify _wpnonce with update-post action.
762 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
763
764 if ( ! is_string( $nonce ) || '' === $nonce ) {
765 return false;
766 }
767
768 return wp_verify_nonce( $nonce, 'update-post_' . $post_id );
769 }
770 }
771