PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.1.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.1.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.1.0, at inc/post-types/donation-form.php

778 lines 23.7 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 // Re-fetch the post content fresh. update_field_slugs() runs on the same
369 // save_post hook and rewrites post_content with generated field slugs via
370 // a nested wp_update_post(); the $post handed to this callback is the
371 // pre-update copy, so reading $post->post_content directly would miss the
372 // slug for a newly added field and the config would be stored without it
373 // (skipping that field in server-side validation until the next save).
374 $fresh = get_post( $post_id );
375 $content = $fresh instanceof \WP_Post ? $fresh->post_content : $post->post_content;
376 $blocks = parse_blocks( $content );
377
378 if ( empty( $blocks ) ) {
379 return;
380 }
381
382 // Store block configuration using Field_Validation class.
383 \SureDonation\Inc\Field_Validation::add_block_config( $blocks, $post_id );
384 }
385
386 /**
387 * Generate unique slugs for SureDonation blocks on form save.
388 *
389 * Parses the form content, generates slugs for blocks that don't have one,
390 * ensures uniqueness, and updates the post content if needed.
391 *
392 * @param int $post_id Post ID.
393 * @param \WP_Post $post Post object.
394 * @return void
395 * @since 0.0.1
396 */
397 public function update_field_slugs( $post_id, $post ) {
398 // Skip autosave.
399 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
400 return;
401 }
402
403 // Skip revisions.
404 if ( wp_is_post_revision( $post_id ) ) {
405 return;
406 }
407
408 // Verify user has permission to edit this post.
409 if ( ! current_user_can( 'edit_post', $post_id ) ) {
410 return;
411 }
412
413 // Verify nonce - handles both block editor (REST API) and classic editor.
414 if ( ! self::verify_save_post_nonce( $post_id ) ) {
415 return;
416 }
417
418 $blocks = parse_blocks( $post->post_content );
419
420 if ( empty( $blocks ) ) {
421 return;
422 }
423
424 // Process blocks to generate slugs.
425 [ $blocks, , $updated ] = \SureDonation\Inc\Helper::process_blocks( $blocks );
426
427 // Only update if blocks were modified.
428 if ( ! $updated ) {
429 return;
430 }
431
432 // Serialize blocks and update post.
433 $post_content = serialize_blocks( $blocks ); // @phpstan-ignore argument.type
434
435 // Remove save action to prevent infinite loop.
436 remove_action( 'save_post_' . self::POST_TYPE, [ $this, 'update_field_slugs' ], 10 );
437
438 wp_update_post(
439 [
440 'ID' => $post_id,
441 'post_content' => $post_content,
442 ]
443 );
444
445 // Re-add save action.
446 add_action( 'save_post_' . self::POST_TYPE, [ $this, 'update_field_slugs' ], 10, 2 );
447 }
448
449 /**
450 * Check if a post is a donation form.
451 *
452 * @param int|\WP_Post $post Post ID or post object.
453 * @return bool
454 * @since 0.0.1
455 */
456 public static function is_donation_form( $post ) {
457 $post = get_post( $post );
458
459 if ( ! $post ) {
460 return false;
461 }
462
463 return self::POST_TYPE === $post->post_type;
464 }
465
466 /**
467 * Get all donation forms.
468 *
469 * @param array<string, mixed> $args Additional WP_Query arguments.
470 * @return array<\WP_Post> Array of donation form posts.
471 * @since 0.0.1
472 */
473 public static function get_forms( $args = [] ) {
474 $defaults = [
475 'post_type' => self::POST_TYPE,
476 'posts_per_page' => -1,
477 'post_status' => 'publish',
478 'orderby' => 'title',
479 'order' => 'ASC',
480 ];
481
482 $query_args = wp_parse_args( $args, $defaults );
483
484 return get_posts( $query_args ); // @phpstan-ignore return.type
485 }
486
487 /**
488 * Get forms linked to a specific campaign.
489 *
490 * @param int $campaign_id Campaign ID.
491 * @return array<\WP_Post> Array of donation form posts.
492 * @since 0.0.1
493 */
494 public static function get_forms_by_campaign( $campaign_id ) {
495 return self::get_forms(
496 [
497 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
498 [
499 'key' => self::META_CAMPAIGN_ID,
500 'value' => $campaign_id,
501 'compare' => '=',
502 'type' => 'NUMERIC',
503 ],
504 ],
505 ]
506 );
507 }
508
509 /**
510 * Get the campaign ID linked to a form.
511 *
512 * @param int $form_id Form ID.
513 * @return int Campaign ID or 0 if not linked.
514 * @since 0.0.1
515 */
516 public static function get_form_campaign_id( $form_id ) {
517 $campaign_id = get_post_meta( $form_id, self::META_CAMPAIGN_ID, true );
518 return is_numeric( $campaign_id ) ? (int) $campaign_id : 0;
519 }
520
521 /**
522 * Set the campaign ID for a form.
523 *
524 * @param int $form_id Form ID.
525 * @param int $campaign_id Campaign ID.
526 * @return bool True on success, false on failure.
527 * @since 0.0.1
528 */
529 public static function set_form_campaign_id( $form_id, $campaign_id ) {
530 return (bool) update_post_meta( $form_id, self::META_CAMPAIGN_ID, absint( $campaign_id ) );
531 }
532
533 /**
534 * Create a default donation form for a campaign.
535 *
536 * Creates a single-page form with all the essential fields matching
537 * the hardcoded template structure.
538 *
539 * @param int $campaign_id Campaign ID to link the form to.
540 * @param string $campaign_name Campaign name for the form title.
541 * @return int|false Form ID on success, false on failure.
542 * @since 0.0.1
543 */
544 public static function create_default_form_for_campaign( $campaign_id, $campaign_name = '' ) {
545 if ( ! $campaign_id ) {
546 return false;
547 }
548
549 // Generate form title.
550 $form_title = $campaign_name
551 ? sprintf(
552 /* translators: %s: campaign name */
553 __( '%s - Donation Form', 'suredonation' ),
554 $campaign_name
555 )
556 : __( 'Donation Form', 'suredonation' );
557
558 // Build the block content.
559 $blocks_content = self::get_default_form_blocks_content();
560
561 // Create the form post.
562 $form_id = wp_insert_post(
563 [
564 'post_title' => $form_title,
565 'post_content' => $blocks_content,
566 'post_status' => 'publish',
567 'post_type' => self::POST_TYPE,
568 'meta_input' => [
569 self::META_CAMPAIGN_ID => $campaign_id,
570 ],
571 ],
572 true
573 );
574
575 if ( is_wp_error( $form_id ) ) {
576 return false;
577 }
578
579 return $form_id;
580 }
581
582 /**
583 * Get the default block template for new forms.
584 *
585 * @return array<int, array<int, mixed>>
586 * @since 0.0.1
587 */
588 private function get_default_template() {
589 return [
590 [
591 'suredonation/input',
592 [
593 'label' => __( 'Full Name', 'suredonation' ),
594 'required' => true,
595 'placeholder' => __( 'Enter your full name', 'suredonation' ),
596 'slug' => 'donor-name',
597 'fieldWidth' => 50,
598 ],
599 ],
600 [
601 'suredonation/email',
602 [
603 'label' => __( 'Email Address', 'suredonation' ),
604 'required' => true,
605 'placeholder' => __( 'Enter your email', 'suredonation' ),
606 'slug' => 'donor-email',
607 'fieldWidth' => 50,
608 ],
609 ],
610 [
611 'suredonation/donation-amount',
612 [
613 'label' => __( 'Select Donation Amount', 'suredonation' ),
614 'required' => true,
615 'choiceType' => 'radio',
616 'layout' => 'horizontal',
617 'slug' => 'donation-amount',
618 'options' => [
619 [
620 'label' => '25',
621 'value' => '25',
622 ],
623 [
624 'label' => '50',
625 'value' => '50',
626 ],
627 [
628 'label' => '100',
629 'value' => '100',
630 ],
631 [
632 'label' => '250',
633 'value' => '250',
634 ],
635 ],
636 ],
637 ],
638 [
639 'suredonation/payment',
640 [
641 'gateway' => 'stripe',
642 'paymentType' => 'one-time',
643 'amountType' => 'variable',
644 'minimumAmount' => 0,
645 'variableAmountField' => 'donation-amount',
646 'customerEmailField' => 'donor-email',
647 'customerNameField' => 'donor-name',
648 ],
649 ],
650 [
651 'suredonation/donate-button',
652 [
653 'buttonText' => __( 'Donate', 'suredonation' ),
654 'slug' => 'donate-button',
655 ],
656 ],
657 ];
658 }
659
660 /**
661 * Get the default form blocks content as serialized block markup.
662 *
663 * Creates a single-page donation form with:
664 * - Multi-choice for preset amounts (radio buttons)
665 * - Input for donor name
666 * - Email for donor email
667 * - Payment block configured for donation-amount variable amount
668 *
669 * @return string Serialized block content.
670 * @since 0.0.1
671 */
672 private static function get_default_form_blocks_content() {
673 $blocks = [];
674
675 // Donor name.
676 $blocks[] = '<!-- wp:suredonation/input ' . wp_json_encode(
677 [
678 'label' => __( 'Full Name', 'suredonation' ),
679 'required' => true,
680 'placeholder' => __( 'Enter your full name', 'suredonation' ),
681 'slug' => 'donor-name',
682 'fieldWidth' => 50,
683 ]
684 ) . ' /-->';
685
686 // Donor email.
687 $blocks[] = '<!-- wp:suredonation/email ' . wp_json_encode(
688 [
689 'label' => __( 'Email Address', 'suredonation' ),
690 'required' => true,
691 'placeholder' => __( 'Enter your email', 'suredonation' ),
692 'slug' => 'donor-email',
693 'fieldWidth' => 50,
694 ]
695 ) . ' /-->';
696
697 // Preset donation amounts using donation-amount (radio buttons).
698 $blocks[] = '<!-- wp:suredonation/donation-amount ' . wp_json_encode(
699 [
700 'label' => __( 'Select Donation Amount', 'suredonation' ),
701 'required' => true,
702 'choiceType' => 'radio',
703 'layout' => 'horizontal',
704 'slug' => 'donation-amount',
705 'options' => [
706 [
707 'label' => '25',
708 'value' => '25',
709 ],
710 [
711 'label' => '50',
712 'value' => '50',
713 ],
714 [
715 'label' => '100',
716 'value' => '100',
717 ],
718 [
719 'label' => '250',
720 'value' => '250',
721 ],
722 ],
723 ]
724 ) . ' /-->';
725
726 // Payment block configured for donation-amount variable amount.
727 $blocks[] = '<!-- wp:suredonation/payment ' . wp_json_encode(
728 [
729 'gateway' => 'stripe',
730 'paymentType' => 'one-time',
731 'amountType' => 'variable',
732 'minimumAmount' => 0,
733 'variableAmountField' => 'donation-amount',
734 'customerEmailField' => 'donor-email',
735 'customerNameField' => 'donor-name',
736 ]
737 ) . ' /-->';
738
739 // Donate button.
740 $blocks[] = '<!-- wp:suredonation/donate-button ' . wp_json_encode(
741 [
742 'buttonText' => __( 'Donate', 'suredonation' ),
743 'slug' => 'donate-button',
744 ]
745 ) . ' /-->';
746
747 return implode( "\n\n", $blocks );
748 }
749
750 /**
751 * Verify nonce for save_post hooks.
752 *
753 * Handles both block editor (REST API) and classic editor nonce verification.
754 * - Block Editor: Verifies the wp_rest nonce via REST_REQUEST constant
755 * - Classic Editor: Verifies _wpnonce with update-post_{$post_id} action
756 *
757 * @param int $post_id Post ID being saved.
758 * @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.
759 * @since 0.0.1
760 */
761 private static function verify_save_post_nonce( $post_id ) {
762 // Block editor saves via REST API - nonce already verified by WordPress REST authentication.
763 // The REST_REQUEST constant is only defined after successful authentication.
764 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
765 return true;
766 }
767
768 // Classic editor - verify _wpnonce with update-post action.
769 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
770
771 if ( ! is_string( $nonce ) || '' === $nonce ) {
772 return false;
773 }
774
775 return wp_verify_nonce( $nonce, 'update-post_' . $post_id );
776 }
777 }
778