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
← All changes | includes/blocks/class-convertkit-block-form-builder.php +209 -52 3.3.1 → 3.4.6 View file →
@@ -24,8 +24,38 @@
24 24 */
25 25 public $subscriber_id = false;
26 26
27 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 + /**
28 58 * Constructor
29 59 *
30 60 * @since 3.0.0
31 61 */
@@ -76,17 +106,20 @@
76 106 if ( ! array_key_exists( 'post_id', $_REQUEST['convertkit'] ) ) {
77 107 return;
78 108 }
79 109
80 - // Check reCAPTCHA.
81 - $recaptcha = new ConvertKit_Recaptcha();
82 - $recaptcha_response = $recaptcha->verify_recaptcha(
83 - ( isset( $_POST['g-recaptcha-response'] ) ? sanitize_text_field( wp_unslash( $_POST['g-recaptcha-response'] ) ) : '' ),
84 - 'convertkit_form_builder'
85 - );
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 + }
86 114
87 - // Bail if reCAPTCHA failed.
88 - if ( is_wp_error( $recaptcha_response ) ) {
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;
89 122 return;
90 123 }
91 124
92 125 // Sanitize form data.
@@ -91,8 +124,18 @@
91 124
92 125 // Sanitize form data.
93 126 $form_data = map_deep( wp_unslash( $_REQUEST['convertkit'] ), 'sanitize_text_field' );
94 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 +
95 138 // Build custom fields, if any were specified.
96 139 $custom_fields = array();
97 140 if ( array_key_exists( 'custom_fields', $form_data ) ) {
98 141 $custom_fields = $form_data['custom_fields'];
@@ -97,12 +140,15 @@
97 140 if ( array_key_exists( 'custom_fields', $form_data ) ) {
98 141 $custom_fields = $form_data['custom_fields'];
99 142 }
100 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 +
101 147 // Get Form, Tag and Sequence IDs, if any were specified.
102 - $form_id = array_key_exists( 'form_id', $form_data ) ? $form_data['form_id'] : false;
103 - $tag_id = array_key_exists( 'tag_id', $form_data ) ? $form_data['tag_id'] : false;
104 - $sequence_id = array_key_exists( 'sequence_id', $form_data ) ? $form_data['sequence_id'] : false;
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;
105 151
106 152 // Initialize classes that will be used.
107 153 $settings = new ConvertKit_Settings();
108 154 $entries = new ConvertKit_Form_Entries();
@@ -114,9 +160,9 @@
114 160 $entries->upsert(
115 161 array(
116 162 'post_id' => $form_data['post_id'],
117 163 'email' => $form_data['email'],
118 - 'first_name' => $form_data['first_name'],
164 + 'first_name' => $first_name,
119 165 'custom_fields' => $custom_fields,
120 166 'form_id' => $form_id,
121 167 'tag_id' => $tag_id,
122 168 'sequence_id' => $sequence_id,
@@ -124,8 +170,13 @@
124 170 'api_error' => __( 'Plugin Access Token not configured', 'convertkit' ),
125 171 )
126 172 );
127 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 + );
128 179 return;
129 180 }
130 181
131 182 // Initialize the API.
@@ -137,17 +188,22 @@
137 188 $settings->debug_enabled(),
138 189 'block_form_builder'
139 190 );
140 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 +
141 197 // Create subscriber.
142 198 $result = $api->create_subscriber(
143 199 sanitize_email( $form_data['email'] ),
144 - array_key_exists( 'first_name', $form_data ) ? $form_data['first_name'] : '',
145 - 'active',
200 + $first_name,
201 + $subscriber_state,
146 202 $custom_fields
147 203 );
148 204
149 - // Bail if an error occured.
205 + // Bail if an error occurred.
150 206 if ( is_wp_error( $result ) ) {
151 207 // Store entry and return.
152 208 if ( $form_data['store_entries'] ) {
153 209 $entries->upsert(
@@ -153,9 +209,9 @@
153 209 $entries->upsert(
154 210 array(
155 211 'post_id' => $form_data['post_id'],
156 212 'email' => $form_data['email'],
157 - 'first_name' => $form_data['first_name'],
213 + 'first_name' => $first_name,
158 214 'custom_fields' => $custom_fields,
159 215 'form_id' => $form_id,
160 216 'tag_id' => $tag_id,
161 217 'sequence_id' => $sequence_id,
@@ -163,8 +219,10 @@
163 219 'api_error' => $result->get_error_message(),
164 220 )
165 221 );
166 222 }
223 +
224 + $this->error = $result;
167 225 return;
168 226 }
169 227
170 228 // Store entry.
@@ -172,9 +230,9 @@
172 230 $entries->upsert(
173 231 array(
174 232 'post_id' => $form_data['post_id'],
175 233 'email' => $form_data['email'],
176 - 'first_name' => $form_data['first_name'],
234 + 'first_name' => $first_name,
177 235 'custom_fields' => $custom_fields,
178 236 'form_id' => $form_id,
179 237 'tag_id' => $tag_id,
180 238 'sequence_id' => $sequence_id,
@@ -182,19 +240,31 @@
182 240 )
183 241 );
184 242 }
185 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 +
186 247 // Store the subscriber ID in a cookie.
187 248 $subscriber = new ConvertKit_Subscriber();
188 - $subscriber->set( $result['subscriber']['id'] );
249 + $subscriber->set( $subscriber_id );
189 250
190 251 // If a form was specified, add the subscriber to the form.
191 252 if ( $form_id ) {
192 - $result = $api->add_subscriber_to_form(
193 - $form_id,
194 - $result['subscriber']['id'],
195 - get_permalink( absint( $form_data['post_id'] ) )
196 - );
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 + }
197 267
198 268 if ( $form_data['store_entries'] ) {
199 269 $entries->upsert(
200 270 array(
@@ -199,9 +269,9 @@
199 269 $entries->upsert(
200 270 array(
201 271 'post_id' => $form_data['post_id'],
202 272 'email' => $form_data['email'],
203 - 'first_name' => $form_data['first_name'],
273 + 'first_name' => $first_name,
204 274 'custom_fields' => $custom_fields,
205 275 'form_id' => $form_id,
206 276 'tag_id' => $tag_id,
207 277 'sequence_id' => $sequence_id,
@@ -213,9 +283,9 @@
213 283 }
214 284
215 285 // If a tag was specified, add the subscriber to the tag.
216 286 if ( $tag_id ) {
217 - $result = $api->tag_subscriber( $tag_id, $result['subscriber']['id'] );
287 + $result = $api->tag_subscriber( $tag_id, $subscriber_id );
218 288
219 289 if ( $form_data['store_entries'] ) {
220 290 $entries->upsert(
221 291 array(
@@ -220,9 +290,9 @@
220 290 $entries->upsert(
221 291 array(
222 292 'post_id' => $form_data['post_id'],
223 293 'email' => $form_data['email'],
224 - 'first_name' => $form_data['first_name'],
294 + 'first_name' => $first_name,
225 295 'custom_fields' => $custom_fields,
226 296 'form_id' => $form_id,
227 297 'tag_id' => $tag_id,
228 298 'sequence_id' => $sequence_id,
@@ -234,9 +304,9 @@
234 304 }
235 305
236 306 // If a sequence was specified, add the subscriber to the sequence.
237 307 if ( $sequence_id ) {
238 - $result = $api->add_subscriber_to_sequence( $sequence_id, $result['subscriber']['id'] );
308 + $result = $api->add_subscriber_to_sequence( $sequence_id, $subscriber_id );
239 309
240 310 if ( $form_data['store_entries'] ) {
241 311 $entries->upsert(
242 312 array(
@@ -241,9 +311,9 @@
241 311 $entries->upsert(
242 312 array(
243 313 'post_id' => $form_data['post_id'],
244 314 'email' => $form_data['email'],
245 - 'first_name' => $form_data['first_name'],
315 + 'first_name' => $first_name,
246 316 'custom_fields' => $custom_fields,
247 317 'form_id' => $form_id,
248 318 'tag_id' => $tag_id,
249 319 'sequence_id' => $sequence_id,
@@ -259,10 +329,10 @@
259 329 if ( array_key_exists( 'redirect', $form_data ) && wp_http_validate_url( sanitize_url( $form_data['redirect'] ) ) ) {
260 330 // Redirect to the URL specified in the form.
261 331 $redirect = sanitize_url( $form_data['redirect'] );
262 332 } else {
263 - // Redirect to the Post the form was displayed on, to show a success message.
264 - $redirect = get_permalink( absint( $form_data['post_id'] ) );
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'] ) );
265 335 }
266 336
267 337 // Redirect.
268 338 wp_redirect( $redirect ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
@@ -386,10 +456,20 @@
386 456 ),
387 457 ),
388 458 ),
389 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 +
390 468 'has_access_token' => $settings->has_access_and_refresh_token(),
391 - 'has_resources' => $convertkit_forms->exist(),
469 +
470 + // This block works without resources, so we don't need to check if resources exist.
471 + 'has_resources' => true,
392 472 );
393 473
394 474 }
395 475
@@ -496,19 +576,29 @@
496 576 * @return bool|array
497 577 */
498 578 public function get_fields() {
499 579
500 - // Get Kit Forms.
501 - $forms = new ConvertKit_Resource_Forms( 'block_form_builder' );
502 - $forms_options = array();
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();
503 587 if ( $forms->exist() ) {
504 588 foreach ( $forms->get() as $form ) {
505 - // Legacy forms don't include a `format` key, so define them as inline.
506 - $forms_options[ $form['id'] ] = sprintf(
589 + $label = sprintf(
507 590 '%s [%s]',
508 591 sanitize_text_field( $form['name'] ),
592 + // Legacy forms don't include a `format` key, so define them as inline.
509 593 ( ! empty( $form['format'] ) ? sanitize_text_field( $form['format'] ) : 'inline' )
510 594 );
595 +
596 + if ( ! empty( $form['format'] ) ) {
597 + $forms_options[ $form['id'] ] = $label;
598 + } else {
599 + $forms_legacy_options[ $form['id'] ] = $label;
600 + }
511 601 }
512 602 }
513 603
514 604 // Get Kit Tags.
@@ -554,12 +644,13 @@
554 644 'value' => 0,
555 645 ),
556 646 ),
557 647 'form_id' => array(
558 - 'label' => __( 'Form', 'convertkit' ),
559 - 'type' => 'select',
560 - 'description' => __( 'The Kit form to add the subscriber to. Useful if you want to send an incentive email.', 'convertkit' ),
561 - 'values' => $forms_options,
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,
562 653 ),
563 654 'tag_id' => array(
564 655 'label' => __( 'Tag', 'convertkit' ),
565 656 'type' => 'select',
@@ -644,8 +735,11 @@
644 735
645 736 // Get Post ID.
646 737 $post_id = is_a( $post, 'WP_Post' ) ? $post->ID : 0;
647 738
739 + // Increment the render count, used to identify this block on the page.
740 + ++$this->render_count;
741 +
648 742 // Parse attributes, defining fallback defaults if required
649 743 // and moving some attributes (such as Gutenberg's styles), if defined.
650 744 $atts = $this->sanitize_and_declare_atts( $atts );
651 745
@@ -706,26 +800,26 @@
706 800 '<button type="submit"$1>$2</button>',
707 801 $block_content
708 802 );
709 803
710 - // Return the button if reCAPTCHA does not need to be used.
711 - $settings = new ConvertKit_Settings();
712 - if ( ! $settings->has_recaptcha_site_and_secret_keys() ) {
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 ) {
713 808 return $block_content;
714 809 }
715 810
716 - // Enqueue reCAPTCHA JS.
717 - $recaptcha = new ConvertKit_Recaptcha();
718 - $recaptcha->enqueue_scripts();
811 + // Enqueue the spam protection provider's JS.
812 + $provider->enqueue_scripts();
719 813
720 - // Add reCAPTCHA attributes to button.
814 + // Parse the button's DOM.
721 815 $parser = new ConvertKit_HTML_Parser( $block_content );
722 816 $button = $parser->xpath->query( '//button' )->item( 0 );
723 - $button->setAttribute( 'data-sitekey', esc_attr( $settings->recaptcha_site_key() ) ); // @phpstan-ignore-line
724 - $button->setAttribute( 'data-callback', 'convertKitRecaptchaFormSubmit' ); // @phpstan-ignore-line
725 - $button->setAttribute( 'data-action', 'convertkit_form_builder' ); // @phpstan-ignore-line
726 - $button->setAttribute( 'class', trim( $button->getAttribute( 'class' ) . ' g-recaptcha' ) ); // @phpstan-ignore-line
727 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 +
728 822 // Return button HTML.
729 823 return $parser->get_body_html();
730 824
731 825 }
@@ -755,9 +849,9 @@
755 849 }
756 850
757 851 // Create form element.
758 852 $form = $parser->html->createElement( 'form' );
759 - $form->setAttribute( 'action', esc_url( get_permalink( $post_id ) ) );
853 + $form->setAttribute( 'action', esc_url( $this->get_current_url( $post_id ) ) );
760 854 $form->setAttribute( 'method', 'post' );
761 855
762 856 // Move form builder div contents into form.
763 857 while ( $block_container->hasChildNodes() ) {
@@ -763,8 +857,22 @@
763 857 while ( $block_container->hasChildNodes() ) {
764 858 $form->appendChild( $block_container->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
765 859 }
766 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 +
767 875 // Add subscribed message if required.
768 876 if ( $this->subscriber_id ) {
769 877 $subscribed_message = $parser->html->createElement( 'div' );
770 878 $subscribed_message->setAttribute( 'class', 'convertkit-form-builder-subscribed-message' );
@@ -771,8 +879,35 @@
771 879 $subscribed_message->appendChild( $parser->html->createTextNode( $atts['text_if_subscribed'] ) );
772 880 $form->insertBefore( $subscribed_message, $form->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
773 881 }
774 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 +
775 910 // Add hidden fields.
776 911 $fields = array(
777 912 'convertkit[post_id]' => absint( $post_id ),
778 913 'convertkit[store_entries]' => $atts['store_entries'] ? '1' : '0',
@@ -779,8 +914,9 @@
779 914 'convertkit[redirect]' => esc_url( $atts['redirect'] ),
780 915 'convertkit[form_id]' => absint( $atts['form_id'] ),
781 916 'convertkit[tag_id]' => absint( $atts['tag_id'] ),
782 917 'convertkit[sequence_id]' => absint( $atts['sequence_id'] ),
918 + 'convertkit[block_index]' => absint( $this->render_count ),
783 919 '_wpnonce' => wp_create_nonce( 'convertkit_block_form_builder' ),
784 920 );
785 921 foreach ( $fields as $name => $value ) {
786 922 $hidden = $parser->html->createElement( 'input' );
@@ -794,8 +930,29 @@
794 930 $block_container->appendChild( $form );
795 931
796 932 // Return modified content.
797 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'] ) ) );
798 955
799 956 }
800 957
801 958 }