PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.4.4
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.4.4
3.4.4 3.4.3 3.4.2 3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.3.0 All 197 releases
convertkit / includes / blocks / class-convertkit-block-form-builder.php

class-convertkit-block-form-builder.php in Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages 3.4.4, at includes/blocks/class-convertkit-block-form-builder.php

899 lines 27.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Kit Form Builder Block class.
4 *
5 * @package ConvertKit
6 * @author ConvertKit
7 */
8
9 /**
10 * Kit Form Builder Block for Gutenberg.
11 *
12 * @package ConvertKit
13 * @author ConvertKit
14 */
15 class ConvertKit_Block_Form_Builder extends ConvertKit_Block {
16
17 /**
18 * Holds the subscriber that was created
19 * when the form was submitted.
20 *
21 * @since 3.0.0
22 *
23 * @var bool|int
24 */
25 public $subscriber_id = false;
26
27 /**
28 * Holds the WP_Error object if the form submission failed,
29 * to display on screen as a notice.
30 *
31 * @since 3.4.4
32 *
33 * @var bool|WP_Error
34 */
35 public $error = false;
36
37 /**
38 * Holds the number of times this block has been rendered on the Post,
39 * to ensure error notice IDs are unique.
40 *
41 * @since 3.4.4
42 *
43 * @var int
44 */
45 public $render_count = 0;
46
47 /**
48 * Constructor
49 *
50 * @since 3.0.0
51 */
52 public function __construct() {
53
54 // Subscribe if the form was submitted.
55 add_action( 'init', array( $this, 'maybe_subscribe' ) );
56
57 // Register this as a Gutenberg block in the Kit Plugin.
58 add_filter( 'convertkit_blocks', array( $this, 'register' ) );
59
60 // Enqueue styles for this Gutenberg Block in the editor view.
61 add_action( 'convertkit_gutenberg_enqueue_styles', array( $this, 'enqueue_styles_editor' ) );
62
63 // Enqueue scripts and styles for this Gutenberg Block in the editor and frontend views.
64 add_action( 'convertkit_gutenberg_enqueue_styles_editor_and_frontend', array( $this, 'enqueue_styles' ) );
65
66 // Replace <a> with <button type="submit"> for the core/button element within the form builder.
67 add_filter( 'render_block_core/button', array( $this, 'render_form_button' ), 10, 2 );
68
69 }
70
71 /**
72 * Checks if the request is a Native Form subscribe request with an email address.
73 * If so, subscribes the email address to the Kit account.
74 *
75 * @since 3.0.0
76 */
77 public function maybe_subscribe() {
78
79 // Bail if no nonce was specified.
80 if ( ! array_key_exists( '_wpnonce', $_REQUEST ) ) {
81 return;
82 }
83
84 // Bail if the nonce failed validation.
85 if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'convertkit_block_form_builder' ) ) {
86 return;
87 }
88
89 // Bail if the expected email, resource ID or Post ID are missing.
90 if ( ! array_key_exists( 'convertkit', $_REQUEST ) ) {
91 return;
92 }
93 if ( ! array_key_exists( 'email', $_REQUEST['convertkit'] ) ) {
94 return;
95 }
96 if ( ! array_key_exists( 'post_id', $_REQUEST['convertkit'] ) ) {
97 return;
98 }
99
100 // Check spam protection.
101 $spam_protection = new ConvertKit_Spam_Protection();
102
103 // Bail if spam protection failed.
104 $spam_protection_result = $spam_protection->verify( 'convertkit_form_builder' );
105 if ( is_wp_error( $spam_protection_result ) ) {
106 $this->error = $spam_protection_result;
107 return;
108 }
109
110 // Sanitize form data.
111 $form_data = map_deep( wp_unslash( $_REQUEST['convertkit'] ), 'sanitize_text_field' );
112
113 // Bail if the email address is invalid. The entry isn't stored, as an invalid
114 // email address is of no use to the creator.
115 if ( ! is_email( $form_data['email'] ) ) {
116 $this->error = new WP_Error(
117 'convertkit_block_form_builder_invalid_email',
118 __( 'Please enter a valid email address.', 'convertkit' )
119 );
120 return;
121 }
122
123 // Build custom fields, if any were specified.
124 $custom_fields = array();
125 if ( array_key_exists( 'custom_fields', $form_data ) ) {
126 $custom_fields = $form_data['custom_fields'];
127 }
128
129 // Get Form, Tag and Sequence IDs, if any were specified.
130 $form_id = array_key_exists( 'form_id', $form_data ) ? $form_data['form_id'] : false;
131 $tag_id = array_key_exists( 'tag_id', $form_data ) ? $form_data['tag_id'] : false;
132 $sequence_id = array_key_exists( 'sequence_id', $form_data ) ? $form_data['sequence_id'] : false;
133
134 // Initialize classes that will be used.
135 $settings = new ConvertKit_Settings();
136 $entries = new ConvertKit_Form_Entries();
137
138 // If the Plugin Access Token has not been configured, we can't add a subscriber.
139 if ( ! $settings->has_access_and_refresh_token() ) {
140 // Store entry and return.
141 if ( $form_data['store_entries'] ) {
142 $entries->upsert(
143 array(
144 'post_id' => $form_data['post_id'],
145 'email' => $form_data['email'],
146 'first_name' => $form_data['first_name'],
147 'custom_fields' => $custom_fields,
148 'form_id' => $form_id,
149 'tag_id' => $tag_id,
150 'sequence_id' => $sequence_id,
151 'api_result' => 'error',
152 'api_error' => __( 'Plugin Access Token not configured', 'convertkit' ),
153 )
154 );
155 }
156
157 $this->error = new WP_Error(
158 'convertkit_block_form_builder_no_access_token',
159 __( 'Sorry, we were unable to subscribe you. Please try again later.', 'convertkit' )
160 );
161 return;
162 }
163
164 // Initialize the API.
165 $api = new ConvertKit_API_V4(
166 CONVERTKIT_OAUTH_CLIENT_ID,
167 CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
168 $settings->get_access_token(),
169 $settings->get_refresh_token(),
170 $settings->debug_enabled(),
171 'block_form_builder'
172 );
173
174 // Determine the subscriber state.
175 // If a Form is specified, mark the subscriber as inactive, so the form's double optin is honored.
176 // If a Tag or Sequence is specified, mark the subscriber as active, as there's no double optin for tags or sequences.
177 $subscriber_state = $form_id !== false ? 'inactive' : 'active';
178
179 // Create subscriber.
180 $result = $api->create_subscriber(
181 sanitize_email( $form_data['email'] ),
182 array_key_exists( 'first_name', $form_data ) ? $form_data['first_name'] : '',
183 $subscriber_state,
184 $custom_fields
185 );
186
187 // Bail if an error occurred.
188 if ( is_wp_error( $result ) ) {
189 // Store entry and return.
190 if ( $form_data['store_entries'] ) {
191 $entries->upsert(
192 array(
193 'post_id' => $form_data['post_id'],
194 'email' => $form_data['email'],
195 'first_name' => $form_data['first_name'],
196 'custom_fields' => $custom_fields,
197 'form_id' => $form_id,
198 'tag_id' => $tag_id,
199 'sequence_id' => $sequence_id,
200 'api_result' => 'error',
201 'api_error' => $result->get_error_message(),
202 )
203 );
204 }
205
206 $this->error = $result;
207 return;
208 }
209
210 // Store entry.
211 if ( $form_data['store_entries'] ) {
212 $entries->upsert(
213 array(
214 'post_id' => $form_data['post_id'],
215 'email' => $form_data['email'],
216 'first_name' => $form_data['first_name'],
217 'custom_fields' => $custom_fields,
218 'form_id' => $form_id,
219 'tag_id' => $tag_id,
220 'sequence_id' => $sequence_id,
221 'api_result' => 'success',
222 )
223 );
224 }
225
226 // Store the subscriber ID in a cookie.
227 $subscriber = new ConvertKit_Subscriber();
228 $subscriber->set( $result['subscriber']['id'] );
229
230 // If a form was specified, add the subscriber to the form.
231 if ( $form_id ) {
232 // For Legacy Forms, a different endpoint is used.
233 $forms = new ConvertKit_Resource_Forms();
234 if ( $forms->is_legacy( $form_id ) ) {
235 $result = $api->add_subscriber_to_legacy_form(
236 $form_id,
237 $result['subscriber']['id']
238 );
239 } else {
240 $result = $api->add_subscriber_to_form(
241 $form_id,
242 $result['subscriber']['id'],
243 get_permalink( absint( $form_data['post_id'] ) )
244 );
245 }
246
247 if ( $form_data['store_entries'] ) {
248 $entries->upsert(
249 array(
250 'post_id' => $form_data['post_id'],
251 'email' => $form_data['email'],
252 'first_name' => $form_data['first_name'],
253 'custom_fields' => $custom_fields,
254 'form_id' => $form_id,
255 'tag_id' => $tag_id,
256 'sequence_id' => $sequence_id,
257 'api_result' => is_wp_error( $result ) ? 'error' : 'success',
258 'api_error' => is_wp_error( $result ) ? $result->get_error_message() : '',
259 )
260 );
261 }
262 }
263
264 // If a tag was specified, add the subscriber to the tag.
265 if ( $tag_id ) {
266 $result = $api->tag_subscriber( $tag_id, $result['subscriber']['id'] );
267
268 if ( $form_data['store_entries'] ) {
269 $entries->upsert(
270 array(
271 'post_id' => $form_data['post_id'],
272 'email' => $form_data['email'],
273 'first_name' => $form_data['first_name'],
274 'custom_fields' => $custom_fields,
275 'form_id' => $form_id,
276 'tag_id' => $tag_id,
277 'sequence_id' => $sequence_id,
278 'api_result' => is_wp_error( $result ) ? 'error' : 'success',
279 'api_error' => is_wp_error( $result ) ? $result->get_error_message() : '',
280 )
281 );
282 }
283 }
284
285 // If a sequence was specified, add the subscriber to the sequence.
286 if ( $sequence_id ) {
287 $result = $api->add_subscriber_to_sequence( $sequence_id, $result['subscriber']['id'] );
288
289 if ( $form_data['store_entries'] ) {
290 $entries->upsert(
291 array(
292 'post_id' => $form_data['post_id'],
293 'email' => $form_data['email'],
294 'first_name' => $form_data['first_name'],
295 'custom_fields' => $custom_fields,
296 'form_id' => $form_id,
297 'tag_id' => $tag_id,
298 'sequence_id' => $sequence_id,
299 'api_result' => is_wp_error( $result ) ? 'error' : 'success',
300 'api_error' => is_wp_error( $result ) ? $result->get_error_message() : '',
301 )
302 );
303 }
304 }
305
306 // Get the redirect URL, based on whether the form is configured to redirect
307 // or not.
308 if ( array_key_exists( 'redirect', $form_data ) && wp_http_validate_url( sanitize_url( $form_data['redirect'] ) ) ) {
309 // Redirect to the URL specified in the form.
310 $redirect = sanitize_url( $form_data['redirect'] );
311 } else {
312 // Redirect to the Post the form was displayed on, to show a success message.
313 $redirect = get_permalink( absint( $form_data['post_id'] ) );
314 }
315
316 // Redirect.
317 wp_redirect( $redirect ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
318 exit();
319
320 }
321
322 /**
323 * Enqueues styles for this Gutenberg Block in the editor view.
324 *
325 * @since 3.0.0
326 */
327 public function enqueue_styles_editor() {
328
329 wp_enqueue_style( 'convertkit-gutenberg', CONVERTKIT_PLUGIN_URL . 'resources/backend/css/gutenberg.css', array( 'wp-edit-blocks' ), CONVERTKIT_PLUGIN_VERSION );
330
331 }
332
333 /**
334 * Enqueues styles for this Gutenberg Block in the editor and frontend views.
335 *
336 * @since 2.3.3
337 */
338 public function enqueue_styles() {
339
340 convertkit_enqueue_frontend_css();
341
342 }
343
344 /**
345 * Returns this block's programmatic name, excluding the convertkit- prefix.
346 *
347 * @since 3.0.0
348 *
349 * @return string
350 */
351 public function get_name() {
352
353 /**
354 * This will register as:
355 * - a Gutenberg block, with the name convertkit/form-builder.
356 */
357 return 'form-builder';
358
359 }
360
361 /**
362 * Returns this block's title.
363 *
364 * @since 3.1.1
365 */
366 public function get_title() {
367
368 return __( 'Kit Form Builder', 'convertkit' );
369
370 }
371
372 /**
373 * Returns this block's icon.
374 *
375 * @since 3.1.1
376 */
377 public function get_icon() {
378
379 return 'resources/backend/images/block-icon-form-builder.svg';
380
381 }
382
383 /**
384 * Returns this block's Title, Icon, Categories, Keywords and properties.
385 *
386 * @since 3.0.0
387 *
388 * @return array
389 */
390 public function get_overview() {
391
392 $convertkit_forms = new ConvertKit_Resource_Forms( 'block_edit' );
393 $settings = new ConvertKit_Settings();
394
395 return array(
396 'title' => $this->get_title(),
397 'description' => __( 'Build a subscription form with Kit.', 'convertkit' ),
398 'icon' => $this->get_icon(),
399 'category' => 'convertkit',
400 'keywords' => array(
401 __( 'ConvertKit', 'convertkit' ),
402 __( 'Kit', 'convertkit' ),
403 __( 'Form Builder', 'convertkit' ),
404 ),
405
406 // Function to call when rendering.
407 'render_callback' => array( $this, 'render' ),
408
409 // Gutenberg: Block Icon in Editor.
410 'gutenberg_icon' => convertkit_get_file_contents( CONVERTKIT_PLUGIN_PATH . '/resources/backend/images/block-icon-form-builder.svg' ),
411
412 // Gutenberg: Example image showing how this block looks when choosing it in Gutenberg.
413 'gutenberg_example_image' => CONVERTKIT_PLUGIN_URL . 'resources/backend/images/block-example-form-builder.png',
414
415 // Gutenberg: Inner blocks to use as a starting template when creating a new block.
416 'gutenberg_template' => array(
417 'convertkit/form-builder-field-name' => array(
418 'label' => 'First name',
419 ),
420 'convertkit/form-builder-field-email' => array(
421 'label' => 'Email address',
422 'lock' => array(
423 'move' => false,
424 'remove' => true,
425 ),
426 ),
427 'core/button' => array(
428 'label' => 'Submit button',
429 'text' => 'Subscribe',
430 'variant' => 'primary',
431 'className' => 'convertkit-form-builder-submit-button',
432 'lock' => array(
433 'move' => true,
434 'remove' => true,
435 ),
436 ),
437 ),
438
439 // Help descriptions, displayed when no Access Token / resources exist and this block/shortcode is added.
440 'no_access_token' => array(
441 'notice' => __( 'Not connected to Kit.', 'convertkit' ),
442 'link' => convertkit_get_setup_wizard_plugin_link(),
443 'link_text' => __( 'Click here to connect your Kit account.', 'convertkit' ),
444 'instruction_text' => __( 'Connect your Kit account at Settings > Kit, and then refresh this page to configure this block.', 'convertkit' ),
445 ),
446
447 'has_access_token' => $settings->has_access_and_refresh_token(),
448
449 // This block works without resources, so we don't need to check if resources exist.
450 'has_resources' => true,
451 );
452
453 }
454
455 /**
456 * Returns this block's Attributes
457 *
458 * @since 3.0.0
459 *
460 * @return array
461 */
462 public function get_attributes() {
463
464 return array(
465 // Block attributes.
466 'redirect' => array(
467 'type' => 'string',
468 'default' => $this->get_default_value( 'redirect' ),
469 ),
470 'store_entries' => array(
471 'type' => 'boolean',
472 'default' => $this->get_default_value( 'store_entries' ),
473 ),
474 'display_form_if_subscribed' => array(
475 'type' => 'boolean',
476 'default' => $this->get_default_value( 'display_form_if_subscribed' ),
477 ),
478 'text_if_subscribed' => array(
479 'type' => 'string',
480 'default' => $this->get_default_value( 'text_if_subscribed' ),
481 ),
482 'form_id' => array(
483 'type' => 'string',
484 'default' => $this->get_default_value( 'form_id' ),
485 ),
486 'tag_id' => array(
487 'type' => 'string',
488 'default' => $this->get_default_value( 'tag_id' ),
489 ),
490 'sequence_id' => array(
491 'type' => 'string',
492 'default' => $this->get_default_value( 'sequence_id' ),
493 ),
494
495 // get_supports() style, color and typography attributes.
496 'align' => array(
497 'type' => 'string',
498 ),
499 'style' => array(
500 'type' => 'object',
501 ),
502 'backgroundColor' => array(
503 'type' => 'string',
504 ),
505 'textColor' => array(
506 'type' => 'string',
507 ),
508 'fontSize' => array(
509 'type' => 'string',
510 ),
511
512 // Always required for Gutenberg.
513 'is_gutenberg_example' => array(
514 'type' => 'boolean',
515 'default' => false,
516 ),
517 );
518
519 }
520
521 /**
522 * Returns this block's supported built-in Attributes.
523 *
524 * @since 3.0.0
525 *
526 * @return array Supports
527 */
528 public function get_supports() {
529
530 return array(
531 'align' => true,
532 'className' => true,
533 'color' => array(
534 'link' => true,
535 'background' => true,
536 'text' => true,
537 ),
538 'typography' => array(
539 'fontSize' => true,
540 'lineHeight' => true,
541 ),
542 'spacing' => array(
543 'margin' => true,
544 'padding' => true,
545 ),
546 );
547
548 }
549
550 /**
551 * Returns this block's Fields
552 *
553 * @since 3.0.0
554 *
555 * @return bool|array
556 */
557 public function get_fields() {
558
559 // Get Kit Forms. Non-legacy forms populate the sidebar dropdown;
560 // legacy forms are exposed separately as a fallback so the sidebar can
561 // keep displaying a previously-saved legacy form as the current
562 // selection without offering other legacy forms as new choices.
563 $forms = new ConvertKit_Resource_Forms( 'block_form_builder' );
564 $forms_options = array();
565 $forms_legacy_options = array();
566 if ( $forms->exist() ) {
567 foreach ( $forms->get() as $form ) {
568 $label = sprintf(
569 '%s [%s]',
570 sanitize_text_field( $form['name'] ),
571 // Legacy forms don't include a `format` key, so define them as inline.
572 ( ! empty( $form['format'] ) ? sanitize_text_field( $form['format'] ) : 'inline' )
573 );
574
575 if ( ! empty( $form['format'] ) ) {
576 $forms_options[ $form['id'] ] = $label;
577 } else {
578 $forms_legacy_options[ $form['id'] ] = $label;
579 }
580 }
581 }
582
583 // Get Kit Tags.
584 $tags = new ConvertKit_Resource_Tags( 'block_form_builder' );
585 $tags_options = array();
586 if ( $tags->exist() ) {
587 foreach ( $tags->get() as $tag ) {
588 $tags_options[ $tag['id'] ] = sanitize_text_field( $tag['name'] );
589 }
590 }
591
592 // Get Kit Sequences.
593 $sequences = new ConvertKit_Resource_Sequences( 'block_form_builder' );
594 $sequences_options = array();
595 if ( $sequences->exist() ) {
596 foreach ( $sequences->get() as $sequence ) {
597 $sequences_options[ $sequence['id'] ] = sanitize_text_field( $sequence['name'] );
598 }
599 }
600
601 return array(
602 'redirect' => array(
603 'label' => __( 'Redirect', 'convertkit' ),
604 'type' => 'url',
605 'description' => __( 'The URL to redirect to after the visitor subscribes. If not specified, the visitor will remain on the current page.', 'convertkit' ),
606 ),
607 'store_entries' => array(
608 'label' => __( 'Store form submissions', 'convertkit' ),
609 'type' => 'toggle',
610 'description' => __( 'If enabled, stores copies of form submissions in the WordPress database. Submissions are always sent to Kit.', 'convertkit' ),
611 ),
612 'display_form_if_subscribed' => array(
613 'label' => __( 'Display form', 'convertkit' ),
614 'type' => 'toggle',
615 'description' => __( 'If enabled, displays the form if the visitor is already subscribed.', 'convertkit' ),
616 ),
617 'text_if_subscribed' => array(
618 'label' => __( 'Text', 'convertkit' ),
619 'type' => 'text',
620 'description' => __( 'The text to display if the visitor is already subscribed.', 'convertkit' ),
621 'display_if' => array(
622 'key' => 'display_form_if_subscribed',
623 'value' => 0,
624 ),
625 ),
626 'form_id' => array(
627 'label' => __( 'Form', 'convertkit' ),
628 'type' => 'select',
629 'description' => __( 'The Kit form to add the subscriber to. Useful if you want to send an incentive email.', 'convertkit' ),
630 'values' => $forms_options,
631 'legacy_values' => $forms_legacy_options,
632 ),
633 'tag_id' => array(
634 'label' => __( 'Tag', 'convertkit' ),
635 'type' => 'select',
636 'description' => __( 'The Kit tag to add the subscriber to.', 'convertkit' ),
637 'values' => $tags_options,
638 ),
639 'sequence_id' => array(
640 'label' => __( 'Sequence', 'convertkit' ),
641 'type' => 'select',
642 'description' => __( 'The Kit sequence to add the subscriber to.', 'convertkit' ),
643 'values' => $sequences_options,
644 ),
645 );
646
647 }
648
649 /**
650 * Returns this block's UI panels / sections.
651 *
652 * @since 3.0.0
653 *
654 * @return bool|array
655 */
656 public function get_panels() {
657
658 return array(
659 'general' => array(
660 'label' => __( 'General', 'convertkit' ),
661 'fields' => array(
662 'form_id',
663 'tag_id',
664 'sequence_id',
665 'redirect',
666 'store_entries',
667 'display_form_if_subscribed',
668 'text_if_subscribed',
669 ),
670 ),
671 );
672
673 }
674
675 /**
676 * Returns this block's Default Values
677 *
678 * @since 3.0.0
679 *
680 * @return array
681 */
682 public function get_default_values() {
683
684 return array(
685 'form_id' => '',
686 'tag_id' => '',
687 'sequence_id' => '',
688 'redirect' => '',
689 'store_entries' => true,
690 'display_form_if_subscribed' => true,
691 'text_if_subscribed' => __( 'Thanks for subscribing!', 'convertkit' ),
692
693 // Built-in Gutenberg block attributes.
694 'align' => 'center',
695 'style' => '',
696 'backgroundColor' => '',
697 'textColor' => '',
698 );
699
700 }
701
702 /**
703 * Returns the block's output, based on the supplied configuration attributes.
704 *
705 * @since 3.0.0
706 *
707 * @param array $atts Block Attributes.
708 * @param string $content Inner blocks content.
709 * @return string
710 */
711 public function render( $atts, $content ) {
712
713 global $post;
714
715 // Get Post ID.
716 $post_id = is_a( $post, 'WP_Post' ) ? $post->ID : 0;
717
718 // Parse attributes, defining fallback defaults if required
719 // and moving some attributes (such as Gutenberg's styles), if defined.
720 $atts = $this->sanitize_and_declare_atts( $atts );
721
722 // Check if subscriber is already subscribed, and whether the form should be displayed.
723 $subscriber = new ConvertKit_Subscriber();
724 $this->subscriber_id = $subscriber->get_subscriber_id();
725 $display_form = $this->subscriber_id && ! $atts['display_form_if_subscribed'] ? false : true;
726
727 // If the form should not be displayed, return the subscribed text.
728 if ( ! $display_form ) {
729 $html = '<div class="' . implode( ' ', map_deep( $this->get_css_classes(), 'sanitize_html_class' ) ) . '" style="' . implode( ';', map_deep( $this->get_css_styles( $atts ), 'esc_attr' ) ) . '">';
730 $html .= esc_html( $atts['text_if_subscribed'] );
731 $html .= '</div>';
732 return $html;
733 }
734
735 // Add the <form> element and hidden fields immediate inside the block's container.
736 $html = $this->add_form_to_block_content( $content, $atts, $post_id );
737
738 /**
739 * Filter the block's content immediately before it is output.
740 *
741 * @since 3.0.0
742 *
743 * @param string $html ConvertKit Native Form HTML.
744 * @param array $atts Block Attributes.
745 */
746 $html = apply_filters( 'convertkit_block_form_builder_render', $html, $atts );
747
748 return $html;
749
750 }
751
752 /**
753 * Replace <a> with <button type="submit"> for the core/button element within the form builder
754 * that has the class convertkit-form-builder-submit-button, as the block editor doesn't
755 * have a core <button> element, and registering our own just for this block would be overkill.
756 *
757 * @since 3.0.0
758 *
759 * @param string $block_content Block content.
760 * @param array $block Block attributes.
761 * @return string
762 */
763 public function render_form_button( $block_content, $block ) {
764
765 if ( ! isset( $block['attrs']['className'] ) ) {
766 return $block_content;
767 }
768
769 if ( strpos( $block['attrs']['className'], 'convertkit-form-builder-submit-button' ) === false ) {
770 return $block_content;
771 }
772
773 // Change link to button.
774 $block_content = preg_replace(
775 '/<a([^>]*)>(.*?)<\/a>/',
776 '<button type="submit"$1>$2</button>',
777 $block_content
778 );
779
780 // Return the button if no spam protection provider is active.
781 $spam_protection = new ConvertKit_Spam_Protection();
782 $provider = $spam_protection->get_active_provider();
783 if ( ! $provider ) {
784 return $block_content;
785 }
786
787 // Enqueue the spam protection provider's JS.
788 $provider->enqueue_scripts();
789
790 // Parse the button's DOM.
791 $parser = new ConvertKit_HTML_Parser( $block_content );
792 $button = $parser->xpath->query( '//button' )->item( 0 );
793
794 // Attach the spam protection provider's attributes/elements to the form/button as necessary.
795 // $button is narrowed from DOMNode to DOMElement by the //button xpath expression above.
796 $provider->attach_to_form_button_dom( $parser, $button, 'convertkit_form_builder' ); // @phpstan-ignore-line
797
798 // Return button HTML.
799 return $parser->get_body_html();
800
801 }
802
803 /**
804 * Wraps the block's content within a <form> element, and adds hidden fields.
805 *
806 * @since 3.0.0
807 *
808 * @param string $content Block content.
809 * @param array $atts Block attributes.
810 * @param int $post_id Post ID.
811 * @return string
812 */
813 private function add_form_to_block_content( $content, $atts, $post_id ) {
814
815 // Load the content into the parser.
816 $parser = new ConvertKit_HTML_Parser( $content );
817
818 // Get block container.
819 $block_container = $parser->xpath->query( '//div[contains(@class, "wp-block-convertkit-form-builder")]' )->item( 0 );
820
821 // If no block container was found, return the original content.
822 // This shouldn't happen, as the block editor supplies the container, but it's a safeguard.
823 if ( ! $block_container ) {
824 return $content;
825 }
826
827 // Create form element.
828 $form = $parser->html->createElement( 'form' );
829 $form->setAttribute( 'action', esc_url( get_permalink( $post_id ) ) );
830 $form->setAttribute( 'method', 'post' );
831
832 // Move form builder div contents into form.
833 while ( $block_container->hasChildNodes() ) {
834 $form->appendChild( $block_container->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
835 }
836
837 // Add subscribed message if required.
838 if ( $this->subscriber_id ) {
839 $subscribed_message = $parser->html->createElement( 'div' );
840 $subscribed_message->setAttribute( 'class', 'convertkit-form-builder-subscribed-message' );
841 $subscribed_message->appendChild( $parser->html->createTextNode( $atts['text_if_subscribed'] ) );
842 $form->insertBefore( $subscribed_message, $form->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
843 }
844
845 // Add error notice if the submission failed.
846 if ( is_wp_error( $this->error ) ) {
847 ++$this->render_count;
848 $error_id = 'convertkit-form-builder-error-' . $this->render_count;
849
850 $error_notice = $parser->html->createElement( 'div' );
851 $error_notice->setAttribute( 'id', $error_id );
852 $error_notice->setAttribute( 'class', 'convertkit-form-builder-notice convertkit-form-builder-notice-error' );
853 $error_notice->setAttribute( 'role', 'alert' );
854 $error_notice->setAttribute( 'tabindex', '-1' );
855 $error_notice->appendChild( $parser->html->createTextNode( $this->error->get_error_message() ) );
856 $form->insertBefore( $error_notice, $form->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
857
858 // Focus the email field if it caused the error, so screen readers and
859 // browsers move to it. Otherwise focus the notice, as the error isn't
860 // specific to a field.
861 // Query within the form, as it's not yet appended to the document.
862 $email_field = $parser->xpath->query( './/input[@name="convertkit[email]"]', $form )->item( 0 );
863 if ( $email_field && $this->error->get_error_code() === 'convertkit_block_form_builder_invalid_email' ) {
864 $email_field->setAttribute( 'aria-invalid', 'true' ); // @phpstan-ignore-line
865 $email_field->setAttribute( 'aria-describedby', $error_id ); // @phpstan-ignore-line
866 $email_field->setAttribute( 'autofocus', 'autofocus' ); // @phpstan-ignore-line
867 } else {
868 $error_notice->setAttribute( 'autofocus', 'autofocus' );
869 }
870 }
871
872 // Add hidden fields.
873 $fields = array(
874 'convertkit[post_id]' => absint( $post_id ),
875 'convertkit[store_entries]' => $atts['store_entries'] ? '1' : '0',
876 'convertkit[redirect]' => esc_url( $atts['redirect'] ),
877 'convertkit[form_id]' => absint( $atts['form_id'] ),
878 'convertkit[tag_id]' => absint( $atts['tag_id'] ),
879 'convertkit[sequence_id]' => absint( $atts['sequence_id'] ),
880 '_wpnonce' => wp_create_nonce( 'convertkit_block_form_builder' ),
881 );
882 foreach ( $fields as $name => $value ) {
883 $hidden = $parser->html->createElement( 'input' );
884 $hidden->setAttribute( 'type', 'hidden' );
885 $hidden->setAttribute( 'name', $name );
886 $hidden->setAttribute( 'value', $value );
887 $form->appendChild( $hidden );
888 }
889
890 // Replace div contents with form.
891 $block_container->appendChild( $form );
892
893 // Return modified content.
894 return $parser->get_body_html();
895
896 }
897
898 }
899