PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.4.6
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.4.6
3.4.6 3.4.5 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 All 199 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.6, at includes/blocks/class-convertkit-block-form-builder.php

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